mirror of
				https://github.com/KevinMidboe/spotify-downloader.git
				synced 2025-10-29 18:00:15 +00:00 
			
		
		
		
	Compare commits
	
		
			88 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9ecf957c81 | ||
| 
						 | 
					ea4ff29e52 | ||
| 
						 | 
					33e07bea9d | ||
| 
						 | 
					94e06f99de | ||
| 
						 | 
					9a594d37c7 | ||
| 
						 | 
					b45f75b5ca | ||
| 
						 | 
					75114bc26e | ||
| 
						 | 
					456b404e73 | ||
| 
						 | 
					43f9dd7f8d | ||
| 
						 | 
					b24802f815 | ||
| 
						 | 
					851d88fdd8 | ||
| 
						 | 
					4ee2b51550 | ||
| 
						 | 
					c73f55b8ce | ||
| 
						 | 
					e47744f99c | ||
| 
						 | 
					5d185844d7 | ||
| 
						 | 
					7f587fe667 | ||
| 
						 | 
					9cac8998f2 | ||
| 
						 | 
					af4ccea206 | ||
| 
						 | 
					12b98c55cc | ||
| 
						 | 
					16f240d4e6 | ||
| 
						 | 
					ca1ab5118c | ||
| 
						 | 
					03a8b50ab4 | ||
| 
						 | 
					ff47523478 | ||
| 
						 | 
					1348c138c9 | ||
| 
						 | 
					3b5adeb1b9 | ||
| 
						 | 
					1b4d4c747c | ||
| 
						 | 
					bfba7fd6e6 | ||
| 
						 | 
					e4658825f7 | ||
| 
						 | 
					5242285637 | ||
| 
						 | 
					cfbf97c028 | ||
| 
						 | 
					0202c65110 | ||
| 
						 | 
					d45655a2b7 | ||
| 
						 | 
					80bbf80090 | ||
| 
						 | 
					94e29e7515 | ||
| 
						 | 
					17600592a8 | ||
| 
						 | 
					34ea3ea91b | ||
| 
						 | 
					647a2089e0 | ||
| 
						 | 
					568ddc52ab | ||
| 
						 | 
					d9d92e5723 | ||
| 
						 | 
					4f6cae9f80 | ||
| 
						 | 
					5bcacf01da | ||
| 
						 | 
					54a1564596 | ||
| 
						 | 
					597828866b | ||
| 
						 | 
					5134459554 | ||
| 
						 | 
					08566e02b5 | ||
| 
						 | 
					0d846cdcce | ||
| 
						 | 
					341af5bce9 | ||
| 
						 | 
					69522331df | ||
| 
						 | 
					5ca4317944 | ||
| 
						 | 
					f4cd70b603 | ||
| 
						 | 
					b6c5c88550 | ||
| 
						 | 
					9f1f361dcb | ||
| 
						 | 
					fd74adb42f | ||
| 
						 | 
					b808265c38 | ||
| 
						 | 
					21a1f1a150 | ||
| 
						 | 
					951ae02e08 | ||
| 
						 | 
					dfd48f75ce | ||
| 
						 | 
					bb385a3bfd | ||
| 
						 | 
					a9477c7873 | ||
| 
						 | 
					c225e5821b | ||
| 
						 | 
					d61309b0ce | ||
| 
						 | 
					5b2a073033 | ||
| 
						 | 
					f17e5f58d8 | ||
| 
						 | 
					d3668f55bb | ||
| 
						 | 
					6ca136f039 | ||
| 
						 | 
					e2a136d885 | ||
| 
						 | 
					d10f3e9df0 | ||
| 
						 | 
					46eb2e3e32 | ||
| 
						 | 
					21fd63be6f | ||
| 
						 | 
					703e228345 | ||
| 
						 | 
					2825f6c593 | ||
| 
						 | 
					ac7d42535f | ||
| 
						 | 
					1767899a8a | ||
| 
						 | 
					e9f046bea1 | ||
| 
						 | 
					4fc23a84dc | ||
| 
						 | 
					c886ccf603 | ||
| 
						 | 
					cf9b0690fd | ||
| 
						 | 
					d215ce685d | ||
| 
						 | 
					0492c711cc | ||
| 
						 | 
					42f33162ea | ||
| 
						 | 
					4a051fee19 | ||
| 
						 | 
					441c75ec64 | ||
| 
						 | 
					72ae2bc0cd | ||
| 
						 | 
					548a87e945 | ||
| 
						 | 
					ed1c068c36 | ||
| 
						 | 
					ec19491f4f | ||
| 
						 | 
					e56cd3caca | ||
| 
						 | 
					eb77880f9f | 
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										51
									
								
								CHANGES.md
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								CHANGES.md
									
									
									
									
									
								
							@@ -4,16 +4,57 @@ All notable changes to this project will be documented in this file.
 | 
				
			|||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 | 
					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]
 | 
				
			||||||
### Added
 | 
					
 | 
				
			||||||
-
 | 
					## [1.2.5] - 2020-03-02
 | 
				
			||||||
 | 
					### Fixed
 | 
				
			||||||
 | 
					- Skip crash when accessing YouTube-API-only fields in scrape mode ([@ritiek](https://github.com/ritiek)) (#672)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Changed
 | 
					### Changed
 | 
				
			||||||
-
 | 
					- Changed FFMPEG args to convert to 48k quality audio instead of the current 44k audio. ([@AvinashReddy3108](https://github.com/AvinashReddy3108)) (#667)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [1.2.4] - 2020-01-10
 | 
				
			||||||
 | 
					### Fixed
 | 
				
			||||||
 | 
					- Fixed a crash occuring when lyrics for a track are not yet released
 | 
				
			||||||
 | 
					  on Genius ([@ritiek](https://github.com/ritiek)) (#654)
 | 
				
			||||||
 | 
					- Fixed a regression where a track would fail to download if it isn't
 | 
				
			||||||
 | 
					  found on Spotify ([@ritiek](https://github.com/ritiek)) (#653)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [1.2.3] - 2019-12-20
 | 
				
			||||||
 | 
					### 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
 | 
				
			||||||
 | 
					- 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
 | 
				
			||||||
 | 
					### Added
 | 
				
			||||||
 | 
					- `--write-to` parameter for setting custom file to write Spotify track URLs to ([@ritiek](https://github.com/ritiek)) (#507)
 | 
				
			||||||
 | 
					- Set custom Spotify Client ID and Client Secret via config.yml ([@ManveerBasra](https://github.com/ManveerBasra)) (#502)
 | 
				
			||||||
 | 
					- Use YouTube as fallback metadata if track not found on Spotify. Also added `--no-fallback-metadata`
 | 
				
			||||||
 | 
					  to preserve old behaviour ([@ritiek](https://github.com/ritiek)) (#457)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fixed
 | 
				
			||||||
 | 
					- Fix already downloaded prompt when using "/" in `--file-format` to create sub-directories ([@ritiek](https://github.com/ritiek)) (#503)
 | 
				
			||||||
 | 
					- Fix writing playlist tracks to file ([@ritiek](https://github.com/ritiek)) (#506)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## [1.1.2] - 2019-02-10
 | 
					## [1.1.2] - 2019-02-10
 | 
				
			||||||
### Changed
 | 
					### Changed
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.md
									
									
									
									
									
								
							@@ -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.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										3
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								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",
 | 
				
			||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +1 @@
 | 
				
			|||||||
__version__ = "1.1.2"
 | 
					__version__ = "1.2.5"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 "
 | 
				
			||||||
@@ -106,7 +117,7 @@ class Converter:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if self.input_ext == ".m4a":
 | 
					        if self.input_ext == ".m4a":
 | 
				
			||||||
            if self.output_ext == ".mp3":
 | 
					            if self.output_ext == ".mp3":
 | 
				
			||||||
                ffmpeg_params = "-codec:v copy -codec:a libmp3lame -ar 44100 "
 | 
					                ffmpeg_params = "-codec:v copy -codec:a libmp3lame -ar 48000 "
 | 
				
			||||||
            elif self.output_ext == ".webm":
 | 
					            elif self.output_ext == ".webm":
 | 
				
			||||||
                ffmpeg_params = "-codec:a libopus -vbr on "
 | 
					                ffmpeg_params = "-codec:a libopus -vbr on "
 | 
				
			||||||
            elif self.output_ext == ".m4a":
 | 
					            elif self.output_ext == ".m4a":
 | 
				
			||||||
@@ -114,12 +125,12 @@ class Converter:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        elif self.input_ext == ".webm":
 | 
					        elif self.input_ext == ".webm":
 | 
				
			||||||
            if self.output_ext == ".mp3":
 | 
					            if self.output_ext == ".mp3":
 | 
				
			||||||
                ffmpeg_params = "-codec:a libmp3lame -ar 44100 "
 | 
					                ffmpeg_params = "-codec:a libmp3lame -ar 48000 "
 | 
				
			||||||
            elif self.output_ext == ".m4a":
 | 
					            elif self.output_ext == ".m4a":
 | 
				
			||||||
                ffmpeg_params = "-cutoff 20000 -codec:a aac -ar 44100 "
 | 
					                ffmpeg_params = "-cutoff 20000 -codec:a aac -ar 48000 "
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.output_ext == ".flac":
 | 
					        if self.output_ext == ".flac":
 | 
				
			||||||
            ffmpeg_params = "-codec:a flac -ar 44100 "
 | 
					            ffmpeg_params = "-codec:a flac -ar 48000 "
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # add common params for any of the above combination
 | 
					        # add common params for any of the above combination
 | 
				
			||||||
        ffmpeg_params += "-b:a 192k -vn "
 | 
					        ffmpeg_params += "-b:a 192k -vn "
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,16 +14,20 @@ from spotdl import youtube_tools
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class CheckExists:
 | 
					class CheckExists:
 | 
				
			||||||
    def __init__(self, music_file, meta_tags=None):
 | 
					    def __init__(self, music_file, meta_tags=None):
 | 
				
			||||||
        self.music_file = music_file
 | 
					 | 
				
			||||||
        self.meta_tags = meta_tags
 | 
					        self.meta_tags = meta_tags
 | 
				
			||||||
 | 
					        basepath, filename = os.path.split(music_file)
 | 
				
			||||||
 | 
					        filepath = os.path.join(const.args.folder, basepath)
 | 
				
			||||||
 | 
					        os.makedirs(filepath, exist_ok=True)
 | 
				
			||||||
 | 
					        self.filepath = filepath
 | 
				
			||||||
 | 
					        self.filename = filename
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def already_exists(self, raw_song):
 | 
					    def already_exists(self, raw_song):
 | 
				
			||||||
        """ Check if the input song already exists in the given folder. """
 | 
					        """ Check if the input song already exists in the given folder. """
 | 
				
			||||||
        log.debug(
 | 
					        log.debug(
 | 
				
			||||||
            "Cleaning any temp files and checking "
 | 
					            "Cleaning any temp files and checking "
 | 
				
			||||||
            'if "{}" already exists'.format(self.music_file)
 | 
					            'if "{}" already exists'.format(self.filename)
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        songs = os.listdir(const.args.folder)
 | 
					        songs = os.listdir(self.filepath)
 | 
				
			||||||
        self._remove_temp_files(songs)
 | 
					        self._remove_temp_files(songs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for song in songs:
 | 
					        for song in songs:
 | 
				
			||||||
@@ -45,17 +49,17 @@ class CheckExists:
 | 
				
			|||||||
    def _remove_temp_files(self, songs):
 | 
					    def _remove_temp_files(self, songs):
 | 
				
			||||||
        for song in songs:
 | 
					        for song in songs:
 | 
				
			||||||
            if song.endswith(".temp"):
 | 
					            if song.endswith(".temp"):
 | 
				
			||||||
                os.remove(os.path.join(const.args.folder, song))
 | 
					                os.remove(os.path.join(self.filepath, song))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _has_metadata(self, song):
 | 
					    def _has_metadata(self, song):
 | 
				
			||||||
        # check if the already downloaded song has correct metadata
 | 
					        # check if the already downloaded song has correct metadata
 | 
				
			||||||
        # if not, remove it and download again without prompt
 | 
					        # if not, remove it and download again without prompt
 | 
				
			||||||
        already_tagged = metadata.compare(
 | 
					        already_tagged = metadata.compare(
 | 
				
			||||||
            os.path.join(const.args.folder, song), self.meta_tags
 | 
					            os.path.join(self.filepath, song), self.meta_tags
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        log.debug("Checking if it is already tagged correctly? {}", already_tagged)
 | 
					        log.debug("Checking if it is already tagged correctly? {}", already_tagged)
 | 
				
			||||||
        if not already_tagged:
 | 
					        if not already_tagged:
 | 
				
			||||||
            os.remove(os.path.join(const.args.folder, song))
 | 
					            os.remove(os.path.join(self.filepath, song))
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
@@ -80,7 +84,7 @@ class CheckExists:
 | 
				
			|||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _match_filenames(self, song):
 | 
					    def _match_filenames(self, song):
 | 
				
			||||||
        if os.path.splitext(song)[0] == self.music_file:
 | 
					        if os.path.splitext(song)[0] == self.filename:
 | 
				
			||||||
            log.debug('Found an already existing song: "{}"'.format(song))
 | 
					            log.debug('Found an already existing song: "{}"'.format(song))
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -92,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. """
 | 
				
			||||||
@@ -129,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"
 | 
				
			||||||
@@ -153,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(
 | 
				
			||||||
@@ -163,8 +172,6 @@ class Downloader:
 | 
				
			|||||||
            if not refined_songname == " - ":
 | 
					            if not refined_songname == " - ":
 | 
				
			||||||
                songname = refined_songname
 | 
					                songname = refined_songname
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            if not const.args.no_metadata:
 | 
					 | 
				
			||||||
                log.warning("Could not find metadata")
 | 
					 | 
				
			||||||
            songname = internals.sanitize_title(songname)
 | 
					            songname = internals.sanitize_title(songname)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return songname
 | 
					        return songname
 | 
				
			||||||
@@ -204,12 +211,8 @@ class ListDownloader:
 | 
				
			|||||||
            try:
 | 
					            try:
 | 
				
			||||||
                track_dl = Downloader(raw_song, number=number)
 | 
					                track_dl = Downloader(raw_song, number=number)
 | 
				
			||||||
                track_dl.download_single()
 | 
					                track_dl.download_single()
 | 
				
			||||||
            except spotipy.client.SpotifyException:
 | 
					 | 
				
			||||||
                # token expires after 1 hour
 | 
					 | 
				
			||||||
                self._regenerate_token()
 | 
					 | 
				
			||||||
                track_dl.download_single()
 | 
					 | 
				
			||||||
            # detect network problems
 | 
					 | 
				
			||||||
            except (urllib.request.URLError, TypeError, IOError) as e:
 | 
					            except (urllib.request.URLError, TypeError, IOError) as e:
 | 
				
			||||||
 | 
					                # detect network problems
 | 
				
			||||||
                self._cleanup(raw_song, e)
 | 
					                self._cleanup(raw_song, e)
 | 
				
			||||||
                # TODO: remove this sleep once #397 is fixed
 | 
					                # TODO: remove this sleep once #397 is fixed
 | 
				
			||||||
                # wait 0.5 sec to avoid infinite looping
 | 
					                # wait 0.5 sec to avoid infinite looping
 | 
				
			||||||
@@ -235,11 +238,6 @@ class ListDownloader:
 | 
				
			|||||||
        with open(self.write_successful_file, "a") as f:
 | 
					        with open(self.write_successful_file, "a") as f:
 | 
				
			||||||
            f.write("\n" + raw_song)
 | 
					            f.write("\n" + raw_song)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @staticmethod
 | 
					 | 
				
			||||||
    def _regenerate_token():
 | 
					 | 
				
			||||||
        log.debug("Token expired, generating new one and authorizing")
 | 
					 | 
				
			||||||
        spotify_tools.refresh_token()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _cleanup(self, raw_song, exception):
 | 
					    def _cleanup(self, raw_song, exception):
 | 
				
			||||||
        self.tracks.append(raw_song)
 | 
					        self.tracks.append(raw_song)
 | 
				
			||||||
        # remove the downloaded song from file
 | 
					        # remove the downloaded song from file
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,13 +15,16 @@ _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,
 | 
				
			||||||
        "avconv": False,
 | 
					        "avconv": False,
 | 
				
			||||||
        "folder": internals.get_music_dir(),
 | 
					        "folder": internals.get_music_dir(),
 | 
				
			||||||
        "overwrite": "prompt",
 | 
					        "overwrite": "prompt",
 | 
				
			||||||
        "input-ext": ".m4a",
 | 
					        "input-ext": ".m4a",
 | 
				
			||||||
        "output-ext": ".mp3",
 | 
					        "output-ext": ".mp3",
 | 
				
			||||||
 | 
					        "write-to": None,
 | 
				
			||||||
        "trim-silence": False,
 | 
					        "trim-silence": False,
 | 
				
			||||||
        "download-only-metadata": False,
 | 
					        "download-only-metadata": False,
 | 
				
			||||||
        "dry-run": False,
 | 
					        "dry-run": False,
 | 
				
			||||||
@@ -33,6 +36,8 @@ default_conf = {
 | 
				
			|||||||
        "skip": None,
 | 
					        "skip": None,
 | 
				
			||||||
        "write-successful": None,
 | 
					        "write-successful": None,
 | 
				
			||||||
        "log-level": "INFO",
 | 
					        "log-level": "INFO",
 | 
				
			||||||
 | 
					        "spotify_client_id": "4fe3fecfe5334023a1472516cc99d805",
 | 
				
			||||||
 | 
					        "spotify_client_secret": "0f02b7c483c04257984695007a4a8d5c",
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -54,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:
 | 
				
			||||||
@@ -132,7 +137,14 @@ def get_arguments(raw_args=None, to_group=True, to_merge=True):
 | 
				
			|||||||
        "-m",
 | 
					        "-m",
 | 
				
			||||||
        "--manual",
 | 
					        "--manual",
 | 
				
			||||||
        default=config["manual"],
 | 
					        default=config["manual"],
 | 
				
			||||||
        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",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "-nr",
 | 
				
			||||||
 | 
					        "--no-remove-original",
 | 
				
			||||||
 | 
					        default=config["no-remove-original"],
 | 
				
			||||||
 | 
					        help="do not remove the original file after conversion",
 | 
				
			||||||
        action="store_true",
 | 
					        action="store_true",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    parser.add_argument(
 | 
					    parser.add_argument(
 | 
				
			||||||
@@ -142,6 +154,13 @@ def get_arguments(raw_args=None, to_group=True, to_merge=True):
 | 
				
			|||||||
        help="do not embed metadata in tracks",
 | 
					        help="do not embed metadata in tracks",
 | 
				
			||||||
        action="store_true",
 | 
					        action="store_true",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "-nf",
 | 
				
			||||||
 | 
					        "--no-fallback-metadata",
 | 
				
			||||||
 | 
					        default=config["no-fallback-metadata"],
 | 
				
			||||||
 | 
					        help="do not use YouTube as fallback for metadata if track not found on Spotify",
 | 
				
			||||||
 | 
					        action="store_true",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    parser.add_argument(
 | 
					    parser.add_argument(
 | 
				
			||||||
        "-a",
 | 
					        "-a",
 | 
				
			||||||
        "--avconv",
 | 
					        "--avconv",
 | 
				
			||||||
@@ -174,6 +193,11 @@ def get_arguments(raw_args=None, to_group=True, to_merge=True):
 | 
				
			|||||||
        default=config["output-ext"],
 | 
					        default=config["output-ext"],
 | 
				
			||||||
        help="preferred output format .mp3, .m4a (AAC), .flac, etc.",
 | 
					        help="preferred output format .mp3, .m4a (AAC), .flac, etc.",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "--write-to",
 | 
				
			||||||
 | 
					        default=config["write-to"],
 | 
				
			||||||
 | 
					        help="write tracks from Spotify playlist, album, etc. to this file",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    parser.add_argument(
 | 
					    parser.add_argument(
 | 
				
			||||||
        "-ff",
 | 
					        "-ff",
 | 
				
			||||||
        "--file-format",
 | 
					        "--file-format",
 | 
				
			||||||
@@ -252,6 +276,18 @@ def get_arguments(raw_args=None, to_group=True, to_merge=True):
 | 
				
			|||||||
        default=config["write-successful"],
 | 
					        default=config["write-successful"],
 | 
				
			||||||
        help="path to file to write successful tracks to",
 | 
					        help="path to file to write successful tracks to",
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "-sci",
 | 
				
			||||||
 | 
					        "--spotify-client-id",
 | 
				
			||||||
 | 
					        default=config["spotify_client_id"],
 | 
				
			||||||
 | 
					        help=argparse.SUPPRESS,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    parser.add_argument(
 | 
				
			||||||
 | 
					        "-scs",
 | 
				
			||||||
 | 
					        "--spotify-client-secret",
 | 
				
			||||||
 | 
					        default=config["spotify_client_secret"],
 | 
				
			||||||
 | 
					        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"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@@ -284,6 +320,13 @@ 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 or parsed.album or parsed.all_albums or parsed.username
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        parser.error(
 | 
				
			||||||
 | 
					            "--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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return parsed
 | 
					    return parsed
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,9 @@
 | 
				
			|||||||
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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from spotdl import const
 | 
					from spotdl import const
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,6 +32,7 @@ formats = {
 | 
				
			|||||||
    9: "track_number",
 | 
					    9: "track_number",
 | 
				
			||||||
    10: "total_tracks",
 | 
					    10: "total_tracks",
 | 
				
			||||||
    11: "isrc",
 | 
					    11: "isrc",
 | 
				
			||||||
 | 
					    12: "track_id",
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -50,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:
 | 
				
			||||||
@@ -73,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"]
 | 
				
			||||||
@@ -88,14 +93,25 @@ 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"]
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        format_tags[12] = tags["id"]
 | 
				
			||||||
 | 
					    except KeyError:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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:
 | 
				
			||||||
@@ -253,3 +269,12 @@ def remove_duplicates(tracks):
 | 
				
			|||||||
    local_set = set()
 | 
					    local_set = set()
 | 
				
			||||||
    local_set_add = local_set.add
 | 
					    local_set_add = local_set.add
 | 
				
			||||||
    return [x for x in tracks if not (x in local_set or local_set_add(x))]
 | 
					    return [x for x in tracks if not (x in local_set or local_set_add(x))]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def content_available(url):
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        response = urllib.request.urlopen(url)
 | 
				
			||||||
 | 
					    except urllib.request.HTTPError:
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        return response.getcode() < 300
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								spotdl/lyrics/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								spotdl/lyrics/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					from spotdl.lyrics.lyric_base import LyricBase
 | 
				
			||||||
							
								
								
									
										5
									
								
								spotdl/lyrics/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								spotdl/lyrics/exceptions.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					class LyricsNotFound(Exception):
 | 
				
			||||||
 | 
					    __module__ = Exception.__module__
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, message=None):
 | 
				
			||||||
 | 
					        super(LyricsNotFound, self).__init__(message)
 | 
				
			||||||
							
								
								
									
										14
									
								
								spotdl/lyrics/lyric_base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								spotdl/lyrics/lyric_base.py
									
									
									
									
									
										Normal 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
 | 
				
			||||||
							
								
								
									
										4
									
								
								spotdl/lyrics/providers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								spotdl/lyrics/providers/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					from spotdl.lyrics.providers.genius import Genius
 | 
				
			||||||
 | 
					from spotdl.lyrics.providers.lyricwikia_wrapper import LyricWikia
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LyricClasses = (Genius, LyricWikia)
 | 
				
			||||||
							
								
								
									
										49
									
								
								spotdl/lyrics/providers/genius.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								spotdl/lyrics/providers/genius.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					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")
 | 
				
			||||||
 | 
					        if lyrics_paragraph:
 | 
				
			||||||
 | 
					            return lyrics_paragraph.get_text()
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            raise LyricsNotFound("The lyrics for this track are yet to be released.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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)
 | 
				
			||||||
							
								
								
									
										18
									
								
								spotdl/lyrics/providers/lyricwikia_wrapper.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								spotdl/lyrics/providers/lyricwikia_wrapper.py
									
									
									
									
									
										Normal 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
 | 
				
			||||||
							
								
								
									
										0
									
								
								spotdl/lyrics/providers/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								spotdl/lyrics/providers/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										37
									
								
								spotdl/lyrics/providers/tests/test_genius.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								spotdl/lyrics/providers/tests/test_genius.py
									
									
									
									
									
										Normal 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()
 | 
				
			||||||
							
								
								
									
										35
									
								
								spotdl/lyrics/providers/tests/test_lyricwikia_wrapper.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								spotdl/lyrics/providers/tests/test_lyricwikia_wrapper.py
									
									
									
									
									
										Normal 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()
 | 
				
			||||||
@@ -46,6 +46,8 @@ class EmbedMetadata:
 | 
				
			|||||||
    def __init__(self, music_file, meta_tags):
 | 
					    def __init__(self, music_file, meta_tags):
 | 
				
			||||||
        self.music_file = music_file
 | 
					        self.music_file = music_file
 | 
				
			||||||
        self.meta_tags = meta_tags
 | 
					        self.meta_tags = meta_tags
 | 
				
			||||||
 | 
					        self.spotify_metadata = meta_tags["spotify_metadata"]
 | 
				
			||||||
 | 
					        self.provider = "spotify" if meta_tags["spotify_metadata"] else "youtube"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def as_mp3(self):
 | 
					    def as_mp3(self):
 | 
				
			||||||
        """ Embed metadata to MP3 files. """
 | 
					        """ Embed metadata to MP3 files. """
 | 
				
			||||||
@@ -62,7 +64,7 @@ class EmbedMetadata:
 | 
				
			|||||||
        audiofile["lyricist"] = meta_tags["artists"][0]["name"]
 | 
					        audiofile["lyricist"] = meta_tags["artists"][0]["name"]
 | 
				
			||||||
        audiofile["arranger"] = meta_tags["artists"][0]["name"]
 | 
					        audiofile["arranger"] = meta_tags["artists"][0]["name"]
 | 
				
			||||||
        audiofile["performer"] = meta_tags["artists"][0]["name"]
 | 
					        audiofile["performer"] = meta_tags["artists"][0]["name"]
 | 
				
			||||||
        audiofile["website"] = meta_tags["external_urls"]["spotify"]
 | 
					        audiofile["website"] = meta_tags["external_urls"][self.provider]
 | 
				
			||||||
        audiofile["length"] = str(meta_tags["duration"])
 | 
					        audiofile["length"] = str(meta_tags["duration"])
 | 
				
			||||||
        if meta_tags["publisher"]:
 | 
					        if meta_tags["publisher"]:
 | 
				
			||||||
            audiofile["encodedby"] = meta_tags["publisher"]
 | 
					            audiofile["encodedby"] = meta_tags["publisher"]
 | 
				
			||||||
@@ -78,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"]["spotify"])
 | 
					        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"]
 | 
				
			||||||
@@ -106,7 +110,7 @@ class EmbedMetadata:
 | 
				
			|||||||
        audiofile = MP4(music_file)
 | 
					        audiofile = MP4(music_file)
 | 
				
			||||||
        self._embed_basic_metadata(audiofile, preset=M4A_TAG_PRESET)
 | 
					        self._embed_basic_metadata(audiofile, preset=M4A_TAG_PRESET)
 | 
				
			||||||
        audiofile[M4A_TAG_PRESET["year"]] = meta_tags["year"]
 | 
					        audiofile[M4A_TAG_PRESET["year"]] = meta_tags["year"]
 | 
				
			||||||
        audiofile[M4A_TAG_PRESET["comment"]] = meta_tags["external_urls"]["spotify"]
 | 
					        audiofile[M4A_TAG_PRESET["comment"]] = meta_tags["external_urls"][self.provider]
 | 
				
			||||||
        if meta_tags["lyrics"]:
 | 
					        if meta_tags["lyrics"]:
 | 
				
			||||||
            audiofile[M4A_TAG_PRESET["lyrics"]] = meta_tags["lyrics"]
 | 
					            audiofile[M4A_TAG_PRESET["lyrics"]] = meta_tags["lyrics"]
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
@@ -127,7 +131,7 @@ class EmbedMetadata:
 | 
				
			|||||||
        audiofile = FLAC(music_file)
 | 
					        audiofile = FLAC(music_file)
 | 
				
			||||||
        self._embed_basic_metadata(audiofile)
 | 
					        self._embed_basic_metadata(audiofile)
 | 
				
			||||||
        audiofile["year"] = meta_tags["year"]
 | 
					        audiofile["year"] = meta_tags["year"]
 | 
				
			||||||
        audiofile["comment"] = meta_tags["external_urls"]["spotify"]
 | 
					        audiofile["comment"] = meta_tags["external_urls"][self.provider]
 | 
				
			||||||
        if meta_tags["lyrics"]:
 | 
					        if meta_tags["lyrics"]:
 | 
				
			||||||
            audiofile["lyrics"] = meta_tags["lyrics"]
 | 
					            audiofile["lyrics"] = meta_tags["lyrics"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -146,7 +150,9 @@ class EmbedMetadata:
 | 
				
			|||||||
    def _embed_basic_metadata(self, audiofile, preset=TAG_PRESET):
 | 
					    def _embed_basic_metadata(self, audiofile, preset=TAG_PRESET):
 | 
				
			||||||
        meta_tags = self.meta_tags
 | 
					        meta_tags = self.meta_tags
 | 
				
			||||||
        audiofile[preset["artist"]] = meta_tags["artists"][0]["name"]
 | 
					        audiofile[preset["artist"]] = meta_tags["artists"][0]["name"]
 | 
				
			||||||
 | 
					        if meta_tags["album"]["artists"][0]["name"]:
 | 
				
			||||||
            audiofile[preset["albumartist"]] = meta_tags["album"]["artists"][0]["name"]
 | 
					            audiofile[preset["albumartist"]] = meta_tags["album"]["artists"][0]["name"]
 | 
				
			||||||
 | 
					        if meta_tags["album"]["name"]:
 | 
				
			||||||
            audiofile[preset["album"]] = meta_tags["album"]["name"]
 | 
					            audiofile[preset["album"]] = meta_tags["album"]["name"]
 | 
				
			||||||
        audiofile[preset["title"]] = meta_tags["name"]
 | 
					        audiofile[preset["title"]] = meta_tags["name"]
 | 
				
			||||||
        audiofile[preset["date"]] = meta_tags["release_date"]
 | 
					        audiofile[preset["date"]] = meta_tags["release_date"]
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										64
									
								
								spotdl/patcher.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								spotdl/patcher.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
				
			|||||||
 | 
					from pafy import backend_youtube_dl
 | 
				
			||||||
 | 
					import pafy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from spotdl import internals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def _getbestthumb(self):
 | 
				
			||||||
 | 
					    url = self._ydl_info["thumbnails"][0]["url"]
 | 
				
			||||||
 | 
					    if url:
 | 
				
			||||||
 | 
					        return url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    part_url = "https://i.ytimg.com/vi/%s/" % self.videoid
 | 
				
			||||||
 | 
					    # Thumbnail resolution sorted in descending order
 | 
				
			||||||
 | 
					    thumbs = (
 | 
				
			||||||
 | 
					        "maxresdefault.jpg",
 | 
				
			||||||
 | 
					        "sddefault.jpg",
 | 
				
			||||||
 | 
					        "hqdefault.jpg",
 | 
				
			||||||
 | 
					        "mqdefault.jpg",
 | 
				
			||||||
 | 
					        "default.jpg",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    for thumb in thumbs:
 | 
				
			||||||
 | 
					        url = part_url + thumb
 | 
				
			||||||
 | 
					        if self._content_available(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
 | 
				
			||||||
 | 
					def _content_available(cls, url):
 | 
				
			||||||
 | 
					    return internals.content_available(url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PatchPafy:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
@@ -28,7 +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(
 | 
				
			||||||
 | 
					                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,
 | 
				
			||||||
@@ -37,13 +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(
 | 
				
			||||||
 | 
					            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(
 | 
				
			||||||
 | 
					            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(
 | 
				
			||||||
 | 
					            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(
 | 
				
			||||||
 | 
					            username=const.args.username, text_file=const.args.write_to
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def main():
 | 
					def main():
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
@@ -8,34 +7,41 @@ from logzero import logger as log
 | 
				
			|||||||
import pprint
 | 
					import pprint
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def generate_token():
 | 
					def generate_token():
 | 
				
			||||||
    """ Generate the token. Please respect these credentials :) """
 | 
					    """ Generate the token. """
 | 
				
			||||||
    credentials = oauth2.SpotifyClientCredentials(
 | 
					    credentials = oauth2.SpotifyClientCredentials(
 | 
				
			||||||
        client_id="4fe3fecfe5334023a1472516cc99d805",
 | 
					        client_id=const.args.spotify_client_id,
 | 
				
			||||||
        client_secret="0f02b7c483c04257984695007a4a8d5c",
 | 
					        client_secret=const.args.spotify_client_secret,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    token = credentials.get_access_token()
 | 
					    token = credentials.get_access_token()
 | 
				
			||||||
    return token
 | 
					    return token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def refresh_token():
 | 
					def must_be_authorized(func, spotify=spotify):
 | 
				
			||||||
    """ Refresh expired token"""
 | 
					    def wrapper(*args, **kwargs):
 | 
				
			||||||
        global spotify
 | 
					        global spotify
 | 
				
			||||||
    new_token = generate_token()
 | 
					        try:
 | 
				
			||||||
    spotify = spotipy.Spotify(auth=new_token)
 | 
					            assert spotify
 | 
				
			||||||
 | 
					            return func(*args, **kwargs)
 | 
				
			||||||
 | 
					        except (AssertionError, spotipy.client.SpotifyException):
 | 
				
			||||||
# token is mandatory when using Spotify's API
 | 
					            token = generate_token()
 | 
				
			||||||
# https://developer.spotify.com/news-stories/2017/01/27/removing-unauthenticated-calls-to-the-web-api/
 | 
					            spotify = spotipy.Spotify(auth=token)
 | 
				
			||||||
_token = generate_token()
 | 
					            return func(*args, **kwargs)
 | 
				
			||||||
spotify = spotipy.Spotify(auth=_token)
 | 
					
 | 
				
			||||||
 | 
					    return wrapper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@must_be_authorized
 | 
				
			||||||
def generate_metadata(raw_song):
 | 
					def generate_metadata(raw_song):
 | 
				
			||||||
    """ Fetch a song's metadata from Spotify. """
 | 
					    """ Fetch a song's metadata from Spotify. """
 | 
				
			||||||
    if internals.is_spotify(raw_song):
 | 
					    if internals.is_spotify(raw_song):
 | 
				
			||||||
@@ -70,17 +76,21 @@ 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")
 | 
				
			||||||
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        meta_tags["lyrics"] = lyricwikia.get_lyrics(
 | 
					 | 
				
			||||||
            meta_tags["artists"][0]["name"], meta_tags["name"]
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    except lyricwikia.LyricsNotFound:
 | 
					 | 
				
			||||||
    meta_tags["lyrics"] = None
 | 
					    meta_tags["lyrics"] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for LyricClass in LyricClasses:
 | 
				
			||||||
 | 
					        track = LyricClass(meta_tags["artists"][0]["name"], meta_tags["name"])
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            meta_tags["lyrics"] = track.get_lyrics()
 | 
				
			||||||
 | 
					        except LyricsNotFound:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Some sugar
 | 
					    # Some sugar
 | 
				
			||||||
    meta_tags["year"], *_ = meta_tags["release_date"].split("-")
 | 
					    meta_tags["year"], *_ = meta_tags["release_date"].split("-")
 | 
				
			||||||
    meta_tags["duration"] = meta_tags["duration_ms"] / 1000.0
 | 
					    meta_tags["duration"] = meta_tags["duration_ms"] / 1000.0
 | 
				
			||||||
 | 
					    meta_tags["spotify_metadata"] = True
 | 
				
			||||||
    # Remove unwanted parameters
 | 
					    # Remove unwanted parameters
 | 
				
			||||||
    del meta_tags["duration_ms"]
 | 
					    del meta_tags["duration_ms"]
 | 
				
			||||||
    del meta_tags["available_markets"]
 | 
					    del meta_tags["available_markets"]
 | 
				
			||||||
@@ -90,6 +100,15 @@ def generate_metadata(raw_song):
 | 
				
			|||||||
    return meta_tags
 | 
					    return meta_tags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@must_be_authorized
 | 
				
			||||||
 | 
					def write_user_playlist(username, text_file=None):
 | 
				
			||||||
 | 
					    """ Write user playlists to text_file """
 | 
				
			||||||
 | 
					    links = get_playlists(username=username)
 | 
				
			||||||
 | 
					    playlist = internals.input_link(links)
 | 
				
			||||||
 | 
					    return write_playlist(playlist, text_file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@must_be_authorized
 | 
				
			||||||
def get_playlists(username):
 | 
					def get_playlists(username):
 | 
				
			||||||
    """ Fetch user playlists when using the -u option. """
 | 
					    """ Fetch user playlists when using the -u option. """
 | 
				
			||||||
    playlists = spotify.user_playlists(username)
 | 
					    playlists = spotify.user_playlists(username)
 | 
				
			||||||
@@ -118,12 +137,7 @@ def get_playlists(username):
 | 
				
			|||||||
    return links
 | 
					    return links
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def write_user_playlist(username, text_file=None):
 | 
					@must_be_authorized
 | 
				
			||||||
    links = get_playlists(username=username)
 | 
					 | 
				
			||||||
    playlist = internals.input_link(links)
 | 
					 | 
				
			||||||
    return write_playlist(playlist, text_file)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def fetch_playlist(playlist):
 | 
					def fetch_playlist(playlist):
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        playlist_id = internals.extract_spotify_id(playlist)
 | 
					        playlist_id = internals.extract_spotify_id(playlist)
 | 
				
			||||||
@@ -143,21 +157,23 @@ def fetch_playlist(playlist):
 | 
				
			|||||||
    return results
 | 
					    return results
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@must_be_authorized
 | 
				
			||||||
def write_playlist(playlist_url, text_file=None):
 | 
					def write_playlist(playlist_url, text_file=None):
 | 
				
			||||||
    playlist = fetch_playlist(playlist_url)
 | 
					    playlist = fetch_playlist(playlist_url)
 | 
				
			||||||
    tracks = playlist["tracks"]
 | 
					    tracks = playlist["tracks"]
 | 
				
			||||||
    if not text_file:
 | 
					    if not text_file:
 | 
				
			||||||
        text_file = u"{0}.txt".format(slugify(playlist["name"], ok="-_()[]{}"))
 | 
					        text_file = u"{0}.txt".format(slugify(playlist["name"], ok="-_()[]{}"))
 | 
				
			||||||
    filepath = os.path.join(const.args.folder if const.args.folder else "", text_file)
 | 
					    return write_tracks(tracks, text_file)
 | 
				
			||||||
    return write_tracks(tracks, filepath)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@must_be_authorized
 | 
				
			||||||
def fetch_album(album):
 | 
					def fetch_album(album):
 | 
				
			||||||
    album_id = internals.extract_spotify_id(album)
 | 
					    album_id = internals.extract_spotify_id(album)
 | 
				
			||||||
    album = spotify.album(album_id)
 | 
					    album = spotify.album(album_id)
 | 
				
			||||||
    return album
 | 
					    return album
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@must_be_authorized
 | 
				
			||||||
def fetch_albums_from_artist(artist_url, album_type=None):
 | 
					def fetch_albums_from_artist(artist_url, album_type=None):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    This funcction returns all the albums from a give artist_url using the US
 | 
					    This funcction returns all the albums from a give artist_url using the US
 | 
				
			||||||
@@ -183,6 +199,7 @@ def fetch_albums_from_artist(artist_url, album_type=None):
 | 
				
			|||||||
    return albums
 | 
					    return albums
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@must_be_authorized
 | 
				
			||||||
def write_all_albums_from_artist(artist_url, text_file=None):
 | 
					def write_all_albums_from_artist(artist_url, text_file=None):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    This function gets all albums from an artist and writes it to a file in the
 | 
					    This function gets all albums from an artist and writes it to a file in the
 | 
				
			||||||
@@ -208,15 +225,16 @@ def write_all_albums_from_artist(artist_url, text_file=None):
 | 
				
			|||||||
        write_album(album_base_url + album["id"], text_file=text_file)
 | 
					        write_album(album_base_url + album["id"], text_file=text_file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@must_be_authorized
 | 
				
			||||||
def write_album(album_url, text_file=None):
 | 
					def write_album(album_url, text_file=None):
 | 
				
			||||||
    album = fetch_album(album_url)
 | 
					    album = fetch_album(album_url)
 | 
				
			||||||
    tracks = spotify.album_tracks(album["id"])
 | 
					    tracks = spotify.album_tracks(album["id"])
 | 
				
			||||||
    if not text_file:
 | 
					    if not text_file:
 | 
				
			||||||
        text_file = u"{0}.txt".format(slugify(album["name"], ok="-_()[]{}"))
 | 
					        text_file = u"{0}.txt".format(slugify(album["name"], ok="-_()[]{}"))
 | 
				
			||||||
    filepath = os.path.join(const.args.folder if const.args.folder else "", text_file)
 | 
					    return write_tracks(tracks, text_file)
 | 
				
			||||||
    return write_tracks(tracks, filepath)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@must_be_authorized
 | 
				
			||||||
def write_tracks(tracks, text_file):
 | 
					def write_tracks(tracks, text_file):
 | 
				
			||||||
    log.info(u"Writing {0} tracks to {1}".format(tracks["total"], text_file))
 | 
					    log.info(u"Writing {0} tracks to {1}".format(tracks["total"], text_file))
 | 
				
			||||||
    track_urls = []
 | 
					    track_urls = []
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,16 @@ from spotdl import const
 | 
				
			|||||||
# Read more on mps-youtube/pafy#199
 | 
					# Read more on mps-youtube/pafy#199
 | 
				
			||||||
pafy.g.opener.addheaders.append(("Range", "bytes=0-"))
 | 
					pafy.g.opener.addheaders.append(("Range", "bytes=0-"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Implement unreleased methods on Pafy object
 | 
				
			||||||
 | 
					# More info: https://github.com/mps-youtube/pafy/pull/211
 | 
				
			||||||
 | 
					if pafy.__version__ <= "0.5.5":
 | 
				
			||||||
 | 
					    from spotdl import patcher
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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():
 | 
				
			||||||
    if const.args.youtube_api_key:
 | 
					    if const.args.youtube_api_key:
 | 
				
			||||||
@@ -39,28 +49,90 @@ def go_pafy(raw_song, meta_tags=None):
 | 
				
			|||||||
    return track_info
 | 
					    return track_info
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def match_video_and_metadata(track, force_pafy=True):
 | 
					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):
 | 
				
			||||||
 | 
					        fallback_metadata_info = (
 | 
				
			||||||
 | 
					            "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 const.args.no_fallback_metadata:
 | 
				
			||||||
 | 
					                log.warning(skip_fallback_metadata_warning)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                log.info(fallback_metadata_info)
 | 
				
			||||||
 | 
					                meta_tags = generate_metadata(content)
 | 
				
			||||||
 | 
					        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)
 | 
				
			||||||
        track = slugify(content.title).replace("-", " ")
 | 
					        track = slugify(content.title).replace("-", " ")
 | 
				
			||||||
        if not const.args.no_metadata:
 | 
					        if not const.args.no_metadata:
 | 
				
			||||||
            meta_tags = spotify_tools.generate_metadata(track)
 | 
					            meta_tags = spotify_tools.generate_metadata(track)
 | 
				
			||||||
    else:
 | 
					            meta_tags = fallback_metadata(meta_tags)
 | 
				
			||||||
        # Let it generate metadata, youtube doesn't know spotify slang
 | 
					 | 
				
			||||||
        if not const.args.no_metadata or internals.is_spotify(track):
 | 
					 | 
				
			||||||
            meta_tags = spotify_tools.generate_metadata(track)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if force_pafy:
 | 
					    elif internals.is_spotify(track):
 | 
				
			||||||
 | 
					        log.debug("Input song is a Spotify URL")
 | 
				
			||||||
 | 
					        # Let it generate metadata, YouTube doesn't know Spotify slang
 | 
				
			||||||
 | 
					        meta_tags = spotify_tools.generate_metadata(track)
 | 
				
			||||||
        content = go_pafy(track, meta_tags)
 | 
					        content = go_pafy(track, meta_tags)
 | 
				
			||||||
 | 
					        if const.args.no_metadata:
 | 
				
			||||||
 | 
					            meta_tags = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
            content = None
 | 
					        log.debug("Input song is plain text based")
 | 
				
			||||||
 | 
					        if const.args.no_metadata:
 | 
				
			||||||
 | 
					            content = go_pafy(track, meta_tags=None)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            meta_tags = spotify_tools.generate_metadata(track)
 | 
				
			||||||
 | 
					            content = go_pafy(track, meta_tags=meta_tags)
 | 
				
			||||||
 | 
					            meta_tags = fallback_metadata(meta_tags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return content, meta_tags
 | 
					    return content, meta_tags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def generate_metadata(content):
 | 
				
			||||||
 | 
					    """ Fetch a song's metadata from YouTube. """
 | 
				
			||||||
 | 
					    meta_tags = {
 | 
				
			||||||
 | 
					        "spotify_metadata": False,
 | 
				
			||||||
 | 
					        "name": content.title,
 | 
				
			||||||
 | 
					        "artists": [{"name": content.author}],
 | 
				
			||||||
 | 
					        "duration": content.length,
 | 
				
			||||||
 | 
					        "external_urls": {"youtube": content.watchv_url},
 | 
				
			||||||
 | 
					        "album": {
 | 
				
			||||||
 | 
					            "images": [{"url": content.getbestthumb()}],
 | 
				
			||||||
 | 
					            "artists": [{"name": None}],
 | 
				
			||||||
 | 
					            "name": None,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "year": None,
 | 
				
			||||||
 | 
					        "release_date": None,
 | 
				
			||||||
 | 
					        "type": "track",
 | 
				
			||||||
 | 
					        "disc_number": 1,
 | 
				
			||||||
 | 
					        "track_number": 1,
 | 
				
			||||||
 | 
					        "total_tracks": 1,
 | 
				
			||||||
 | 
					        "publisher": None,
 | 
				
			||||||
 | 
					        "external_ids": {"isrc": None},
 | 
				
			||||||
 | 
					        "lyrics": None,
 | 
				
			||||||
 | 
					        "copyright": None,
 | 
				
			||||||
 | 
					        "genre": None,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Workaround for
 | 
				
			||||||
 | 
					    # https://github.com/ritiek/spotify-downloader/issues/671
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        meta_tags["year"] = content.published.split("-")[0]
 | 
				
			||||||
 | 
					        meta_tags["release_date"] = content.published.split(" ")[0]
 | 
				
			||||||
 | 
					    except pafy.util.GdataError:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return meta_tags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_youtube_title(content, number=None):
 | 
					def get_youtube_title(content, number=None):
 | 
				
			||||||
    """ Get the YouTube video's title. """
 | 
					    """ Get the YouTube video's title. """
 | 
				
			||||||
    title = content.title
 | 
					    title = content.title
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
from spotdl import const
 | 
					from spotdl import const
 | 
				
			||||||
from spotdl import handle
 | 
					from spotdl import handle
 | 
				
			||||||
from spotdl import spotdl
 | 
					from spotdl import spotdl
 | 
				
			||||||
 | 
					import urllib
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -13,3 +14,13 @@ def load_defaults():
 | 
				
			|||||||
    spotdl.log = const.logzero.setup_logger(
 | 
					    spotdl.log = const.logzero.setup_logger(
 | 
				
			||||||
        formatter=const._formatter, level=const.args.log_level
 | 
					        formatter=const._formatter, level=const.args.log_level
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# GIST_URL is the monkeypatched version of: https://www.youtube.com/results?search_query=janji+-+heroes
 | 
				
			||||||
 | 
					# 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"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def monkeypatch_youtube_search_page(*args, **kwargs):
 | 
				
			||||||
 | 
					    fake_urlopen = urllib.request.urlopen(GIST_URL)
 | 
				
			||||||
 | 
					    return fake_urlopen
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
import urllib
 | 
					 | 
				
			||||||
import subprocess
 | 
					import subprocess
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -39,7 +38,7 @@ def metadata_fixture():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_metadata(metadata_fixture):
 | 
					def test_metadata(metadata_fixture):
 | 
				
			||||||
    expect_number = 23
 | 
					    expect_number = 24
 | 
				
			||||||
    assert len(metadata_fixture) == expect_number
 | 
					    assert len(metadata_fixture) == expect_number
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -54,16 +53,11 @@ class TestFileFormat:
 | 
				
			|||||||
        assert title == EXPECTED_SPOTIFY_TITLE.replace(" ", "_")
 | 
					        assert title == EXPECTED_SPOTIFY_TITLE.replace(" ", "_")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def monkeypatch_youtube_search_page(*args, **kwargs):
 | 
					 | 
				
			||||||
    fake_urlopen = urllib.request.urlopen(GIST_URL)
 | 
					 | 
				
			||||||
    return fake_urlopen
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def test_youtube_url(metadata_fixture, monkeypatch):
 | 
					def test_youtube_url(metadata_fixture, monkeypatch):
 | 
				
			||||||
    monkeypatch.setattr(
 | 
					    monkeypatch.setattr(
 | 
				
			||||||
        youtube_tools.GenerateYouTubeURL,
 | 
					        youtube_tools.GenerateYouTubeURL,
 | 
				
			||||||
        "_fetch_response",
 | 
					        "_fetch_response",
 | 
				
			||||||
        monkeypatch_youtube_search_page,
 | 
					        loader.monkeypatch_youtube_search_page,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    url = youtube_tools.generate_youtube_url(SPOTIFY_TRACK_URL, metadata_fixture)
 | 
					    url = youtube_tools.generate_youtube_url(SPOTIFY_TRACK_URL, metadata_fixture)
 | 
				
			||||||
    assert url == EXPECTED_YOUTUBE_URL
 | 
					    assert url == EXPECTED_YOUTUBE_URL
 | 
				
			||||||
@@ -73,7 +67,7 @@ def test_youtube_title(metadata_fixture, monkeypatch):
 | 
				
			|||||||
    monkeypatch.setattr(
 | 
					    monkeypatch.setattr(
 | 
				
			||||||
        youtube_tools.GenerateYouTubeURL,
 | 
					        youtube_tools.GenerateYouTubeURL,
 | 
				
			||||||
        "_fetch_response",
 | 
					        "_fetch_response",
 | 
				
			||||||
        monkeypatch_youtube_search_page,
 | 
					        loader.monkeypatch_youtube_search_page,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    content = youtube_tools.go_pafy(SPOTIFY_TRACK_URL, metadata_fixture)
 | 
					    content = youtube_tools.go_pafy(SPOTIFY_TRACK_URL, metadata_fixture)
 | 
				
			||||||
    pytest.content_fixture = content
 | 
					    pytest.content_fixture = content
 | 
				
			||||||
@@ -110,6 +104,9 @@ class TestDownload:
 | 
				
			|||||||
        monkeypatch.setattr(
 | 
					        monkeypatch.setattr(
 | 
				
			||||||
            "pafy.backend_shared.BaseStream.download", self.blank_audio_generator
 | 
					            "pafy.backend_shared.BaseStream.download", self.blank_audio_generator
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					        monkeypatch.setattr(
 | 
				
			||||||
 | 
					            "pafy.backend_youtube_dl.YtdlStream.download", self.blank_audio_generator
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        download = youtube_tools.download_song(
 | 
					        download = youtube_tools.download_song(
 | 
				
			||||||
            filename_fixture + ".m4a", pytest.content_fixture
 | 
					            filename_fixture + ".m4a", pytest.content_fixture
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@@ -120,6 +117,9 @@ class TestDownload:
 | 
				
			|||||||
        monkeypatch.setattr(
 | 
					        monkeypatch.setattr(
 | 
				
			||||||
            "pafy.backend_shared.BaseStream.download", self.blank_audio_generator
 | 
					            "pafy.backend_shared.BaseStream.download", self.blank_audio_generator
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					        monkeypatch.setattr(
 | 
				
			||||||
 | 
					            "pafy.backend_youtube_dl.YtdlStream.download", self.blank_audio_generator
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        download = youtube_tools.download_song(
 | 
					        download = youtube_tools.download_song(
 | 
				
			||||||
            filename_fixture + ".webm", pytest.content_fixture
 | 
					            filename_fixture + ".webm", pytest.content_fixture
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@@ -128,7 +128,7 @@ class TestDownload:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
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)
 | 
				
			||||||
@@ -138,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)
 | 
				
			||||||
@@ -148,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)
 | 
				
			||||||
@@ -158,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)
 | 
				
			||||||
@@ -168,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)
 | 
				
			||||||
@@ -178,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(
 | 
				
			||||||
@@ -188,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(
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										36
									
								
								test/test_patcher.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								test/test_patcher.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					from spotdl import patcher
 | 
				
			||||||
 | 
					import pafy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import pytest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pafy_patcher = patcher.PatchPafy()
 | 
				
			||||||
 | 
					pafy_patcher.patch_getbestthumb()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestPafyContentAvailable:
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestMethodAssignment:
 | 
				
			||||||
 | 
					    def test_pafy_getbestthumb(self):
 | 
				
			||||||
 | 
					        pafy.backend_shared.BasePafy.getbestthumb == patcher._getbestthumb
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestMethodCalls:
 | 
				
			||||||
 | 
					    @pytest.fixture(scope="module")
 | 
				
			||||||
 | 
					    def content_fixture(self):
 | 
				
			||||||
 | 
					        content = pafy.new("http://youtube.com/watch?v=3nQNiWdeH2Q")
 | 
				
			||||||
 | 
					        return content
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_pafy_getbestthumb(self, content_fixture):
 | 
				
			||||||
 | 
					        thumbnail = patcher._getbestthumb(content_fixture)
 | 
				
			||||||
 | 
					        assert thumbnail == "https://i.ytimg.com/vi/3nQNiWdeH2Q/hqdefault.jpg"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_pafy_getbestthumb_without_ytdl(self, content_fixture):
 | 
				
			||||||
 | 
					        content_fixture._ydl_info["thumbnails"][0]["url"] = None
 | 
				
			||||||
 | 
					        thumbnail = patcher._getbestthumb(content_fixture)
 | 
				
			||||||
 | 
					        assert thumbnail == "https://i.ytimg.com/vi/3nQNiWdeH2Q/sddefault.jpg"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_pafy_content_available(self):
 | 
				
			||||||
 | 
					        TestPafyContentAvailable._content_available = patcher._content_available
 | 
				
			||||||
 | 
					        assert TestPafyContentAvailable()._content_available("https://youtube.com/")
 | 
				
			||||||
@@ -1,7 +1,13 @@
 | 
				
			|||||||
from spotdl import spotify_tools
 | 
					from spotdl import spotify_tools
 | 
				
			||||||
 | 
					from spotdl import const
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import spotipy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					import loader
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					loader.load_defaults()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_generate_token():
 | 
					def test_generate_token():
 | 
				
			||||||
@@ -9,11 +15,33 @@ def test_generate_token():
 | 
				
			|||||||
    assert len(token) == 83
 | 
					    assert len(token) == 83
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_refresh_token():
 | 
					class TestMustBeAuthorizedDecorator:
 | 
				
			||||||
    old_instance = spotify_tools.spotify
 | 
					    def test_spotify_instance_is_unset(self):
 | 
				
			||||||
    spotify_tools.refresh_token()
 | 
					        spotify_tools.spotify = None
 | 
				
			||||||
    new_instance = spotify_tools.spotify
 | 
					
 | 
				
			||||||
    assert not old_instance == new_instance
 | 
					        @spotify_tools.must_be_authorized
 | 
				
			||||||
 | 
					        def sample_func():
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert sample_func()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_spotify_instance_forces_assertion_error(self):
 | 
				
			||||||
 | 
					        @spotify_tools.must_be_authorized
 | 
				
			||||||
 | 
					        def sample_func():
 | 
				
			||||||
 | 
					            raise AssertionError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with pytest.raises(AssertionError):
 | 
				
			||||||
 | 
					            sample_func()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_fake_token_generator(self, monkeypatch):
 | 
				
			||||||
 | 
					        spotify_tools.spotify = None
 | 
				
			||||||
 | 
					        monkeypatch.setattr(spotify_tools, "generate_token", lambda: 123123)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with pytest.raises(spotipy.client.SpotifyException):
 | 
				
			||||||
 | 
					            spotify_tools.generate_metadata("ncs - spectre")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_correct_token(self):
 | 
				
			||||||
 | 
					        assert spotify_tools.generate_metadata("ncs - spectre")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestGenerateMetadata:
 | 
					class TestGenerateMetadata:
 | 
				
			||||||
@@ -23,7 +51,7 @@ class TestGenerateMetadata:
 | 
				
			|||||||
        return metadata
 | 
					        return metadata
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_len(self, metadata_fixture):
 | 
					    def test_len(self, metadata_fixture):
 | 
				
			||||||
        assert len(metadata_fixture) == 23
 | 
					        assert len(metadata_fixture) == 24
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_trackname(self, metadata_fixture):
 | 
					    def test_trackname(self, metadata_fixture):
 | 
				
			||||||
        assert metadata_fixture["name"] == "Spectre"
 | 
					        assert metadata_fixture["name"] == "Spectre"
 | 
				
			||||||
@@ -87,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):
 | 
				
			||||||
@@ -103,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):
 | 
				
			||||||
@@ -113,8 +141,7 @@ class TestFetchAlbumsFromArtist:
 | 
				
			|||||||
        return albums
 | 
					        return albums
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_len(self, albums_from_artist_fixture):
 | 
					    def test_len(self, albums_from_artist_fixture):
 | 
				
			||||||
        # TODO: Mock this test (failed in #493)
 | 
					        assert len(albums_from_artist_fixture) == 54
 | 
				
			||||||
        assert len(albums_from_artist_fixture) == 52
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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"
 | 
				
			||||||
@@ -129,7 +156,6 @@ class TestFetchAlbumsFromArtist:
 | 
				
			|||||||
        assert albums_from_artist_fixture[0]["total_tracks"] == 12
 | 
					        assert albums_from_artist_fixture[0]["total_tracks"] == 12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# TODO: Mock this test (failed in #493)
 | 
					 | 
				
			||||||
def test_write_all_albums_from_artist(tmpdir):
 | 
					def test_write_all_albums_from_artist(tmpdir):
 | 
				
			||||||
    expect_tracks = 282
 | 
					    expect_tracks = 282
 | 
				
			||||||
    text_file = os.path.join(str(tmpdir), "test_ab.txt")
 | 
					    text_file = os.path.join(str(tmpdir), "test_ab.txt")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,6 @@ YT_API_KEY = "AIzaSyAnItl3udec-Q1d5bkjKJGL-RgrKO_vU90"
 | 
				
			|||||||
TRACK_SEARCH = "Tony's Videos VERY SHORT VIDEO 28.10.2016"
 | 
					TRACK_SEARCH = "Tony's Videos VERY SHORT VIDEO 28.10.2016"
 | 
				
			||||||
EXPECTED_TITLE = TRACK_SEARCH
 | 
					EXPECTED_TITLE = TRACK_SEARCH
 | 
				
			||||||
EXPECTED_YT_URL = "http://youtube.com/watch?v=qOOcy2-tmbk"
 | 
					EXPECTED_YT_URL = "http://youtube.com/watch?v=qOOcy2-tmbk"
 | 
				
			||||||
EXPECTED_YT_URLS = (EXPECTED_YT_URL, "http://youtube.com/watch?v=5USR1Omo7f0")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
RESULT_COUNT_SEARCH = "she is still sleeping SAO"
 | 
					RESULT_COUNT_SEARCH = "she is still sleeping SAO"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -71,8 +70,7 @@ class TestYouTubeURL:
 | 
				
			|||||||
    def test_only_music_category(self, metadata_fixture):
 | 
					    def test_only_music_category(self, metadata_fixture):
 | 
				
			||||||
        const.args.music_videos_only = True
 | 
					        const.args.music_videos_only = True
 | 
				
			||||||
        url = youtube_tools.generate_youtube_url(TRACK_SEARCH, metadata_fixture)
 | 
					        url = youtube_tools.generate_youtube_url(TRACK_SEARCH, metadata_fixture)
 | 
				
			||||||
        # YouTube keeps changing its results
 | 
					        assert url == EXPECTED_YT_URL
 | 
				
			||||||
        assert url in EXPECTED_YT_URLS
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_all_categories(self, metadata_fixture):
 | 
					    def test_all_categories(self, metadata_fixture):
 | 
				
			||||||
        const.args.music_videos_only = False
 | 
					        const.args.music_videos_only = False
 | 
				
			||||||
@@ -99,6 +97,72 @@ def content_fixture(metadata_fixture):
 | 
				
			|||||||
    return content
 | 
					    return content
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# True = Metadata must be fetched from Spotify
 | 
				
			||||||
 | 
					# False = Metadata must be fetched from YouTube
 | 
				
			||||||
 | 
					# None = Metadata must be `None`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MATCH_METADATA_NO_FALLBACK_TEST_TABLE = [
 | 
				
			||||||
 | 
					    ("https://open.spotify.com/track/5nWduGwBGBn1PSqYTJUDbS", True),
 | 
				
			||||||
 | 
					    ("http://youtube.com/watch?v=3nQNiWdeH2Q", None),
 | 
				
			||||||
 | 
					    ("Linux Talk | Working with Drives and Filesystems", None),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MATCH_METADATA_FALLBACK_TEST_TABLE = [
 | 
				
			||||||
 | 
					    ("https://open.spotify.com/track/5nWduGwBGBn1PSqYTJUDbS", True),
 | 
				
			||||||
 | 
					    ("http://youtube.com/watch?v=3nQNiWdeH2Q", False),
 | 
				
			||||||
 | 
					    ("Linux Talk | Working with Drives and Filesystems", False),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MATCH_METADATA_NO_METADATA_TEST_TABLE = [
 | 
				
			||||||
 | 
					    ("https://open.spotify.com/track/5nWduGwBGBn1PSqYTJUDbS", None),
 | 
				
			||||||
 | 
					    ("http://youtube.com/watch?v=3nQNiWdeH2Q", None),
 | 
				
			||||||
 | 
					    ("Linux Talk | Working with Drives and Filesystems", None),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestMetadataOrigin:
 | 
				
			||||||
 | 
					    def match_metadata(self, track, metadata_type):
 | 
				
			||||||
 | 
					        _, metadata = youtube_tools.match_video_and_metadata(track)
 | 
				
			||||||
 | 
					        if metadata_type is None:
 | 
				
			||||||
 | 
					            assert metadata == metadata_type
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            assert metadata["spotify_metadata"] == metadata_type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @pytest.mark.parametrize(
 | 
				
			||||||
 | 
					        "track, metadata_type", MATCH_METADATA_NO_FALLBACK_TEST_TABLE
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					        self.match_metadata(track, metadata_type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @pytest.mark.parametrize("track, metadata_type", MATCH_METADATA_FALLBACK_TEST_TABLE)
 | 
				
			||||||
 | 
					    def test_match_metadata_with_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 = False
 | 
				
			||||||
 | 
					        self.match_metadata(track, metadata_type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @pytest.mark.parametrize(
 | 
				
			||||||
 | 
					        "track, metadata_type", MATCH_METADATA_NO_METADATA_TEST_TABLE
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					        self.match_metadata(track, metadata_type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.fixture(scope="module")
 | 
					@pytest.fixture(scope="module")
 | 
				
			||||||
def title_fixture(content_fixture):
 | 
					def title_fixture(content_fixture):
 | 
				
			||||||
    title = youtube_tools.get_youtube_title(content_fixture)
 | 
					    title = youtube_tools.get_youtube_title(content_fixture)
 | 
				
			||||||
@@ -136,6 +200,30 @@ def test_check_exists(metadata_fixture, filename_fixture, tmpdir):
 | 
				
			|||||||
    assert check == expect_check
 | 
					    assert check == expect_check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_generate_m3u(tmpdir, monkeypatch):
 | 
				
			||||||
 | 
					    monkeypatch.setattr(
 | 
				
			||||||
 | 
					        youtube_tools.GenerateYouTubeURL,
 | 
				
			||||||
 | 
					        "_fetch_response",
 | 
				
			||||||
 | 
					        loader.monkeypatch_youtube_search_page,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    expect_m3u = (
 | 
				
			||||||
 | 
					        "#EXTM3U\n\n"
 | 
				
			||||||
 | 
					        "#EXTINF:208,Janji - Heroes Tonight (feat. Johnning) [NCS Release]\n"
 | 
				
			||||||
 | 
					        "http://www.youtube.com/watch?v=3nQNiWdeH2Q\n"
 | 
				
			||||||
 | 
					        "#EXTINF:226,Alan Walker - Spectre [NCS Release]\n"
 | 
				
			||||||
 | 
					        "http://www.youtube.com/watch?v=AOeY-nDp7hI\n"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    m3u_track_file = os.path.join(str(tmpdir), "m3u_test.txt")
 | 
				
			||||||
 | 
					    with open(m3u_track_file, "w") as track_file:
 | 
				
			||||||
 | 
					        track_file.write("\nhttps://open.spotify.com/track/3SipFlNddvL0XNZRLXvdZD")
 | 
				
			||||||
 | 
					        track_file.write("\nhttp://www.youtube.com/watch?v=AOeY-nDp7hI")
 | 
				
			||||||
 | 
					    youtube_tools.generate_m3u(m3u_track_file)
 | 
				
			||||||
 | 
					    m3u_file = "{}.m3u".format(m3u_track_file.split(".")[0])
 | 
				
			||||||
 | 
					    with open(m3u_file, "r") as m3u_in:
 | 
				
			||||||
 | 
					        m3u = m3u_in.readlines()
 | 
				
			||||||
 | 
					    assert "".join(m3u) == expect_m3u
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestDownload:
 | 
					class TestDownload:
 | 
				
			||||||
    def test_webm(self, content_fixture, filename_fixture, monkeypatch):
 | 
					    def test_webm(self, content_fixture, filename_fixture, monkeypatch):
 | 
				
			||||||
        # content_fixture does not have any .webm audiostream
 | 
					        # content_fixture does not have any .webm audiostream
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user