52 Commits

Author SHA1 Message Date
Ritiek Malhotra
9cac8998f2 Merge pull request #641 from ritiek/release-v1.2.3
Bump version for v1.2.3 release
2019-12-20 03:12:47 +05:30
Ritiek Malhotra
af4ccea206 Bump version for v1.2.3 release
Also removed "Beta status" from the classifier list in setup.py
2019-12-20 03:09:16 +05:30
Ritiek Malhotra
12b98c55cc Merge pull request #638 from ritiek/fix-crash
Patch all Pafy versions till v0.5.5
2019-12-20 03:04:34 +05:30
Ritiek Malhotra
16f240d4e6 Add a changelog entry
For the commit ca1ab51.
2019-12-20 02:59:05 +05:30
Ritiek Malhotra
ca1ab5118c Patch all Pafy versions till v0.5.5
For some reason, the newer release v0.5.5 of Pafy still does not
contain the new methods that were supposed to be a part of the release.
With this commit, we change to also apply patches on v0.5.5.

Addresses #633, #631.
2019-12-17 12:58:51 +05:30
Ritiek Malhotra
03a8b50ab4 Merge pull request #568 from kadaliao/feat/keep-trackid-as-songname
feat: add file-format key to use track id as saved filename
2019-09-07 19:43:00 +05:30
Linus Groh
ff47523478 Merge branch 'master' into feat/keep-trackid-as-songname 2019-09-07 11:49:10 +01:00
Kada Liao
1348c138c9 docs: add changlog 2019-09-07 18:46:11 +08:00
Ritiek Malhotra
3b5adeb1b9 Merge pull request #600 from cclauss/patch-1
Travis CI: Remove sudo and dist lines
2019-08-25 11:04:41 +05:30
Ritiek Malhotra
1b4d4c747c Merge pull request #597 from arthurlutz/patch-1
[spotdl] generate_m3u only takes track_file as argument
2019-08-25 11:03:01 +05:30
Christian Clauss
bfba7fd6e6 Travis CI: Remove sudo and dist lines
Sudo is deprecated in Travis and Xenial is the current default distro
2019-08-25 03:19:59 +02:00
Arthur Lutz
e4658825f7 [CHANGES] fixed changelog 2019-08-24 08:50:30 +02:00
Arthur Lutz
5242285637 [spotdl] generate_m3u only takes track_file as argument
Fixes #559
2019-08-23 12:34:29 +02:00
Ritiek Malhotra
cfbf97c028 Merge pull request #594 from Dsujan/#592_add_leading_zeros
Added leading zeros in track_number.Fixed issue #592
2019-08-01 22:27:19 +05:30
py-coder
0202c65110 Added leading zeros in track_number.Fixed issue #592 2019-08-01 10:17:24 +05:45
Ritiek Malhotra
d45655a2b7 Merge pull request #591 from ritiek/fix-docker-build
Fix missing packages with Docker build
2019-07-28 14:53:17 +05:30
Ritiek Malhotra
80bbf80090 Fix missing packages with Docker build 2019-07-28 14:41:03 +05:30
Kada Liao
94e29e7515 add key track_id for file-format parameter 2019-07-27 18:38:50 +08:00
Ritiek Malhotra
17600592a8 Merge pull request #585 from ritiek/refactor
Scrape lyrics from Genius and lyrics refactor
2019-07-25 12:05:25 +05:30
Ritiek Malhotra
34ea3ea91b Mention about Genius lyric provider 2019-07-25 11:41:07 +05:30
Ritiek Malhotra
647a2089e0 Merge pull request #587 from ritiek/missing-changelog
Add changelog entry for #580
2019-07-24 13:33:52 +05:30
Ritiek Malhotra
568ddc52ab Automatically retry randomly failed Travis jobs 2019-07-24 11:50:10 +05:30
Ritiek Malhotra
d9d92e5723 Add changelog entry for #580 2019-07-24 11:42:06 +05:30
Ritiek Malhotra
4f6cae9f80 Update CHANGES.md 2019-07-24 11:29:37 +05:30
Ritiek Malhotra
5bcacf01da Fallback to LyricWikia if lyrics not found on Genius 2019-07-24 10:56:04 +05:30
Ritiek Malhotra
54a1564596 Merge pull request #580 from NightMachinary/master
Added --no-remove-original-file. Fixed a bug with ffmpeg accessing stdin.
2019-07-23 16:05:15 +05:30
Fereidoon Mehri
597828866b Added --no-remove-original-file. Fixed bug with ffmpeg accessing stdin.
Fixed tests
2019-07-23 14:43:02 +04:30
Ritiek Malhotra
5134459554 Maybe stop calling pytest as module works? 2019-07-22 16:10:10 +05:30
Ritiek Malhotra
08566e02b5 Update command to run tests 2019-07-22 15:58:54 +05:30
Ritiek Malhotra
0d846cdcce Scrape lyrics from Genius and lyrics refactor 2019-07-22 15:55:05 +05:30
Ritiek Malhotra
341af5bce9 Merge pull request #584 from ritiek/fix-tests
Fix tests
2019-07-22 11:07:40 +05:30
Ritiek Malhotra
69522331df Fix tests 2019-07-20 21:49:25 +05:30
Ritiek Malhotra
5ca4317944 Merge pull request #558 from ritiek/pafy-prefer-secure-by-default
Pafy prefer secure HTTPS by default
2019-06-05 23:36:11 +05:30
Ritiek Malhotra
f4cd70b603 Bump to v1.2.2 2019-06-03 14:18:31 +05:30
Ritiek Malhotra
b6c5c88550 Fix tests for now and rephrase comments for clarity 2019-06-03 14:15:35 +05:30
Ritiek Malhotra
9f1f361dcb Add docs on what this is about 2019-06-03 14:15:23 +05:30
Ritiek Malhotra
fd74adb42f Prefer secure HTTPS by default 2019-06-03 14:04:41 +05:30
Ritiek Malhotra
b808265c38 Merge pull request #540 from ritiek/release-v1.2.1
Bump to v1.2.1
2019-04-28 17:09:33 +05:30
Ritiek Malhotra
21a1f1a150 Bump to v1.2.1 2019-04-28 17:05:44 +05:30
Ritiek Malhotra
951ae02e08 Merge pull request #539 from ritiek/patch-audiostream-urls
Patch bug in Pafy when fetching audiostreams with latest youtube-dl
2019-04-28 17:03:25 +05:30
Ritiek Malhotra
dfd48f75ce Update CHANGES.md 2019-04-28 16:46:30 +05:30
Ritiek Malhotra
bb385a3bfd Skip avconv tests as it is no longer provided in later distros 2019-04-28 15:31:43 +05:30
Ritiek Malhotra
a9477c7873 Fix tests 2019-04-28 15:26:18 +05:30
Ritiek Malhotra
c225e5821b Patch bug in Pafy when fetching audiostreams with latest youtube-dl 2019-04-28 15:09:42 +05:30
Ritiek Malhotra
d61309b0ce Merge pull request #522 from ritiek/hightlight-shell-code-blocks
Use "console" as language to highlight shell code blocks with
2019-03-17 10:02:33 +05:30
Ritiek Malhotra
5b2a073033 Merge pull request #519 from ritiek/remove-duplicate-debuglog-entry
Remove duplicate debuglog entry
2019-03-17 10:02:23 +05:30
Linus Groh
f17e5f58d8 Update README.md 2019-03-16 17:44:49 +00:00
Ritiek Malhotra
d3668f55bb Update CHANGES.md 2019-03-14 20:13:27 +05:30
Ritiek Malhotra
6ca136f039 Remove duplicate debuglog entry 2019-03-14 20:12:53 +05:30
Sumanjay
e2a136d885 Update CHANGES.md (#518)
* Update CHANGES.md

* Update CHANGES.md
2019-03-14 19:58:15 +05:30
Ritiek Malhotra
d10f3e9df0 Merge pull request #517 from cyberboysumanjay/master
Fix YAMLLoadWarning
2019-03-14 18:22:47 +05:30
Sumanjay
46eb2e3e32 Fix YAMLLoadWarning 2019-03-14 13:26:35 +05:30
29 changed files with 417 additions and 116 deletions

View File

@@ -1,6 +1,4 @@
dist: xenial
language: python language: python
sudo: required
python: python:
- "3.4" - "3.4"
- "3.5" - "3.5"
@@ -40,7 +38,7 @@ install:
- tinydownload 07426048687547254773 -o ~/bin/ffmpeg - tinydownload 07426048687547254773 -o ~/bin/ffmpeg
- chmod 755 ~/bin/ffmpeg - chmod 755 ~/bin/ffmpeg
- xdg-user-dirs-update - xdg-user-dirs-update
script: python -m pytest test --cov=. script: travis_retry pytest --cov=.
after_success: after_success:
- pip install codecov - pip install codecov
- codecov - codecov

View File

@@ -5,14 +5,31 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
## [1.2.3] - 2019-12-20
### Added ### Added
- - 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)
### Fixed ### Fixed
- - Some tracks randomly fail to download with Pafy v0.5.5 ([@ritiek](https://github.com/ritiek)) (#638)
- 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)
## [1.2.2] - 2019-06-03
### Fixed
- Patch bug in Pafy to prefer secure HTTPS ([@ritiek](https://github.com/ritiek)) (#558)
## [1.2.1] - 2019-04-28
### Fixed
- Patch bug in Pafy when fetching audiostreams with latest youtube-dl ([@ritiek](https://github.com/ritiek)) (#539)
### Changed
- Removed duplicate debug log entry from `internals.trim_song` ([@ritiek](https://github.com/ritiek)) (#519)
- Fix YAMLLoadWarning ([@cyberboysumanjay](https://github.com/cyberboysumanjay)) (#517)
## [1.2.0] - 2019-03-01 ## [1.2.0] - 2019-03-01
### Added ### Added

View File

@@ -24,7 +24,7 @@ don't feel bad. Open an issue any way!
unless mentioned otherwise. unless mentioned otherwise.
- Code should be formatted using [black](https://github.com/ambv/black). Don't worry if you forgot or don't know how to do this, the codebase will be black-formatted with each release. - Code should be formatted using [black](https://github.com/ambv/black). Don't worry if you forgot or don't know how to do this, the codebase will be black-formatted with each release.
- All tests are placed in the [test directory](https://github.com/ritiek/spotify-downloader/tree/master/test). We use [pytest](https://github.com/pytest-dev/pytest) - All tests are placed in the [test directory](https://github.com/ritiek/spotify-downloader/tree/master/test). We use [pytest](https://github.com/pytest-dev/pytest)
to run the test suite: `$ python3 -m pytest test`. to run the test suite: `$ pytest`.
If you don't have pytest, you can install it with `$ pip3 install pytest`. If you don't have pytest, you can install it with `$ pip3 install pytest`.
- Add a note about the changes, your GitHub username and a reference to the PR to the `Unreleased` section of the [`CHANGES.md`](CHANGES.md) file (see existing releases for examples), add the appropriate section ("Added", "Changed", "Fixed" etc.) if necessary. You don't have to increment version numbers. See https://keepachangelog.com/en/1.0.0/ for more information. - Add a note about the changes, your GitHub username and a reference to the PR to the `Unreleased` section of the [`CHANGES.md`](CHANGES.md) file (see existing releases for examples), add the appropriate section ("Added", "Changed", "Fixed" etc.) if necessary. You don't have to increment version numbers. See https://keepachangelog.com/en/1.0.0/ for more information.
- If you are planning to work on something big, let us know through an issue. So we can discuss more about it. - If you are planning to work on something big, let us know through an issue. So we can discuss more about it.

View File

@@ -11,7 +11,7 @@
- Can also download a song by entering its artist and song name (in case if you don't have the Spotify's HTTP link for some song). - Can also download a song by entering its artist and song name (in case if you don't have the Spotify's HTTP link for some song).
- Automatically applies metadata to the downloaded song which includes: - Automatically applies metadata to the downloaded song which includes:
- `Title`, `Artist`, `Album`, `Album art`, `Lyrics` (if found on [lyrics wikia](http://lyrics.wikia.com)), `Album artist`, `Genre`, `Track number`, `Disc number`, `Release date`, and more... - `Title`, `Artist`, `Album`, `Album art`, `Lyrics` (if found either on [Genius](https://genius.com/) or [LyricsWikia](http://lyrics.wikia.com)), `Album artist`, `Genre`, `Track number`, `Disc number`, `Release date`, and more...
- Works straight out of the box and does not require you to generate or mess with your API keys (already included). - Works straight out of the box and does not require you to generate or mess with your API keys (already included).
@@ -30,7 +30,7 @@ If you still need to use Python 2 - check out the (outdated)
spotify-downloader works with all major distributions and even on low-powered devices such as a Raspberry Pi. spotify-downloader works with all major distributions and even on low-powered devices such as a Raspberry Pi.
spotify-downloader can be installed via pip with: spotify-downloader can be installed via pip with:
``` ```console
$ pip3 install spotdl $ pip3 install spotdl
``` ```
@@ -41,7 +41,7 @@ page for detailed OS-specific instructions to get it and other dependencies it r
For the most basic usage, downloading tracks is as easy as For the most basic usage, downloading tracks is as easy as
``` ```console
$ spotdl --song https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ $ spotdl --song https://open.spotify.com/track/2DGa7iaidT5s0qnINlwMjJ
$ spotdl --song "ncs - spectre" $ spotdl --song "ncs - spectre"
``` ```
@@ -49,7 +49,7 @@ $ spotdl --song "ncs - spectre"
For downloading playlist and albums, you need to first load all the tracks into text file and then pass For downloading playlist and albums, you need to first load all the tracks into text file and then pass
this text file to `--list` argument. Here is how you would do it for a playlist this text file to `--list` argument. Here is how you would do it for a playlist
``` ```console
$ spotdl --playlist https://open.spotify.com/user/nocopyrightsounds/playlist/7sZbq8QGyMnhKPcLJvCUFD $ spotdl --playlist https://open.spotify.com/user/nocopyrightsounds/playlist/7sZbq8QGyMnhKPcLJvCUFD
INFO: Writing 62 tracks to ncs-releases.txt INFO: Writing 62 tracks to ncs-releases.txt
$ spotdl --list ncs-releases.txt $ spotdl --list ncs-releases.txt
@@ -73,8 +73,8 @@ Check out [CONTRIBUTING.md](CONTRIBUTING.md) for more info.
## Running Tests ## Running Tests
``` ```console
$ python3 -m pytest test $ pytest
``` ```
Obviously this requires the `pytest` module to be installed. Obviously this requires the `pytest` module to be installed.

View File

@@ -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",
@@ -46,7 +46,6 @@ setup(
"metadata", "metadata",
], ],
classifiers=[ classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: End Users/Desktop", "Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Programming Language :: Python", "Programming Language :: Python",

View File

@@ -1 +1 @@
__version__ = "1.2.0" __version__ = "1.2.3"

View File

@@ -16,7 +16,14 @@ https://trac.ffmpeg.org/wiki/Encode/AAC
""" """
def song(input_song, output_song, folder, avconv=False, trim_silence=False): 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):
else: else:
return 0 return 0
convert = Converter(input_song, output_song, folder, delete_original=True) 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 " 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 "

View File

@@ -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. """
@@ -133,6 +134,7 @@ class Downloader:
const.args.folder, const.args.folder,
avconv=const.args.avconv, avconv=const.args.avconv,
trim_silence=const.args.trim_silence, trim_silence=const.args.trim_silence,
delete_original=not const.args.no_remove_original,
) )
except FileNotFoundError: except FileNotFoundError:
encoder = "avconv" if const.args.avconv else "ffmpeg" encoder = "avconv" if const.args.avconv else "ffmpeg"
@@ -157,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(

View File

@@ -15,6 +15,7 @@ _LOG_LEVELS_STR = ["INFO", "WARNING", "ERROR", "DEBUG"]
default_conf = { default_conf = {
"spotify-downloader": { "spotify-downloader": {
"no-remove-original": False,
"manual": False, "manual": False,
"no-metadata": False, "no-metadata": False,
"no-fallback-metadata": False, "no-fallback-metadata": False,
@@ -36,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",
} }
} }
@@ -58,7 +59,7 @@ def merge(default, config):
def get_config(config_file): def get_config(config_file):
try: try:
with open(config_file, "r") as ymlfile: with open(config_file, "r") as ymlfile:
cfg = yaml.load(ymlfile) cfg = yaml.safe_load(ymlfile)
except FileNotFoundError: except FileNotFoundError:
log.info("Writing default configuration to {0}:".format(config_file)) log.info("Writing default configuration to {0}:".format(config_file))
with open(config_file, "w") as ymlfile: with open(config_file, "w") as ymlfile:
@@ -139,6 +140,13 @@ def get_arguments(raw_args=None, to_group=True, to_merge=True):
help="choose the track to download manually from a list of matching tracks", help="choose the track to download manually from a list of matching tracks",
action="store_true", action="store_true",
) )
parser.add_argument(
"-nr",
"--no-remove-original",
default=config["no-remove-original"],
help="do not remove the original file after conversion",
action="store_true",
)
parser.add_argument( parser.add_argument(
"-nm", "-nm",
"--no-metadata", "--no-metadata",
@@ -272,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"
@@ -312,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)

View File

@@ -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:
@@ -30,6 +32,7 @@ formats = {
9: "track_number", 9: "track_number",
10: "total_tracks", 10: "total_tracks",
11: "isrc", 11: "isrc",
12: "track_id",
} }
@@ -51,7 +54,6 @@ def input_link(links):
def trim_song(tracks_file): def trim_song(tracks_file):
""" Remove the first song from file. """ """ Remove the first song from file. """
log.debug("Removing downloaded song from tracks file")
with open(tracks_file, "r") as file_in: with open(tracks_file, "r") as file_in:
data = file_in.read().splitlines(True) data = file_in.read().splitlines(True)
with open(tracks_file, "w") as file_out: with open(tracks_file, "w") as file_out:
@@ -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"]
@@ -89,14 +93,22 @@ def format_string(string_format, tags, slugification=False, force_spaces=False):
format_tags[9] = tags["track_number"] format_tags[9] = tags["track_number"]
format_tags[10] = tags["total_tracks"] format_tags[10] = tags["total_tracks"]
format_tags[11] = tags["external_ids"]["isrc"] format_tags[11] = tags["external_ids"]["isrc"]
format_tags[12] = tags["id"]
format_tags_sanitized = { format_tags_sanitized = {
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:
@@ -259,7 +271,7 @@ def remove_duplicates(tracks):
def content_available(url): def content_available(url):
try: try:
response = urllib.request.urlopen(url) response = urllib.request.urlopen(url)
except HTTPError: except urllib.request.HTTPError:
return False return False
else: else:
return response.getcode() < 300 return response.getcode() < 300

View File

@@ -0,0 +1 @@
from spotdl.lyrics.lyric_base import LyricBase

View File

@@ -0,0 +1,5 @@
class LyricsNotFound(Exception):
__module__ = Exception.__module__
def __init__(self, message=None):
super(LyricsNotFound, self).__init__(message)

View File

@@ -0,0 +1,14 @@
import lyricwikia
from abc import ABC
from abc import abstractmethod
class LyricBase(ABC):
@abstractmethod
def __init__(self, artist, song):
pass
@abstractmethod
def get_lyrics(self, linesep="\n", timeout=None):
pass

View File

@@ -0,0 +1,4 @@
from spotdl.lyrics.providers.genius import Genius
from spotdl.lyrics.providers.lyricwikia_wrapper import LyricWikia
LyricClasses = (Genius, LyricWikia)

View File

@@ -0,0 +1,47 @@
from bs4 import BeautifulSoup
import urllib.request
from spotdl.lyrics.lyric_base import LyricBase
from spotdl.lyrics.exceptions import LyricsNotFound
BASE_URL = "https://genius.com"
class Genius(LyricBase):
def __init__(self, artist, song):
self.artist = artist
self.song = song
self.base_url = BASE_URL
def _guess_lyric_url(self):
query = "/{} {} lyrics".format(self.artist, self.song)
query = query.replace(" ", "-")
encoded_query = urllib.request.quote(query)
lyric_url = self.base_url + encoded_query
return lyric_url
def _fetch_page(self, url, timeout=None):
request = urllib.request.Request(url)
request.add_header("User-Agent", "urllib")
try:
response = urllib.request.urlopen(request, timeout=timeout)
except urllib.request.HTTPError:
raise LyricsNotFound(
"Could not find lyrics for {} - {} at URL: {}".format(
self.artist, self.song, url
)
)
else:
return response.read()
def _get_lyrics_text(self, html):
soup = BeautifulSoup(html, "html.parser")
lyrics_paragraph = soup.find("p")
lyrics = lyrics_paragraph.get_text()
return lyrics
def get_lyrics(self, linesep="\n", timeout=None):
url = self._guess_lyric_url()
html_page = self._fetch_page(url, timeout=timeout)
lyrics = self._get_lyrics_text(html_page)
return lyrics.replace("\n", linesep)

View File

@@ -0,0 +1,18 @@
import lyricwikia
from spotdl.lyrics.lyric_base import LyricBase
from spotdl.lyrics.exceptions import LyricsNotFound
class LyricWikia(LyricBase):
def __init__(self, artist, song):
self.artist = artist
self.song = song
def get_lyrics(self, linesep="\n", timeout=None):
try:
lyrics = lyricwikia.get_lyrics(self.artist, self.song, linesep, timeout)
except lyricwikia.LyricsNotFound as e:
raise LyricsNotFound(e.args[0])
else:
return lyrics

View File

@@ -0,0 +1,37 @@
from spotdl.lyrics import LyricBase
from spotdl.lyrics import exceptions
from spotdl.lyrics.providers import Genius
import urllib.request
import pytest
class TestGenius:
def test_subclass(self):
assert issubclass(Genius, LyricBase)
@pytest.fixture(scope="module")
def track(self):
return Genius("artist", "song")
def test_base_url(self, track):
assert track.base_url == "https://genius.com"
def test_get_lyrics(self, track, monkeypatch):
def mocked_urlopen(url, timeout=None):
class DummyHTTPResponse:
def read(self):
return "<p>amazing lyrics!</p>"
return DummyHTTPResponse()
monkeypatch.setattr("urllib.request.urlopen", mocked_urlopen)
assert track.get_lyrics() == "amazing lyrics!"
def test_lyrics_not_found_error(self, track, monkeypatch):
def mocked_urlopen(url, timeout=None):
raise urllib.request.HTTPError("", "", "", "", "")
monkeypatch.setattr("urllib.request.urlopen", mocked_urlopen)
with pytest.raises(exceptions.LyricsNotFound):
track.get_lyrics()

View File

@@ -0,0 +1,35 @@
import lyricwikia
from spotdl.lyrics import LyricBase
from spotdl.lyrics import exceptions
from spotdl.lyrics.providers import LyricWikia
import pytest
class TestLyricWikia:
def test_subclass(self):
assert issubclass(LyricWikia, LyricBase)
def test_get_lyrics(self, monkeypatch):
# `LyricWikia` class uses the 3rd party method `lyricwikia.get_lyrics`
# internally and there is no need to test a 3rd party library as they
# have their own implementation of tests.
monkeypatch.setattr(
"lyricwikia.get_lyrics", lambda a, b, c, d: "awesome lyrics!"
)
track = LyricWikia("Lyricwikia", "Lyricwikia")
assert track.get_lyrics() == "awesome lyrics!"
def test_lyrics_not_found_error(self, monkeypatch):
def lyricwikia_lyrics_not_found(msg):
raise lyricwikia.LyricsNotFound(msg)
# Wrap `lyricwikia.LyricsNotFound` with `exceptions.LyricsNotFound` error.
monkeypatch.setattr(
"lyricwikia.get_lyrics",
lambda a, b, c, d: lyricwikia_lyrics_not_found("Nope, no lyrics."),
)
track = LyricWikia("Lyricwikia", "Lyricwikia")
with pytest.raises(exceptions.LyricsNotFound):
track.get_lyrics()

View File

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

View File

@@ -1,3 +1,4 @@
from pafy import backend_youtube_dl
import pafy import pafy
from spotdl import internals from spotdl import internals
@@ -10,22 +11,54 @@ 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):
for format_index in range(len(self._ydl_info["formats"])):
try:
self._ydl_info["formats"][format_index]["url"] = self._ydl_info["formats"][
format_index
]["fragment_base_url"]
except KeyError:
pass
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)
def patch_pafy():
pafy.backend_shared.BasePafy._bestthumb = None class PatchPafy:
pafy.backend_shared.BasePafy._content_available = _content_available """
pafy.backend_shared.BasePafy.getbestthumb = _getbestthumb These patches have not been released by pafy on PyPI yet but
are useful to us.
"""
def patch_getbestthumb(self):
# https://github.com/mps-youtube/pafy/pull/211
pafy.backend_shared.BasePafy._bestthumb = None
pafy.backend_shared.BasePafy._content_available = _content_available
pafy.backend_shared.BasePafy.getbestthumb = _getbestthumb
def patch_process_streams(self):
# 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._process_streams = _process_streams
def patch_insecure_streams(self):
# https://github.com/mps-youtube/pafy/pull/235
pafy.g.def_ydl_opts["prefer_insecure"] = False

View File

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

View File

@@ -1,6 +1,5 @@
import spotipy import spotipy
import spotipy.oauth2 as oauth2 import spotipy.oauth2 as oauth2
import lyricwikia
from slugify import slugify from slugify import slugify
from titlecase import titlecase from titlecase import titlecase
@@ -12,6 +11,8 @@ import functools
from spotdl import const from spotdl import const
from spotdl import internals from spotdl import internals
from spotdl.lyrics.providers import LyricClasses
from spotdl.lyrics.exceptions import LyricsNotFound
spotify = None spotify = None
@@ -36,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
@@ -74,13 +76,16 @@ def generate_metadata(raw_song):
meta_tags[u"total_tracks"] = album["tracks"]["total"] meta_tags[u"total_tracks"] = album["tracks"]["total"]
log.debug("Fetching lyrics") log.debug("Fetching lyrics")
meta_tags["lyrics"] = None
try: for LyricClass in LyricClasses:
meta_tags["lyrics"] = lyricwikia.get_lyrics( track = LyricClass(meta_tags["artists"][0]["name"], meta_tags["name"])
meta_tags["artists"][0]["name"], meta_tags["name"] try:
) meta_tags["lyrics"] = track.get_lyrics()
except lyricwikia.LyricsNotFound: except LyricsNotFound:
meta_tags["lyrics"] = None continue
else:
break
# Some sugar # Some sugar
meta_tags["year"], *_ = meta_tags["release_date"].split("-") meta_tags["year"], *_ = meta_tags["release_date"].split("-")

View File

@@ -16,9 +16,13 @@ pafy.g.opener.addheaders.append(("Range", "bytes=0-"))
# Implement unreleased methods on Pafy object # Implement unreleased methods on Pafy object
# 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.5":
from spotdl import patcher from spotdl import patcher
patcher.patch_pafy()
pafy_patcher = patcher.PatchPafy()
pafy_patcher.patch_getbestthumb()
pafy_patcher.patch_process_streams()
pafy_patcher.patch_insecure_streams()
def set_api_key(): def set_api_key():
@@ -49,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)
@@ -61,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)
@@ -92,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

View File

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

View File

@@ -101,22 +101,34 @@ 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
class TestFFmpeg: class TestFFmpeg:
def test_convert_from_webm_to_mp3(self, filename_fixture, monkeypatch): def test_convert_from_webm_to_mp3(self, filename_fixture, monkeypatch):
expect_command = "ffmpeg -y -hide_banner -nostats -v panic -i {0}.webm -codec:a libmp3lame -ar 44100 -b:a 192k -vn {0}.mp3".format( expect_command = "ffmpeg -y -nostdin -hide_banner -nostats -v panic -i {0}.webm -codec:a libmp3lame -ar 44100 -b:a 192k -vn {0}.mp3".format(
os.path.join(const.args.folder, filename_fixture) os.path.join(const.args.folder, filename_fixture)
) )
monkeypatch.setattr("os.remove", lambda x: None) monkeypatch.setattr("os.remove", lambda x: None)
@@ -126,7 +138,7 @@ class TestFFmpeg:
assert " ".join(command) == expect_command assert " ".join(command) == expect_command
def test_convert_from_webm_to_m4a(self, filename_fixture, monkeypatch): def test_convert_from_webm_to_m4a(self, filename_fixture, monkeypatch):
expect_command = "ffmpeg -y -hide_banner -nostats -v panic -i {0}.webm -cutoff 20000 -codec:a aac -ar 44100 -b:a 192k -vn {0}.m4a".format( expect_command = "ffmpeg -y -nostdin -hide_banner -nostats -v panic -i {0}.webm -cutoff 20000 -codec:a aac -ar 44100 -b:a 192k -vn {0}.m4a".format(
os.path.join(const.args.folder, filename_fixture) os.path.join(const.args.folder, filename_fixture)
) )
monkeypatch.setattr("os.remove", lambda x: None) monkeypatch.setattr("os.remove", lambda x: None)
@@ -136,7 +148,7 @@ class TestFFmpeg:
assert " ".join(command) == expect_command assert " ".join(command) == expect_command
def test_convert_from_m4a_to_mp3(self, filename_fixture, monkeypatch): def test_convert_from_m4a_to_mp3(self, filename_fixture, monkeypatch):
expect_command = "ffmpeg -y -hide_banner -nostats -v panic -i {0}.m4a -codec:v copy -codec:a libmp3lame -ar 44100 -b:a 192k -vn {0}.mp3".format( expect_command = "ffmpeg -y -nostdin -hide_banner -nostats -v panic -i {0}.m4a -codec:v copy -codec:a libmp3lame -ar 44100 -b:a 192k -vn {0}.mp3".format(
os.path.join(const.args.folder, filename_fixture) os.path.join(const.args.folder, filename_fixture)
) )
monkeypatch.setattr("os.remove", lambda x: None) monkeypatch.setattr("os.remove", lambda x: None)
@@ -146,7 +158,7 @@ class TestFFmpeg:
assert " ".join(command) == expect_command assert " ".join(command) == expect_command
def test_convert_from_m4a_to_webm(self, filename_fixture, monkeypatch): def test_convert_from_m4a_to_webm(self, filename_fixture, monkeypatch):
expect_command = "ffmpeg -y -hide_banner -nostats -v panic -i {0}.m4a -codec:a libopus -vbr on -b:a 192k -vn {0}.webm".format( expect_command = "ffmpeg -y -nostdin -hide_banner -nostats -v panic -i {0}.m4a -codec:a libopus -vbr on -b:a 192k -vn {0}.webm".format(
os.path.join(const.args.folder, filename_fixture) os.path.join(const.args.folder, filename_fixture)
) )
monkeypatch.setattr("os.remove", lambda x: None) monkeypatch.setattr("os.remove", lambda x: None)
@@ -156,7 +168,7 @@ class TestFFmpeg:
assert " ".join(command) == expect_command assert " ".join(command) == expect_command
def test_convert_from_m4a_to_flac(self, filename_fixture, monkeypatch): def test_convert_from_m4a_to_flac(self, filename_fixture, monkeypatch):
expect_command = "ffmpeg -y -hide_banner -nostats -v panic -i {0}.m4a -codec:a flac -ar 44100 -b:a 192k -vn {0}.flac".format( expect_command = "ffmpeg -y -nostdin -hide_banner -nostats -v panic -i {0}.m4a -codec:a flac -ar 44100 -b:a 192k -vn {0}.flac".format(
os.path.join(const.args.folder, filename_fixture) os.path.join(const.args.folder, filename_fixture)
) )
monkeypatch.setattr("os.remove", lambda x: None) monkeypatch.setattr("os.remove", lambda x: None)
@@ -166,7 +178,7 @@ class TestFFmpeg:
assert " ".join(command) == expect_command assert " ".join(command) == expect_command
def test_correct_container_for_m4a(self, filename_fixture, monkeypatch): def test_correct_container_for_m4a(self, filename_fixture, monkeypatch):
expect_command = "ffmpeg -y -hide_banner -nostats -v panic -i {0}.m4a.temp -acodec copy -b:a 192k -vn {0}.m4a".format( expect_command = "ffmpeg -y -nostdin -hide_banner -nostats -v panic -i {0}.m4a.temp -acodec copy -b:a 192k -vn {0}.m4a".format(
os.path.join(const.args.folder, filename_fixture) os.path.join(const.args.folder, filename_fixture)
) )
_, command = convert.song( _, command = convert.song(
@@ -176,6 +188,7 @@ class TestFFmpeg:
class TestAvconv: class TestAvconv:
@pytest.mark.skip(reason="avconv is no longer provided with FFmpeg")
def test_convert_from_m4a_to_mp3(self, filename_fixture, monkeypatch): def test_convert_from_m4a_to_mp3(self, filename_fixture, monkeypatch):
monkeypatch.setattr("os.remove", lambda x: None) monkeypatch.setattr("os.remove", lambda x: None)
expect_command = "avconv -loglevel 0 -i {0}.m4a -ab 192k {0}.mp3 -y".format( expect_command = "avconv -loglevel 0 -i {0}.m4a -ab 192k {0}.mp3 -y".format(

View File

@@ -3,7 +3,9 @@ import pafy
import pytest import pytest
patcher.patch_pafy() pafy_patcher = patcher.PatchPafy()
pafy_patcher.patch_getbestthumb()
class TestPafyContentAvailable: class TestPafyContentAvailable:
pass pass
@@ -22,13 +24,12 @@ class TestMethodCalls:
def test_pafy_getbestthumb(self, content_fixture): def test_pafy_getbestthumb(self, content_fixture):
thumbnail = patcher._getbestthumb(content_fixture) thumbnail = patcher._getbestthumb(content_fixture)
assert thumbnail == "https://i.ytimg.com/vi/3nQNiWdeH2Q/maxresdefault.jpg" assert thumbnail == "https://i.ytimg.com/vi/3nQNiWdeH2Q/hqdefault.jpg"
def test_pafy_getbestthumb_without_ytdl(self, content_fixture): def test_pafy_getbestthumb_without_ytdl(self, content_fixture):
content_fixture._ydl_info["thumbnails"][0]["url"] = None content_fixture._ydl_info["thumbnails"][0]["url"] = None
thumbnail = patcher._getbestthumb(content_fixture) thumbnail = patcher._getbestthumb(content_fixture)
assert thumbnail == "https://i.ytimg.com/vi/3nQNiWdeH2Q/maxresdefault.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

View File

@@ -115,7 +115,7 @@ def test_write_playlist(tmpdir):
assert tracks == expect_tracks assert tracks == expect_tracks
# XXX: Mock this test off if it fails in future # XXX: Monkeypatch these tests if they fail in future
class TestFetchAlbum: class TestFetchAlbum:
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def album_fixture(self): def album_fixture(self):
@@ -131,7 +131,7 @@ class TestFetchAlbum:
assert album_fixture["tracks"]["total"] == 15 assert album_fixture["tracks"]["total"] == 15
# XXX: Mock this test off if it fails in future # XXX: Monkeypatch these tests if they fail in future
class TestFetchAlbumsFromArtist: class TestFetchAlbumsFromArtist:
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def albums_from_artist_fixture(self): def albums_from_artist_fixture(self):
@@ -141,7 +141,7 @@ class TestFetchAlbumsFromArtist:
return albums return albums
def test_len(self, albums_from_artist_fixture): def test_len(self, albums_from_artist_fixture):
assert len(albums_from_artist_fixture) == 52 assert len(albums_from_artist_fixture) == 54
def test_zeroth_album_name(self, albums_from_artist_fixture): def test_zeroth_album_name(self, albums_from_artist_fixture):
assert albums_from_artist_fixture[0]["name"] == "Revolution Radio" assert albums_from_artist_fixture[0]["name"] == "Revolution Radio"

View File

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