From d38e1d91807d2de258bfece80149f0392d1c5d0f Mon Sep 17 00:00:00 2001 From: Geoffrey Frogeye Date: Sat, 4 Aug 2018 12:43:13 +0200 Subject: [PATCH] ReplayGain ? --- scripts/replayGain | 97 +++++++++++++++++++++++++++++++++++ scripts/updateCompressedMusic | 41 +++++++++++---- 2 files changed, 127 insertions(+), 11 deletions(-) create mode 100755 scripts/replayGain diff --git a/scripts/replayGain b/scripts/replayGain new file mode 100755 index 0000000..5e0b7d9 --- /dev/null +++ b/scripts/replayGain @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +# Normalisation is done at the default of each program, +# which is usually -89.0 dB + +import os +import subprocess +import coloredlogs +import logging +import progressbar + +coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s') +log = logging.getLogger() + + +# Constants +FORCE = False # TODO UX cli argument +SOURCE_FOLDER = os.path.join(os.path.expanduser("~"), "Musique") +GAIN_COMMANDS = {("flac",): ["metaflac", "--add-replay-gain"], + ("mp3",): ["mp3gain", "-a", "-k"] + (["-s", "r"] if FORCE else []), + ("m4a", "mp4"): ["aacgain", "-a", "-k"] + (["-s", "r"] if FORCE else []), + ("ogg",): ["vorbisgain", "--album"] + ([] if FORCE else ["--fast"]), + } + +# Since metaflac ALWAYS recalculate the tags, we need to specificaly verify +# that the tags are present on every track of an album to skip it +def isFlacTagged(f): + # TODO PERF Run metaflac --show-tag for the whole album and compare the + # output with the number of files tagged + for tag in ["REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN"]: + cmd = ["metaflac", "--show-tag", tag, f] + proc = subprocess.run(cmd, stdout=subprocess.PIPE) + res = len(proc.stdout.strip()) > 0 + if not res: + return False + return True + +# TODO UX Do the same thing with other formats so the output is not +# inconsistent + +# Get album paths +albums = set() +for root, dirs, files in os.walk(SOURCE_FOLDER): + + relRoot = os.path.relpath(root, SOURCE_FOLDER) + + # See if it's an album (only 2 components in the path) + head, tail = os.path.split(relRoot) + if not len(head): + continue + head, tail = os.path.split(head) + if len(head): + continue + albums.add(root) + +cmds = list() +for album in albums: + albumName = os.path.relpath(album, SOURCE_FOLDER) + log.info("Processing album {}".format(albumName)) + + musicFiles = dict() + for root, dirs, files in os.walk(album): + for f in files: + ext = os.path.splitext(f)[1][1:].lower() + + for exts in GAIN_COMMANDS.keys(): + if ext in exts: + if exts not in musicFiles.keys(): + musicFiles[exts] = set() + fullPath = os.path.join(root, f) + musicFiles[exts].add(fullPath) + + if len(musicFiles) >= 2: + log.warn("Different extensions for album {}. AlbumGain won't be on par.".format(albumName)) + + for exts, files in musicFiles.items(): + + if exts == ("flac",) and not FORCE: + allTagged = True + for f in files: + if not isFlacTagged(f): + allTaged = False + break + if allTagged: + log.debug("Already tagged (for flac only)!") + break + + cmd = GAIN_COMMANDS[exts] + list(files) + log.debug("Registering command: `{}`".format(" ".join(cmd))) + cmds.append(cmd) + +logging.info("Executing commands") +for cmd in progressbar.progressbar(cmds): + subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + + diff --git a/scripts/updateCompressedMusic b/scripts/updateCompressedMusic index 543151e..252fc5e 100755 --- a/scripts/updateCompressedMusic +++ b/scripts/updateCompressedMusic @@ -1,8 +1,13 @@ #!/usr/bin/env python3 import os -import shutil import subprocess +import progressbar +import logging +import coloredlogs + +coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s') +log = logging.getLogger() # Constants SOURCE_FOLDER = os.path.join(os.path.expanduser("~"), "Musique") @@ -12,7 +17,11 @@ FORBIDDEN_EXTENSIONS = ["jpg", "pdf", "ffs_db"] FORGIVEN_FILENAMES = ["cover.jpg"] IGNORED_EMPTY_FOLDER = [".stfolder"] +# TODO FEAT Make the directory structure the same as the base one and +# remove IGNORED_EMPTY_FOLDER variable + # Listing files +log.info("Listing files") sourceFiles = dict() for root, dirs, files in os.walk(SOURCE_FOLDER): for f in files: @@ -29,7 +38,7 @@ for root, dirs, files in os.walk(OUTPUT_FOLDER): # Sorting files remainingConversions = dict() -extraFiles = list(outputFiles.keys()) +extraFiles = set(outputFiles.keys()) def convertPath(path): @@ -50,6 +59,7 @@ def convertPath(path): return path +log.info("Determining action over {} files".format(len(sourceFiles))) for sourceFile in sourceFiles: outputFile = convertPath(sourceFile) # If the file should not be converted, do nothing @@ -65,7 +75,9 @@ for sourceFile in sourceFiles: # If the file needs to be converted, do it remainingConversions[sourceFile] = outputFile -# Converting files +log.debug("{} actions will need to be taken".format(len(remainingConversions))) +log.info("Copying files that do not require a conversion") +conversions = set() for sourceFile in remainingConversions: outputFile = remainingConversions[sourceFile] @@ -76,24 +88,31 @@ for sourceFile in remainingConversions: # Converting fullSourceFile = os.path.join(SOURCE_FOLDER, sourceFile) - print(fullSourceFile, "→", fullOutputFile) if sourceFile == outputFile: - # shutil.copy(fullSourceFile, fullOutputFile) + log.debug('{} → {}'.format(fullSourceFile, fullOutputFile)) if os.path.isfile(fullOutputFile): os.remove(fullOutputFile) os.link(fullSourceFile, fullOutputFile) else: - subprocess.run( - ["ffmpeg", "-y", "-i", fullSourceFile, "-c:a", "libopus", - "-movflags", "+faststart", "-b:a", "128k", "-vbr", "on", - "-compression_level", "10", fullOutputFile]) + conversions.add((fullSourceFile, fullOutputFile)) -# Removing extra files +log.info("Removing extra files") for extraFile in extraFiles: fullExtraFile = os.path.join(OUTPUT_FOLDER, extraFile) - print("×", fullExtraFile) + log.debug('× {}'.format(fullExtraFile)) os.remove(fullExtraFile) +log.info("Listing files that will be converted") +for fullSourceFile, fullOutputFile in conversions: + log.debug('{} ⇒ {}'.format(fullSourceFile, fullOutputFile)) + +log.info("Converting files") +for fullSourceFile, fullOutputFile in progressbar.progressbar(conversions): + cmd = ["ffmpeg", "-y", "-i", fullSourceFile, "-c:a", "libopus", + "-movflags", "+faststart", "-b:a", "128k", "-vbr", "on", + "-compression_level", "10", fullOutputFile] + subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + # Removing empty dirs for root, dirs, files in os.walk(OUTPUT_FOLDER): if not dirs and not files: