From 68c25e2aaafe2bad2ba1913c32481b6889ec100b Mon Sep 17 00:00:00 2001 From: Ritiek Malhotra Date: Fri, 27 Mar 2020 04:33:00 +0530 Subject: [PATCH] Write tests for YouTube metadata --- setup.cfg | 5 + spotdl/encode/encode_base.py | 2 +- spotdl/encode/encoders/avconv.py | 18 +- spotdl/encode/encoders/ffmpeg.py | 16 +- spotdl/encode/encoders/tests/test_avconv.py | 8 +- spotdl/encode/encoders/tests/test_ffmpeg.py | 1 - spotdl/encode/exceptions.py | 6 +- spotdl/encode/tests/test_encode_base.py | 19 +- spotdl/encode/tests/test_exceptions.py | 2 - spotdl/lyrics/exceptions.py | 2 +- spotdl/metadata/__init__.py | 4 + spotdl/metadata/embedder_base.py | 2 +- spotdl/metadata/exceptions.py | 20 ++ spotdl/metadata/provider_base.py | 7 +- spotdl/metadata/providers/spotify.py | 12 +- spotdl/metadata/providers/tests/__init__.py | 0 .../providers/tests/data/streams.dump | Bin 0 -> 41240 bytes .../tests/data/youtube_no_search_results.html | 1 + .../tests/data/youtube_search_results.html | 1 + .../metadata/providers/tests/test_spotify.py | 13 + .../metadata/providers/tests/test_youtube.py | 242 ++++++++++++++++++ spotdl/metadata/providers/youtube.py | 10 +- spotdl/metadata/tests/__init__.py | 0 spotdl/metadata/tests/test_exceptions.py | 15 ++ spotdl/metadata/tests/test_provider_base.py | 60 +++++ 25 files changed, 411 insertions(+), 55 deletions(-) create mode 100644 setup.cfg create mode 100644 spotdl/metadata/exceptions.py create mode 100644 spotdl/metadata/providers/tests/__init__.py create mode 100644 spotdl/metadata/providers/tests/data/streams.dump create mode 100644 spotdl/metadata/providers/tests/data/youtube_no_search_results.html create mode 100644 spotdl/metadata/providers/tests/data/youtube_search_results.html create mode 100644 spotdl/metadata/providers/tests/test_spotify.py create mode 100644 spotdl/metadata/providers/tests/test_youtube.py create mode 100644 spotdl/metadata/tests/__init__.py create mode 100644 spotdl/metadata/tests/test_exceptions.py create mode 100644 spotdl/metadata/tests/test_provider_base.py diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..f206a32 --- /dev/null +++ b/setup.cfg @@ -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"') + diff --git a/spotdl/encode/encode_base.py b/spotdl/encode/encode_base.py index 656c49d..650aacb 100644 --- a/spotdl/encode/encode_base.py +++ b/spotdl/encode/encode_base.py @@ -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, diff --git a/spotdl/encode/encoders/avconv.py b/spotdl/encode/encoders/avconv.py index 4a13429..7975f43 100644 --- a/spotdl/encode/encoders/avconv.py +++ b/spotdl/encode/encoders/avconv.py @@ -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) diff --git a/spotdl/encode/encoders/ffmpeg.py b/spotdl/encode/encoders/ffmpeg.py index 02a1187..98909c5 100644 --- a/spotdl/encode/encoders/ffmpeg.py +++ b/spotdl/encode/encoders/ffmpeg.py @@ -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, diff --git a/spotdl/encode/encoders/tests/test_avconv.py b/spotdl/encode/encoders/tests/test_avconv.py index 9cb3631..ca1f058 100644 --- a/spotdl/encode/encoders/tests/test_avconv.py +++ b/spotdl/encode/encoders/tests/test_avconv.py @@ -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 diff --git a/spotdl/encode/encoders/tests/test_ffmpeg.py b/spotdl/encode/encoders/tests/test_ffmpeg.py index 9fcdeb7..e18f3cd 100644 --- a/spotdl/encode/encoders/tests/test_ffmpeg.py +++ b/spotdl/encode/encoders/tests/test_ffmpeg.py @@ -4,7 +4,6 @@ from spotdl.encode.encoders import EncoderFFmpeg import pytest - class TestEncoderFFmpeg: def test_subclass(self): assert issubclass(EncoderFFmpeg, EncoderBase) diff --git a/spotdl/encode/exceptions.py b/spotdl/encode/exceptions.py index bffa07e..f8f8e2a 100644 --- a/spotdl/encode/exceptions.py +++ b/spotdl/encode/exceptions.py @@ -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) diff --git a/spotdl/encode/tests/test_encode_base.py b/spotdl/encode/tests/test_encode_base.py index f494276..21b813a 100644 --- a/spotdl/encode/tests/test_encode_base.py +++ b/spotdl/encode/tests/test_encode_base.py @@ -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 diff --git a/spotdl/encode/tests/test_exceptions.py b/spotdl/encode/tests/test_exceptions.py index 654b452..13d5846 100644 --- a/spotdl/encode/tests/test_exceptions.py +++ b/spotdl/encode/tests/test_exceptions.py @@ -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): diff --git a/spotdl/lyrics/exceptions.py b/spotdl/lyrics/exceptions.py index 0143a80..f7eb43d 100644 --- a/spotdl/lyrics/exceptions.py +++ b/spotdl/lyrics/exceptions.py @@ -2,4 +2,4 @@ class LyricsNotFoundError(Exception): __module__ = Exception.__module__ def __init__(self, message=None): - super(LyricsNotFoundError, self).__init__(message) + super().__init__(message) diff --git a/spotdl/metadata/__init__.py b/spotdl/metadata/__init__.py index 565363f..e8e5d15 100644 --- a/spotdl/metadata/__init__.py +++ b/spotdl/metadata/__init__.py @@ -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 diff --git a/spotdl/metadata/embedder_base.py b/spotdl/metadata/embedder_base.py index c61ab93..0ab1145 100644 --- a/spotdl/metadata/embedder_base.py +++ b/spotdl/metadata/embedder_base.py @@ -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") diff --git a/spotdl/metadata/exceptions.py b/spotdl/metadata/exceptions.py new file mode 100644 index 0000000..484141a --- /dev/null +++ b/spotdl/metadata/exceptions.py @@ -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) + diff --git a/spotdl/metadata/provider_base.py b/spotdl/metadata/provider_base.py index 28ba19d..e08ceb5 100644 --- a/spotdl/metadata/provider_base.py +++ b/spotdl/metadata/provider_base.py @@ -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 + diff --git a/spotdl/metadata/providers/spotify.py b/spotdl/metadata/providers/spotify.py index 5260006..b8d6407 100644 --- a/spotdl/metadata/providers/spotify.py +++ b/spotdl/metadata/providers/spotify.py @@ -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. """ diff --git a/spotdl/metadata/providers/tests/__init__.py b/spotdl/metadata/providers/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spotdl/metadata/providers/tests/data/streams.dump b/spotdl/metadata/providers/tests/data/streams.dump new file mode 100644 index 0000000000000000000000000000000000000000..acae3d20fbb839bda87643c787ccb5b4125f8fd6 GIT binary patch literal 41240 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

(SB5WO%n(IqDgiH@QvDgKlFRg)?W8+ z?K7I%!SN&78`!&akmJJsaqW#yYHxZ{d-Idp<60~v>?dy9|1=`(2@v)n5cY7?+%0l{ z?0-g-{UmD^r-9l(wNKGM2Sqdu#ESk4kn~eqlGyp6H)=2a@|$}kX;%{R)3@z^84)r9 zA@9D6L;s5C_}7;l3Qh>Le`cSaf3w4(p9MKTvn7X}8F-`imB0GU5AAV?sK)^;3;W+* z#h{6`vm>_T?YLJQ1pviir9f-H)=orli#vOk#;2^ zzjWLFj}ajT@=rJ%=uR2>Wg_H1u^)|)mOEz)*}t++(tqCJ(XWD{U)fT`4jZ~rd$IHJ z&(dD^cJ6v?~eu;BEW&B0}DV5yKwiJ4ft)5+T38H)3pyvi+fbl0Lk{qmO{14{a%8 zdzNq1w70(vJi5<_ee|~dzaoOZ5S)4EUG)3`(euCW8nHjzqsDm%ztwR`kn=41`iS<2 zTbkIW#v8TIJ1FmsoA%fh!{t01Z{K#FgKI?EJ3!h8?iw`bxzxa){N2XM8 zODtQNd878lx!3OzJG~c;uffZ=okwwvX#5(CKh{~@IsUJuRzTwo{NEdYwphz~9d7Sa z`x*4YC1+nxUmuxLxh=75CD)DG_kSG``_$^~9N@*<&Kq!z2>k{y^L=;G`4}|-I^T%@ z`{N&%oj2j;K9z5#;+IrDPG29HQn@XaY}v<++8?A|y2nyB{v6LQoEVk9V*IgO1SH?W zH6r<&z*|h~-sy!najF4CbNIg%fCRqm6Pu)BI|9Jd_eZ8wa7!(lFuqay?H9dvkJ{c=5KA!!;uGJ0Sq?3F+J>wVltV20-Uq@t?V&+vK+MIk**Zl)d2OsqiI@ zhxGN4ruLdC6%Bk(mivbKe+3^fl>Jx^mk?%{*>MU~jQOHhc@a(#k8e9ATqByl2Q+^; z6o7zc@-3Y*)dQj{_|F83vT@E4zU|XnrE)tGfW%x#FqI7mX4$$MwO@GK%R+)_XMQTq z=sl)V_olaw$G4pZt`WUI0D3=iH@!`&2lTe^|B~J|zU|Z7p>jL)p3ry3U@9BX%hC!r zYBlcFJM?A(dQYj;z3J`Z@onb}*NEO91ic@>qR@Ec3dN7(ek zh){k4!NE_%5&t>zfqw~p=dZ!X`yFzxJ_wiQqsJeZl1~8(Jtfn9{EjIZ#^d)-$=Cq5 vrv&K7@0$`_0&1p&LD1$Ysrm7Hrlds2AD)t29)Dy?!+reGDRB|8+|B<3(RWS8 literal 0 HcmV?d00001 diff --git a/spotdl/metadata/providers/tests/data/youtube_no_search_results.html b/spotdl/metadata/providers/tests/data/youtube_no_search_results.html new file mode 100644 index 0000000..96398df --- /dev/null +++ b/spotdl/metadata/providers/tests/data/youtube_no_search_results.html @@ -0,0 +1 @@ +b' \n \n \n\n\n\n\n \n\n \n\n\n \n \n \n\n \n \n\n \nn0 v1d305 3x157 f0r 7h15 53arc4 qu3ry - YouTube \n\n \n\n\n \n\n

\n
\n \n\n
\n
\n
\n
\n \nIN
\n
\n
\n\n
\n\n
\n
\n \n \n
\n
\n\n\n
\n to add this to Watch Later\n\n
\n
\n

\nAdd to\n

\n
\n
\n

\n \n\n \n Loading playlists...\n \n

\n\n
\n
\n \n \n \n\n \n \n\n\n\n' diff --git a/spotdl/metadata/providers/tests/data/youtube_search_results.html b/spotdl/metadata/providers/tests/data/youtube_search_results.html new file mode 100644 index 0000000..5d4c029 --- /dev/null +++ b/spotdl/metadata/providers/tests/data/youtube_search_results.html @@ -0,0 +1 @@ +b' \n \n \n\n\n\n\n \n\n \n\n\n \n \n \n\n \n \n\n \nselena gomez wolves - YouTube \n\n \n\n\n \n\n
\n
\n \n\n
\n
\n
\n
\n \nIN
\n
\n
\n\n
\n\n
\n
\n \n \n
\n
\n\n\n
\n to add this to Watch Later\n\n
\n
\n

\nAdd to\n

\n
\n
\n

\n \n\n \n Loading playlists...\n \n

\n\n
\n
\n \n \n \n\n \n \n\n\n\n' diff --git a/spotdl/metadata/providers/tests/test_spotify.py b/spotdl/metadata/providers/tests/test_spotify.py new file mode 100644 index 0000000..bf6f785 --- /dev/null +++ b/spotdl/metadata/providers/tests/test_spotify.py @@ -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.") + diff --git a/spotdl/metadata/providers/tests/test_youtube.py b/spotdl/metadata/providers/tests/test_youtube.py new file mode 100644 index 0000000..f61fb1d --- /dev/null +++ b/spotdl/metadata/providers/tests/test_youtube.py @@ -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) + diff --git a/spotdl/metadata/providers/youtube.py b/spotdl/metadata/providers/youtube.py index 4ef5a0a..2b0cdc9 100644 --- a/spotdl/metadata/providers/youtube.py +++ b/spotdl/metadata/providers/youtube.py @@ -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, diff --git a/spotdl/metadata/tests/__init__.py b/spotdl/metadata/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/spotdl/metadata/tests/test_exceptions.py b/spotdl/metadata/tests/test_exceptions.py new file mode 100644 index 0000000..ec5c32c --- /dev/null +++ b/spotdl/metadata/tests/test_exceptions.py @@ -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) + diff --git a/spotdl/metadata/tests/test_provider_base.py b/spotdl/metadata/tests/test_provider_base.py new file mode 100644 index 0000000..dd3a1f0 --- /dev/null +++ b/spotdl/metadata/tests/test_provider_base.py @@ -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() +