mirror of
				https://github.com/KevinMidboe/spotify-downloader.git
				synced 2025-10-29 18:00:15 +00:00 
			
		
		
		
	Refactor encoding
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -1,6 +1,9 @@ | ||||
| # Spotdl generated files | ||||
| *.m4a | ||||
| *.webm | ||||
| *.mp3 | ||||
| *.opus | ||||
| *.flac | ||||
| config.yml | ||||
| Music/ | ||||
| *.txt | ||||
|   | ||||
							
								
								
									
										9
									
								
								setup.py
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										9
									
								
								setup.py
									
									
									
									
									
										
										
										Executable file → Normal file
									
								
							| @@ -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", | ||||
|   | ||||
| @@ -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 | ||||
							
								
								
									
										3
									
								
								spotdl/encoders/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								spotdl/encoders/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| from spotdl.encoders.base import EncoderBase | ||||
| from spotdl.encoders.ffmpeg import EncoderFFmpeg | ||||
| from spotdl.encoders.avconv import EncoderAvconv | ||||
							
								
								
									
										77
									
								
								spotdl/encoders/avconv.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								spotdl/encoders/avconv.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										56
									
								
								spotdl/encoders/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								spotdl/encoders/base.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										92
									
								
								spotdl/encoders/ffmpeg.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								spotdl/encoders/ffmpeg.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
		Reference in New Issue
	
	Block a user