#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Dependencies: # – python-mutagen import argparse import logging import os from mutagen.id3 import ID3 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'] TAGS_ID3 = ['TALB', 'TDRC', 'TIT2', 'TPE1', 'TRCK'] def __init__(self, folder): """Construct a new tag cleaner instancce.""" self._logger = logging.getLogger(__class__.__name__) self._dry = False 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 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".format(len(self._files))) def _clean_file(self, filename): """Clean a file.""" self._logger.info("Clean file \"%s\"", filename) if os.path.isfile(filename): # ID3 self._clean_id3(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.error("Cleaning of ID3 tags failed: %s", e) 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('folder', help="source folder to read audio files from") 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) # Run action tag_cleaner.clean_files()