From 08248719a11f918ca1a75190591242bab18d07ec Mon Sep 17 00:00:00 2001 From: ChaotiCryptidz Date: Fri, 4 Feb 2022 16:19:11 +0000 Subject: [PATCH] work more on transcoder --- formatter.sh | 4 ++ musicutil/__main__.py | 22 ++++++ musicutil/commands/copy_command.py | 44 ++++-------- musicutil/commands/transcode_command.py | 32 +++++++++ musicutil/meta.py | 4 ++ musicutil/transcode_levels.py | 46 +++++++++++++ musicutil/utils/scan_for_music.py | 16 +++-- musicutil/utils/substitutions.py | 1 - musicutil/utils/transcoder.py | 90 +++++++++++++++++++++++++ shell.nix | 2 +- 10 files changed, 222 insertions(+), 39 deletions(-) create mode 100755 formatter.sh create mode 100644 musicutil/commands/transcode_command.py create mode 100644 musicutil/transcode_levels.py create mode 100644 musicutil/utils/transcoder.py diff --git a/formatter.sh b/formatter.sh new file mode 100755 index 0000000..fea8b00 --- /dev/null +++ b/formatter.sh @@ -0,0 +1,4 @@ +#!/run/current-system/sw/bin/env nix-shell +#!nix-shell -i bash + +autopep8 $(fd -e py) --in-place \ No newline at end of file diff --git a/musicutil/__main__.py b/musicutil/__main__.py index 074fb95..f3768f0 100644 --- a/musicutil/__main__.py +++ b/musicutil/__main__.py @@ -4,6 +4,7 @@ import argparse from .commands.process_command import ProcessCommand from .commands.copy_command import CopyCommand +from .commands.transcode_command import TranscodeCommand parser = argparse.ArgumentParser( description="chaos's musicutil") @@ -38,6 +39,21 @@ copy_parser.add_argument( '--single-directory', action='store_true') +transcode_parser = subparsers.add_parser('transcode') +transcode_parser.add_argument( + 'src', + type=str, + help='src base music directory') +transcode_parser.add_argument( + 'dest', + type=str, + help='dest music directory') +transcode_parser.add_argument( + '--transcode-level', + type=str, + help='transcode level', + default="opus-96k") + args = parser.parse_args() if args.subparser_name == "process": @@ -50,3 +66,9 @@ elif args.subparser_name == "copy": args.single_directory, args.skip_existing ).run() +elif args.subparser_name == "transcode": + TranscodeCommand( + args.src, + args.dest, + args.transcode_level, + ).run() \ No newline at end of file diff --git a/musicutil/commands/copy_command.py b/musicutil/commands/copy_command.py index 5751d17..b9eb9a3 100644 --- a/musicutil/commands/copy_command.py +++ b/musicutil/commands/copy_command.py @@ -1,19 +1,19 @@ from ..types import File from ..utils.scan_for_music import scan_for_music from ..utils.load_tag_information import load_tag_information +from ..transcode_levels import transcode_levels +from ..utils.transcoder import transcode, get_transcode_config from os import makedirs as make_directories from os.path import exists as path_exists from shutil import copy as copy_file from copy import deepcopy as deep_copy -from subprocess import run as run_command class CopyCommandState: files: list[File] = [] transcoded_files: list[File] = [] - class CopyCommand(): def __init__(self, src: str, @@ -36,6 +36,8 @@ class CopyCommand(): if self.single_directory: self.check_for_collisions() self.transcode_files() + if self.single_directory: + self.create_mappings() def scan_for_music(self): print("Scanning For Music") @@ -89,12 +91,12 @@ class CopyCommand(): self.state.transcoded_files.append(file) def _transcode_with_level(self, file: File, level: str): - transcoded_file_extension = "opus" + trans_config = get_transcode_config(file, level) src = file.join_path_to() new_file = deep_copy(file) - new_file.extension = transcoded_file_extension + new_file.extension = trans_config.file_extension dest_filepath = new_file.join_filename( ) if self.single_directory else new_file.join_path_from_src() @@ -105,33 +107,9 @@ class CopyCommand(): self.state.transcoded_files.append(new_file) return - bitrate = "" - - if self.transcode_level == "high": - bitrate = "128K" - elif self.transcode_level == "medium": - bitrate = "96K" - elif self.transcode_level == "low": - bitrate = "64K" - print("Transcoding", src, "to", dest_filepath) - title = file.tags.title - artist = file.tags.artist - - # TODO: check for errors - run_command([ - "ffmpeg", - "-y", - "-hide_banner", - "-loglevel", "warning", - "-i", src, - "-c:a", "libopus", - "-b:a", bitrate, - "-metadata", f"title=\"{title}\"", - "-metadata", f"artist=\"{artist}\"", - dest_filepath - ]) + transcode(file, trans_config, level, dest_filepath) self.state.transcoded_files.append(new_file) @@ -150,7 +128,13 @@ class CopyCommand(): for file in self.state.files: self._transcode_copy(file) return - elif self.transcode_level in ["high", "medium", "low"]: + elif self.transcode_level in transcode_levels: for file in self.state.files: self._transcode_with_level( file, self.transcode_level) + + def create_mappings(self): + with open(self.dest + "/" + "mappings.txt", "w") as f: + f.write("\n".join([ + f"{file.path_from_src} <- {file.filename}" for file in self.state.files + ])) diff --git a/musicutil/commands/transcode_command.py b/musicutil/commands/transcode_command.py new file mode 100644 index 0000000..9c37ef4 --- /dev/null +++ b/musicutil/commands/transcode_command.py @@ -0,0 +1,32 @@ +from ..utils.transcoder import get_transcode_config, transcode +from ..utils.scan_for_music import file_from_path +from ..transcode_levels import transcode_levels +from pathlib import Path + +class TranscodeCommand: + def __init__(self, src: str, dest: str, transcode_level: str): + self.src = src + self.dest = dest + self.transcode_level = transcode_level + + def run(self): + if self.transcode_level == "list": + print("Transcode Levels:", ", ".join(transcode_levels)) + exit() + + print("Transcoding...") + input_file = file_from_path(Path(self.src), "") + + trans_config = get_transcode_config(input_file, self.transcode_level) + + output_file = file_from_path(Path(self.dest), "") + + if trans_config.file_extension != output_file.extension: + print( + f"{output_file.extension} is not the recommended "+ + f"extension for transcode_level {self.transcode_level} "+ + f"please change it to {trans_config.file_extension} "+ + f"or TODO(add --ignore-extension)" + ) + exit() + transcode(input_file, trans_config, self.transcode_level, self.dest) \ No newline at end of file diff --git a/musicutil/meta.py b/musicutil/meta.py index cc3e67b..e9fe41e 100644 --- a/musicutil/meta.py +++ b/musicutil/meta.py @@ -7,3 +7,7 @@ sub_char = "_" substitutions = { "α": "a", } + +# Patch to whatever path ffmpeg is at +ffmpeg_path = "ffmpeg" +ffprobe_path = "ffprobe" \ No newline at end of file diff --git a/musicutil/transcode_levels.py b/musicutil/transcode_levels.py new file mode 100644 index 0000000..4bbd419 --- /dev/null +++ b/musicutil/transcode_levels.py @@ -0,0 +1,46 @@ +def add_to_arr(arr: list[str], items: list[str]) -> list[str]: + for item in items: + arr.append(item) + +# does not include copy +transcode_levels = [ + "speex" +] + +# mp3 v0 -> v9 +add_to_arr(transcode_levels, [ + f"mp3-v{quality}" for quality in range(0, 10) +]) +# mp3 bitrates +mp3_bitrates = [8, 16, 24, 32, 40, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] +add_to_arr(transcode_levels, [ + f"mp3-{bitrate}k" for bitrate in mp3_bitrates +]) + +opus_bitrates = ["16", "24", "32", "64", "96", "128", "256"] +add_to_arr(transcode_levels, [ + f"opus-{bitrate}k" for bitrate in opus_bitrates +]) + +add_to_arr(transcode_levels, [ + f"vorbis-q{quality}" for quality in range(-2, 11) +]) + +# Extra Default Mappings +preset_transcode_levels = { + "mp3-low": "mp3-v4", + "mp3-medium": "mp3-v2", + "mp3-high": "mp3-v0", + "opus-low": "opus-32k", + "opus-medium": "opus-64k", + "opus-high": "opus-96k", + "opus-higher": "opus-128k", + "opus-extreme": "opus-256k" +} + +add_to_arr(transcode_levels, preset_transcode_levels.keys()) + +#transcode_levels.sort() +#import json +#print(json.dumps(transcode_levels,indent=2)) +#exit() \ No newline at end of file diff --git a/musicutil/utils/scan_for_music.py b/musicutil/utils/scan_for_music.py index 8a91784..c2546ed 100644 --- a/musicutil/utils/scan_for_music.py +++ b/musicutil/utils/scan_for_music.py @@ -4,16 +4,18 @@ from os.path import relpath from ..types import File from ..meta import supported_formats +def file_from_path(path: Path, src: str) -> File: + file = File() + file.path_to = str(path.parent) + file.path_from_src = relpath( + str(path.parent), src) + file.filename = path.stem + file.extension = path.suffix.replace(".", "") + return file def scan_for_music(src: str) -> list[File]: files: list[File] = [] for format in supported_formats: for path in Path(src).rglob("*." + format): - file = File() - file.path_to = str(path.parent) - file.path_from_src = relpath( - str(path.parent), src) - file.filename = path.stem - file.extension = path.suffix.replace(".", "") - files.append(file) + files.append(file_from_path(path, src)) return files diff --git a/musicutil/utils/substitutions.py b/musicutil/utils/substitutions.py index db2a613..1430924 100644 --- a/musicutil/utils/substitutions.py +++ b/musicutil/utils/substitutions.py @@ -1,7 +1,6 @@ from ..meta import sub_char, substitutions from fold_to_ascii import fold - def reduce_to_ascii_and_substitute(filename: str): filename = filename.replace("/", sub_char) filename = filename.replace("\\", sub_char) diff --git a/musicutil/utils/transcoder.py b/musicutil/utils/transcoder.py new file mode 100644 index 0000000..d54677a --- /dev/null +++ b/musicutil/utils/transcoder.py @@ -0,0 +1,90 @@ +from ..transcode_levels import transcode_levels, preset_transcode_levels +from ..types import File +from ..meta import ffmpeg_path + +from subprocess import run as run_command + +class TranscodeConfig: + use_quality = False + use_bitrate = False + encoder = "" + file_extension = "" + bitrate = "" + quality = "" + +def get_transcode_config(file: File, level: str): + conf = TranscodeConfig() + if level in preset_transcode_levels.keys(): + level = preset_transcode_levels["level"] + + if level.startswith("opus-") and level.endswith("k"): + conf.file_extension = "opus" + conf.encoder = "libopus" + conf.use_bitrate = True + # includes the k at end + conf.bitrate = level.replace("opus-", "") + return conf + + if level.startswith("mp3-v"): + conf.file_extension = "mp3" + conf.encoder = "libmp3lame" + conf.use_quality = True + conf.quality = level.replace("mp3-v", "") + return conf + elif level.startswith("mp3-") and level.endswith("k"): + conf.file_extension = "mp3" + conf.encoder = "libmp3lame" + conf.use_bitrate = True + # includes the k + conf.bitrate = level.replace("mp3-", "") + return conf + + if level.startswith("vorbis-q"): + conf.file_extension = "ogg" + conf.encoder = "libvorbis" + conf.use_quality = True + conf.quality = level.replace("vorbis-q", "") + return conf + + if level == "speex": + conf.encoder = "libspeex" + conf.file_extension = "ogg" + return conf + + print("Unknown Level") + exit() + +def transcode(file: File, config: TranscodeConfig, level: str, dest: str): + title = file.tags.title + artist = file.tags.artist + + ffmpeg_command = [ + ffmpeg_path, + "-y", + "-hide_banner", + "-loglevel", "warning", + "-i", file.join_path_to(), + ] + + ffmpeg_command.append("-c:a") + ffmpeg_command.append(config.encoder) + + if config.use_quality: + ffmpeg_command.append("-q:a") + ffmpeg_command.append(config.quality) + elif config.use_bitrate: + ffmpeg_command.append("-b:a") + ffmpeg_command.append(config.bitrate) + else: + pass + + # Add Metadata + ffmpeg_command.append("-metadata") + ffmpeg_command.append(f"title=\"{title}\"") + ffmpeg_command.append("-metadata") + ffmpeg_command.append(f"artist=\"{artist}\"") + + ffmpeg_command.append(dest) + + # TODO: check for errors + run_command(ffmpeg_command) diff --git a/shell.nix b/shell.nix index 049056a..1ea5fba 100644 --- a/shell.nix +++ b/shell.nix @@ -4,4 +4,4 @@ let my_python = pkgs.python39.withPackages (py: with py; [ py.mutagen (fold-to-ascii py) py.autopep8 ]); -in pkgs.mkShell { packages = with pkgs; [ my_python ffmpeg ]; } +in pkgs.mkShell { packages = with pkgs; [ my_python ffmpeg fd ]; }