ReplayGain ?
This commit is contained in:
parent
8e43663661
commit
d38e1d9180
97
scripts/replayGain
Executable file
97
scripts/replayGain
Executable 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue