scripts/clean-tags.py

226 lines
7.3 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()