ReplayGain ?

This commit is contained in:
Geoffrey Frogeye 2018-08-04 12:43:13 +02:00
parent 8e43663661
commit d38e1d9180
2 changed files with 127 additions and 11 deletions

97
scripts/replayGain Executable file
View file

@ -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)

View file

@ -1,8 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import shutil
import subprocess import subprocess
import progressbar
import logging
import coloredlogs
coloredlogs.install(level='DEBUG', fmt='%(levelname)s %(message)s')
log = logging.getLogger()
# Constants # Constants
SOURCE_FOLDER = os.path.join(os.path.expanduser("~"), "Musique") SOURCE_FOLDER = os.path.join(os.path.expanduser("~"), "Musique")
@ -12,7 +17,11 @@ FORBIDDEN_EXTENSIONS = ["jpg", "pdf", "ffs_db"]
FORGIVEN_FILENAMES = ["cover.jpg"] FORGIVEN_FILENAMES = ["cover.jpg"]
IGNORED_EMPTY_FOLDER = [".stfolder"] IGNORED_EMPTY_FOLDER = [".stfolder"]
# TODO FEAT Make the directory structure the same as the base one and
# remove IGNORED_EMPTY_FOLDER variable
# Listing files # Listing files
log.info("Listing files")
sourceFiles = dict() sourceFiles = dict()
for root, dirs, files in os.walk(SOURCE_FOLDER): for root, dirs, files in os.walk(SOURCE_FOLDER):
for f in files: for f in files:
@ -29,7 +38,7 @@ for root, dirs, files in os.walk(OUTPUT_FOLDER):
# Sorting files # Sorting files
remainingConversions = dict() remainingConversions = dict()
extraFiles = list(outputFiles.keys()) extraFiles = set(outputFiles.keys())
def convertPath(path): def convertPath(path):
@ -50,6 +59,7 @@ def convertPath(path):
return path return path
log.info("Determining action over {} files".format(len(sourceFiles)))
for sourceFile in sourceFiles: for sourceFile in sourceFiles:
outputFile = convertPath(sourceFile) outputFile = convertPath(sourceFile)
# If the file should not be converted, do nothing # 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 # If the file needs to be converted, do it
remainingConversions[sourceFile] = outputFile 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: for sourceFile in remainingConversions:
outputFile = remainingConversions[sourceFile] outputFile = remainingConversions[sourceFile]
@ -76,24 +88,31 @@ for sourceFile in remainingConversions:
# Converting # Converting
fullSourceFile = os.path.join(SOURCE_FOLDER, sourceFile) fullSourceFile = os.path.join(SOURCE_FOLDER, sourceFile)
print(fullSourceFile, "→", fullOutputFile)
if sourceFile == outputFile: if sourceFile == outputFile:
# shutil.copy(fullSourceFile, fullOutputFile) log.debug('{} → {}'.format(fullSourceFile, fullOutputFile))
if os.path.isfile(fullOutputFile): if os.path.isfile(fullOutputFile):
os.remove(fullOutputFile) os.remove(fullOutputFile)
os.link(fullSourceFile, fullOutputFile) os.link(fullSourceFile, fullOutputFile)
else: else:
subprocess.run( conversions.add((fullSourceFile, fullOutputFile))
["ffmpeg", "-y", "-i", fullSourceFile, "-c:a", "libopus",
"-movflags", "+faststart", "-b:a", "128k", "-vbr", "on",
"-compression_level", "10", fullOutputFile])
# Removing extra files log.info("Removing extra files")
for extraFile in extraFiles: for extraFile in extraFiles:
fullExtraFile = os.path.join(OUTPUT_FOLDER, extraFile) fullExtraFile = os.path.join(OUTPUT_FOLDER, extraFile)
print("×", fullExtraFile) log.debug('× {}'.format(fullExtraFile))
os.remove(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 # Removing empty dirs
for root, dirs, files in os.walk(OUTPUT_FOLDER): for root, dirs, files in os.walk(OUTPUT_FOLDER):
if not dirs and not files: if not dirs and not files: