2018-06-24 18:28:37 +02:00
|
|
|
|
#!/usr/bin/env python3
|
2019-10-17 12:44:30 +02:00
|
|
|
|
# pylint: disable=C0103
|
2018-06-24 18:28:37 +02:00
|
|
|
|
|
2019-08-04 19:05:57 +02:00
|
|
|
|
import logging
|
2018-06-24 18:28:37 +02:00
|
|
|
|
import os
|
|
|
|
|
import subprocess
|
2019-10-17 12:44:30 +02:00
|
|
|
|
import typing
|
|
|
|
|
import re
|
2019-08-04 19:05:57 +02:00
|
|
|
|
|
2018-08-04 12:43:13 +02:00
|
|
|
|
import coloredlogs
|
2019-08-04 19:05:57 +02:00
|
|
|
|
import progressbar
|
2018-08-04 12:43:13 +02:00
|
|
|
|
|
2021-06-13 11:49:21 +02:00
|
|
|
|
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
|
2018-08-04 12:43:13 +02:00
|
|
|
|
log = logging.getLogger()
|
2018-06-24 18:28:37 +02:00
|
|
|
|
|
|
|
|
|
# Constants
|
2018-11-24 13:45:14 +01:00
|
|
|
|
SOURCE_FOLDER = os.path.join(os.path.expanduser("~"), "Musiques")
|
2020-08-08 11:19:48 +02:00
|
|
|
|
OUTPUT_FOLDER = os.path.join(os.path.expanduser("~"), ".MusiqueCompressed")
|
2018-07-10 14:50:07 +02:00
|
|
|
|
CONVERSIONS = {"flac": "opus"}
|
2019-08-04 19:05:57 +02:00
|
|
|
|
FORBIDDEN_EXTENSIONS = ["jpg", "png", "pdf", "ffs_db"]
|
2021-06-13 11:49:21 +02:00
|
|
|
|
FORGIVEN_FILENAMES = [
|
|
|
|
|
"cover.jpg",
|
|
|
|
|
"front.jpg",
|
|
|
|
|
"folder.jpg",
|
|
|
|
|
"cover.png",
|
|
|
|
|
"front.png",
|
|
|
|
|
"folder.png",
|
|
|
|
|
]
|
2018-06-24 18:28:37 +02:00
|
|
|
|
IGNORED_EMPTY_FOLDER = [".stfolder"]
|
2019-10-17 12:44:30 +02:00
|
|
|
|
RESTRICT_CHARACTERS = '[\0\\/:*"<>|]' # FAT32, NTFS
|
|
|
|
|
# RESTRICT_CHARACTERS = '[:/]' # HFS, HFS+
|
|
|
|
|
# RESTRICT_CHARACTERS = '[\0/]' # ext2-4, linux-based?
|
2020-08-08 11:19:48 +02:00
|
|
|
|
act = True
|
2018-06-24 18:28:37 +02:00
|
|
|
|
|
2018-08-04 12:43:13 +02:00
|
|
|
|
# TODO FEAT Make the directory structure the same as the base one and
|
|
|
|
|
# remove IGNORED_EMPTY_FOLDER variable
|
|
|
|
|
|
2018-06-24 18:28:37 +02:00
|
|
|
|
# Listing files
|
2018-08-04 12:43:13 +02:00
|
|
|
|
log.info("Listing files")
|
2018-06-24 18:28:37 +02:00
|
|
|
|
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()
|
2018-08-04 12:43:13 +02:00
|
|
|
|
extraFiles = set(outputFiles.keys())
|
2018-06-24 18:28:37 +02:00
|
|
|
|
|
2018-07-10 14:50:07 +02:00
|
|
|
|
|
2019-10-17 12:44:30 +02:00
|
|
|
|
def convertPath(path: str) -> typing.Optional[str]:
|
2018-06-24 18:28:37 +02:00
|
|
|
|
filename, extension = os.path.splitext(path)
|
|
|
|
|
extension = extension[1:].lower()
|
2019-10-17 12:44:30 +02:00
|
|
|
|
# Remove unwanted characters from filename
|
|
|
|
|
filename_parts = os.path.normpath(filename).split(os.path.sep)
|
2021-06-13 11:49:21 +02:00
|
|
|
|
filename_parts = [re.sub(RESTRICT_CHARACTERS, "_", part) for part in filename_parts]
|
2019-10-17 12:44:30 +02:00
|
|
|
|
filename = os.path.sep.join(filename_parts)
|
2018-06-24 18:28:37 +02:00
|
|
|
|
# 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
|
2019-10-17 12:44:30 +02:00
|
|
|
|
return None
|
2018-06-24 18:28:37 +02:00
|
|
|
|
# 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
|
|
|
|
|
|
2018-07-10 14:50:07 +02:00
|
|
|
|
|
2019-10-17 12:44:30 +02:00
|
|
|
|
log.info("Determining action over %d files", len(sourceFiles))
|
2018-06-24 18:28:37 +02:00
|
|
|
|
for sourceFile in sourceFiles:
|
|
|
|
|
outputFile = convertPath(sourceFile)
|
|
|
|
|
# If the file should not be converted, do nothing
|
2019-10-17 12:44:30 +02:00
|
|
|
|
if not outputFile:
|
2018-06-24 18:28:37 +02:00
|
|
|
|
continue
|
|
|
|
|
# If the file already has something as an output
|
|
|
|
|
elif outputFile in outputFiles:
|
|
|
|
|
extraFiles.remove(outputFile)
|
2018-07-10 14:50:07 +02:00
|
|
|
|
# If the output file is newer than the source file, do not initiate a
|
|
|
|
|
# conversion
|
2018-06-24 18:28:37 +02:00
|
|
|
|
if outputFiles[outputFile] >= sourceFiles[sourceFile]:
|
|
|
|
|
continue
|
|
|
|
|
# If the file needs to be converted, do it
|
|
|
|
|
remainingConversions[sourceFile] = outputFile
|
|
|
|
|
|
2019-10-17 12:44:30 +02:00
|
|
|
|
log.debug("%d actions will need to be taken", len(remainingConversions))
|
2018-08-04 12:43:13 +02:00
|
|
|
|
log.info("Copying files that do not require a conversion")
|
|
|
|
|
conversions = set()
|
2018-06-24 18:28:37 +02:00
|
|
|
|
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)
|
2020-08-08 11:19:48 +02:00
|
|
|
|
if act:
|
|
|
|
|
os.makedirs(fullOutputDir, exist_ok=True)
|
2018-06-24 18:28:37 +02:00
|
|
|
|
|
|
|
|
|
# Converting
|
|
|
|
|
fullSourceFile = os.path.join(SOURCE_FOLDER, sourceFile)
|
|
|
|
|
if sourceFile == outputFile:
|
2021-06-13 11:49:21 +02:00
|
|
|
|
log.debug("%s → %s", fullSourceFile, fullOutputFile)
|
2020-08-08 11:19:48 +02:00
|
|
|
|
if act and os.path.isfile(fullOutputFile):
|
2018-07-10 14:50:07 +02:00
|
|
|
|
os.remove(fullOutputFile)
|
2018-06-24 18:28:37 +02:00
|
|
|
|
os.link(fullSourceFile, fullOutputFile)
|
|
|
|
|
else:
|
2018-08-04 12:43:13 +02:00
|
|
|
|
conversions.add((fullSourceFile, fullOutputFile))
|
2018-06-24 18:28:37 +02:00
|
|
|
|
|
2018-08-04 12:43:13 +02:00
|
|
|
|
log.info("Removing extra files")
|
2018-06-24 18:28:37 +02:00
|
|
|
|
for extraFile in extraFiles:
|
|
|
|
|
fullExtraFile = os.path.join(OUTPUT_FOLDER, extraFile)
|
2021-06-13 11:49:21 +02:00
|
|
|
|
log.debug("× %s", fullExtraFile)
|
2020-08-08 11:19:48 +02:00
|
|
|
|
if act:
|
|
|
|
|
os.remove(fullExtraFile)
|
2018-06-24 18:28:37 +02:00
|
|
|
|
|
2018-08-04 12:43:13 +02:00
|
|
|
|
log.info("Listing files that will be converted")
|
|
|
|
|
for fullSourceFile, fullOutputFile in conversions:
|
2021-06-13 11:49:21 +02:00
|
|
|
|
log.debug("%s ⇒ %s", fullSourceFile, fullOutputFile)
|
2018-08-04 12:43:13 +02:00
|
|
|
|
|
|
|
|
|
log.info("Converting files")
|
|
|
|
|
for fullSourceFile, fullOutputFile in progressbar.progressbar(conversions):
|
2021-06-13 11:49:21 +02:00
|
|
|
|
cmd = [
|
|
|
|
|
"ffmpeg",
|
|
|
|
|
"-y",
|
|
|
|
|
"-i",
|
|
|
|
|
fullSourceFile,
|
|
|
|
|
"-c:a",
|
|
|
|
|
"libopus",
|
|
|
|
|
"-movflags",
|
|
|
|
|
"+faststart",
|
|
|
|
|
"-b:a",
|
|
|
|
|
"128k",
|
|
|
|
|
"-vbr",
|
|
|
|
|
"on",
|
|
|
|
|
"-compression_level",
|
|
|
|
|
"10",
|
|
|
|
|
fullOutputFile,
|
|
|
|
|
]
|
2020-08-08 11:19:48 +02:00
|
|
|
|
if act:
|
|
|
|
|
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
|
else:
|
|
|
|
|
print(cmd)
|
2018-08-04 12:43:13 +02:00
|
|
|
|
|
2018-06-24 18:28:37 +02:00
|
|
|
|
# Removing empty dirs
|
|
|
|
|
for root, dirs, files in os.walk(OUTPUT_FOLDER):
|
|
|
|
|
if not dirs and not files:
|
|
|
|
|
dirBasename = os.path.basename(root)
|
2020-08-08 11:19:48 +02:00
|
|
|
|
if act and dirBasename not in IGNORED_EMPTY_FOLDER:
|
2018-06-24 18:28:37 +02:00
|
|
|
|
os.rmdir(root)
|