mirror of
https://github.com/KevinMidboe/spotify-downloader.git
synced 2025-10-29 18:00:15 +00:00
Merge branch 'master' into feat/keep-trackid-as-songname
This commit is contained in:
@@ -1,6 +1,4 @@
|
|||||||
dist: xenial
|
|
||||||
language: python
|
language: python
|
||||||
sudo: required
|
|
||||||
python:
|
python:
|
||||||
- "3.4"
|
- "3.4"
|
||||||
- "3.5"
|
- "3.5"
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
- Added `--no-remove-original-file` ([@NightMachinary](https://github.com/NightMachinary)) (#580)
|
- Added `--no-remove-original-file` ([@NightMachinary](https://github.com/NightMachinary)) (#580)
|
||||||
|
- Added leading Zeros in `track_number` for correct sorting ([@Dsujan](https://github.com/Dsujan)) (#592)
|
||||||
- Added `track_id` key for `--file-format` parameter ([@kadaliao](https://github.com/kadaliao)) (#568)
|
- Added `track_id` key for `--file-format` parameter ([@kadaliao](https://github.com/kadaliao)) (#568)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
-
|
- Generate list error --write-m3u ([@arthurlutz](https://github.com/arthurlutz)) (#559)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Fetch lyrics from Genius and fallback to LyricWikia if not found ([@ritiek](https://github.com/ritiek)) (#585)
|
- Fetch lyrics from Genius and fallback to LyricWikia if not found ([@ritiek](https://github.com/ritiek)) (#585)
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -10,7 +10,7 @@ setup(
|
|||||||
name="spotdl",
|
name="spotdl",
|
||||||
# Tests are included automatically:
|
# Tests are included automatically:
|
||||||
# https://docs.python.org/3.6/distutils/sourcedist.html#specifying-the-files-to-distribute
|
# https://docs.python.org/3.6/distutils/sourcedist.html#specifying-the-files-to-distribute
|
||||||
packages=["spotdl"],
|
packages=["spotdl", "spotdl.lyrics", "spotdl.lyrics.providers"],
|
||||||
version=spotdl.__version__,
|
version=spotdl.__version__,
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"pathlib >= 1.0.1",
|
"pathlib >= 1.0.1",
|
||||||
|
|||||||
@@ -16,7 +16,14 @@ https://trac.ffmpeg.org/wiki/Encode/AAC
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def song(input_song, output_song, folder, avconv=False, trim_silence=False, delete_original=True):
|
def song(
|
||||||
|
input_song,
|
||||||
|
output_song,
|
||||||
|
folder,
|
||||||
|
avconv=False,
|
||||||
|
trim_silence=False,
|
||||||
|
delete_original=True,
|
||||||
|
):
|
||||||
""" Do the audio format conversion. """
|
""" Do the audio format conversion. """
|
||||||
if avconv and trim_silence:
|
if avconv and trim_silence:
|
||||||
raise ValueError("avconv does not support trim_silence")
|
raise ValueError("avconv does not support trim_silence")
|
||||||
@@ -28,7 +35,9 @@ def song(input_song, output_song, folder, avconv=False, trim_silence=False, dele
|
|||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
convert = Converter(input_song, output_song, folder, delete_original=delete_original)
|
convert = Converter(
|
||||||
|
input_song, output_song, folder, delete_original=delete_original
|
||||||
|
)
|
||||||
if avconv:
|
if avconv:
|
||||||
exit_code, command = convert.with_avconv()
|
exit_code, command = convert.with_avconv()
|
||||||
else:
|
else:
|
||||||
@@ -97,7 +106,9 @@ class Converter:
|
|||||||
return code, command
|
return code, command
|
||||||
|
|
||||||
def with_ffmpeg(self, trim_silence=False):
|
def with_ffmpeg(self, trim_silence=False):
|
||||||
ffmpeg_pre = "ffmpeg -y -nostdin " # -nostdin is necessary for spotdl to be able to run in the backgroung.
|
ffmpeg_pre = (
|
||||||
|
"ffmpeg -y -nostdin "
|
||||||
|
) # -nostdin is necessary for spotdl to be able to run in the backgroung.
|
||||||
|
|
||||||
if not log.level == 10:
|
if not log.level == 10:
|
||||||
ffmpeg_pre += "-hide_banner -nostats -v panic "
|
ffmpeg_pre += "-hide_banner -nostats -v panic "
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ class Downloader:
|
|||||||
self.raw_song = raw_song
|
self.raw_song = raw_song
|
||||||
self.number = number
|
self.number = number
|
||||||
self.content, self.meta_tags = youtube_tools.match_video_and_metadata(raw_song)
|
self.content, self.meta_tags = youtube_tools.match_video_and_metadata(raw_song)
|
||||||
|
self.total_songs = int(self.meta_tags["total_tracks"])
|
||||||
|
|
||||||
def download_single(self):
|
def download_single(self):
|
||||||
""" Logic behind downloading a song. """
|
""" Logic behind downloading a song. """
|
||||||
@@ -158,7 +159,10 @@ class Downloader:
|
|||||||
def refine_songname(self, songname):
|
def refine_songname(self, songname):
|
||||||
if self.meta_tags is not None:
|
if self.meta_tags is not None:
|
||||||
refined_songname = internals.format_string(
|
refined_songname = internals.format_string(
|
||||||
const.args.file_format, self.meta_tags, slugification=True
|
const.args.file_format,
|
||||||
|
self.meta_tags,
|
||||||
|
slugification=True,
|
||||||
|
total_songs=self.total_songs,
|
||||||
)
|
)
|
||||||
log.debug(
|
log.debug(
|
||||||
'Refining songname from "{0}" to "{1}"'.format(
|
'Refining songname from "{0}" to "{1}"'.format(
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ default_conf = {
|
|||||||
"write-successful": None,
|
"write-successful": None,
|
||||||
"log-level": "INFO",
|
"log-level": "INFO",
|
||||||
"spotify_client_id": "4fe3fecfe5334023a1472516cc99d805",
|
"spotify_client_id": "4fe3fecfe5334023a1472516cc99d805",
|
||||||
"spotify_client_secret": "0f02b7c483c04257984695007a4a8d5c"
|
"spotify_client_secret": "0f02b7c483c04257984695007a4a8d5c",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,13 +280,13 @@ def get_arguments(raw_args=None, to_group=True, to_merge=True):
|
|||||||
"-sci",
|
"-sci",
|
||||||
"--spotify-client-id",
|
"--spotify-client-id",
|
||||||
default=config["spotify_client_id"],
|
default=config["spotify_client_id"],
|
||||||
help=argparse.SUPPRESS
|
help=argparse.SUPPRESS,
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-scs",
|
"-scs",
|
||||||
"--spotify-client-secret",
|
"--spotify-client-secret",
|
||||||
default=config["spotify_client_secret"],
|
default=config["spotify_client_secret"],
|
||||||
help=argparse.SUPPRESS
|
help=argparse.SUPPRESS,
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-c", "--config", default=None, help="path to custom config.yml file"
|
"-c", "--config", default=None, help="path to custom config.yml file"
|
||||||
@@ -320,11 +320,12 @@ def get_arguments(raw_args=None, to_group=True, to_merge=True):
|
|||||||
if parsed.avconv and parsed.trim_silence:
|
if parsed.avconv and parsed.trim_silence:
|
||||||
parser.error("--trim-silence can only be used with FFmpeg")
|
parser.error("--trim-silence can only be used with FFmpeg")
|
||||||
|
|
||||||
if parsed.write_to and not (parsed.playlist \
|
if parsed.write_to and not (
|
||||||
or parsed.album \
|
parsed.playlist or parsed.album or parsed.all_albums or parsed.username
|
||||||
or parsed.all_albums \
|
):
|
||||||
or parsed.username):
|
parser.error(
|
||||||
parser.error("--write-to can only be used with --playlist, --album, --all-albums, or --username")
|
"--write-to can only be used with --playlist, --album, --all-albums, or --username"
|
||||||
|
)
|
||||||
|
|
||||||
parsed.log_level = log_leveller(parsed.log_level)
|
parsed.log_level = log_leveller(parsed.log_level)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from logzero import logger as log
|
from logzero import logger as log
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import math
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
from spotdl import const
|
from spotdl import const
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -74,7 +76,9 @@ def is_youtube(raw_song):
|
|||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
||||||
def format_string(string_format, tags, slugification=False, force_spaces=False):
|
def format_string(
|
||||||
|
string_format, tags, slugification=False, force_spaces=False, total_songs=0
|
||||||
|
):
|
||||||
""" Generate a string of the format '[artist] - [song]' for the given spotify song. """
|
""" Generate a string of the format '[artist] - [song]' for the given spotify song. """
|
||||||
format_tags = dict(formats)
|
format_tags = dict(formats)
|
||||||
format_tags[0] = tags["name"]
|
format_tags[0] = tags["name"]
|
||||||
@@ -95,9 +99,16 @@ def format_string(string_format, tags, slugification=False, force_spaces=False):
|
|||||||
k: sanitize_title(str(v), ok="'-_()[]{}") if slugification else str(v)
|
k: sanitize_title(str(v), ok="'-_()[]{}") if slugification else str(v)
|
||||||
for k, v in format_tags.items()
|
for k, v in format_tags.items()
|
||||||
}
|
}
|
||||||
|
# calculating total digits presnet in total_songs to prepare a zfill.
|
||||||
|
total_digits = 0 if total_songs == 0 else int(math.log10(total_songs)) + 1
|
||||||
|
|
||||||
for x in formats:
|
for x in formats:
|
||||||
format_tag = "{" + formats[x] + "}"
|
format_tag = "{" + formats[x] + "}"
|
||||||
|
# Making consistent track number by prepending zero
|
||||||
|
# on it according to number of digits in total songs
|
||||||
|
if format_tag == "{track_number}":
|
||||||
|
format_tags_sanitized[x] = format_tags_sanitized[x].zfill(total_digits)
|
||||||
|
|
||||||
string_format = string_format.replace(format_tag, format_tags_sanitized[x])
|
string_format = string_format.replace(format_tag, format_tags_sanitized[x])
|
||||||
|
|
||||||
if const.args.no_spaces and not force_spaces:
|
if const.args.no_spaces and not force_spaces:
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class TestGenius:
|
|||||||
assert track.base_url == "https://genius.com"
|
assert track.base_url == "https://genius.com"
|
||||||
|
|
||||||
def test_get_lyrics(self, track, monkeypatch):
|
def test_get_lyrics(self, track, monkeypatch):
|
||||||
|
|
||||||
def mocked_urlopen(url, timeout=None):
|
def mocked_urlopen(url, timeout=None):
|
||||||
class DummyHTTPResponse:
|
class DummyHTTPResponse:
|
||||||
def read(self):
|
def read(self):
|
||||||
@@ -30,7 +29,6 @@ class TestGenius:
|
|||||||
assert track.get_lyrics() == "amazing lyrics!"
|
assert track.get_lyrics() == "amazing lyrics!"
|
||||||
|
|
||||||
def test_lyrics_not_found_error(self, track, monkeypatch):
|
def test_lyrics_not_found_error(self, track, monkeypatch):
|
||||||
|
|
||||||
def mocked_urlopen(url, timeout=None):
|
def mocked_urlopen(url, timeout=None):
|
||||||
raise urllib.request.HTTPError("", "", "", "", "")
|
raise urllib.request.HTTPError("", "", "", "", "")
|
||||||
|
|
||||||
|
|||||||
@@ -15,17 +15,21 @@ class TestLyricWikia:
|
|||||||
# `LyricWikia` class uses the 3rd party method `lyricwikia.get_lyrics`
|
# `LyricWikia` class uses the 3rd party method `lyricwikia.get_lyrics`
|
||||||
# internally and there is no need to test a 3rd party library as they
|
# internally and there is no need to test a 3rd party library as they
|
||||||
# have their own implementation of tests.
|
# have their own implementation of tests.
|
||||||
monkeypatch.setattr("lyricwikia.get_lyrics", lambda a, b, c, d: "awesome lyrics!")
|
monkeypatch.setattr(
|
||||||
|
"lyricwikia.get_lyrics", lambda a, b, c, d: "awesome lyrics!"
|
||||||
|
)
|
||||||
track = LyricWikia("Lyricwikia", "Lyricwikia")
|
track = LyricWikia("Lyricwikia", "Lyricwikia")
|
||||||
assert track.get_lyrics() == "awesome lyrics!"
|
assert track.get_lyrics() == "awesome lyrics!"
|
||||||
|
|
||||||
def test_lyrics_not_found_error(self, monkeypatch):
|
def test_lyrics_not_found_error(self, monkeypatch):
|
||||||
|
|
||||||
def lyricwikia_lyrics_not_found(msg):
|
def lyricwikia_lyrics_not_found(msg):
|
||||||
raise lyricwikia.LyricsNotFound(msg)
|
raise lyricwikia.LyricsNotFound(msg)
|
||||||
|
|
||||||
# Wrap `lyricwikia.LyricsNotFound` with `exceptions.LyricsNotFound` error.
|
# Wrap `lyricwikia.LyricsNotFound` with `exceptions.LyricsNotFound` error.
|
||||||
monkeypatch.setattr("lyricwikia.get_lyrics", lambda a, b, c, d: lyricwikia_lyrics_not_found("Nope, no lyrics."))
|
monkeypatch.setattr(
|
||||||
|
"lyricwikia.get_lyrics",
|
||||||
|
lambda a, b, c, d: lyricwikia_lyrics_not_found("Nope, no lyrics."),
|
||||||
|
)
|
||||||
track = LyricWikia("Lyricwikia", "Lyricwikia")
|
track = LyricWikia("Lyricwikia", "Lyricwikia")
|
||||||
with pytest.raises(exceptions.LyricsNotFound):
|
with pytest.raises(exceptions.LyricsNotFound):
|
||||||
track.get_lyrics()
|
track.get_lyrics()
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ class EmbedMetadata:
|
|||||||
audiofile["TYER"] = TYER(encoding=3, text=meta_tags["year"])
|
audiofile["TYER"] = TYER(encoding=3, text=meta_tags["year"])
|
||||||
if meta_tags["publisher"]:
|
if meta_tags["publisher"]:
|
||||||
audiofile["TPUB"] = TPUB(encoding=3, text=meta_tags["publisher"])
|
audiofile["TPUB"] = TPUB(encoding=3, text=meta_tags["publisher"])
|
||||||
audiofile["COMM"] = COMM(encoding=3, text=meta_tags["external_urls"][self.provider])
|
audiofile["COMM"] = COMM(
|
||||||
|
encoding=3, text=meta_tags["external_urls"][self.provider]
|
||||||
|
)
|
||||||
if meta_tags["lyrics"]:
|
if meta_tags["lyrics"]:
|
||||||
audiofile["USLT"] = USLT(
|
audiofile["USLT"] = USLT(
|
||||||
encoding=3, desc=u"Lyrics", text=meta_tags["lyrics"]
|
encoding=3, desc=u"Lyrics", text=meta_tags["lyrics"]
|
||||||
|
|||||||
@@ -11,24 +11,30 @@ def _getbestthumb(self):
|
|||||||
|
|
||||||
part_url = "https://i.ytimg.com/vi/%s/" % self.videoid
|
part_url = "https://i.ytimg.com/vi/%s/" % self.videoid
|
||||||
# Thumbnail resolution sorted in descending order
|
# Thumbnail resolution sorted in descending order
|
||||||
thumbs = ("maxresdefault.jpg",
|
thumbs = (
|
||||||
"sddefault.jpg",
|
"maxresdefault.jpg",
|
||||||
"hqdefault.jpg",
|
"sddefault.jpg",
|
||||||
"mqdefault.jpg",
|
"hqdefault.jpg",
|
||||||
"default.jpg")
|
"mqdefault.jpg",
|
||||||
|
"default.jpg",
|
||||||
|
)
|
||||||
for thumb in thumbs:
|
for thumb in thumbs:
|
||||||
url = part_url + thumb
|
url = part_url + thumb
|
||||||
if self._content_available(url):
|
if self._content_available(url):
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
def _process_streams(self):
|
def _process_streams(self):
|
||||||
for format_index in range(len(self._ydl_info['formats'])):
|
for format_index in range(len(self._ydl_info["formats"])):
|
||||||
try:
|
try:
|
||||||
self._ydl_info['formats'][format_index]['url'] = self._ydl_info['formats'][format_index]['fragment_base_url']
|
self._ydl_info["formats"][format_index]["url"] = self._ydl_info["formats"][
|
||||||
|
format_index
|
||||||
|
]["fragment_base_url"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
return backend_youtube_dl.YtdlPafy._old_process_streams(self)
|
return backend_youtube_dl.YtdlPafy._old_process_streams(self)
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _content_available(cls, url):
|
def _content_available(cls, url):
|
||||||
return internals.content_available(url)
|
return internals.content_available(url)
|
||||||
@@ -39,6 +45,7 @@ class PatchPafy:
|
|||||||
These patches have not been released by pafy on PyPI yet but
|
These patches have not been released by pafy on PyPI yet but
|
||||||
are useful to us.
|
are useful to us.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def patch_getbestthumb(self):
|
def patch_getbestthumb(self):
|
||||||
# https://github.com/mps-youtube/pafy/pull/211
|
# https://github.com/mps-youtube/pafy/pull/211
|
||||||
pafy.backend_shared.BasePafy._bestthumb = None
|
pafy.backend_shared.BasePafy._bestthumb = None
|
||||||
@@ -47,7 +54,9 @@ class PatchPafy:
|
|||||||
|
|
||||||
def patch_process_streams(self):
|
def patch_process_streams(self):
|
||||||
# https://github.com/mps-youtube/pafy/pull/230
|
# https://github.com/mps-youtube/pafy/pull/230
|
||||||
backend_youtube_dl.YtdlPafy._old_process_streams = backend_youtube_dl.YtdlPafy._process_streams
|
backend_youtube_dl.YtdlPafy._old_process_streams = (
|
||||||
|
backend_youtube_dl.YtdlPafy._process_streams
|
||||||
|
)
|
||||||
backend_youtube_dl.YtdlPafy._process_streams = _process_streams
|
backend_youtube_dl.YtdlPafy._process_streams = _process_streams
|
||||||
|
|
||||||
def patch_insecure_streams(self):
|
def patch_insecure_streams(self):
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ def match_args():
|
|||||||
track_dl.download_single()
|
track_dl.download_single()
|
||||||
elif const.args.list:
|
elif const.args.list:
|
||||||
if const.args.write_m3u:
|
if const.args.write_m3u:
|
||||||
youtube_tools.generate_m3u(track_file=const.args.list,
|
youtube_tools.generate_m3u(
|
||||||
text_file=const.args.write_to)
|
track_file=const.args.list
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
list_dl = downloader.ListDownloader(
|
list_dl = downloader.ListDownloader(
|
||||||
tracks_file=const.args.list,
|
tracks_file=const.args.list,
|
||||||
@@ -38,17 +39,21 @@ def match_args():
|
|||||||
)
|
)
|
||||||
list_dl.download_list()
|
list_dl.download_list()
|
||||||
elif const.args.playlist:
|
elif const.args.playlist:
|
||||||
spotify_tools.write_playlist(playlist_url=const.args.playlist,
|
spotify_tools.write_playlist(
|
||||||
text_file=const.args.write_to)
|
playlist_url=const.args.playlist, text_file=const.args.write_to
|
||||||
|
)
|
||||||
elif const.args.album:
|
elif const.args.album:
|
||||||
spotify_tools.write_album(album_url=const.args.album,
|
spotify_tools.write_album(
|
||||||
text_file=const.args.write_to)
|
album_url=const.args.album, text_file=const.args.write_to
|
||||||
|
)
|
||||||
elif const.args.all_albums:
|
elif const.args.all_albums:
|
||||||
spotify_tools.write_all_albums_from_artist(artist_url=const.args.all_albums,
|
spotify_tools.write_all_albums_from_artist(
|
||||||
text_file=const.args.write_to)
|
artist_url=const.args.all_albums, text_file=const.args.write_to
|
||||||
|
)
|
||||||
elif const.args.username:
|
elif const.args.username:
|
||||||
spotify_tools.write_user_playlist(username=const.args.username,
|
spotify_tools.write_user_playlist(
|
||||||
text_file=const.args.write_to)
|
username=const.args.username, text_file=const.args.write_to
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ from spotdl.lyrics.exceptions import LyricsNotFound
|
|||||||
spotify = None
|
spotify = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def generate_token():
|
def generate_token():
|
||||||
""" Generate the token. """
|
""" Generate the token. """
|
||||||
credentials = oauth2.SpotifyClientCredentials(
|
credentials = oauth2.SpotifyClientCredentials(
|
||||||
@@ -39,6 +37,7 @@ def must_be_authorized(func, spotify=spotify):
|
|||||||
token = generate_token()
|
token = generate_token()
|
||||||
spotify = spotipy.Spotify(auth=token)
|
spotify = spotipy.Spotify(auth=token)
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pafy.g.opener.addheaders.append(("Range", "bytes=0-"))
|
|||||||
# More info: https://github.com/mps-youtube/pafy/pull/211
|
# More info: https://github.com/mps-youtube/pafy/pull/211
|
||||||
if pafy.__version__ <= "0.5.4":
|
if pafy.__version__ <= "0.5.4":
|
||||||
from spotdl import patcher
|
from spotdl import patcher
|
||||||
|
|
||||||
pafy_patcher = patcher.PatchPafy()
|
pafy_patcher = patcher.PatchPafy()
|
||||||
pafy_patcher.patch_getbestthumb()
|
pafy_patcher.patch_getbestthumb()
|
||||||
pafy_patcher.patch_process_streams()
|
pafy_patcher.patch_process_streams()
|
||||||
@@ -52,10 +53,13 @@ def match_video_and_metadata(track):
|
|||||||
""" Get and match track data from YouTube and Spotify. """
|
""" Get and match track data from YouTube and Spotify. """
|
||||||
meta_tags = None
|
meta_tags = None
|
||||||
|
|
||||||
|
|
||||||
def fallback_metadata(meta_tags):
|
def fallback_metadata(meta_tags):
|
||||||
fallback_metadata_info = "Track not found on Spotify, falling back on YouTube metadata"
|
fallback_metadata_info = (
|
||||||
skip_fallback_metadata_warning = "Fallback condition not met, shall not embed metadata"
|
"Track not found on Spotify, falling back on YouTube metadata"
|
||||||
|
)
|
||||||
|
skip_fallback_metadata_warning = (
|
||||||
|
"Fallback condition not met, shall not embed metadata"
|
||||||
|
)
|
||||||
if meta_tags is None:
|
if meta_tags is None:
|
||||||
if const.args.no_fallback_metadata:
|
if const.args.no_fallback_metadata:
|
||||||
log.warning(skip_fallback_metadata_warning)
|
log.warning(skip_fallback_metadata_warning)
|
||||||
@@ -64,7 +68,6 @@ def match_video_and_metadata(track):
|
|||||||
meta_tags = generate_metadata(content)
|
meta_tags = generate_metadata(content)
|
||||||
return meta_tags
|
return meta_tags
|
||||||
|
|
||||||
|
|
||||||
if internals.is_youtube(track):
|
if internals.is_youtube(track):
|
||||||
log.debug("Input song is a YouTube URL")
|
log.debug("Input song is a YouTube URL")
|
||||||
content = go_pafy(track, meta_tags=None)
|
content = go_pafy(track, meta_tags=None)
|
||||||
@@ -95,25 +98,29 @@ def match_video_and_metadata(track):
|
|||||||
|
|
||||||
def generate_metadata(content):
|
def generate_metadata(content):
|
||||||
""" Fetch a song's metadata from YouTube. """
|
""" Fetch a song's metadata from YouTube. """
|
||||||
meta_tags = {"spotify_metadata": False,
|
meta_tags = {
|
||||||
"name": content.title,
|
"spotify_metadata": False,
|
||||||
"artists": [{"name": content.author}],
|
"name": content.title,
|
||||||
"duration": content.length,
|
"artists": [{"name": content.author}],
|
||||||
"external_urls": {"youtube": content.watchv_url},
|
"duration": content.length,
|
||||||
"album": {"images" : [{"url": content.getbestthumb()}],
|
"external_urls": {"youtube": content.watchv_url},
|
||||||
"artists": [{"name": None}],"name": None},
|
"album": {
|
||||||
"year": content.published.split("-")[0],
|
"images": [{"url": content.getbestthumb()}],
|
||||||
"release_date": content.published.split(" ")[0],
|
"artists": [{"name": None}],
|
||||||
"type": "track",
|
"name": None,
|
||||||
"disc_number": 1,
|
},
|
||||||
"track_number": 1,
|
"year": content.published.split("-")[0],
|
||||||
"total_tracks": 1,
|
"release_date": content.published.split(" ")[0],
|
||||||
"publisher": None,
|
"type": "track",
|
||||||
"external_ids": {"isrc": None},
|
"disc_number": 1,
|
||||||
"lyrics": None,
|
"track_number": 1,
|
||||||
"copyright": None,
|
"total_tracks": 1,
|
||||||
"genre": None,
|
"publisher": None,
|
||||||
}
|
"external_ids": {"isrc": None},
|
||||||
|
"lyrics": None,
|
||||||
|
"copyright": None,
|
||||||
|
"genre": None,
|
||||||
|
}
|
||||||
|
|
||||||
return meta_tags
|
return meta_tags
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def load_defaults():
|
|||||||
# so that we get same results even if YouTube changes the list/order of videos on their page.
|
# so that we get same results even if YouTube changes the list/order of videos on their page.
|
||||||
GIST_URL = "https://gist.githubusercontent.com/ritiek/e731338e9810e31c2f00f13c249a45f5/raw/c11a27f3b5d11a8d082976f1cdd237bd605ec2c2/search_results.html"
|
GIST_URL = "https://gist.githubusercontent.com/ritiek/e731338e9810e31c2f00f13c249a45f5/raw/c11a27f3b5d11a8d082976f1cdd237bd605ec2c2/search_results.html"
|
||||||
|
|
||||||
|
|
||||||
def monkeypatch_youtube_search_page(*args, **kwargs):
|
def monkeypatch_youtube_search_page(*args, **kwargs):
|
||||||
fake_urlopen = urllib.request.urlopen(GIST_URL)
|
fake_urlopen = urllib.request.urlopen(GIST_URL)
|
||||||
return fake_urlopen
|
return fake_urlopen
|
||||||
|
|
||||||
|
|||||||
@@ -101,16 +101,28 @@ class TestDownload:
|
|||||||
|
|
||||||
def test_m4a(self, monkeypatch, filename_fixture):
|
def test_m4a(self, monkeypatch, filename_fixture):
|
||||||
expect_download = True
|
expect_download = True
|
||||||
monkeypatch.setattr("pafy.backend_shared.BaseStream.download", self.blank_audio_generator)
|
monkeypatch.setattr(
|
||||||
monkeypatch.setattr("pafy.backend_youtube_dl.YtdlStream.download", self.blank_audio_generator)
|
"pafy.backend_shared.BaseStream.download", self.blank_audio_generator
|
||||||
download = youtube_tools.download_song(filename_fixture + ".m4a", pytest.content_fixture)
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"pafy.backend_youtube_dl.YtdlStream.download", self.blank_audio_generator
|
||||||
|
)
|
||||||
|
download = youtube_tools.download_song(
|
||||||
|
filename_fixture + ".m4a", pytest.content_fixture
|
||||||
|
)
|
||||||
assert download == expect_download
|
assert download == expect_download
|
||||||
|
|
||||||
def test_webm(self, monkeypatch, filename_fixture):
|
def test_webm(self, monkeypatch, filename_fixture):
|
||||||
expect_download = True
|
expect_download = True
|
||||||
monkeypatch.setattr("pafy.backend_shared.BaseStream.download", self.blank_audio_generator)
|
monkeypatch.setattr(
|
||||||
monkeypatch.setattr("pafy.backend_youtube_dl.YtdlStream.download", self.blank_audio_generator)
|
"pafy.backend_shared.BaseStream.download", self.blank_audio_generator
|
||||||
download = youtube_tools.download_song(filename_fixture + ".webm", pytest.content_fixture)
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"pafy.backend_youtube_dl.YtdlStream.download", self.blank_audio_generator
|
||||||
|
)
|
||||||
|
download = youtube_tools.download_song(
|
||||||
|
filename_fixture + ".webm", pytest.content_fixture
|
||||||
|
)
|
||||||
assert download == expect_download
|
assert download == expect_download
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import pytest
|
|||||||
pafy_patcher = patcher.PatchPafy()
|
pafy_patcher = patcher.PatchPafy()
|
||||||
pafy_patcher.patch_getbestthumb()
|
pafy_patcher.patch_getbestthumb()
|
||||||
|
|
||||||
|
|
||||||
class TestPafyContentAvailable:
|
class TestPafyContentAvailable:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -30,7 +31,6 @@ class TestMethodCalls:
|
|||||||
thumbnail = patcher._getbestthumb(content_fixture)
|
thumbnail = patcher._getbestthumb(content_fixture)
|
||||||
assert thumbnail == "https://i.ytimg.com/vi/3nQNiWdeH2Q/sddefault.jpg"
|
assert thumbnail == "https://i.ytimg.com/vi/3nQNiWdeH2Q/sddefault.jpg"
|
||||||
|
|
||||||
|
|
||||||
def test_pafy_content_available(self):
|
def test_pafy_content_available(self):
|
||||||
TestPafyContentAvailable._content_available = patcher._content_available
|
TestPafyContentAvailable._content_available = patcher._content_available
|
||||||
assert TestPafyContentAvailable()._content_available("https://youtube.com/")
|
assert TestPafyContentAvailable()._content_available("https://youtube.com/")
|
||||||
|
|||||||
@@ -104,19 +104,19 @@ def content_fixture(metadata_fixture):
|
|||||||
MATCH_METADATA_NO_FALLBACK_TEST_TABLE = [
|
MATCH_METADATA_NO_FALLBACK_TEST_TABLE = [
|
||||||
("https://open.spotify.com/track/5nWduGwBGBn1PSqYTJUDbS", True),
|
("https://open.spotify.com/track/5nWduGwBGBn1PSqYTJUDbS", True),
|
||||||
("http://youtube.com/watch?v=3nQNiWdeH2Q", None),
|
("http://youtube.com/watch?v=3nQNiWdeH2Q", None),
|
||||||
("Linux Talk | Working with Drives and Filesystems", None)
|
("Linux Talk | Working with Drives and Filesystems", None),
|
||||||
]
|
]
|
||||||
|
|
||||||
MATCH_METADATA_FALLBACK_TEST_TABLE = [
|
MATCH_METADATA_FALLBACK_TEST_TABLE = [
|
||||||
("https://open.spotify.com/track/5nWduGwBGBn1PSqYTJUDbS", True),
|
("https://open.spotify.com/track/5nWduGwBGBn1PSqYTJUDbS", True),
|
||||||
("http://youtube.com/watch?v=3nQNiWdeH2Q", False),
|
("http://youtube.com/watch?v=3nQNiWdeH2Q", False),
|
||||||
("Linux Talk | Working with Drives and Filesystems", False)
|
("Linux Talk | Working with Drives and Filesystems", False),
|
||||||
]
|
]
|
||||||
|
|
||||||
MATCH_METADATA_NO_METADATA_TEST_TABLE = [
|
MATCH_METADATA_NO_METADATA_TEST_TABLE = [
|
||||||
("https://open.spotify.com/track/5nWduGwBGBn1PSqYTJUDbS", None),
|
("https://open.spotify.com/track/5nWduGwBGBn1PSqYTJUDbS", None),
|
||||||
("http://youtube.com/watch?v=3nQNiWdeH2Q", None),
|
("http://youtube.com/watch?v=3nQNiWdeH2Q", None),
|
||||||
("Linux Talk | Working with Drives and Filesystems", None)
|
("Linux Talk | Working with Drives and Filesystems", None),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -128,21 +128,37 @@ class TestMetadataOrigin:
|
|||||||
else:
|
else:
|
||||||
assert metadata["spotify_metadata"] == metadata_type
|
assert metadata["spotify_metadata"] == metadata_type
|
||||||
|
|
||||||
@pytest.mark.parametrize("track, metadata_type", MATCH_METADATA_NO_FALLBACK_TEST_TABLE)
|
@pytest.mark.parametrize(
|
||||||
def test_match_metadata_with_no_fallback(self, track, metadata_type, content_fixture, monkeypatch):
|
"track, metadata_type", MATCH_METADATA_NO_FALLBACK_TEST_TABLE
|
||||||
monkeypatch.setattr(youtube_tools, "go_pafy", lambda track, meta_tags: content_fixture)
|
)
|
||||||
|
def test_match_metadata_with_no_fallback(
|
||||||
|
self, track, metadata_type, content_fixture, monkeypatch
|
||||||
|
):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
youtube_tools, "go_pafy", lambda track, meta_tags: content_fixture
|
||||||
|
)
|
||||||
const.args.no_fallback_metadata = True
|
const.args.no_fallback_metadata = True
|
||||||
self.match_metadata(track, metadata_type)
|
self.match_metadata(track, metadata_type)
|
||||||
|
|
||||||
@pytest.mark.parametrize("track, metadata_type", MATCH_METADATA_FALLBACK_TEST_TABLE)
|
@pytest.mark.parametrize("track, metadata_type", MATCH_METADATA_FALLBACK_TEST_TABLE)
|
||||||
def test_match_metadata_with_fallback(self, track, metadata_type, content_fixture, monkeypatch):
|
def test_match_metadata_with_fallback(
|
||||||
monkeypatch.setattr(youtube_tools, "go_pafy", lambda track, meta_tags: content_fixture)
|
self, track, metadata_type, content_fixture, monkeypatch
|
||||||
|
):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
youtube_tools, "go_pafy", lambda track, meta_tags: content_fixture
|
||||||
|
)
|
||||||
const.args.no_fallback_metadata = False
|
const.args.no_fallback_metadata = False
|
||||||
self.match_metadata(track, metadata_type)
|
self.match_metadata(track, metadata_type)
|
||||||
|
|
||||||
@pytest.mark.parametrize("track, metadata_type", MATCH_METADATA_NO_METADATA_TEST_TABLE)
|
@pytest.mark.parametrize(
|
||||||
def test_match_metadata_with_no_metadata(self, track, metadata_type, content_fixture, monkeypatch):
|
"track, metadata_type", MATCH_METADATA_NO_METADATA_TEST_TABLE
|
||||||
monkeypatch.setattr(youtube_tools, "go_pafy", lambda track, meta_tags: content_fixture)
|
)
|
||||||
|
def test_match_metadata_with_no_metadata(
|
||||||
|
self, track, metadata_type, content_fixture, monkeypatch
|
||||||
|
):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
youtube_tools, "go_pafy", lambda track, meta_tags: content_fixture
|
||||||
|
)
|
||||||
const.args.no_metadata = True
|
const.args.no_metadata = True
|
||||||
self.match_metadata(track, metadata_type)
|
self.match_metadata(track, metadata_type)
|
||||||
|
|
||||||
@@ -185,7 +201,11 @@ def test_check_exists(metadata_fixture, filename_fixture, tmpdir):
|
|||||||
|
|
||||||
|
|
||||||
def test_generate_m3u(tmpdir, monkeypatch):
|
def test_generate_m3u(tmpdir, monkeypatch):
|
||||||
monkeypatch.setattr(youtube_tools.GenerateYouTubeURL, "_fetch_response", loader.monkeypatch_youtube_search_page)
|
monkeypatch.setattr(
|
||||||
|
youtube_tools.GenerateYouTubeURL,
|
||||||
|
"_fetch_response",
|
||||||
|
loader.monkeypatch_youtube_search_page,
|
||||||
|
)
|
||||||
expect_m3u = (
|
expect_m3u = (
|
||||||
"#EXTM3U\n\n"
|
"#EXTM3U\n\n"
|
||||||
"#EXTINF:208,Janji - Heroes Tonight (feat. Johnning) [NCS Release]\n"
|
"#EXTINF:208,Janji - Heroes Tonight (feat. Johnning) [NCS Release]\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user