work more on transcoder
This commit is contained in:
parent
1c57673dc4
commit
08248719a1
4
formatter.sh
Executable file
4
formatter.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/run/current-system/sw/bin/env nix-shell
|
||||||
|
#!nix-shell -i bash
|
||||||
|
|
||||||
|
autopep8 $(fd -e py) --in-place
|
|
@ -4,6 +4,7 @@ import argparse
|
||||||
|
|
||||||
from .commands.process_command import ProcessCommand
|
from .commands.process_command import ProcessCommand
|
||||||
from .commands.copy_command import CopyCommand
|
from .commands.copy_command import CopyCommand
|
||||||
|
from .commands.transcode_command import TranscodeCommand
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="chaos's musicutil")
|
description="chaos's musicutil")
|
||||||
|
@ -38,6 +39,21 @@ copy_parser.add_argument(
|
||||||
'--single-directory',
|
'--single-directory',
|
||||||
action='store_true')
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.subparser_name == "process":
|
if args.subparser_name == "process":
|
||||||
|
@ -50,3 +66,9 @@ elif args.subparser_name == "copy":
|
||||||
args.single_directory,
|
args.single_directory,
|
||||||
args.skip_existing
|
args.skip_existing
|
||||||
).run()
|
).run()
|
||||||
|
elif args.subparser_name == "transcode":
|
||||||
|
TranscodeCommand(
|
||||||
|
args.src,
|
||||||
|
args.dest,
|
||||||
|
args.transcode_level,
|
||||||
|
).run()
|
|
@ -1,19 +1,19 @@
|
||||||
from ..types import File
|
from ..types import File
|
||||||
from ..utils.scan_for_music import scan_for_music
|
from ..utils.scan_for_music import scan_for_music
|
||||||
from ..utils.load_tag_information import load_tag_information
|
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 import makedirs as make_directories
|
||||||
from os.path import exists as path_exists
|
from os.path import exists as path_exists
|
||||||
from shutil import copy as copy_file
|
from shutil import copy as copy_file
|
||||||
from copy import deepcopy as deep_copy
|
from copy import deepcopy as deep_copy
|
||||||
from subprocess import run as run_command
|
|
||||||
|
|
||||||
|
|
||||||
class CopyCommandState:
|
class CopyCommandState:
|
||||||
files: list[File] = []
|
files: list[File] = []
|
||||||
transcoded_files: list[File] = []
|
transcoded_files: list[File] = []
|
||||||
|
|
||||||
|
|
||||||
class CopyCommand():
|
class CopyCommand():
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
src: str,
|
src: str,
|
||||||
|
@ -36,6 +36,8 @@ class CopyCommand():
|
||||||
if self.single_directory:
|
if self.single_directory:
|
||||||
self.check_for_collisions()
|
self.check_for_collisions()
|
||||||
self.transcode_files()
|
self.transcode_files()
|
||||||
|
if self.single_directory:
|
||||||
|
self.create_mappings()
|
||||||
|
|
||||||
def scan_for_music(self):
|
def scan_for_music(self):
|
||||||
print("Scanning For Music")
|
print("Scanning For Music")
|
||||||
|
@ -89,12 +91,12 @@ class CopyCommand():
|
||||||
self.state.transcoded_files.append(file)
|
self.state.transcoded_files.append(file)
|
||||||
|
|
||||||
def _transcode_with_level(self, file: File, level: str):
|
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()
|
src = file.join_path_to()
|
||||||
|
|
||||||
new_file = deep_copy(file)
|
new_file = deep_copy(file)
|
||||||
new_file.extension = transcoded_file_extension
|
new_file.extension = trans_config.file_extension
|
||||||
|
|
||||||
dest_filepath = new_file.join_filename(
|
dest_filepath = new_file.join_filename(
|
||||||
) if self.single_directory else new_file.join_path_from_src()
|
) if self.single_directory else new_file.join_path_from_src()
|
||||||
|
@ -105,33 +107,9 @@ class CopyCommand():
|
||||||
self.state.transcoded_files.append(new_file)
|
self.state.transcoded_files.append(new_file)
|
||||||
return
|
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)
|
print("Transcoding", src, "to", dest_filepath)
|
||||||
|
|
||||||
title = file.tags.title
|
transcode(file, trans_config, level, dest_filepath)
|
||||||
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
|
|
||||||
])
|
|
||||||
|
|
||||||
self.state.transcoded_files.append(new_file)
|
self.state.transcoded_files.append(new_file)
|
||||||
|
|
||||||
|
@ -150,7 +128,13 @@ class CopyCommand():
|
||||||
for file in self.state.files:
|
for file in self.state.files:
|
||||||
self._transcode_copy(file)
|
self._transcode_copy(file)
|
||||||
return
|
return
|
||||||
elif self.transcode_level in ["high", "medium", "low"]:
|
elif self.transcode_level in transcode_levels:
|
||||||
for file in self.state.files:
|
for file in self.state.files:
|
||||||
self._transcode_with_level(
|
self._transcode_with_level(
|
||||||
file, self.transcode_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
|
||||||
|
]))
|
||||||
|
|
32
musicutil/commands/transcode_command.py
Normal file
32
musicutil/commands/transcode_command.py
Normal file
|
@ -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)
|
|
@ -7,3 +7,7 @@ sub_char = "_"
|
||||||
substitutions = {
|
substitutions = {
|
||||||
"α": "a",
|
"α": "a",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Patch to whatever path ffmpeg is at
|
||||||
|
ffmpeg_path = "ffmpeg"
|
||||||
|
ffprobe_path = "ffprobe"
|
46
musicutil/transcode_levels.py
Normal file
46
musicutil/transcode_levels.py
Normal file
|
@ -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()
|
|
@ -4,16 +4,18 @@ from os.path import relpath
|
||||||
from ..types import File
|
from ..types import File
|
||||||
from ..meta import supported_formats
|
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]:
|
def scan_for_music(src: str) -> list[File]:
|
||||||
files: list[File] = []
|
files: list[File] = []
|
||||||
for format in supported_formats:
|
for format in supported_formats:
|
||||||
for path in Path(src).rglob("*." + format):
|
for path in Path(src).rglob("*." + format):
|
||||||
file = File()
|
files.append(file_from_path(path, src))
|
||||||
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)
|
|
||||||
return files
|
return files
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from ..meta import sub_char, substitutions
|
from ..meta import sub_char, substitutions
|
||||||
from fold_to_ascii import fold
|
from fold_to_ascii import fold
|
||||||
|
|
||||||
|
|
||||||
def reduce_to_ascii_and_substitute(filename: str):
|
def reduce_to_ascii_and_substitute(filename: str):
|
||||||
filename = filename.replace("/", sub_char)
|
filename = filename.replace("/", sub_char)
|
||||||
filename = filename.replace("\\", sub_char)
|
filename = filename.replace("\\", sub_char)
|
||||||
|
|
90
musicutil/utils/transcoder.py
Normal file
90
musicutil/utils/transcoder.py
Normal file
|
@ -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)
|
|
@ -4,4 +4,4 @@ let
|
||||||
my_python = pkgs.python39.withPackages
|
my_python = pkgs.python39.withPackages
|
||||||
(py: with py; [ py.mutagen (fold-to-ascii py) py.autopep8 ]);
|
(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 ]; }
|
||||||
|
|
Loading…
Reference in a new issue