From 5adb3d0a4d19f3c6c49f2a21a56e0cb8e939e024 Mon Sep 17 00:00:00 2001 From: Ritiek Malhotra Date: Mon, 16 Mar 2020 18:12:52 +0530 Subject: [PATCH] Refactor encoding --- .gitignore | 3 + setup.py | 9 +- spotdl/{handle.py => command_line.py} | 0 spotdl/convert.py | 164 ------------------ .../youtube.py} | 0 spotdl/encoders/__init__.py | 3 + spotdl/encoders/avconv.py | 77 ++++++++ spotdl/encoders/base.py | 56 ++++++ spotdl/encoders/ffmpeg.py | 92 ++++++++++ spotdl/{ => patch}/patcher.py | 0 spotdl/{internals.py => util.py} | 0 11 files changed, 239 insertions(+), 165 deletions(-) mode change 100755 => 100644 .gitignore mode change 100755 => 100644 setup.py rename spotdl/{handle.py => command_line.py} (100%) delete mode 100644 spotdl/convert.py rename spotdl/{youtube_tools.py => downloaders/youtube.py} (100%) create mode 100644 spotdl/encoders/__init__.py create mode 100644 spotdl/encoders/avconv.py create mode 100644 spotdl/encoders/base.py create mode 100644 spotdl/encoders/ffmpeg.py rename spotdl/{ => patch}/patcher.py (100%) rename spotdl/{internals.py => util.py} (100%) diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 index 225ea5c..4345a0f --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Spotdl generated files *.m4a +*.webm *.mp3 +*.opus +*.flac config.yml Music/ *.txt diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index af22122..fc6d520 --- a/setup.py +++ b/setup.py @@ -10,7 +10,14 @@ setup( name="spotdl", # Tests are included automatically: # https://docs.python.org/3.6/distutils/sourcedist.html#specifying-the-files-to-distribute - packages=["spotdl", "spotdl.lyrics", "spotdl.lyrics.providers"], + packages=[ + "spotdl", + "spotdl.lyrics", + "spotdl.lyrics.providers", + "spotdl.encoders", + "spotdl.downloaders", + "spotdl.patch", + ], version=spotdl.__version__, install_requires=[ "pathlib >= 1.0.1", diff --git a/spotdl/handle.py b/spotdl/command_line.py similarity index 100% rename from spotdl/handle.py rename to spotdl/command_line.py diff --git a/spotdl/convert.py b/spotdl/convert.py deleted file mode 100644 index 4e5811f..0000000 --- a/spotdl/convert.py +++ /dev/null @@ -1,164 +0,0 @@ -import subprocess -import os -from logzero import logger as log - - -""" -What are the differences and similarities between ffmpeg, libav, and avconv? -https://stackoverflow.com/questions/9477115 - -ffmeg encoders high to lower quality -libopus > libvorbis >= libfdk_aac > aac > libmp3lame - -libfdk_aac due to copyrights needs to be compiled by end user -on MacOS brew install ffmpeg --with-fdk-aac will do just that. Other OS? -https://trac.ffmpeg.org/wiki/Encode/AAC -""" - - -def song( - input_song, - output_song, - folder, - avconv=False, - trim_silence=False, - delete_original=True, -): - """ Do the audio format conversion. """ - if avconv and trim_silence: - raise ValueError("avconv does not support trim_silence") - - if not input_song == output_song: - log.info("Converting {0} to {1}".format(input_song, output_song.split(".")[-1])) - elif input_song.endswith(".m4a"): - log.info('Correcting container in "{}"'.format(input_song)) - else: - return 0 - - convert = Converter( - input_song, output_song, folder, delete_original=delete_original - ) - if avconv: - exit_code, command = convert.with_avconv() - else: - exit_code, command = convert.with_ffmpeg(trim_silence=trim_silence) - return exit_code, command - - -class Converter: - def __init__(self, input_song, output_song, folder, delete_original): - _, self.input_ext = os.path.splitext(input_song) - _, self.output_ext = os.path.splitext(output_song) - - self.output_file = os.path.join(folder, output_song) - rename_to_temp = False - - same_file = os.path.abspath(input_song) == os.path.abspath(output_song) - if same_file: - # FFmpeg/avconv cannot have the same file for both input and output - # This would happen when the extensions are same, so rename - # the input track to append ".temp" - log.debug( - 'Input file and output file are going will be same during encoding, will append ".temp" to input file just before starting encoding to avoid conflict' - ) - input_song = output_song + ".temp" - rename_to_temp = True - delete_original = True - - self.input_file = os.path.join(folder, input_song) - - self.rename_to_temp = rename_to_temp - self.delete_original = delete_original - - def with_avconv(self): - if log.level == 10: - level = "debug" - else: - level = "0" - - command = [ - "avconv", - "-loglevel", - level, - "-i", - self.input_file, - "-ab", - "192k", - self.output_file, - "-y", - ] - - if self.rename_to_temp: - os.rename(self.output_file, self.input_file) - - log.debug(command) - try: - code = subprocess.call(command) - except FileNotFoundError: - if self.rename_to_temp: - os.rename(self.input_file, self.output_file) - raise - - if self.delete_original: - log.debug('Removing original file: "{}"'.format(self.input_file)) - os.remove(self.input_file) - - return code, command - - def with_ffmpeg(self, trim_silence=False): - ffmpeg_pre = ( - "ffmpeg -y -nostdin " - ) # -nostdin is necessary for spotdl to be able to run in the backgroung. - - if not log.level == 10: - ffmpeg_pre += "-hide_banner -nostats -v panic " - - ffmpeg_params = "" - - if self.input_ext == ".m4a": - if self.output_ext == ".mp3": - ffmpeg_params = "-codec:v copy -codec:a libmp3lame -ar 48000 " - elif self.output_ext == ".webm": - ffmpeg_params = "-codec:a libopus -vbr on " - elif self.output_ext == ".m4a": - ffmpeg_params = "-acodec copy " - - elif self.input_ext == ".webm": - if self.output_ext == ".mp3": - ffmpeg_params = "-codec:a libmp3lame -ar 48000 " - elif self.output_ext == ".m4a": - ffmpeg_params = "-cutoff 20000 -codec:a aac -ar 48000 " - - if self.output_ext == ".flac": - ffmpeg_params = "-codec:a flac -ar 48000 " - - # add common params for any of the above combination - ffmpeg_params += "-b:a 192k -vn " - ffmpeg_pre += "-i " - - if trim_silence: - ffmpeg_params += "-af silenceremove=start_periods=1 " - - command = ( - ffmpeg_pre.split() - + [self.input_file] - + ffmpeg_params.split() - + [self.output_file] - ) - - if self.rename_to_temp: - os.rename(self.output_file, self.input_file) - - log.debug(command) - try: - code = subprocess.call(command) - except FileNotFoundError: - if self.rename_to_temp: - os.rename(self.input_file, self.output_file) - raise - - if self.delete_original: - log.debug('Removing original file: "{}"'.format(self.input_file)) - os.remove(self.input_file) - - return code, command diff --git a/spotdl/youtube_tools.py b/spotdl/downloaders/youtube.py similarity index 100% rename from spotdl/youtube_tools.py rename to spotdl/downloaders/youtube.py diff --git a/spotdl/encoders/__init__.py b/spotdl/encoders/__init__.py new file mode 100644 index 0000000..cd8d70c --- /dev/null +++ b/spotdl/encoders/__init__.py @@ -0,0 +1,3 @@ +from spotdl.encoders.base import EncoderBase +from spotdl.encoders.ffmpeg import EncoderFFmpeg +from spotdl.encoders.avconv import EncoderAvconv diff --git a/spotdl/encoders/avconv.py b/spotdl/encoders/avconv.py new file mode 100644 index 0000000..239daa6 --- /dev/null +++ b/spotdl/encoders/avconv.py @@ -0,0 +1,77 @@ +import subprocess +import os +from logzero import logger as log +from spotdl.encoders import EncoderBase + +class EncoderAvconv(EncoderBase): + def __init__(self, encoder_path="avconv"): + print("Using avconv is deprecated and this will be removed in", + "future versions. Use ffmpeg instead.") + encoder_path = encoder_path + _loglevel = "-loglevel 0" + _additional_arguments = ["-ab", "192k"] + + super().__init__(encoder_path, _loglevel, _additional_arguments) + + def set_argument(self, argument): + super().set_argument(argument) + + def get_encoding(self, filename): + return super().get_encoding(filename) + + def _generate_encoding_arguments(self, input_encoding, output_encoding): + initial_arguments = self._rules.get(input_encoding) + if initial_arguments is None: + raise TypeError( + 'The input format ("{}") is not supported.'.format( + input_extension, + ) + ) + + arguments = initial_arguments.get(output_encoding) + if arguments is None: + raise TypeError( + 'The output format ("{}") is not supported.'.format( + output_extension, + ) + ) + + return arguments + + def _generate_encoding_arguments(self, input_encoding, output_encoding): + return "" + + def set_debuglog(self): + self._loglevel = "-loglevel debug" + + def _generate_encode_command(self, input_file, output_file): + input_encoding = self.get_encoding(input_file) + output_encoding = self.get_encoding(output_file) + + arguments = self._generate_encoding_arguments( + input_encoding, + output_encoding + ) + + command = [self.encoder_path] \ + + ["-y"] \ + + self._loglevel.split() \ + + ["-i", input_file] \ + + self._additional_arguments \ + + [output_file] + + return command + + def re_encode(self, input_file, output_file, delete_original=False): + encode_command = self._generate_encode_command( + input_file, + output_file + ) + + returncode = subprocess.call(encode_command) + encode_successful = returncode == 0 + + if encode_successful and delete_original: + os.remove(input_file) + + return returncode diff --git a/spotdl/encoders/base.py b/spotdl/encoders/base.py new file mode 100644 index 0000000..8f13df3 --- /dev/null +++ b/spotdl/encoders/base.py @@ -0,0 +1,56 @@ +from abc import ABCMeta +from abc import abstractmethod +from abc import abstractproperty + +import os + +""" + NOTE ON ENCODERS + ================ + +* A comparision between FFmpeg, avconv, and libav: + https://stackoverflow.com/questions/9477115 + +* FFmeg encoders sorted in descending order based + on the quality of audio produced: + libopus > libvorbis >= libfdk_aac > aac > libmp3lame + +* libfdk_aac encoder, due to copyrights needs to be compiled + by end user on MacOS brew install ffmpeg --with-fdk-aac + will do just that. Other OS? See: + https://trac.ffmpeg.org/wiki/Encode/AAC + +""" + +class EncoderBase(metaclass=ABCMeta): + @abstractmethod + def __init__(self, encoder_path, loglevel, additional_arguments): + self.encoder_path = encoder_path + self._loglevel = loglevel + self._additional_arguments = additional_arguments + + @abstractmethod + def set_argument(self, argument): + self._additional_arguments += argument.split() + + @abstractmethod + def get_encoding(self, filename): + _, extension = os.path.splitext(filename) + # Ignore the initial dot from file extension + return extension[1:] + + @abstractmethod + def set_debuglog(self): + pass + + @abstractmethod + def _generate_encode_command(self, input_file, output_file): + pass + + @abstractmethod + def _generate_encoding_arguments(self, input_encoding, output_encoding): + pass + + @abstractmethod + def re_encode(self, input_encoding, output_encoding): + pass diff --git a/spotdl/encoders/ffmpeg.py b/spotdl/encoders/ffmpeg.py new file mode 100644 index 0000000..2e70d95 --- /dev/null +++ b/spotdl/encoders/ffmpeg.py @@ -0,0 +1,92 @@ +import subprocess +import os +from logzero import logger as log +from spotdl.encoders import EncoderBase + +RULES = { + "m4a": { + "mp3": "-codec:v copy -codec:a libmp3lame -ar 48000", + "webm": "-codec:a libopus -vbr on ", + "m4a": "-acodec copy ", + "flac": "-codec:a flac -ar 48000", + }, + "webm": { + "mp3": "-codec:a libmp3lame -ar 48000", + "m4a": "-cutoff 20000 -codec:a aac -ar 48000", + "flac": "-codec:a flac -ar 48000", + }, +} + +class EncoderFFmpeg(EncoderBase): + def __init__(self, encoder_path="ffmpeg"): + encoder_path = encoder_path + _loglevel = "-hide_banner -nostats -v panic" + _additional_arguments = ["-b:a", "192k", "-vn"] + + super().__init__(encoder_path, _loglevel, _additional_arguments) + + self._rules = RULES + + def set_argument(self, argument): + super().set_argument(argument) + + def set_trim_silence(self): + self.set_argument("-af silenceremove=start_periods=1") + + def get_encoding(self, filename): + return super().get_encoding(filename) + + def _generate_encoding_arguments(self, input_encoding, output_encoding): + initial_arguments = self._rules.get(input_encoding) + if initial_arguments is None: + raise TypeError( + 'The input format ("{}") is not supported.'.format( + input_extension, + ) + ) + + arguments = initial_arguments.get(output_encoding) + if arguments is None: + raise TypeError( + 'The output format ("{}") is not supported.'.format( + output_extension, + ) + ) + + return arguments + + def set_debuglog(self): + self._loglevel = "-loglevel debug" + + def _generate_encode_command(self, input_file, output_file): + input_encoding = self.get_encoding(input_file) + output_encoding = self.get_encoding(output_file) + + arguments = self._generate_encoding_arguments( + input_encoding, + output_encoding + ) + + command = [self.encoder_path] \ + + ["-y", "-nostdin"] \ + + self._loglevel.split() \ + + ["-i", input_file] \ + + arguments.split() \ + + self._additional_arguments \ + + [output_file] + + return command + + def re_encode(self, input_file, output_file, delete_original=False): + encode_command = self._generate_encode_command( + input_file, + output_file + ) + + returncode = subprocess.call(encode_command) + encode_successful = returncode == 0 + + if encode_successful and delete_original: + os.remove(input_file) + + return returncode diff --git a/spotdl/patcher.py b/spotdl/patch/patcher.py similarity index 100% rename from spotdl/patcher.py rename to spotdl/patch/patcher.py diff --git a/spotdl/internals.py b/spotdl/util.py similarity index 100% rename from spotdl/internals.py rename to spotdl/util.py