#!/usr/bin/env python3 # pylint: disable=C0103 import logging import os import subprocess import typing import re import coloredlogs import progressbar coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s") log = logging.getLogger() # Constants SOURCE_FOLDER = os.path.join(os.path.expanduser("~"), "Musiques") OUTPUT_FOLDER = os.path.join(os.path.expanduser("~"), ".MusiqueCompressed") CONVERSIONS = {"flac": "opus"} FORBIDDEN_EXTENSIONS = ["jpg", "png", "pdf", "ffs_db"] FORGIVEN_FILENAMES = [ "cover.jpg", "front.jpg", "folder.jpg", "cover.png", "front.png", "folder.png", ] IGNORED_EMPTY_FOLDER = [".stfolder"] RESTRICT_CHARACTERS = '[\0\\/:*"<>|]' # FAT32, NTFS # RESTRICT_CHARACTERS = '[:/]' # HFS, HFS+ # RESTRICT_CHARACTERS = '[\0/]' # ext2-4, linux-based? act = True # 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: fullPath = os.path.join(root, f) path = os.path.relpath(fullPath, SOURCE_FOLDER) sourceFiles[path] = os.path.getctime(fullPath) outputFiles = dict() for root, dirs, files in os.walk(OUTPUT_FOLDER): for f in files: fullPath = os.path.join(root, f) path = os.path.relpath(fullPath, OUTPUT_FOLDER) outputFiles[path] = os.path.getctime(fullPath) # Sorting files remainingConversions = dict() extraFiles = set(outputFiles.keys()) def convertPath(path: str) -> typing.Optional[str]: filename, extension = os.path.splitext(path) extension = extension[1:].lower() # Remove unwanted characters from filename filename_parts = os.path.normpath(filename).split(os.path.sep) filename_parts = [re.sub(RESTRICT_CHARACTERS, "_", part) for part in filename_parts] filename = os.path.sep.join(filename_parts) # If the extension isn't allowed if extension in FORBIDDEN_EXTENSIONS: basename = os.path.basename(path) # And the filename is not an exception if basename not in FORGIVEN_FILENAMES: # This file shouldn't be copied nor converted return None # If this needs a conversion elif extension in CONVERSIONS: extension = CONVERSIONS[extension] return filename + "." + extension # In all other case, this is a simple copy return path log.info("Determining action over %d files", len(sourceFiles)) for sourceFile in sourceFiles: outputFile = convertPath(sourceFile) # If the file should not be converted, do nothing if not outputFile: continue # If the file already has something as an output elif outputFile in outputFiles: extraFiles.remove(outputFile) # If the output file is newer than the source file, do not initiate a # conversion if outputFiles[outputFile] >= sourceFiles[sourceFile]: continue # If the file needs to be converted, do it remainingConversions[sourceFile] = outputFile log.debug("%d actions will need to be taken", len(remainingConversions)) log.info("Copying files that do not require a conversion") conversions = set() for sourceFile in remainingConversions: outputFile = remainingConversions[sourceFile] # Creating folder if it doesn't exists fullOutputFile = os.path.join(OUTPUT_FOLDER, outputFile) fullOutputDir = os.path.dirname(fullOutputFile) if act: os.makedirs(fullOutputDir, exist_ok=True) # Converting fullSourceFile = os.path.join(SOURCE_FOLDER, sourceFile) if sourceFile == outputFile: log.debug("%s → %s", fullSourceFile, fullOutputFile) if act and os.path.isfile(fullOutputFile): os.remove(fullOutputFile) os.link(fullSourceFile, fullOutputFile) else: conversions.add((fullSourceFile, fullOutputFile)) log.info("Removing extra files") for extraFile in extraFiles: fullExtraFile = os.path.join(OUTPUT_FOLDER, extraFile) log.debug("× %s", fullExtraFile) if act: os.remove(fullExtraFile) log.info("Listing files that will be converted") for fullSourceFile, fullOutputFile in conversions: log.debug("%s ⇒ %s", 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, ] if act: subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) else: print(cmd) # Removing empty dirs for root, dirs, files in os.walk(OUTPUT_FOLDER): if not dirs and not files: dirBasename = os.path.basename(root) if act and dirBasename not in IGNORED_EMPTY_FOLDER: os.rmdir(root)