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
@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,

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

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

View File

@@ -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)

View File

@@ -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

View File

@@ -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):

View File

@@ -2,4 +2,4 @@ class LyricsNotFoundError(Exception):
__module__ = Exception.__module__
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 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

View File

@@ -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")

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 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

View File

@@ -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. """

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 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,

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()