Use Converter in .convert function in Track
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from tempfile import gettempdir
|
||||
from typing import Optional
|
||||
|
||||
from .exceptions import ConversionError
|
||||
from .exceptions import BadEncoderOption, ConversionError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,6 +15,8 @@ class Converter:
|
||||
codec_lib = None
|
||||
container = None
|
||||
lossless = False
|
||||
cbr_options = None
|
||||
vbr_options = None
|
||||
default_ffmpeg_arg = ""
|
||||
|
||||
def __init__(
|
||||
@@ -25,7 +25,8 @@ class Converter:
|
||||
ffmpeg_arg: Optional[str] = None,
|
||||
sampling_rate: Optional[int] = None,
|
||||
bit_depth: Optional[int] = None,
|
||||
copy_art: bool = False,
|
||||
copy_art: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
:param ffmpeg_arg: The codec ffmpeg argument (defaults to an "optimal value")
|
||||
@@ -41,11 +42,12 @@ class Converter:
|
||||
|
||||
self.filename = filename
|
||||
self.final_fn = f"{os.path.splitext(filename)[0]}.{self.container}"
|
||||
self.tempfile = os.path.join(gettempdir(), os.path.basename(self.final_fn))
|
||||
self.sampling_rate = sampling_rate
|
||||
self.bit_depth = bit_depth
|
||||
self.copy_art = copy_art
|
||||
|
||||
self.overwrite = kwargs.get("overwrite")
|
||||
|
||||
if ffmpeg_arg is None:
|
||||
logger.debug("No arguments provided. Codec defaults will be used")
|
||||
self.ffmpeg_arg = self.default_ffmpeg_arg
|
||||
@@ -55,7 +57,7 @@ class Converter:
|
||||
|
||||
logger.debug("Ffmpeg codec extra argument: %s", self.ffmpeg_arg)
|
||||
|
||||
def convert(self, custom_fn: Optional[str] = None, remove_source: bool = True):
|
||||
def convert(self, custom_fn: Optional[str] = None, remove_source: bool = False):
|
||||
"""Convert the file.
|
||||
|
||||
:param custom_fn: Custom output filename (defaults to the original
|
||||
@@ -74,12 +76,10 @@ class Converter:
|
||||
process.wait()
|
||||
if os.path.isfile(self.final_fn):
|
||||
if remove_source:
|
||||
os.remove(self.filename)
|
||||
logger.debug("Source removed: %s", self.filename)
|
||||
os.remove(self.filename)
|
||||
|
||||
shutil.move(self.tempfile, self.final_fn)
|
||||
logger.debug("Moved: %s -> %s", self.tempfile, self.final_fn)
|
||||
logger.debug("Converted: %s -> %s", self.filename, self.final_fn)
|
||||
logger.debug("OK: %s -> %s", self.filename, self.final_fn)
|
||||
else:
|
||||
raise ConversionError("No file was returned from conversion")
|
||||
|
||||
@@ -101,23 +101,21 @@ class Converter:
|
||||
command.extend(self.ffmpeg_arg.split())
|
||||
|
||||
if self.lossless:
|
||||
if isinstance(self.sampling_rate, int):
|
||||
if self.sampling_rate:
|
||||
command.extend(["-ar", str(self.sampling_rate)])
|
||||
if self.bit_depth == 16:
|
||||
command.extend(["-sample_fmt", "s16"])
|
||||
elif self.bit_depth in (24, 32):
|
||||
command.extend(["-sample_fmt", "s32"])
|
||||
|
||||
if isinstance(self.bit_depth, int):
|
||||
if int(self.bit_depth) == 16:
|
||||
command.extend(["-sample_fmt", "s16"])
|
||||
elif int(self.bit_depth) in (24, 32):
|
||||
command.extend(["-sample_fmt", "s32"])
|
||||
else:
|
||||
raise ValueError("Bit depth must be 16, 24, or 32")
|
||||
if self.overwrite:
|
||||
command.append("-y")
|
||||
|
||||
command.extend(["-y", self.tempfile])
|
||||
command.append(self.final_fn)
|
||||
|
||||
return command
|
||||
|
||||
def _is_command_valid(self):
|
||||
# TODO: add error handling for lossy codecs
|
||||
if self.ffmpeg_arg is not None and self.lossless:
|
||||
logger.debug(
|
||||
"Lossless codecs don't support extra arguments; "
|
||||
@@ -126,18 +124,51 @@ class Converter:
|
||||
self.ffmpeg_arg = self.default_ffmpeg_arg
|
||||
return
|
||||
|
||||
arg_value = self.ffmpeg_arg.split()[-1].strip().replace("k", "")
|
||||
|
||||
try:
|
||||
arg_value = int(self.ffmpeg_arg.split()[-1].strip())
|
||||
except ValueError:
|
||||
raise BadEncoderOption(f"Invalid bitrate argument: {self.ffmpeg_arg}")
|
||||
|
||||
logger.debug("Arg value provided: %d", arg_value)
|
||||
options = []
|
||||
|
||||
if self.ffmpeg_arg.startswith("-b:a"):
|
||||
if self.cbr_options is None:
|
||||
raise BadEncoderOption("This codec doesn't support constant bitrate")
|
||||
|
||||
options = self.cbr_options
|
||||
|
||||
if self.ffmpeg_arg.startswith("-q:a"):
|
||||
if self.vbr_options is None:
|
||||
raise BadEncoderOption("This codec doesn't support variable bitrate")
|
||||
|
||||
options = self.vbr_options
|
||||
|
||||
if arg_value not in options:
|
||||
raise BadEncoderOption(
|
||||
f"VBR value is not in the codec range: {', '.join(options)}"
|
||||
)
|
||||
|
||||
@property
|
||||
def ext(self):
|
||||
return self.filename.split('.')[-1]
|
||||
|
||||
|
||||
class LAME(Converter):
|
||||
"""
|
||||
Class for libmp3lame converter. Defaul ffmpeg_arg: `-q:a 0`.
|
||||
|
||||
See available options:
|
||||
Class for libmp3lame converter. See available options:
|
||||
https://trac.ffmpeg.org/wiki/Encode/MP3
|
||||
"""
|
||||
|
||||
codec_name = "lame"
|
||||
codec_lib = "libmp3lame"
|
||||
container = "mp3"
|
||||
lossless = False
|
||||
# Blatantly assume nobody will ever convert CBR at less than 96
|
||||
cbr_options = tuple(range(96, 321, 16))
|
||||
vbr_options = tuple(range(10))
|
||||
default_ffmpeg_arg = "-q:a 0" # V0
|
||||
|
||||
|
||||
@@ -149,23 +180,31 @@ class ALAC(Converter):
|
||||
lossless = True
|
||||
|
||||
|
||||
class Vorbis(Converter):
|
||||
"""
|
||||
Class for libvorbis converter. Default ffmpeg_arg: `-q:a 6`.
|
||||
class FLAC(Converter):
|
||||
codec_name = "flac"
|
||||
codec_lib = "flac"
|
||||
container = "flac"
|
||||
lossless = True
|
||||
|
||||
See available options:
|
||||
|
||||
class VORBIS(Converter):
|
||||
"""
|
||||
Class for libvorbis converter. See available options:
|
||||
https://trac.ffmpeg.org/wiki/TheoraVorbisEncodingGuide
|
||||
"""
|
||||
|
||||
codec_name = "vorbis"
|
||||
codec_lib = "libvorbis"
|
||||
container = "ogg"
|
||||
lossless = False
|
||||
vbr_options = tuple(range(-1, 11))
|
||||
default_ffmpeg_arg = "-q:a 6" # 160, aka the "high" quality profile from Spotify
|
||||
|
||||
|
||||
class OPUS(Converter):
|
||||
"""
|
||||
Class for libopus. Default ffmpeg_arg: `-b:a 128 -vbr on`.
|
||||
Class for libopus. Currently, this codec takes only `-b:a` as an argument
|
||||
but, unlike other codecs, it will convert to a variable bitrate.
|
||||
|
||||
See more:
|
||||
http://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
||||
@@ -174,18 +213,21 @@ class OPUS(Converter):
|
||||
codec_name = "opus"
|
||||
codec_lib = "libopus"
|
||||
container = "opus"
|
||||
lossless = False
|
||||
cbr_options = tuple(range(16, 513, 16))
|
||||
default_ffmpeg_arg = "-b:a 128k" # Transparent
|
||||
|
||||
|
||||
class AAC(Converter):
|
||||
"""
|
||||
Class for libfdk_aac converter. Default ffmpeg_arg: `-b:a 256k`.
|
||||
|
||||
See available options:
|
||||
Class for libfdk_aac converter. See available options:
|
||||
https://trac.ffmpeg.org/wiki/Encode/AAC
|
||||
"""
|
||||
|
||||
codec_name = "aac"
|
||||
codec_lib = "libfdk_aac"
|
||||
container = "m4a"
|
||||
lossless = False
|
||||
cbr_options = tuple(range(16, 513, 16))
|
||||
# TODO: vbr_options
|
||||
default_ffmpeg_arg = "-b:a 256k"
|
||||
|
||||
@@ -15,6 +15,7 @@ from .constants import EXT, FLAC_MAX_BLOCKSIZE
|
||||
from .exceptions import InvalidQuality, NonStreamable, TooLargeCoverArt
|
||||
from .metadata import TrackMetadata
|
||||
from .util import quality_id, safe_get, tqdm_download
|
||||
from . import converter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -222,50 +223,16 @@ class Track:
|
||||
def convert(self, codec='ALAC', **kwargs):
|
||||
assert self.__is_downloaded, 'track must be downloaded before conversion'
|
||||
|
||||
codec = codec.upper()
|
||||
sampling_rate = kwargs.get("sampling_rate")
|
||||
ext = EXT[self.quality]
|
||||
conversion_command = [
|
||||
"ffmpeg",
|
||||
"-i",
|
||||
self.final_path,
|
||||
"-c:v", # copy cover art
|
||||
"copy",
|
||||
"-loglevel", # no annoying logs
|
||||
"warning",
|
||||
]
|
||||
CONV_CLASS = {
|
||||
'ALAC': converter.ALAC,
|
||||
'MP3': converter.LAME,
|
||||
'FLAC': converter.FLAC,
|
||||
'OGG': converter.VORBIS,
|
||||
'AAC': converter.AAC,
|
||||
}
|
||||
|
||||
if codec == 'ALAC':
|
||||
new_ext = '.m4a'
|
||||
conversion_command.extend(["-c:a", "alac"])
|
||||
elif codec == 'MP3':
|
||||
new_ext = '.mp3'
|
||||
# TODO: add further customization of mp3 quality
|
||||
# https://trac.ffmpeg.org/wiki/Encode/MP3
|
||||
# VBR from 22-26KB/s
|
||||
conversion_command.extend(["-c:a", "libmp3lame", "-q:a", "0"])
|
||||
elif codec == 'AAC':
|
||||
new_ext = '.m4a'
|
||||
# highest quality VBR
|
||||
conversion_command.extend(["-c:a", "libfdk_aac", "-vbr", "5"])
|
||||
elif codec == 'FLAC':
|
||||
new_ext = '.flac'
|
||||
else:
|
||||
raise NotImplementedError(f"Codec {codec} not implemented")
|
||||
|
||||
if sampling_rate is not None and codec not in ("aac", "mp3"):
|
||||
conversion_command.extend(["-ar", str(sampling_rate)])
|
||||
|
||||
# tempfile needs to use new_ext so that ffmpeg detects the container
|
||||
tempfile = os.path.join(gettempdir(), f"~qdl_track_conv{new_ext}")
|
||||
conversion_command.extend(["-y", tempfile])
|
||||
logger.debug(f"coverting with command {conversion_command}")
|
||||
|
||||
process = subprocess.Popen(conversion_command)
|
||||
process.wait()
|
||||
out_path = self.final_path.replace(ext, new_ext)
|
||||
shutil.move(tempfile, out_path)
|
||||
self.final_path = out_path
|
||||
engine = CONV_CLASS[codec.upper()](filename=self.final_path, sampling_rate=kwargs.get("sampling_rate"), overwrite=True)
|
||||
engine.convert(remove_source=kwargs.get("remove_source", False))
|
||||
|
||||
def get(self, *keys, default=None):
|
||||
"""Safe get method that allows for layered access.
|
||||
|
||||
Reference in New Issue
Block a user