Refactor exceptions

* Suffix names for custom exceptions with "Error"
* Introduce exceptions for when the coressponding encoder isn't found
This commit is contained in:
Ritiek Malhotra
2020-03-17 03:09:56 +05:30
parent 083c430489
commit 29005f24ed
16 changed files with 276 additions and 28 deletions

View File

@@ -14,7 +14,8 @@ setup(
"spotdl", "spotdl",
"spotdl.lyrics", "spotdl.lyrics",
"spotdl.lyrics.providers", "spotdl.lyrics.providers",
"spotdl.encoders", "spotdl.encode",
"spotdl.encode.encoders",
"spotdl.downloaders", "spotdl.downloaders",
"spotdl.patch", "spotdl.patch",
], ],

View File

@@ -1,8 +1,11 @@
import shutil
import os import os
from abc import ABC from abc import ABC
from abc import abstractmethod from abc import abstractmethod
from spotdl.encode.exceptions import EncoderNotFoundError
""" """
NOTE ON ENCODERS NOTE ON ENCODERS
================ ================
@@ -25,6 +28,12 @@ from abc import abstractmethod
class EncoderBase(ABC): class EncoderBase(ABC):
@abstractmethod @abstractmethod
def __init__(self, encoder_path, loglevel, additional_arguments): def __init__(self, encoder_path, loglevel, additional_arguments):
if shutil.which(encoder_path) is None:
raise EncoderNotFoundError(
"{} executable does not exist or was not found in PATH.".format(
encoder_path
)
)
self.encoder_path = encoder_path self.encoder_path = encoder_path
self._loglevel = loglevel self._loglevel = loglevel
self._additional_arguments = additional_arguments self._additional_arguments = additional_arguments

View File

@@ -2,16 +2,21 @@ import subprocess
import os import os
from logzero import logger as log from logzero import logger as log
from spotdl.encode import EncoderBase from spotdl.encode import EncoderBase
from spotdl.encode.exceptions import EncoderNotFoundError
from spotdl.encode.exceptions import AvconvNotFoundError
class EncoderAvconv(EncoderBase): class EncoderAvconv(EncoderBase):
def __init__(self, encoder_path="avconv"): def __init__(self, encoder_path="avconv"):
print("Using avconv is deprecated and this will be removed in", print("Using EncoderAvconv is deprecated and will be removed"
"future versions. Use ffmpeg instead.") "in future versions. Use EncoderFFmpeg instead.")
encoder_path = encoder_path encoder_path = encoder_path
_loglevel = "-loglevel 0" _loglevel = "-loglevel 0"
_additional_arguments = ["-ab", "192k"] _additional_arguments = ["-ab", "192k"]
super().__init__(encoder_path, _loglevel, _additional_arguments) try:
super().__init__(encoder_path, _loglevel, _additional_arguments)
except EncoderNotFoundError as e:
raise AvconvNotFoundError(e.args[0])
def set_argument(self, argument): def set_argument(self, argument):
super().set_argument(argument) super().set_argument(argument)

View File

@@ -1,7 +1,10 @@
import subprocess import subprocess
import os import os
from logzero import logger as log from logzero import logger as log
from spotdl.encode import EncoderBase from spotdl.encode import EncoderBase
from spotdl.encode.exceptions import EncoderNotFoundError
from spotdl.encode.exceptions import FFmpegNotFoundError
RULES = { RULES = {
"m4a": { "m4a": {
@@ -17,14 +20,15 @@ RULES = {
}, },
} }
class EncoderFFmpeg(EncoderBase): class EncoderFFmpeg(EncoderBase):
def __init__(self, encoder_path="ffmpeg"): def __init__(self, encoder_path="ffmpeg"):
encoder_path = encoder_path
_loglevel = "-hide_banner -nostats -v panic" _loglevel = "-hide_banner -nostats -v panic"
_additional_arguments = ["-b:a", "192k", "-vn"] _additional_arguments = ["-b:a", "192k", "-vn"]
try:
super().__init__(encoder_path, _loglevel, _additional_arguments) super().__init__(encoder_path, _loglevel, _additional_arguments)
except EncoderNotFoundError as e:
raise FFmpegNotFoundError(e.args[0])
self._rules = RULES self._rules = RULES
def set_argument(self, argument): def set_argument(self, argument):
@@ -41,18 +45,16 @@ class EncoderFFmpeg(EncoderBase):
if initial_arguments is None: if initial_arguments is None:
raise TypeError( raise TypeError(
'The input format ("{}") is not supported.'.format( 'The input format ("{}") is not supported.'.format(
input_extension, input_encoding,
) )
) )
arguments = initial_arguments.get(output_encoding) arguments = initial_arguments.get(output_encoding)
if arguments is None: if arguments is None:
raise TypeError( raise TypeError(
'The output format ("{}") is not supported.'.format( 'The output format ("{}") is not supported.'.format(
output_extension, output_encoding,
) )
) )
return arguments return arguments
def set_debuglog(self): def set_debuglog(self):
@@ -61,12 +63,10 @@ class EncoderFFmpeg(EncoderBase):
def _generate_encode_command(self, input_file, output_file): def _generate_encode_command(self, input_file, output_file):
input_encoding = self.get_encoding(input_file) input_encoding = self.get_encoding(input_file)
output_encoding = self.get_encoding(output_file) output_encoding = self.get_encoding(output_file)
arguments = self._generate_encoding_arguments( arguments = self._generate_encoding_arguments(
input_encoding, input_encoding,
output_encoding output_encoding
) )
command = [self.encoder_path] \ command = [self.encoder_path] \
+ ["-y", "-nostdin"] \ + ["-y", "-nostdin"] \
+ self._loglevel.split() \ + self._loglevel.split() \
@@ -82,10 +82,8 @@ class EncoderFFmpeg(EncoderBase):
input_file, input_file,
output_file output_file
) )
returncode = subprocess.call(encode_command) returncode = subprocess.call(encode_command)
encode_successful = returncode == 0 encode_successful = returncode == 0
if encode_successful and delete_original: if encode_successful and delete_original:
os.remove(input_file) os.remove(input_file)

View File

View File

@@ -0,0 +1,188 @@
from spotdl.encode import EncoderBase
# from spotdl.encode import exceptions
from spotdl.encode.encoders import EncoderFFmpeg
import pytest
class TestEncoderFFmpeg:
def test_subclass(self):
assert issubclass(EncoderFFmpeg, EncoderBase)
class TestEncodingDefaults:
def m4a_to_mp3_encoder(input_file, output_file):
command = [
'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic',
'-i', input_file,
'-codec:v', 'copy',
'-codec:a', 'libmp3lame',
'-ar', '48000',
'-b:a', '192k',
'-vn', output_file
]
return command
def m4a_to_webm_encoder(input_file, output_file):
command = [
'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic',
'-i', input_file,
'-codec:a', 'libopus',
'-vbr', 'on',
'-b:a', '192k',
'-vn', output_file
]
return command
def m4a_to_m4a_encoder(input_file, output_file):
command = [
'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic',
'-i', input_file,
'-acodec', 'copy',
'-b:a', '192k',
'-vn', output_file
]
return command
def m4a_to_flac_encoder(input_file, output_file):
command = [
'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic',
'-i', input_file,
'-codec:a', 'flac',
'-ar', '48000',
'-b:a', '192k',
'-vn', output_file
]
return command
@pytest.mark.parametrize("files, expected_command", [
(("test.m4a", "test.mp3"), m4a_to_mp3_encoder("test.m4a", "test.mp3")),
(("abc.m4a", "cba.webm"), m4a_to_webm_encoder("abc.m4a", "cba.webm")),
(("bla bla.m4a", "ble ble.m4a"), m4a_to_m4a_encoder("bla bla.m4a", "ble ble.m4a")),
(("😛.m4a", "• tongue.flac"), m4a_to_flac_encoder("😛.m4a", "• tongue.flac")),
])
def test_generate_encode_command(self, files, expected_command):
encoder = EncoderFFmpeg()
assert encoder._generate_encode_command(*files) == expected_command
class TestEncodingInDebugMode:
def m4a_to_mp3_encoder_with_debug(input_file, output_file):
command = [
'ffmpeg', '-y', '-nostdin', '-loglevel', 'debug',
'-i', input_file,
'-codec:v', 'copy',
'-codec:a', 'libmp3lame',
'-ar', '48000',
'-b:a', '192k',
'-vn', output_file
]
return command
def m4a_to_webm_encoder_with_debug(input_file, output_file):
command = [
'ffmpeg', '-y', '-nostdin', '-loglevel', 'debug',
'-i', input_file,
'-codec:a', 'libopus',
'-vbr', 'on',
'-b:a', '192k',
'-vn', output_file
]
return command
def m4a_to_m4a_encoder_with_debug(input_file, output_file):
command = [
'ffmpeg', '-y', '-nostdin', '-loglevel', 'debug',
'-i', input_file,
'-acodec', 'copy',
'-b:a', '192k',
'-vn', output_file
]
return command
def m4a_to_flac_encoder_with_debug(input_file, output_file):
command = [
'ffmpeg', '-y', '-nostdin', '-loglevel', 'debug',
'-i', input_file,
'-codec:a', 'flac',
'-ar', '48000',
'-b:a', '192k',
'-vn', output_file
]
return command
@pytest.mark.parametrize("files, expected_command", [
(("test.m4a", "test.mp3"), m4a_to_mp3_encoder_with_debug("test.m4a", "test.mp3")),
(("abc.m4a", "cba.webm"), m4a_to_webm_encoder_with_debug("abc.m4a", "cba.webm")),
(("bla bla.m4a", "ble ble.m4a"), m4a_to_m4a_encoder_with_debug("bla bla.m4a", "ble ble.m4a")),
(("😛.m4a", "• tongue.flac"), m4a_to_flac_encoder_with_debug("😛.m4a", "• tongue.flac")),
])
def test_generate_encode_command_with_debug(self, files, expected_command):
encoder = EncoderFFmpeg()
encoder.set_debuglog()
assert encoder._generate_encode_command(*files) == expected_command
class TestEncodingAndTrimSilence:
def m4a_to_mp3_encoder_and_trim_silence(input_file, output_file):
command = [
'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic',
'-i', input_file,
'-codec:v', 'copy',
'-codec:a', 'libmp3lame',
'-ar', '48000',
'-b:a', '192k',
'-vn',
'-af', 'silenceremove=start_periods=1',
output_file
]
return command
def m4a_to_webm_encoder_and_trim_silence(input_file, output_file):
command = [
'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic',
'-i', input_file,
'-codec:a', 'libopus',
'-vbr', 'on',
'-b:a', '192k',
'-vn',
'-af', 'silenceremove=start_periods=1',
output_file
]
return command
def m4a_to_m4a_encoder_and_trim_silence(input_file, output_file):
command = [
'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic',
'-i', input_file,
'-acodec', 'copy',
'-b:a', '192k',
'-vn',
'-af', 'silenceremove=start_periods=1',
output_file
]
return command
def m4a_to_flac_encoder_and_trim_silence(input_file, output_file):
command = [
'ffmpeg', '-y', '-nostdin', '-hide_banner', '-nostats', '-v', 'panic',
'-i', input_file,
'-codec:a', 'flac',
'-ar', '48000',
'-b:a', '192k',
'-vn',
'-af', 'silenceremove=start_periods=1',
output_file
]
return command
@pytest.mark.parametrize("files, expected_command", [
(("test.m4a", "test.mp3"), m4a_to_mp3_encoder_and_trim_silence("test.m4a", "test.mp3")),
(("abc.m4a", "cba.webm"), m4a_to_webm_encoder_and_trim_silence("abc.m4a", "cba.webm")),
(("bla bla.m4a", "ble ble.m4a"), m4a_to_m4a_encoder_and_trim_silence("bla bla.m4a", "ble ble.m4a")),
(("😛.m4a", "• tongue.flac"), m4a_to_flac_encoder_and_trim_silence("😛.m4a", "• tongue.flac")),
])
def test_generate_encode_command_and_trim_silence(self, files, expected_command):
encoder = EncoderFFmpeg()
encoder.set_trim_silence()
assert encoder._generate_encode_command(*files) == expected_command

View File

@@ -0,0 +1,20 @@
class EncoderNotFoundError(Exception):
__module__ = Exception.__module__
def __init__(self, message=None):
super(EncoderNotFoundError, self).__init__(message)
class FFmpegNotFoundError(EncoderNotFoundError):
__module__ = Exception.__module__
def __init__(self, message=None):
super(FFmpegNotFoundError, self).__init__(message)
class AvconvNotFoundError(EncoderNotFoundError):
__module__ = Exception.__module__
def __init__(self, message=None):
super(AvconvNotFoundError, self).__init__(message)

View File

View File

@@ -0,0 +1,13 @@
from spotdl.encode import EncoderBase
import pytest
def test_abstract_base_class_encoderbase():
encoder_path = "ffmpeg"
_loglevel = "-hide_banner -nostats -v panic"
_additional_arguments = ["-b:a", "192k", "-vn"]
with pytest.raises(TypeError):
# This abstract base class must be inherited from
# for instantiation
EncoderBase(encoder_path, _loglevel, _additional_arguments)

View File

@@ -0,0 +1,14 @@
from spotdl.encode.exceptions import EncoderNotFoundError
from spotdl.encode.exceptions import FFmpegNotFoundError
from spotdl.encode.exceptions import AvconvNotFoundError
import pytest
class TestEncoderNotFoundSubclass:
def test_ffmpeg_not_found_subclass(self):
assert issubclass(FFmpegNotFoundError, EncoderNotFoundError)
def test_avconv_not_found_subclass(self):
assert issubclass(AvconvNotFoundError, EncoderNotFoundError)

View File

@@ -1,5 +1,5 @@
class LyricsNotFound(Exception): class LyricsNotFoundError(Exception):
__module__ = Exception.__module__ __module__ = Exception.__module__
def __init__(self, message=None): def __init__(self, message=None):
super(LyricsNotFound, self).__init__(message) super(LyricsNotFoundError, self).__init__(message)

View File

@@ -2,7 +2,7 @@ from bs4 import BeautifulSoup
import urllib.request import urllib.request
from spotdl.lyrics.lyric_base import LyricBase from spotdl.lyrics.lyric_base import LyricBase
from spotdl.lyrics.exceptions import LyricsNotFound from spotdl.lyrics.exceptions import LyricsNotFoundError
BASE_URL = "https://genius.com" BASE_URL = "https://genius.com"
@@ -26,7 +26,7 @@ class Genius(LyricBase):
try: try:
response = urllib.request.urlopen(request, timeout=timeout) response = urllib.request.urlopen(request, timeout=timeout)
except urllib.request.HTTPError: except urllib.request.HTTPError:
raise LyricsNotFound( raise LyricsNotFoundError(
"Could not find lyrics for {} - {} at URL: {}".format( "Could not find lyrics for {} - {} at URL: {}".format(
self.artist, self.song, url self.artist, self.song, url
) )
@@ -40,7 +40,7 @@ class Genius(LyricBase):
if lyrics_paragraph: if lyrics_paragraph:
return lyrics_paragraph.get_text() return lyrics_paragraph.get_text()
else: else:
raise LyricsNotFound("The lyrics for this track are yet to be released.") raise LyricsNotFoundError("The lyrics for this track are yet to be released.")
def get_lyrics(self, linesep="\n", timeout=None): def get_lyrics(self, linesep="\n", timeout=None):
url = self._guess_lyric_url() url = self._guess_lyric_url()

View File

@@ -1,7 +1,7 @@
import lyricwikia import lyricwikia
from spotdl.lyrics.lyric_base import LyricBase from spotdl.lyrics.lyric_base import LyricBase
from spotdl.lyrics.exceptions import LyricsNotFound from spotdl.lyrics.exceptions import LyricsNotFoundError
class LyricWikia(LyricBase): class LyricWikia(LyricBase):
@@ -13,6 +13,6 @@ class LyricWikia(LyricBase):
try: try:
lyrics = lyricwikia.get_lyrics(self.artist, self.song, linesep, timeout) lyrics = lyricwikia.get_lyrics(self.artist, self.song, linesep, timeout)
except lyricwikia.LyricsNotFound as e: except lyricwikia.LyricsNotFound as e:
raise LyricsNotFound(e.args[0]) raise LyricsNotFoundError(e.args[0])
else: else:
return lyrics return lyrics

View File

@@ -33,5 +33,5 @@ class TestGenius:
raise urllib.request.HTTPError("", "", "", "", "") raise urllib.request.HTTPError("", "", "", "", "")
monkeypatch.setattr("urllib.request.urlopen", mocked_urlopen) monkeypatch.setattr("urllib.request.urlopen", mocked_urlopen)
with pytest.raises(exceptions.LyricsNotFound): with pytest.raises(exceptions.LyricsNotFoundError):
track.get_lyrics() track.get_lyrics()

View File

@@ -25,11 +25,11 @@ class TestLyricWikia:
def lyricwikia_lyrics_not_found(msg): def lyricwikia_lyrics_not_found(msg):
raise lyricwikia.LyricsNotFound(msg) raise lyricwikia.LyricsNotFound(msg)
# Wrap `lyricwikia.LyricsNotFound` with `exceptions.LyricsNotFound` error. # Wrap `lyricwikia.LyricsNotFoundError` with `exceptions.LyricsNotFoundError` error.
monkeypatch.setattr( monkeypatch.setattr(
"lyricwikia.get_lyrics", "lyricwikia.get_lyrics",
lambda a, b, c, d: lyricwikia_lyrics_not_found("Nope, no lyrics."), lambda a, b, c, d: lyricwikia_lyrics_not_found("Nope, no lyrics."),
) )
track = LyricWikia("Lyricwikia", "Lyricwikia") track = LyricWikia("Lyricwikia", "Lyricwikia")
with pytest.raises(exceptions.LyricsNotFound): with pytest.raises(exceptions.LyricsNotFoundError):
track.get_lyrics() track.get_lyrics()

View File

@@ -12,7 +12,7 @@ import functools
from spotdl import const from spotdl import const
from spotdl import internals from spotdl import internals
from spotdl.lyrics.providers import LyricClasses from spotdl.lyrics.providers import LyricClasses
from spotdl.lyrics.exceptions import LyricsNotFound from spotdl.lyrics.exceptions import LyricsNotFoundError
spotify = None spotify = None
@@ -82,7 +82,7 @@ def generate_metadata(raw_song):
track = LyricClass(meta_tags["artists"][0]["name"], meta_tags["name"]) track = LyricClass(meta_tags["artists"][0]["name"], meta_tags["name"])
try: try:
meta_tags["lyrics"] = track.get_lyrics() meta_tags["lyrics"] = track.get_lyrics()
except LyricsNotFound: except LyricsNotFoundError:
continue continue
else: else:
break break