Write tests for YouTube metadata

This commit is contained in:
Ritiek Malhotra
2020-03-27 04:33:00 +05:30
parent c9a804268d
commit 68c25e2aaa
25 changed files with 411 additions and 55 deletions

5
setup.cfg Normal file
View File

@@ -0,0 +1,5 @@
[tool:pytest]
addopts = --strict-markers -m "not network"
markers =
network: marks test which rely on external network resources (select with '-m network' or run all with '-m "network, not network"')

View File

@@ -81,7 +81,7 @@ class EncoderBase(ABC):
pass pass
@abstractmethod @abstractmethod
def _generate_encoding_arguments(self, input_encoding, output_encoding): def _generate_encoding_arguments(self, input_encoding, target_encoding):
""" """
This method must return the core arguments for the defined This method must return the core arguments for the defined
encoder such as defining the sample rate, audio bitrate, encoder such as defining the sample rate, audio bitrate,

View File

@@ -26,7 +26,7 @@ class EncoderAvconv(EncoderBase):
def get_encoding(self, filename): def get_encoding(self, filename):
return super().get_encoding(filename) return super().get_encoding(filename)
def _generate_encoding_arguments(self, input_encoding, output_encoding): def _generate_encoding_arguments(self, input_encoding, target_encoding):
initial_arguments = self._rules.get(input_encoding) initial_arguments = self._rules.get(input_encoding)
if initial_arguments is None: if initial_arguments is None:
raise TypeError( raise TypeError(
@@ -35,7 +35,7 @@ class EncoderAvconv(EncoderBase):
) )
) )
arguments = initial_arguments.get(output_encoding) arguments = initial_arguments.get(target_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(
@@ -45,19 +45,19 @@ class EncoderAvconv(EncoderBase):
return arguments return arguments
def _generate_encoding_arguments(self, input_encoding, output_encoding): def _generate_encoding_arguments(self, input_encoding, target_encoding):
return "" return ""
def set_debuglog(self): def set_debuglog(self):
self._loglevel = "-loglevel debug" self._loglevel = "-loglevel debug"
def _generate_encode_command(self, input_file, output_file): def _generate_encode_command(self, input_file, target_file):
input_encoding = self.get_encoding(input_file) input_encoding = self.get_encoding(input_file)
output_encoding = self.get_encoding(output_file) target_encoding = self.get_encoding(target_file)
arguments = self._generate_encoding_arguments( arguments = self._generate_encoding_arguments(
input_encoding, input_encoding,
output_encoding target_encoding
) )
command = [self.encoder_path] \ command = [self.encoder_path] \
@@ -65,14 +65,14 @@ class EncoderAvconv(EncoderBase):
+ self._loglevel.split() \ + self._loglevel.split() \
+ ["-i", input_file] \ + ["-i", input_file] \
+ self._additional_arguments \ + self._additional_arguments \
+ [output_file] + [target_file]
return command return command
def re_encode(self, input_file, output_file, delete_original=False): def re_encode(self, input_file, target_file, delete_original=False):
encode_command = self._generate_encode_command( encode_command = self._generate_encode_command(
input_file, input_file,
output_file target_file
) )
returncode = subprocess.call(encode_command) returncode = subprocess.call(encode_command)

View File

@@ -37,18 +37,18 @@ class EncoderFFmpeg(EncoderBase):
def get_encoding(self, path): def get_encoding(self, path):
return super().get_encoding(path) return super().get_encoding(path)
def _generate_encoding_arguments(self, input_encoding, output_encoding): def _generate_encoding_arguments(self, input_encoding, target_encoding):
initial_arguments = self._rules.get(input_encoding) initial_arguments = self._rules.get(input_encoding)
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_encoding, input_encoding,
)) ))
arguments = initial_arguments.get(output_encoding) arguments = initial_arguments.get(target_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_encoding, target_encoding,
)) ))
return arguments return arguments
@@ -56,14 +56,14 @@ class EncoderFFmpeg(EncoderBase):
self._loglevel = "-loglevel debug" self._loglevel = "-loglevel debug"
def _generate_encode_command(self, input_path, target_path, def _generate_encode_command(self, input_path, target_path,
input_encoding=None, output_encoding=None): input_encoding=None, target_encoding=None):
if input_encoding is None: if input_encoding is None:
input_encoding = self.get_encoding(input_path) input_encoding = self.get_encoding(input_path)
if output_encoding is None: if target_encoding is None:
output_encoding = self.get_encoding(target_path) target_encoding = self.get_encoding(target_path)
arguments = self._generate_encoding_arguments( arguments = self._generate_encoding_arguments(
input_encoding, input_encoding,
output_encoding target_encoding
) )
command = [self.encoder_path] \ command = [self.encoder_path] \
+ ["-y", "-nostdin"] \ + ["-y", "-nostdin"] \
@@ -88,7 +88,7 @@ class EncoderFFmpeg(EncoderBase):
return process return process
def re_encode_from_stdin(self, input_encoding, target_path): def re_encode_from_stdin(self, input_encoding, target_path):
output_encoding = self.get_encoding(target_path) target_encoding = self.get_encoding(target_path)
encode_command = self._generate_encode_command( encode_command = self._generate_encode_command(
"-", "-",
target_path, target_path,

View File

@@ -15,12 +15,12 @@ class TestEncoderAvconv:
class TestEncodingDefaults: class TestEncodingDefaults:
def encode_command(input_file, output_file): def encode_command(input_file, target_file):
command = [ command = [
'avconv', '-y', '-loglevel', '0', 'avconv', '-y', '-loglevel', '0',
'-i', input_file, '-i', input_file,
'-ab', '192k', '-ab', '192k',
output_file, target_file,
] ]
return command return command
@@ -36,12 +36,12 @@ class TestEncodingDefaults:
class TestEncodingInDebugMode: class TestEncodingInDebugMode:
def debug_encode_command(input_file, output_file): def debug_encode_command(input_file, target_file):
command = [ command = [
'avconv', '-y', '-loglevel', 'debug', 'avconv', '-y', '-loglevel', 'debug',
'-i', input_file, '-i', input_file,
'-ab', '192k', '-ab', '192k',
output_file, target_file,
] ]
return command return command

View File

@@ -4,7 +4,6 @@ from spotdl.encode.encoders import EncoderFFmpeg
import pytest import pytest
class TestEncoderFFmpeg: class TestEncoderFFmpeg:
def test_subclass(self): def test_subclass(self):
assert issubclass(EncoderFFmpeg, EncoderBase) assert issubclass(EncoderFFmpeg, EncoderBase)

View File

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

View File

@@ -3,7 +3,6 @@ from spotdl.encode.exceptions import EncoderNotFoundError
import pytest import pytest
class TestAbstractBaseClass: class TestAbstractBaseClass:
def test_error_abstract_base_class_encoderbase(self): def test_error_abstract_base_class_encoderbase(self):
encoder_path = "ffmpeg" encoder_path = "ffmpeg"
@@ -27,15 +26,9 @@ class TestAbstractBaseClass:
def _generate_encoding_arguments(self): def _generate_encoding_arguments(self):
pass pass
def get_encoding(self):
pass
def re_encode(self): def re_encode(self):
pass pass
def set_argument(self):
pass
def set_debuglog(self): def set_debuglog(self):
pass pass
@@ -52,21 +45,15 @@ class TestMethods:
def __init__(self, encoder_path, _loglevel, _additional_arguments): def __init__(self, encoder_path, _loglevel, _additional_arguments):
super().__init__(encoder_path, _loglevel, _additional_arguments) super().__init__(encoder_path, _loglevel, _additional_arguments)
def _generate_encode_command(self, input_file, output_file): def _generate_encode_command(self, input_file, target_file):
pass pass
def _generate_encoding_arguments(self, input_encoding, output_encoding): def _generate_encoding_arguments(self, input_encoding, target_encoding):
pass pass
def get_encoding(self, filename): def re_encode(self, input_encoding, target_encoding):
return super().get_encoding(filename)
def re_encode(self, input_encoding, output_encoding):
pass pass
def set_argument(self, argument):
super().set_argument(argument)
def set_debuglog(self): def set_debuglog(self):
pass pass

View File

@@ -2,8 +2,6 @@ from spotdl.encode.exceptions import EncoderNotFoundError
from spotdl.encode.exceptions import FFmpegNotFoundError from spotdl.encode.exceptions import FFmpegNotFoundError
from spotdl.encode.exceptions import AvconvNotFoundError from spotdl.encode.exceptions import AvconvNotFoundError
import pytest
class TestEncoderNotFoundSubclass: class TestEncoderNotFoundSubclass:
def test_encoder_not_found_subclass(self): def test_encoder_not_found_subclass(self):

View File

@@ -2,4 +2,4 @@ class LyricsNotFoundError(Exception):
__module__ = Exception.__module__ __module__ = Exception.__module__
def __init__(self, message=None): def __init__(self, message=None):
super(LyricsNotFoundError, self).__init__(message) super().__init__(message)

View File

@@ -1,5 +1,9 @@
from spotdl.metadata.provider_base import ProviderBase from spotdl.metadata.provider_base import ProviderBase
from spotdl.metadata.provider_base import StreamsBase from spotdl.metadata.provider_base import StreamsBase
from spotdl.metadata.exceptions import MetadataNotFoundError
from spotdl.metadata.exceptions import SpotifyMetadataNotFoundError
from spotdl.metadata.exceptions import YouTubeMetadataNotFoundError
from spotdl.metadata.embedder_base import EmbedderBase from spotdl.metadata.embedder_base import EmbedderBase

View File

@@ -5,7 +5,7 @@ from abc import abstractmethod
class EmbedderBase(ABC): class EmbedderBase(ABC):
""" """
The class must define the supported media file encoding The subclass must define the supported media file encoding
formats here using a static variable - such as: formats here using a static variable - such as:
>>> supported_formats = ("mp3", "opus", "flac") >>> supported_formats = ("mp3", "opus", "flac")

View File

@@ -0,0 +1,20 @@
class MetadataNotFoundError(Exception):
__module__ = Exception.__module__
def __init__(self, message=None):
super().__init__(message)
class SpotifyMetadataNotFoundError(MetadataNotFoundError):
__module__ = Exception.__module__
def __init__(self, message=None):
super().__init__(message)
class YouTubeMetadataNotFoundError(MetadataNotFoundError):
__module__ = Exception.__module__
def __init__(self, message=None):
super().__init__(message)

View File

@@ -1,7 +1,6 @@
from abc import ABC from abc import ABC
from abc import abstractmethod from abc import abstractmethod
class StreamsBase(ABC): class StreamsBase(ABC):
@abstractmethod @abstractmethod
def __init__(self, streams): def __init__(self, streams):
@@ -17,7 +16,6 @@ class StreamsBase(ABC):
""" """
self.all = streams self.all = streams
@abstractmethod
def getbest(self): def getbest(self):
""" """
This method must return the audio stream with the This method must return the audio stream with the
@@ -25,7 +23,6 @@ class StreamsBase(ABC):
""" """
return self.all[0] return self.all[0]
@abstractmethod
def getworst(self): def getworst(self):
""" """
This method must return the audio stream with the This method must return the audio stream with the
@@ -51,13 +48,12 @@ class ProviderBase(ABC):
""" """
pass pass
@abstractmethod
def from_query(self, query): def from_query(self, query):
""" """
This method must return track metadata from the This method must return track metadata from the
corresponding search query. corresponding search query.
""" """
pass raise NotImplementedError
@abstractmethod @abstractmethod
def metadata_to_standard_form(self, metadata): def metadata_to_standard_form(self, metadata):
@@ -67,3 +63,4 @@ class ProviderBase(ABC):
providers, for easy utilization. providers, for easy utilization.
""" """
pass pass

View File

@@ -2,7 +2,7 @@ import spotipy
import spotipy.oauth2 as oauth2 import spotipy.oauth2 as oauth2
from spotdl.metadata import ProviderBase from spotdl.metadata import ProviderBase
from spotdl.metadata.exceptions import SpotifyMetadataNotFoundError
class ProviderSpotify(ProviderBase): class ProviderSpotify(ProviderBase):
def __init__(self, spotify=None): def __init__(self, spotify=None):
@@ -17,8 +17,14 @@ class ProviderSpotify(ProviderBase):
return self.metadata_to_standard_form(metadata) return self.metadata_to_standard_form(metadata)
def from_query(self, query): def from_query(self, query):
metadata = self.spotify.search(query, limit=1)["tracks"]["items"][0] tracks = self.spotify.search(query, limit=1)["tracks"]["items"]
return self.metadata_to_standard_form(metadata) if tracks is None:
raise SpotifyMetadataNotFoundError(
'Could not find any tracks matching the given search query ("{}")'.format(
query,
)
)
return self.metadata_to_standard_form(tracks[0])
def _generate_token(self, client_id, client_secret): def _generate_token(self, client_id, client_secret):
""" Generate the token. """ """ Generate the token. """

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,13 @@
from spotdl.metadata import ProviderBase
from spotdl.metadata.exceptions import SpotifyMetadataNotFoundError
from spotdl.metadata.providers import ProviderSpotify
class TestProviderSpotify:
def test_subclass(self):
assert issubclass(ProviderSpotify, ProviderBase)
# def test_metadata_not_found_error(self):
# provider = ProviderSpotify(spotify=spotify)
# with pytest.raises(SpotifyMetadataNotFoundError):
# provider.from_query("This track doesn't exist on Spotify.")

View File

@@ -0,0 +1,242 @@
from spotdl.metadata.providers.youtube import YouTubeSearch
from spotdl.metadata.providers.youtube import YouTubeStreams
from spotdl.metadata.providers import youtube
from spotdl.metadata.providers import ProviderYouTube
from spotdl.metadata.exceptions import YouTubeMetadataNotFoundError
import pytube
import urllib.request
import pickle
import sys
import os
import pytest
@pytest.fixture(scope="module")
def track():
return "selena gomez wolves"
@pytest.fixture(scope="module")
def no_result_track():
return "n0 v1d305 3x157 f0r 7h15 53arc4 qu3ry"
@pytest.fixture(scope="module")
def expect_search_results():
return [
"https://www.youtube.com/watch?v=cH4E_t3m3xM",
"https://www.youtube.com/watch?v=xrbY9gDVms0",
"https://www.youtube.com/watch?v=jX0n2rSmDbE",
"https://www.youtube.com/watch?v=nVzA1uWTydQ",
"https://www.youtube.com/watch?v=rQ6jcpwzQZU",
"https://www.youtube.com/watch?v=-grLLLTza6k",
"https://www.youtube.com/watch?v=j0AxZ4V5WQw",
"https://www.youtube.com/watch?v=zbWsb36U0uo",
"https://www.youtube.com/watch?v=3B1aY9Ob8r0",
"https://www.youtube.com/watch?v=hd2SGk90r9k",
]
@pytest.fixture(scope="module")
def expect_mock_search_results():
return [
"https://www.youtube.com/watch?v=cH4E_t3m3xM",
"https://www.youtube.com/watch?v=xrbY9gDVms0",
"https://www.youtube.com/watch?v=jX0n2rSmDbE",
"https://www.youtube.com/watch?v=rQ6jcpwzQZU",
"https://www.youtube.com/watch?v=nVzA1uWTydQ",
"https://www.youtube.com/watch?v=-grLLLTza6k",
"https://www.youtube.com/watch?v=zbWsb36U0uo",
"https://www.youtube.com/watch?v=rykH1BkGwTo",
"https://www.youtube.com/watch?v=j0AxZ4V5WQw",
"https://www.youtube.com/watch?v=RyxsaKfu-ZY",
]
class TestYouTubeSearch:
@pytest.fixture(scope="module")
def youtube_searcher(self):
return YouTubeSearch()
def test_generate_search_url(self, track, youtube_searcher):
url = youtube_searcher.generate_search_url(track)
expect_url = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q=selena%20gomez%20wolves"
assert url == expect_url
@pytest.mark.network
def test_search(self, track, youtube_searcher, expect_search_results):
results = youtube_searcher.search(track)
assert results == expect_search_results
class MockHTTPResponse:
response_file = ""
def __init__(self, url):
pass
def read(self):
module_directory = os.path.dirname(__file__)
mock_html = os.path.join(module_directory, "data", self.response_file)
with open(mock_html, "r") as fin:
html = fin.read()
return html
# @pytest.mark.mock
def test_mock_search(self, track, youtube_searcher, expect_mock_search_results, monkeypatch):
self.MockHTTPResponse.response_file = "youtube_search_results.html"
monkeypatch.setattr(urllib.request, "urlopen", self.MockHTTPResponse)
self.test_search(track, youtube_searcher, expect_mock_search_results)
@pytest.mark.network
def test_no_videos_search(self, no_result_track, youtube_searcher):
results = youtube_searcher.search(no_result_track)
assert results == []
def test_mock_no_videos_search(self, no_result_track, youtube_searcher, monkeypatch):
self.MockHTTPResponse.response_file = "youtube_no_search_results.html"
monkeypatch.setattr(urllib.request, "urlopen", self.MockHTTPResponse)
self.test_no_videos_search(no_result_track, youtube_searcher)
@pytest.fixture(scope="module")
def content():
return pytube.YouTube("https://www.youtube.com/watch?v=cH4E_t3m3xM")
class MockYouTube:
def __init__(self, url):
self.watch_html = '\\"category\\":\\"Music\\",\\"publishDate\\":\\"2017-11-18\\",\\"ownerChannelName\\":\\"SelenaGomezVEVO\\",'
self.title = "Selena Gomez, Marshmello - Wolves"
self.author = "SelenaGomezVEVO"
self.length = 213
self.watch_url = "https://youtube.com/watch?v=cH4E_t3m3xM"
self.thumbnail_url = "https://i.ytimg.com/vi/cH4E_t3m3xM/maxresdefault.jpg"
@property
def streams(self):
module_directory = os.path.dirname(__file__)
mock_streams = os.path.join(module_directory, "data", "streams.dump")
with open(mock_streams, "rb") as fin:
streams_dump = pickle.load(fin)
return streams_dump
@pytest.fixture(scope="module")
def mock_content():
return MockYouTube("https://www.youtube.com/watch?v=cH4E_t3m3xM")
@pytest.fixture(scope="module")
def expect_formatted_streams():
return [
{"bitrate": 160, "download_url": None, "encoding": "opus", "filesize": 3614184},
{"bitrate": 128, "download_url": None, "encoding": "mp4a.40.2", "filesize": 3444850},
{"bitrate": 70, "download_url": None, "encoding": "opus", "filesize": 1847626},
{"bitrate": 50, "download_url": None, "encoding": "opus", "filesize": 1407962}
]
class TestYouTubeStreams:
@pytest.mark.network
def test_streams(self, content, expect_formatted_streams):
formatted_streams = YouTubeStreams(content.streams)
for index in range(len(formatted_streams.all)):
assert isinstance(formatted_streams.all[index]["download_url"], str)
formatted_streams.all[index]["download_url"] = None
assert formatted_streams.all == expect_formatted_streams
# @pytest.mark.mock
def test_mock_streams(self, mock_content, expect_formatted_streams):
self.test_streams(mock_content, expect_formatted_streams)
@pytest.mark.network
def test_getbest(self, content):
formatted_streams = YouTubeStreams(content.streams)
best_stream = formatted_streams.getbest()
best_stream["download_url"] = None
assert best_stream == {
"bitrate": 160,
"download_url": None,
"encoding": "opus",
"filesize": 3614184
}
# @pytest.mark.mock
def test_mock_getbest(self, mock_content):
self.test_getbest(mock_content)
@pytest.mark.network
def test_getworst(self, content):
formatted_streams = YouTubeStreams(content.streams)
worst_stream = formatted_streams.getworst()
worst_stream["download_url"] = None
assert worst_stream == {
"bitrate": 50,
"download_url": None,
"encoding": 'opus',
"filesize": 1407962
}
# @pytest.mark.mock
def test_mock_getworst(self, mock_content):
self.test_getworst(mock_content)
class TestProviderYouTube:
@pytest.fixture(scope="module")
def youtube_provider(self):
return ProviderYouTube()
class MockYouTubeSearch:
watch_urls = []
def search(self, query):
return self.watch_urls
@pytest.mark.network
def test_from_query(self, track, youtube_provider):
metadata = youtube_provider.from_query(track)
assert isinstance(metadata["streams"], YouTubeStreams)
metadata["streams"] = []
assert metadata == {
'album': {'artists': [{'name': None}],
'images': [{'url': 'https://i.ytimg.com/vi/cH4E_t3m3xM/maxresdefault.jpg'}],
'name': None},
'artists': [{'name': 'SelenaGomezVEVO'}],
'copyright': None,
'disc_number': 1,
'duration': 213,
'external_ids': {'isrc': None},
'external_urls': {'youtube': 'https://youtube.com/watch?v=cH4E_t3m3xM'},
'genre': None,
'lyrics': None,
'name': 'Selena Gomez, Marshmello - Wolves',
'provider': 'youtube',
'publisher': None,
'release_date': '2017-11-1',
'streams': [],
'total_tracks': 1,
'track_number': 1,
'type': 'track',
'year': '2017'
}
def test_mock_from_query(self, track, youtube_provider, expect_mock_search_results, monkeypatch):
self.MockYouTubeSearch.watch_urls = expect_mock_search_results
monkeypatch.setattr(youtube, "YouTubeSearch", self.MockYouTubeSearch)
monkeypatch.setattr(pytube, "YouTube", MockYouTube)
self.test_from_query(track, youtube_provider)
@pytest.mark.network
def test_error_exception_from_query(self, no_result_track, youtube_provider):
with pytest.raises(YouTubeMetadataNotFoundError):
youtube_provider.from_query(no_result_track)
def test_mock_error_exception_from_query(self, no_result_track, youtube_provider, monkeypatch):
self.MockYouTubeSearch.watch_urls = []
monkeypatch.setattr(youtube, "YouTubeSearch", self.MockYouTubeSearch)
monkeypatch.setattr(pytube, "YouTube", MockYouTube)
self.test_error_exception_from_query(no_result_track, youtube_provider)

View File

@@ -5,6 +5,7 @@ import urllib.request
from spotdl.metadata import StreamsBase from spotdl.metadata import StreamsBase
from spotdl.metadata import ProviderBase from spotdl.metadata import ProviderBase
from spotdl.metadata.exceptions import YouTubeMetadataNotFoundError
BASE_URL = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q={}" BASE_URL = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q={}"
@@ -77,6 +78,9 @@ class YouTubeStreams(StreamsBase):
def __init__(self, streams): def __init__(self, streams):
audiostreams = streams.filter(only_audio=True).order_by("abr").desc() audiostreams = streams.filter(only_audio=True).order_by("abr").desc()
self.all = [{ self.all = [{
# Store only the integer part. For example the given
# bitrate would be "192kbps", we store only the integer
# part here and drop the rest.
"bitrate": int(stream.abr[:-4]), "bitrate": int(stream.abr[:-4]),
"download_url": stream.url, "download_url": stream.url,
"encoding": stream.audio_codec, "encoding": stream.audio_codec,
@@ -93,6 +97,11 @@ class YouTubeStreams(StreamsBase):
class ProviderYouTube(ProviderBase): class ProviderYouTube(ProviderBase):
def from_query(self, query): def from_query(self, query):
watch_urls = YouTubeSearch().search(query) watch_urls = YouTubeSearch().search(query)
if not watch_urls:
raise YouTubeMetadataNotFoundError(
'YouTube returned nothing for the given search '
'query ("{}")'.format(query)
)
return self.from_url(watch_urls[0]) return self.from_url(watch_urls[0])
def from_url(self, url): def from_url(self, url):
@@ -111,7 +120,6 @@ class ProviderYouTube(ProviderBase):
def metadata_to_standard_form(self, content): def metadata_to_standard_form(self, content):
""" Fetch a song's metadata from YouTube. """ """ Fetch a song's metadata from YouTube. """
streams = []
publish_date = self._fetch_publish_date(content) publish_date = self._fetch_publish_date(content)
metadata = { metadata = {
"name": content.title, "name": content.title,

View File

View File

@@ -0,0 +1,15 @@
from spotdl.metadata.exceptions import MetadataNotFoundError
from spotdl.metadata.exceptions import SpotifyMetadataNotFoundError
from spotdl.metadata.exceptions import YouTubeMetadataNotFoundError
class TestMetadataNotFoundSubclass:
def test_metadata_not_found_subclass(self):
assert issubclass(MetadataNotFoundError, Exception)
def test_spotify_metadata_not_found(self):
assert issubclass(SpotifyMetadataNotFoundError, MetadataNotFoundError)
def test_youtube_metadata_not_found(self):
assert issubclass(YouTubeMetadataNotFoundError, MetadataNotFoundError)

View File

@@ -0,0 +1,60 @@
from spotdl.metadata import ProviderBase
from spotdl.metadata import StreamsBase
import pytest
class TestStreamsBaseABC:
def test_error_abstract_base_class_streamsbase(self):
with pytest.raises(TypeError):
# This abstract base class must be inherited from
# for instantiation
StreamsBase()
def test_inherit_abstract_base_class_streamsbase(self):
class StreamsKid(StreamsBase):
def __init__(self, streams):
super().__init__(streams)
streams = ("stream1", "stream2", "stream3")
kid = StreamsKid(streams)
assert kid.all == streams
class TestMethods:
class StreamsKid(StreamsBase):
def __init__(self, streams):
super().__init__(streams)
@pytest.fixture(scope="module")
def streamskid(self):
streams = ("stream1", "stream2", "stream3")
streamskid = self.StreamsKid(streams)
return streamskid
def test_getbest(self, streamskid):
best_stream = streamskid.getbest()
assert best_stream == "stream1"
def test_getworst(self, streamskid):
worst_stream = streamskid.getworst()
assert worst_stream == "stream3"
class TestProviderBaseABC:
def test_error_abstract_base_class_providerbase(self):
with pytest.raises(TypeError):
# This abstract base class must be inherited from
# for instantiation
ProviderBase()
def test_inherit_abstract_base_class_providerbase(self):
class ProviderKid(ProviderBase):
def from_url(self, query):
pass
def metadata_to_standard_form(self, metadata):
pass
ProviderKid()