mirror of
https://github.com/KevinMidboe/spotify-downloader.git
synced 2025-10-29 18:00:15 +00:00
Merge branch 'master' into introduce-releases
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,9 +1,9 @@
|
|||||||
config.yml
|
config.yml
|
||||||
Music/
|
Music/
|
||||||
*.txt
|
*.txt
|
||||||
.cache/
|
|
||||||
README.rst
|
|
||||||
upload.sh
|
upload.sh
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ python:
|
|||||||
- "3.6"
|
- "3.6"
|
||||||
before_install:
|
before_install:
|
||||||
- pip install tinydownload
|
- pip install tinydownload
|
||||||
- pip install codecov
|
|
||||||
- pip install pytest-cov
|
- pip install pytest-cov
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
@@ -40,4 +39,6 @@ install:
|
|||||||
- tinydownload 22684734659253158385 -o ~/bin/ffmpeg
|
- tinydownload 22684734659253158385 -o ~/bin/ffmpeg
|
||||||
- chmod 755 ~/bin/ffmpeg
|
- chmod 755 ~/bin/ffmpeg
|
||||||
script: python -m pytest test --cov=.
|
script: python -m pytest test --cov=.
|
||||||
after_success: codecov
|
after_success:
|
||||||
|
- pip install codecov
|
||||||
|
- codecov
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -148,6 +148,8 @@ optional arguments:
|
|||||||
(default: False)
|
(default: False)
|
||||||
-ll {INFO,WARNING,ERROR,DEBUG}, --log-level {INFO,WARNING,ERROR,DEBUG}
|
-ll {INFO,WARNING,ERROR,DEBUG}, --log-level {INFO,WARNING,ERROR,DEBUG}
|
||||||
set log verbosity (default: INFO)
|
set log verbosity (default: INFO)
|
||||||
|
-c CONFIG_FILE_PATH --config CONFIG_FILE_PATH
|
||||||
|
Replace with custom config.yml file (default: None)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Download by Name
|
#### Download by Name
|
||||||
@@ -269,6 +271,16 @@ to override any default options.
|
|||||||
|
|
||||||
Also note that config options are overridden by command-line arguments.
|
Also note that config options are overridden by command-line arguments.
|
||||||
|
|
||||||
|
If you want to use custom `.yml` configuration instead of the default one, you can use `-c`/`--config` option.
|
||||||
|
E.g. `$ python3 spotdl.py -s "adele hello" -c "/home/user/customConfig.yml"`
|
||||||
|
|
||||||
|
## Set YouTube API Key
|
||||||
|
|
||||||
|
By default this tool will scrape YouTube to fetch for matching video tracks.
|
||||||
|
However, you can optionally use YouTube API for faster response time.
|
||||||
|
To do this, [generate your API key](https://developers.google.com/youtube/registering_an_application)
|
||||||
|
and then set it in your `config.yml`.
|
||||||
|
|
||||||
## [Docker Image](https://hub.docker.com/r/ritiek/spotify-downloader/)
|
## [Docker Image](https://hub.docker.com/r/ritiek/spotify-downloader/)
|
||||||
[](https://hub.docker.com/r/ritiek/spotify-downloader)
|
[](https://hub.docker.com/r/ritiek/spotify-downloader)
|
||||||
[](https://hub.docker.com/r/ritiek/spotify-downloader)
|
[](https://hub.docker.com/r/ritiek/spotify-downloader)
|
||||||
|
|||||||
14
config.yml.bak
Executable file
14
config.yml.bak
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
spotify-downloader:
|
||||||
|
avconv: false
|
||||||
|
download-only-metadata: false
|
||||||
|
dry-run: false
|
||||||
|
file-format: '{artist} - {track_name}'
|
||||||
|
folder: /home/linus/GitHub/spotify-downloader/Music
|
||||||
|
input-ext: .m4a
|
||||||
|
log-level: INFO
|
||||||
|
manual: false
|
||||||
|
music-videos-only: false
|
||||||
|
no-metadata: false
|
||||||
|
no-spaces: false
|
||||||
|
output-ext: .mp3
|
||||||
|
overwrite: prompt
|
||||||
@@ -58,16 +58,18 @@ class Converter:
|
|||||||
|
|
||||||
if input_ext == '.m4a':
|
if input_ext == '.m4a':
|
||||||
if output_ext == '.mp3':
|
if output_ext == '.mp3':
|
||||||
ffmpeg_params = '-codec:v copy -codec:a libmp3lame -q:a 2 '
|
ffmpeg_params = '-codec:v copy -codec:a libmp3lame -ar 44100 '
|
||||||
elif output_ext == '.webm':
|
elif output_ext == '.webm':
|
||||||
ffmpeg_params = '-c:a libopus -vbr on -b:a 192k -vn '
|
ffmpeg_params = '-codec:a libopus -vbr on '
|
||||||
|
|
||||||
elif input_ext == '.webm':
|
elif input_ext == '.webm':
|
||||||
if output_ext == '.mp3':
|
if output_ext == '.mp3':
|
||||||
ffmpeg_params = ' -ab 192k -ar 44100 -vn '
|
ffmpeg_params = '-codec:a libmp3lame -ar 44100 '
|
||||||
elif output_ext == '.m4a':
|
elif output_ext == '.m4a':
|
||||||
ffmpeg_params = '-cutoff 20000 -c:a libfdk_aac -b:a 192k -vn '
|
ffmpeg_params = '-cutoff 20000 -codec:a libfdk_aac -ar 44100 '
|
||||||
|
|
||||||
|
# add common params for any of the above combination
|
||||||
|
ffmpeg_params += '-b:a 192k -vn '
|
||||||
ffmpeg_pre += ' -i'
|
ffmpeg_pre += ' -i'
|
||||||
command = ffmpeg_pre.split() + [self.input_file] + ffmpeg_params.split() + [self.output_file]
|
command = ffmpeg_pre.split() + [self.input_file] + ffmpeg_params.split() + [self.output_file]
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ default_conf = { 'spotify-downloader':
|
|||||||
'music-videos-only' : False,
|
'music-videos-only' : False,
|
||||||
'no-spaces' : False,
|
'no-spaces' : False,
|
||||||
'file-format' : '{artist} - {track_name}',
|
'file-format' : '{artist} - {track_name}',
|
||||||
|
'youtube-api-key' : None,
|
||||||
'log-level' : 'INFO' }
|
'log-level' : 'INFO' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +54,29 @@ def get_config(config_file):
|
|||||||
return cfg['spotify-downloader']
|
return cfg['spotify-downloader']
|
||||||
|
|
||||||
|
|
||||||
|
def override_config(config_file, parser, raw_args=None):
|
||||||
|
""" Override default dict with config dict passed as comamnd line argument. """
|
||||||
|
config_file = os.path.realpath(config_file)
|
||||||
|
config = merge(default_conf['spotify-downloader'], get_config(config_file))
|
||||||
|
|
||||||
|
parser.set_defaults(manual=config['manual'])
|
||||||
|
parser.set_defaults(no_metadata=config['no-metadata'])
|
||||||
|
parser.set_defaults(avconv=config['avconv'])
|
||||||
|
parser.set_defaults(folder=os.path.relpath(config['folder'], os.getcwd()))
|
||||||
|
parser.set_defaults(overwrite=config['overwrite'])
|
||||||
|
parser.set_defaults(input_ext=config['input-ext'])
|
||||||
|
parser.set_defaults(output_ext=config['output-ext'])
|
||||||
|
parser.set_defaults(download_only_metadata=config['download-only-metadata'])
|
||||||
|
parser.set_defaults(dry_run=config['dry-run'])
|
||||||
|
parser.set_defaults(music_videos_only=config['music-videos-only'])
|
||||||
|
parser.set_defaults(no_spaces=config['no-spaces'])
|
||||||
|
parser.set_defaults(file_format=config['file-format'])
|
||||||
|
parser.set_defaults(no_spaces=config['youtube-api-key'])
|
||||||
|
parser.set_defaults(log_level=config['log-level'])
|
||||||
|
|
||||||
|
return parser.parse_args(raw_args)
|
||||||
|
|
||||||
|
|
||||||
def get_arguments(raw_args=None, to_group=True, to_merge=True):
|
def get_arguments(raw_args=None, to_group=True, to_merge=True):
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Download and convert songs from Spotify, Youtube etc.',
|
description='Download and convert songs from Spotify, Youtube etc.',
|
||||||
@@ -129,8 +153,18 @@ def get_arguments(raw_args=None, to_group=True, to_merge=True):
|
|||||||
choices=_LOG_LEVELS_STR,
|
choices=_LOG_LEVELS_STR,
|
||||||
type=str.upper,
|
type=str.upper,
|
||||||
help='set log verbosity')
|
help='set log verbosity')
|
||||||
|
parser.add_argument(
|
||||||
|
'-yk', '--youtube-api-key', default=config['youtube-api-key'],
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
parser.add_argument(
|
||||||
|
'-c', '--config', default=None,
|
||||||
|
help='Replace with custom config.yml file')
|
||||||
|
|
||||||
parsed = parser.parse_args(raw_args)
|
parsed = parser.parse_args(raw_args)
|
||||||
|
|
||||||
|
if parsed.config is not None and to_merge:
|
||||||
|
parsed = override_config(parsed.config, parser)
|
||||||
|
|
||||||
parsed.log_level = log_leveller(parsed.log_level)
|
parsed.log_level = log_leveller(parsed.log_level)
|
||||||
|
|
||||||
return parsed
|
return parsed
|
||||||
|
|||||||
@@ -120,6 +120,19 @@ def videotime_from_seconds(time):
|
|||||||
return '{0}:{1:02}:{2:02}'.format((time//60)//60, (time//60) % 60, time % 60)
|
return '{0}:{1:02}:{2:02}'.format((time//60)//60, (time//60) % 60, time % 60)
|
||||||
|
|
||||||
|
|
||||||
|
def get_sec(time_str):
|
||||||
|
v = time_str.split(':', 3)
|
||||||
|
v.reverse()
|
||||||
|
sec = 0
|
||||||
|
if len(v) > 0: # seconds
|
||||||
|
sec += int(v[0])
|
||||||
|
if len(v) > 1: # minutes
|
||||||
|
sec += int(v[1]) * 60
|
||||||
|
if len(v) > 2: # hours
|
||||||
|
sec += int(v[2]) * 3600
|
||||||
|
return sec
|
||||||
|
|
||||||
|
|
||||||
def get_splits(url):
|
def get_splits(url):
|
||||||
if '/' in url:
|
if '/' in url:
|
||||||
if url.endswith('/'):
|
if url.endswith('/'):
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import urllib
|
||||||
import pafy
|
import pafy
|
||||||
|
|
||||||
from core import internals
|
from core import internals
|
||||||
@@ -7,8 +9,19 @@ import os
|
|||||||
import pprint
|
import pprint
|
||||||
|
|
||||||
log = const.log
|
log = const.log
|
||||||
|
|
||||||
|
# Fix download speed throttle on short duration tracks
|
||||||
|
# Read more on mps-youtube/pafy#199
|
||||||
|
pafy.g.opener.addheaders.append(('Range', 'bytes=0-'))
|
||||||
|
|
||||||
|
|
||||||
|
def set_api_key():
|
||||||
|
if const.args.youtube_api_key:
|
||||||
|
key = const.args.youtube_api_key
|
||||||
|
else:
|
||||||
# Please respect this YouTube token :)
|
# Please respect this YouTube token :)
|
||||||
pafy.set_api_key('AIzaSyAnItl3udec-Q1d5bkjKJGL-RgrKO_vU90')
|
key = 'AIzaSyC6cEeKlxtOPybk9sEe5ksFN5sB-7wzYp0'
|
||||||
|
pafy.set_api_key(key)
|
||||||
|
|
||||||
|
|
||||||
def go_pafy(raw_song, meta_tags=None):
|
def go_pafy(raw_song, meta_tags=None):
|
||||||
@@ -55,13 +68,139 @@ def download_song(file_name, content):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def generate_youtube_url(raw_song, meta_tags, tries_remaining=5):
|
def generate_search_url(song):
|
||||||
""" Search for the song on YouTube and generate a URL to its video. """
|
""" Generate YouTube search URL for the given song. """
|
||||||
|
# urllib.request.quote() encodes URL with special characters
|
||||||
|
song = urllib.request.quote(song)
|
||||||
|
# Special YouTube URL filter to search only for videos
|
||||||
|
url = 'https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q={0}'.format(song)
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def is_video(result):
|
||||||
|
# ensure result is not a channel
|
||||||
|
not_video = result.find('channel') is not None or \
|
||||||
|
'yt-lockup-channel' in result.parent.attrs['class'] or \
|
||||||
|
'yt-lockup-channel' in result.attrs['class']
|
||||||
|
|
||||||
|
# ensure result is not a mix/playlist
|
||||||
|
not_video = not_video or \
|
||||||
|
'yt-lockup-playlist' in result.parent.attrs['class']
|
||||||
|
|
||||||
|
# ensure video result is not an advertisement
|
||||||
|
not_video = not_video or \
|
||||||
|
result.find('googleads') is not None
|
||||||
|
|
||||||
|
video = not not_video
|
||||||
|
return video
|
||||||
|
|
||||||
|
|
||||||
|
def generate_youtube_url(raw_song, meta_tags):
|
||||||
|
url_fetch = GenerateYouTubeURL(raw_song, meta_tags)
|
||||||
|
if const.args.youtube_api_key:
|
||||||
|
url = url_fetch.api()
|
||||||
|
else:
|
||||||
|
url = url_fetch.scrape()
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateYouTubeURL:
|
||||||
|
def __init__(self, raw_song, meta_tags):
|
||||||
|
self.raw_song = raw_song
|
||||||
|
self.meta_tags = meta_tags
|
||||||
|
|
||||||
|
def _best_match(self, videos):
|
||||||
|
""" Select the best matching video from a list of videos. """
|
||||||
|
if const.args.manual:
|
||||||
|
log.info(self.raw_song)
|
||||||
|
log.info('0. Skip downloading this song.\n')
|
||||||
|
# fetch all video links on first page on YouTube
|
||||||
|
for i, v in enumerate(videos):
|
||||||
|
log.info(u'{0}. {1} {2} {3}'.format(i+1, v['title'], v['videotime'],
|
||||||
|
"http://youtube.com/watch?v="+v['link']))
|
||||||
|
# let user select the song to download
|
||||||
|
result = internals.input_link(videos)
|
||||||
|
if result is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
if not self.meta_tags:
|
||||||
|
# if the metadata could not be acquired, take the first result
|
||||||
|
# from Youtube because the proper song length is unknown
|
||||||
|
result = videos[0]
|
||||||
|
log.debug('Since no metadata found on Spotify, going with the first result')
|
||||||
|
else:
|
||||||
|
# filter out videos that do not have a similar length to the Spotify song
|
||||||
|
duration_tolerance = 10
|
||||||
|
max_duration_tolerance = 20
|
||||||
|
possible_videos_by_duration = []
|
||||||
|
|
||||||
|
# start with a reasonable duration_tolerance, and increment duration_tolerance
|
||||||
|
# until one of the Youtube results falls within the correct duration or
|
||||||
|
# the duration_tolerance has reached the max_duration_tolerance
|
||||||
|
while len(possible_videos_by_duration) == 0:
|
||||||
|
possible_videos_by_duration = list(filter(lambda x: abs(x['seconds'] - self.meta_tags['duration']) <= duration_tolerance, videos))
|
||||||
|
duration_tolerance += 1
|
||||||
|
if duration_tolerance > max_duration_tolerance:
|
||||||
|
log.error("{0} by {1} was not found.\n".format(self.meta_tags['name'], self.meta_tags['artists'][0]['name']))
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = possible_videos_by_duration[0]
|
||||||
|
|
||||||
|
if result:
|
||||||
|
url = "http://youtube.com/watch?v={0}".format(result['link'])
|
||||||
|
else:
|
||||||
|
url = None
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
def scrape(self, tries_remaining=5):
|
||||||
|
""" Search and scrape YouTube to return a list of matching videos. """
|
||||||
|
|
||||||
# prevents an infinite loop but allows for a few retries
|
# prevents an infinite loop but allows for a few retries
|
||||||
if tries_remaining == 0:
|
if tries_remaining == 0:
|
||||||
log.debug('No tries left. I quit.')
|
log.debug('No tries left. I quit.')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.meta_tags is None:
|
||||||
|
song = self.raw_song
|
||||||
|
search_url = generate_search_url(song)
|
||||||
|
else:
|
||||||
|
song = internals.generate_songname(const.args.file_format,
|
||||||
|
self.meta_tags)
|
||||||
|
search_url = generate_search_url(song)
|
||||||
|
log.debug('Opening URL: {0}'.format(search_url))
|
||||||
|
|
||||||
|
item = urllib.request.urlopen(search_url).read()
|
||||||
|
items_parse = BeautifulSoup(item, "html.parser")
|
||||||
|
|
||||||
|
videos = []
|
||||||
|
for x in items_parse.find_all('div', {'class': 'yt-lockup-dismissable yt-uix-tile'}):
|
||||||
|
|
||||||
|
if not is_video(x):
|
||||||
|
continue
|
||||||
|
|
||||||
|
y = x.find('div', class_='yt-lockup-content')
|
||||||
|
link = y.find('a')['href'][-11:]
|
||||||
|
title = y.find('a')['title']
|
||||||
|
|
||||||
|
try:
|
||||||
|
videotime = x.find('span', class_="video-time").get_text()
|
||||||
|
except AttributeError:
|
||||||
|
log.debug('Could not find video duration on YouTube, retrying..')
|
||||||
|
return generate_youtube_url(self.raw_song, self.meta_tags, tries_remaining - 1)
|
||||||
|
|
||||||
|
youtubedetails = {'link': link, 'title': title, 'videotime': videotime,
|
||||||
|
'seconds': internals.get_sec(videotime)}
|
||||||
|
videos.append(youtubedetails)
|
||||||
|
if self.meta_tags is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
return self._best_match(videos)
|
||||||
|
|
||||||
|
|
||||||
|
def api(self):
|
||||||
|
""" Use YouTube API to search and return a list of matching videos. """
|
||||||
|
|
||||||
query = { 'part' : 'snippet',
|
query = { 'part' : 'snippet',
|
||||||
'maxResults' : 50,
|
'maxResults' : 50,
|
||||||
'type' : 'video' }
|
'type' : 'video' }
|
||||||
@@ -69,16 +208,18 @@ def generate_youtube_url(raw_song, meta_tags, tries_remaining=5):
|
|||||||
if const.args.music_videos_only:
|
if const.args.music_videos_only:
|
||||||
query['videoCategoryId'] = '10'
|
query['videoCategoryId'] = '10'
|
||||||
|
|
||||||
if not meta_tags:
|
if not self.meta_tags:
|
||||||
song = raw_song
|
song = self.raw_song
|
||||||
query['q'] = song
|
query['q'] = song
|
||||||
else:
|
else:
|
||||||
song = '{0} - {1}'.format(meta_tags['artists'][0]['name'],
|
song = '{0} - {1}'.format(self.meta_tags['artists'][0]['name'],
|
||||||
meta_tags['name'])
|
self.meta_tags['name'])
|
||||||
query['q'] = song
|
query['q'] = song
|
||||||
log.debug('query: {0}'.format(query))
|
log.debug('query: {0}'.format(query))
|
||||||
|
|
||||||
data = pafy.call_gdata('search', query)
|
data = pafy.call_gdata('search', query)
|
||||||
|
data['items'] = list(filter(lambda x: x['id'].get('videoId') is not None,
|
||||||
|
data['items']))
|
||||||
query_results = {'part': 'contentDetails,snippet,statistics',
|
query_results = {'part': 'contentDetails,snippet,statistics',
|
||||||
'maxResults': 50,
|
'maxResults': 50,
|
||||||
'id': ','.join(i['id']['videoId'] for i in data['items'])}
|
'id': ','.join(i['id']['videoId'] for i in data['items'])}
|
||||||
@@ -93,52 +234,7 @@ def generate_youtube_url(raw_song, meta_tags, tries_remaining=5):
|
|||||||
'videotime':internals.videotime_from_seconds(duration_s),
|
'videotime':internals.videotime_from_seconds(duration_s),
|
||||||
'seconds': duration_s}
|
'seconds': duration_s}
|
||||||
videos.append(youtubedetails)
|
videos.append(youtubedetails)
|
||||||
if not meta_tags:
|
if not self.meta_tags:
|
||||||
break
|
break
|
||||||
|
|
||||||
if not videos:
|
return self._best_match(videos)
|
||||||
return None
|
|
||||||
|
|
||||||
if const.args.manual:
|
|
||||||
log.info(song)
|
|
||||||
log.info('0. Skip downloading this song.\n')
|
|
||||||
# fetch all video links on first page on YouTube
|
|
||||||
for i, v in enumerate(videos):
|
|
||||||
log.info(u'{0}. {1} {2} {3}'.format(i+1, v['title'], v['videotime'],
|
|
||||||
"http://youtube.com/watch?v="+v['link']))
|
|
||||||
# let user select the song to download
|
|
||||||
result = internals.input_link(videos)
|
|
||||||
if not result:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
if not meta_tags:
|
|
||||||
# if the metadata could not be acquired, take the first result
|
|
||||||
# from Youtube because the proper song length is unknown
|
|
||||||
result = videos[0]
|
|
||||||
log.debug('Since no metadata found on Spotify, going with the first result')
|
|
||||||
else:
|
|
||||||
# filter out videos that do not have a similar length to the Spotify song
|
|
||||||
duration_tolerance = 10
|
|
||||||
max_duration_tolerance = 20
|
|
||||||
possible_videos_by_duration = list()
|
|
||||||
|
|
||||||
'''
|
|
||||||
start with a reasonable duration_tolerance, and increment duration_tolerance
|
|
||||||
until one of the Youtube results falls within the correct duration or
|
|
||||||
the duration_tolerance has reached the max_duration_tolerance
|
|
||||||
'''
|
|
||||||
while len(possible_videos_by_duration) == 0:
|
|
||||||
possible_videos_by_duration = list(filter(lambda x: abs(x['seconds'] - meta_tags['duration']) <= duration_tolerance, videos))
|
|
||||||
duration_tolerance += 1
|
|
||||||
if duration_tolerance > max_duration_tolerance:
|
|
||||||
log.error("{0} by {1} was not found.\n".format(meta_tags['name'], meta_tags['artists'][0]['name']))
|
|
||||||
return None
|
|
||||||
|
|
||||||
result = possible_videos_by_duration[0]
|
|
||||||
|
|
||||||
if result:
|
|
||||||
url = "http://youtube.com/watch?v=" + result['link']
|
|
||||||
else:
|
|
||||||
url = None
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|||||||
11
requirements.txt
Executable file
11
requirements.txt
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
pathlib >= 1.0.1
|
||||||
|
youtube_dl >= 2017.5.1
|
||||||
|
pafy >= 0.5.3.1
|
||||||
|
spotipy >= 2.4.4
|
||||||
|
mutagen >= 1.37
|
||||||
|
beautifulsoup4 >= 4.6.0
|
||||||
|
unicode-slugify >= 0.1.3
|
||||||
|
titlecase >= 0.10.0
|
||||||
|
logzero >= 1.3.1
|
||||||
|
lyricwikia >= 0.1.8
|
||||||
|
PyYAML >= 3.12
|
||||||
6
setup.py
6
setup.py
@@ -2,8 +2,7 @@ import re
|
|||||||
import ast
|
import ast
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
# Created from README.md using pandoc
|
with open('README.md', 'r') as f:
|
||||||
with open('README.rst', 'r') as f:
|
|
||||||
long_description = f.read()
|
long_description = f.read()
|
||||||
|
|
||||||
|
|
||||||
@@ -17,7 +16,8 @@ with open('spotdl.py', 'r') as f:
|
|||||||
version = str(ast.literal_eval(_version_re.search(f.read()).group(1)))
|
version = str(ast.literal_eval(_version_re.search(f.read()).group(1)))
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='spotify-downloader',
|
# 'spotify-downloader' was already taken :/
|
||||||
|
name='spotdl',
|
||||||
py_modules=['spotdl'],
|
py_modules=['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
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ def download_single(raw_song, number=None):
|
|||||||
def main():
|
def main():
|
||||||
const.args = handle.get_arguments()
|
const.args = handle.get_arguments()
|
||||||
internals.filter_path(const.args.folder)
|
internals.filter_path(const.args.folder)
|
||||||
|
youtube_tools.set_api_key()
|
||||||
|
|
||||||
const.log = const.logzero.setup_logger(formatter=const.formatter,
|
const.log = const.logzero.setup_logger(formatter=const.formatter,
|
||||||
level=const.args.log_level)
|
level=const.args.log_level)
|
||||||
|
|||||||
@@ -13,6 +13,22 @@ loader.load_defaults()
|
|||||||
raw_song = "Tony's Videos VERY SHORT VIDEO 28.10.2016"
|
raw_song = "Tony's Videos VERY SHORT VIDEO 28.10.2016"
|
||||||
|
|
||||||
|
|
||||||
|
class TestYouTubeAPIKeys:
|
||||||
|
def test_custom(self):
|
||||||
|
expect_key = 'some_api_key'
|
||||||
|
const.args.youtube_api_key = expect_key
|
||||||
|
youtube_tools.set_api_key()
|
||||||
|
key = youtube_tools.pafy.g.api_key
|
||||||
|
assert key == expect_key
|
||||||
|
|
||||||
|
def test_default(self):
|
||||||
|
expect_key = 'AIzaSyC6cEeKlxtOPybk9sEe5ksFN5sB-7wzYp0'
|
||||||
|
const.args.youtube_api_key = None
|
||||||
|
youtube_tools.set_api_key()
|
||||||
|
key = youtube_tools.pafy.g.api_key
|
||||||
|
assert key == expect_key
|
||||||
|
|
||||||
|
|
||||||
def test_metadata():
|
def test_metadata():
|
||||||
expect_metadata = None
|
expect_metadata = None
|
||||||
global metadata
|
global metadata
|
||||||
@@ -22,10 +38,12 @@ def test_metadata():
|
|||||||
|
|
||||||
class TestYouTubeURL:
|
class TestYouTubeURL:
|
||||||
def test_only_music_category(self):
|
def test_only_music_category(self):
|
||||||
expect_url = 'http://youtube.com/watch?v=P11ou3CXKZo'
|
# YouTube keeps changing its results
|
||||||
|
expect_urls = ('http://youtube.com/watch?v=qOOcy2-tmbk',
|
||||||
|
'http://youtube.com/watch?v=5USR1Omo7f0')
|
||||||
const.args.music_videos_only = True
|
const.args.music_videos_only = True
|
||||||
url = youtube_tools.generate_youtube_url(raw_song, metadata)
|
url = youtube_tools.generate_youtube_url(raw_song, metadata)
|
||||||
assert url == expect_url
|
assert url in expect_urls
|
||||||
|
|
||||||
def test_all_categories(self):
|
def test_all_categories(self):
|
||||||
expect_url = 'http://youtube.com/watch?v=qOOcy2-tmbk'
|
expect_url = 'http://youtube.com/watch?v=qOOcy2-tmbk'
|
||||||
@@ -49,16 +67,21 @@ class TestYouTubeURL:
|
|||||||
|
|
||||||
|
|
||||||
class TestYouTubeTitle:
|
class TestYouTubeTitle:
|
||||||
def test_single_download(self):
|
def test_single_download_with_youtube_api(self):
|
||||||
global content
|
global content
|
||||||
global title
|
global title
|
||||||
expect_title = "Tony's Videos VERY SHORT VIDEO 28.10.2016"
|
expect_title = "Tony's Videos VERY SHORT VIDEO 28.10.2016"
|
||||||
|
key = 'AIzaSyAnItl3udec-Q1d5bkjKJGL-RgrKO_vU90'
|
||||||
|
const.args.youtube_api_key = key
|
||||||
|
youtube_tools.set_api_key()
|
||||||
content = youtube_tools.go_pafy(raw_song, metadata)
|
content = youtube_tools.go_pafy(raw_song, metadata)
|
||||||
title = youtube_tools.get_youtube_title(content)
|
title = youtube_tools.get_youtube_title(content)
|
||||||
assert title == expect_title
|
assert title == expect_title
|
||||||
|
|
||||||
def test_download_from_list(self):
|
def test_download_from_list_without_youtube_api(self):
|
||||||
expect_title = "1. Tony's Videos VERY SHORT VIDEO 28.10.2016"
|
expect_title = "1. Tony's Videos VERY SHORT VIDEO 28.10.2016"
|
||||||
|
const.args.youtube_api_key = None
|
||||||
|
youtube_tools.set_api_key()
|
||||||
content = youtube_tools.go_pafy(raw_song, metadata)
|
content = youtube_tools.go_pafy(raw_song, metadata)
|
||||||
title = youtube_tools.get_youtube_title(content, 1)
|
title = youtube_tools.get_youtube_title(content, 1)
|
||||||
assert title == expect_title
|
assert title == expect_title
|
||||||
|
|||||||
Reference in New Issue
Block a user