dotfiles/config/scripts/updateCompressedMusic

134 lines
4.7 KiB
Plaintext
Raw Normal View History

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