diff --git a/clean-tags.py b/clean-tags.py new file mode 100755 index 0000000..13d4fea --- /dev/null +++ b/clean-tags.py @@ -0,0 +1,165 @@ +#!/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()