mirror of
https://github.com/KevinMidboe/spotify-downloader.git
synced 2026-07-27 12:02:29 +00:00
Write tests for YouTube metadata
This commit is contained in:
@@ -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
|
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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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 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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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.")
|
||||||
|
|
||||||
@@ -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 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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
@@ -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