mirror of
https://github.com/KevinMidboe/spotify-downloader.git
synced 2025-10-29 01:40:16 +00:00
Write tests for YouTube metadata
This commit is contained in:
5
setup.cfg
Normal file
5
setup.cfg
Normal 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"')
|
||||
|
||||
@@ -81,7 +81,7 @@ class EncoderBase(ABC):
|
||||
pass
|
||||
|
||||
@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
|
||||
encoder such as defining the sample rate, audio bitrate,
|
||||
|
||||
@@ -26,7 +26,7 @@ class EncoderAvconv(EncoderBase):
|
||||
def get_encoding(self, 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)
|
||||
if initial_arguments is None:
|
||||
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:
|
||||
raise TypeError(
|
||||
'The output format ("{}") is not supported.'.format(
|
||||
@@ -45,19 +45,19 @@ class EncoderAvconv(EncoderBase):
|
||||
|
||||
return arguments
|
||||
|
||||
def _generate_encoding_arguments(self, input_encoding, output_encoding):
|
||||
def _generate_encoding_arguments(self, input_encoding, target_encoding):
|
||||
return ""
|
||||
|
||||
def set_debuglog(self):
|
||||
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)
|
||||
output_encoding = self.get_encoding(output_file)
|
||||
target_encoding = self.get_encoding(target_file)
|
||||
|
||||
arguments = self._generate_encoding_arguments(
|
||||
input_encoding,
|
||||
output_encoding
|
||||
target_encoding
|
||||
)
|
||||
|
||||
command = [self.encoder_path] \
|
||||
@@ -65,14 +65,14 @@ class EncoderAvconv(EncoderBase):
|
||||
+ self._loglevel.split() \
|
||||
+ ["-i", input_file] \
|
||||
+ self._additional_arguments \
|
||||
+ [output_file]
|
||||
+ [target_file]
|
||||
|
||||
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(
|
||||
input_file,
|
||||
output_file
|
||||
target_file
|
||||
)
|
||||
|
||||
returncode = subprocess.call(encode_command)
|
||||
|
||||
@@ -37,18 +37,18 @@ class EncoderFFmpeg(EncoderBase):
|
||||
def get_encoding(self, 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)
|
||||
if initial_arguments is None:
|
||||
raise TypeError(
|
||||
'The input format ("{}") is not supported.'.format(
|
||||
input_encoding,
|
||||
))
|
||||
arguments = initial_arguments.get(output_encoding)
|
||||
arguments = initial_arguments.get(target_encoding)
|
||||
if arguments is None:
|
||||
raise TypeError(
|
||||
'The output format ("{}") is not supported.'.format(
|
||||
output_encoding,
|
||||
target_encoding,
|
||||
))
|
||||
return arguments
|
||||
|
||||
@@ -56,14 +56,14 @@ class EncoderFFmpeg(EncoderBase):
|
||||
self._loglevel = "-loglevel debug"
|
||||
|
||||
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:
|
||||
input_encoding = self.get_encoding(input_path)
|
||||
if output_encoding is None:
|
||||
output_encoding = self.get_encoding(target_path)
|
||||
if target_encoding is None:
|
||||
target_encoding = self.get_encoding(target_path)
|
||||
arguments = self._generate_encoding_arguments(
|
||||
input_encoding,
|
||||
output_encoding
|
||||
target_encoding
|
||||
)
|
||||
command = [self.encoder_path] \
|
||||
+ ["-y", "-nostdin"] \
|
||||
@@ -88,7 +88,7 @@ class EncoderFFmpeg(EncoderBase):
|
||||
return process
|
||||
|
||||
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(
|
||||
"-",
|
||||
target_path,
|
||||
|
||||
@@ -15,12 +15,12 @@ class TestEncoderAvconv:
|
||||
|
||||
|
||||
class TestEncodingDefaults:
|
||||
def encode_command(input_file, output_file):
|
||||
def encode_command(input_file, target_file):
|
||||
command = [
|
||||
'avconv', '-y', '-loglevel', '0',
|
||||
'-i', input_file,
|
||||
'-ab', '192k',
|
||||
output_file,
|
||||
target_file,
|
||||
]
|
||||
return command
|
||||
|
||||
@@ -36,12 +36,12 @@ class TestEncodingDefaults:
|
||||
|
||||
|
||||
class TestEncodingInDebugMode:
|
||||
def debug_encode_command(input_file, output_file):
|
||||
def debug_encode_command(input_file, target_file):
|
||||
command = [
|
||||
'avconv', '-y', '-loglevel', 'debug',
|
||||
'-i', input_file,
|
||||
'-ab', '192k',
|
||||
output_file,
|
||||
target_file,
|
||||
]
|
||||
return command
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from spotdl.encode.encoders import EncoderFFmpeg
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestEncoderFFmpeg:
|
||||
def test_subclass(self):
|
||||
assert issubclass(EncoderFFmpeg, EncoderBase)
|
||||
|
||||
@@ -2,19 +2,19 @@ class EncoderNotFoundError(Exception):
|
||||
__module__ = Exception.__module__
|
||||
|
||||
def __init__(self, message=None):
|
||||
super(EncoderNotFoundError, self).__init__(message)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class FFmpegNotFoundError(EncoderNotFoundError):
|
||||
__module__ = Exception.__module__
|
||||
|
||||
def __init__(self, message=None):
|
||||
super(FFmpegNotFoundError, self).__init__(message)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class AvconvNotFoundError(EncoderNotFoundError):
|
||||
__module__ = Exception.__module__
|
||||
|
||||
def __init__(self, message=None):
|
||||
super(AvconvNotFoundError, self).__init__(message)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ from spotdl.encode.exceptions import EncoderNotFoundError
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestAbstractBaseClass:
|
||||
def test_error_abstract_base_class_encoderbase(self):
|
||||
encoder_path = "ffmpeg"
|
||||
@@ -27,15 +26,9 @@ class TestAbstractBaseClass:
|
||||
def _generate_encoding_arguments(self):
|
||||
pass
|
||||
|
||||
def get_encoding(self):
|
||||
pass
|
||||
|
||||
def re_encode(self):
|
||||
pass
|
||||
|
||||
def set_argument(self):
|
||||
pass
|
||||
|
||||
def set_debuglog(self):
|
||||
pass
|
||||
|
||||
@@ -52,21 +45,15 @@ class TestMethods:
|
||||
def __init__(self, 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
|
||||
|
||||
def _generate_encoding_arguments(self, input_encoding, output_encoding):
|
||||
def _generate_encoding_arguments(self, input_encoding, target_encoding):
|
||||
pass
|
||||
|
||||
def get_encoding(self, filename):
|
||||
return super().get_encoding(filename)
|
||||
|
||||
def re_encode(self, input_encoding, output_encoding):
|
||||
def re_encode(self, input_encoding, target_encoding):
|
||||
pass
|
||||
|
||||
def set_argument(self, argument):
|
||||
super().set_argument(argument)
|
||||
|
||||
def set_debuglog(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ from spotdl.encode.exceptions import EncoderNotFoundError
|
||||
from spotdl.encode.exceptions import FFmpegNotFoundError
|
||||
from spotdl.encode.exceptions import AvconvNotFoundError
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestEncoderNotFoundSubclass:
|
||||
def test_encoder_not_found_subclass(self):
|
||||
|
||||
@@ -2,4 +2,4 @@ class LyricsNotFoundError(Exception):
|
||||
__module__ = Exception.__module__
|
||||
|
||||
def __init__(self, message=None):
|
||||
super(LyricsNotFoundError, self).__init__(message)
|
||||
super().__init__(message)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from spotdl.metadata.provider_base import ProviderBase
|
||||
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
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from abc import abstractmethod
|
||||
|
||||
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:
|
||||
|
||||
>>> supported_formats = ("mp3", "opus", "flac")
|
||||
|
||||
20
spotdl/metadata/exceptions.py
Normal file
20
spotdl/metadata/exceptions.py
Normal 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)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
|
||||
|
||||
class StreamsBase(ABC):
|
||||
@abstractmethod
|
||||
def __init__(self, streams):
|
||||
@@ -17,7 +16,6 @@ class StreamsBase(ABC):
|
||||
"""
|
||||
self.all = streams
|
||||
|
||||
@abstractmethod
|
||||
def getbest(self):
|
||||
"""
|
||||
This method must return the audio stream with the
|
||||
@@ -25,7 +23,6 @@ class StreamsBase(ABC):
|
||||
"""
|
||||
return self.all[0]
|
||||
|
||||
@abstractmethod
|
||||
def getworst(self):
|
||||
"""
|
||||
This method must return the audio stream with the
|
||||
@@ -51,13 +48,12 @@ class ProviderBase(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def from_query(self, query):
|
||||
"""
|
||||
This method must return track metadata from the
|
||||
corresponding search query.
|
||||
"""
|
||||
pass
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def metadata_to_standard_form(self, metadata):
|
||||
@@ -67,3 +63,4 @@ class ProviderBase(ABC):
|
||||
providers, for easy utilization.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import spotipy
|
||||
import spotipy.oauth2 as oauth2
|
||||
|
||||
from spotdl.metadata import ProviderBase
|
||||
|
||||
from spotdl.metadata.exceptions import SpotifyMetadataNotFoundError
|
||||
|
||||
class ProviderSpotify(ProviderBase):
|
||||
def __init__(self, spotify=None):
|
||||
@@ -17,8 +17,14 @@ class ProviderSpotify(ProviderBase):
|
||||
return self.metadata_to_standard_form(metadata)
|
||||
|
||||
def from_query(self, query):
|
||||
metadata = self.spotify.search(query, limit=1)["tracks"]["items"][0]
|
||||
return self.metadata_to_standard_form(metadata)
|
||||
tracks = self.spotify.search(query, limit=1)["tracks"]["items"]
|
||||
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):
|
||||
""" Generate the token. """
|
||||
|
||||
0
spotdl/metadata/providers/tests/__init__.py
Normal file
0
spotdl/metadata/providers/tests/__init__.py
Normal file
BIN
spotdl/metadata/providers/tests/data/streams.dump
Normal file
BIN
spotdl/metadata/providers/tests/data/streams.dump
Normal file
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
13
spotdl/metadata/providers/tests/test_spotify.py
Normal file
13
spotdl/metadata/providers/tests/test_spotify.py
Normal 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.")
|
||||
|
||||
242
spotdl/metadata/providers/tests/test_youtube.py
Normal file
242
spotdl/metadata/providers/tests/test_youtube.py
Normal 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)
|
||||
|
||||
@@ -5,6 +5,7 @@ import urllib.request
|
||||
|
||||
from spotdl.metadata import StreamsBase
|
||||
from spotdl.metadata import ProviderBase
|
||||
from spotdl.metadata.exceptions import YouTubeMetadataNotFoundError
|
||||
|
||||
BASE_URL = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q={}"
|
||||
|
||||
@@ -77,6 +78,9 @@ class YouTubeStreams(StreamsBase):
|
||||
def __init__(self, streams):
|
||||
audiostreams = streams.filter(only_audio=True).order_by("abr").desc()
|
||||
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]),
|
||||
"download_url": stream.url,
|
||||
"encoding": stream.audio_codec,
|
||||
@@ -93,6 +97,11 @@ class YouTubeStreams(StreamsBase):
|
||||
class ProviderYouTube(ProviderBase):
|
||||
def from_query(self, 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])
|
||||
|
||||
def from_url(self, url):
|
||||
@@ -111,7 +120,6 @@ class ProviderYouTube(ProviderBase):
|
||||
|
||||
def metadata_to_standard_form(self, content):
|
||||
""" Fetch a song's metadata from YouTube. """
|
||||
streams = []
|
||||
publish_date = self._fetch_publish_date(content)
|
||||
metadata = {
|
||||
"name": content.title,
|
||||
|
||||
0
spotdl/metadata/tests/__init__.py
Normal file
0
spotdl/metadata/tests/__init__.py
Normal file
15
spotdl/metadata/tests/test_exceptions.py
Normal file
15
spotdl/metadata/tests/test_exceptions.py
Normal 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)
|
||||
|
||||
60
spotdl/metadata/tests/test_provider_base.py
Normal file
60
spotdl/metadata/tests/test_provider_base.py
Normal 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()
|
||||
|
||||
Reference in New Issue
Block a user