#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Dependencies: # – python-mutagen import argparse import logging import os from mutagen.id3 import ID3 from mutagen.flac import FLAC from mutagen import MutagenError class Styling: """Output styling constants.""" ENDC = '\x1b[0m' BOLD = '\x1b[1m' class TagCleaner: """ Class to check audio files for unwanted tags and provide routines to clean them. """ INDENTATION = " " FILE_EXTENSIONS = ['.mp3', '.flac', '.ogg'] TAGS_ID3 = ['TALB', 'TDRC', 'TIT2', 'TPE1', 'TRCK'] TAGS_VORBIS_COMMENTS = ['ALBUM', 'ARTIST', 'DATE', 'TITLE', 'TRACKNUMBER'] def __init__(self, folder): """Construct a new tag cleaner instancce.""" self._logger = logging.getLogger(__class__.__name__) self._dry = False self._id3 = True self._flac = True self._folder = folder self._files = [] self._read_files(folder) def set_dry(self, dry): """Set dry mode (do not save any file).""" self._dry = dry def set_id3(self, do): """Set whether to clean ID3 tags or not.""" self._id3 = do def set_flac(self, do): """Set whether to clean FLAC vorbis comments or not.""" self._flac = do def clean_files(self): """Clean all found files.""" self._logger.info("Clean files") for filename in self._files: self._clean_file(filename) def _read_files(self, folder): self._logger.info("Read files from \"{}\"".format(folder)) for dirname, dirnames, filenames in os.walk(folder): for filename in filenames: if filename.startswith("."): continue (name, ext) = os.path.splitext(filename) if ext.lower() not in TagCleaner.FILE_EXTENSIONS: continue self._logger.debug("Found file \"%s\"", os.path.join(dirname, filename)) self._files.append(os.path.join(dirname, filename)) self._logger.info("Found %d files", len(self._files)) def _clean_file(self, filename): """Clean a file.""" self._logger.info("Clean file \"%s\"", filename) if os.path.isfile(filename): # ID3 if self._id3: self._clean_id3(filename) # FLAC if self._flac: self._clean_flac(filename) else: self._logger.info("Not a file: \"%s\"", filename) def _clean_id3(self, filename): """Clean ID3 tags.""" self._logger.info("Clean ID3") try: tags = ID3(filename) print(Styling.BOLD + filename[len(self._folder):] + Styling.ENDC) print("ID3", "v{}.{}.{}".format(*tags.version)) valid = True # Check version if tags.version != (2, 4, 0): valid = False # Unknown tags if tags.unknown_frames: valid = False print("Unknown frames:") for frame in tags.unknown_frames: print(frame) # Invalid tags invalid_tags = [] for tag in tags: if len(tag) > 4: tag = tag[0:4] if tag not in TagCleaner.TAGS_ID3: invalid_tags.append(tag) if invalid_tags: valid = False print("Unwanted tags:") for tag in invalid_tags: for frame in tags.getall(tag): if hasattr(frame, 'text'): for value in frame.text: print("{}{}: {}".format(TagCleaner.INDENTATION, tag, value)) else: print("{}{}:".format(TagCleaner.INDENTATION, tag), frame) # Save if not valid: # Delete tags for tag in invalid_tags: print("Delete", tag) tags.delall(tag) # Save file if not self._dry: try: tags.save() print("File saved") except Exception as e: self._logger.error("Saving of file \"%s\" failed: %s", filename, e) else: print("File not saved (running in dry mode)") else: self._logger.info("Clean, nothing to do") except MutagenError as e: self._logger.info("Cleaning of ID3 tags failed: %s", e) def _clean_flac(self, filename): """Clean FLAC vorbis comments.""" self._logger.info("Clean FLAC vorbis comments") try: flac = FLAC(filename) invalid_comments = {} if flac.tags: invalid_comments = self._clean_vorbis_comments(flac.tags) # Delete comments if invalid_comments: for key in invalid_comments.keys(): print("Delete", key) del flac.tags[key] if not self._dry: flac.save() print("File saved") else: print("File not saved (running in dry mode)") else: self._logger.info("Clean, nothing to do") except MutagenError as e: self._logger.info("Cleaning of FLAC vorbis comments failed: %s", e) def _clean_vorbis_comments(self, comments): invalid_comments = {} for key, value in comments: if key not in TagCleaner.TAGS_VORBIS_COMMENTS: invalid_comments[key] = value if invalid_comments: print("Unwanted comments:") for key in invalid_comments.keys(): print("{}{}: {}".format(TagCleaner.INDENTATION, key, invalid_comments[key])) return invalid_comments if __name__ == "__main__": # Setup command line parser = argparse.ArgumentParser("Clean unwanted tags from audio files to keep your music library clean.") parser.add_argument('-d', '--dry', dest='dry', action='store_true', default=False, help="dry run, do not modify any file, just print information") parser.add_argument('-v', '--verbose', dest='verbosity', action='count', default=0, help="be verbose, show more information") parser.add_argument('-l', '--logfile', dest='logfile', help="specify name of logfile") parser.add_argument('--no-id3', dest='id3', action='store_false', help="disable cleaning of ID3 tags") parser.add_argument('--no-fac', dest='flac', action='store_false', help="disable cleaning of FLAC vorbis comments") parser.add_argument('folder', help="source folder to read audio files from") parser.set_defaults(id3=True, flac=True) args = parser.parse_args() # Setup logging logging.basicConfig( filename=args.logfile, level=logging.ERROR-(10*args.verbosity), format="%(asctime)s %(levelname)s: %(message)s" ) # Create tag cleaner instance tag_cleaner = TagCleaner(args.folder) tag_cleaner.set_dry(args.dry) tag_cleaner.set_id3(args.id3) tag_cleaner.set_flac(args.flac) # Run action tag_cleaner.clean_files()