ReplayGain ?
This commit is contained in:
		
							parent
							
								
									8e43663661
								
							
						
					
					
						commit
						d38e1d9180
					
				
					 2 changed files with 127 additions and 11 deletions
				
			
		
							
								
								
									
										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…
	
	Add table
		Add a link
		
	
		Reference in a new issue