226 lines
7.3 KiB
Python
Executable file
226 lines
7.3 KiB
Python
Executable file
#!/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()
|