Add logging capability (#175)

* Refactoring and addition of logzero (#172)

* Refactored convert.py and added logging.

* Added logging and refactored.

* Added logzero to requirements.txt

* Added logging to metadata.py

* Created a log in misc.py. Updated slugify import.

* Some general improvement

* Improve message layout

* Improve test mechanism

* Implement debug level logging

* Fix some minor mistakes

* Make pytest happy

* Remove unimplemented --verbose option

* Update ISSUE_TEMPLATE.md

* Rename LICENSE

* Remove obvious from log.debug()

* Show track URL when writing to file (debug)
This commit is contained in:
Ritiek Malhotra
2017-12-15 19:57:57 +05:30
committed by GitHub
parent 8ea89f9d1c
commit df513acc35
12 changed files with 319 additions and 216 deletions

View File

@@ -10,22 +10,23 @@ Please follow the guide below
- [ ] [Searched](https://github.com/ritiek/spotify-downloader/issues?utf8=%E2%9C%93&q=is%3Aissue) for similar issues including closed ones
#### What is the purpose of your *issue*?
- [ ] Script won't run
- [ ] Encountered bug
- [ ] Feature request
- [ ] Bug
- [ ] Feature Request
- [ ] Question
- [ ] Other
#### System information
- Your `python` version: `python 3.x`
- Your operating system: `Ubuntu 16.04`
### Description
<!-- Provide as much information possible with relevant examples and whatever you have tried below -->
<!-- Provide as much information possible and whatever you have tried below -->
### Log
<!-- Run the script with `--log-level=DEBUG` and paste the output below within the code block-->
<details>
```
paste the output over here
```
</details>
<!-- Give your issue a relevant title and you are good to go -->

View File

@@ -17,7 +17,7 @@
- Track number
- Disc number
- Release date
- And some more...
- And more...
- Works straight out of the box and does not require to generate or mess with your API keys.
@@ -78,8 +78,10 @@ Assuming you have Python 3 ([preferably v3.6 or above to stay away from Unicode
- For all available options, run `python3 spotdl.py --help`.
```
usage: spotdl.py [-h] (-s SONG | -l LIST | -p PLAYLIST | -u USERNAME) [-m]
[-nm] [-a] [-f FOLDER] [-v] [-i INPUT_EXT] [-o OUTPUT_EXT]
usage: spotdl.py [-h]
(-s SONG | -l LIST | -p PLAYLIST | -b ALBUM | -u USERNAME)
[-m] [-nm] [-a] [-f FOLDER] [-v] [-i INPUT_EXT]
[-o OUTPUT_EXT] [-ll {INFO,WARNING,ERROR,DEBUG}]
Download and convert songs from Spotify, Youtube etc.
@@ -103,13 +105,15 @@ optional arguments:
-f FOLDER, --folder FOLDER
path to folder where files will be stored in (default:
Music/)
-v, --verbose show debug output (default: False)
-i INPUT_EXT, --input_ext INPUT_EXT
-i INPUT_EXT, --input-ext INPUT_EXT
prefered input format .m4a or .webm (Opus) (default:
.m4a)
-o OUTPUT_EXT, --output_ext OUTPUT_EXT
-o OUTPUT_EXT, --output-ext OUTPUT_EXT
prefered output extension .mp3 or .m4a (AAC) (default:
.mp3)
-ll {INFO,WARNING,ERROR,DEBUG}, --log-level {INFO,WARNING,ERROR,DEBUG}
possible values - ['INFO', 'WARNING', 'ERROR',
'DEBUG'] (default: INFO)
```
#### Download by Name
@@ -204,11 +208,12 @@ Beside some other characters, spaces will be replaced by underscores. There's no
Just make sure your working directory is the one you have the music files in.
## Return codes
## Exit codes
- `0` - Success
- `1` - Unknown error
- `2` - Command line error (e.g. invalid args)
- `-1` - KeyboardInterrupt
- `10` - Invalid playlist URL
- `11` - Playlist not found
@@ -218,7 +223,7 @@ Just make sure your working directory is the one you have the music files in.
python3 -m pytest test
```
Obviously this requires the `pytest` module to be installed.
Obviously this requires the `pytest` module to be installed.
## Disclaimer

View File

@@ -1,50 +1,54 @@
import subprocess
import os
from core.logger import log
"""
What are the differences and similarities between ffmpeg, libav, and avconv?
"""What are the differences and similarities between ffmpeg, libav, and avconv?
https://stackoverflow.com/questions/9477115
ffmeg encoders high to lower quality
libopus > libvorbis >= libfdk_aac > aac > libmp3lame
libfdk_aac due to copyrights needs to be compiled by end user
on MacOS brew install ffmpeg --with-fdk-aac will do just that. Other OS?
https://trac.ffmpeg.org/wiki/Encode/AAC
"""
def song(input_song, output_song, folder, avconv=False, verbose=False):
"""Do the audio format conversion."""
def song(input_song, output_song, folder, avconv=False):
""" Do the audio format conversion. """
if not input_song == output_song:
print('Converting {0} to {1}'.format(
log.info('Converting {0} to {1}'.format(
input_song, output_song.split('.')[-1]))
if avconv:
exit_code = convert_with_avconv(input_song, output_song, folder, verbose)
exit_code = convert_with_avconv(input_song, output_song, folder)
else:
exit_code = convert_with_ffmpeg(input_song, output_song, folder, verbose)
exit_code = convert_with_ffmpeg(input_song, output_song, folder)
return exit_code
return 0
def convert_with_avconv(input_song, output_song, folder, verbose):
"""Convert the audio file using avconv."""
if verbose:
def convert_with_avconv(input_song, output_song, folder):
""" Convert the audio file using avconv. """
if log.level == 10:
level = 'debug'
else:
level = '0'
command = ['avconv',
'-loglevel', level,
'-i', os.path.join(folder, input_song),
'-ab', '192k',
command = ['avconv', '-loglevel', level, '-i',
os.path.join(folder, input_song), '-ab', '192k',
os.path.join(folder, output_song)]
log.debug(command)
return subprocess.call(command)
def convert_with_ffmpeg(input_song, output_song, folder, verbose):
"""Convert the audio file using FFmpeg."""
def convert_with_ffmpeg(input_song, output_song, folder):
""" Convert the audio file using FFmpeg. """
ffmpeg_pre = 'ffmpeg -y '
if not verbose:
if not log.level == 10:
ffmpeg_pre += '-hide_banner -nostats -v panic '
input_ext = input_song.split('.')[-1]
@@ -63,6 +67,9 @@ def convert_with_ffmpeg(input_song, output_song, folder, verbose):
ffmpeg_params = '-cutoff 20000 -c:a libfdk_aac -b:a 192k -vn '
command = '{0}-i {1} {2}{3}'.format(
ffmpeg_pre, os.path.join(folder, input_song), ffmpeg_params, os.path.join(folder, output_song)).split(' ')
ffmpeg_pre, os.path.join(folder, input_song),
ffmpeg_params, os.path.join(folder, output_song)).split(' ')
log.debug(command)
return subprocess.call(command)

View File

@@ -1,28 +1,31 @@
import sys
import os
import argparse
import spotipy.oauth2 as oauth2
from urllib.request import quote
from slugify import slugify
from slugify import SLUG_OK, slugify
import sys
import os
from core.logger import log, log_leveller, _LOG_LEVELS_STR
def input_link(links):
"""Let the user input a number."""
""" Let the user input a choice. """
while True:
try:
the_chosen_one = int(input('>> Choose your number: '))
log.info('Choose your number:')
the_chosen_one = int(input('> '))
if 1 <= the_chosen_one <= len(links):
return links[the_chosen_one - 1]
elif the_chosen_one == 0:
return None
else:
print('Choose a valid number!')
log.warning('Choose a valid number!')
except ValueError:
print('Choose a valid number!')
log.warning('Choose a valid number!')
def trim_song(file):
"""Remove the first song from file."""
""" Remove the first song from file. """
with open(file, 'r') as file_in:
data = file_in.read().splitlines(True)
with open(file, 'w') as file_out:
@@ -60,26 +63,32 @@ def get_arguments():
'-f', '--folder', default=(os.path.join(sys.path[0], 'Music')),
help='path to folder where files will be stored in')
parser.add_argument(
'-v', '--verbose', default=False, help='show debug output',
action='store_true')
parser.add_argument(
'-i', '--input_ext', default='.m4a',
'-i', '--input-ext', default='.m4a',
help='prefered input format .m4a or .webm (Opus)')
parser.add_argument(
'-o', '--output_ext', default='.mp3',
'-o', '--output-ext', default='.mp3',
help='prefered output extension .mp3 or .m4a (AAC)')
parser.add_argument(
'-ll', '--log-level', default='INFO',
choices=_LOG_LEVELS_STR,
type=str.upper,
help='possible values - {}'.format(_LOG_LEVELS_STR))
return parser.parse_args()
parsed = parser.parse_args()
parsed.log_level = log_leveller(parsed.log_level)
return parsed
def is_spotify(raw_song):
"""Check if the input song is a Spotify link."""
""" Check if the input song is a Spotify link. """
status = len(raw_song) == 22 and raw_song.replace(" ", "%20") == raw_song
status = status or raw_song.find('spotify') > -1
return status
def is_youtube(raw_song):
"""Check if the input song is a YouTube link."""
""" Check if the input song is a YouTube link. """
status = len(raw_song) == 11 and raw_song.replace(" ", "%20") == raw_song
status = status and not raw_song.lower() == raw_song
status = status or 'youtube.com/watch?v=' in raw_song
@@ -87,7 +96,7 @@ def is_youtube(raw_song):
def sanitize_title(title):
"""Generate filename of the song to be downloaded."""
""" Generate filename of the song to be downloaded. """
title = title.replace(' ', '_')
title = title.replace('/', '_')
@@ -97,7 +106,7 @@ def sanitize_title(title):
def generate_token():
"""Generate the token. Please respect these credentials :)"""
""" Generate the token. Please respect these credentials :) """
credentials = oauth2.SpotifyClientCredentials(
client_id='4fe3fecfe5334023a1472516cc99d805',
client_secret='0f02b7c483c04257984695007a4a8d5c')
@@ -106,7 +115,7 @@ def generate_token():
def generate_search_url(song, viewsort=False):
"""Generate YouTube search URL for the given song."""
""" Generate YouTube search URL for the given song. """
# urllib.request.quote() encodes URL with special characters
song = quote(song)
if viewsort:
@@ -125,18 +134,14 @@ def filter_path(path):
os.remove(os.path.join(path, temp))
def grace_quit():
print('\n\nExiting.')
sys.exit(0)
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
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

16
core/logger.py Normal file
View File

@@ -0,0 +1,16 @@
import logzero
import logging
_LOG_LEVELS_STR = ['INFO', 'WARNING', 'ERROR', 'DEBUG']
def log_leveller(log_level_str):
loggin_levels = [logging.INFO, logging.WARNING, logging.ERROR, logging.DEBUG]
log_level_str_index = _LOG_LEVELS_STR.index(log_level_str)
loggin_level = loggin_levels[log_level_str_index]
return loggin_level
# Create a logger
log_format = ("%(color)s%(levelname)s:%(end_color)s %(message)s")
formatter = logzero.LogFormatter(fmt=log_format)
log = logzero.setup_logger(formatter=formatter, level=logging.INFO)

View File

@@ -1,6 +1,7 @@
from mutagen.easyid3 import EasyID3
from mutagen.id3 import ID3, APIC
from mutagen.mp4 import MP4, MP4Cover
from core.logger import log
import urllib.request
@@ -24,23 +25,23 @@ def compare(music_file, metadata):
def embed(music_file, meta_tags):
"""Embed metadata."""
""" Embed metadata. """
if meta_tags is None:
print('Could not find meta-tags')
log.warning('Could not find metadata')
return None
elif music_file.endswith('.m4a'):
print('Fixing meta-tags')
log.info('Applying metadata')
return embed_m4a(music_file, meta_tags)
elif music_file.endswith('.mp3'):
print('Fixing meta-tags')
log.info('Applying metadata')
return embed_mp3(music_file, meta_tags)
else:
print('Cannot embed meta-tags into given output extension')
log.warning('Cannot embed metadata into given output extension')
return False
def embed_mp3(music_file, meta_tags):
"""Embed metadata to MP3 files."""
""" Embed metadata to MP3 files. """
# EasyID3 is fun to use ;)
audiofile = EasyID3(music_file)
audiofile['artist'] = meta_tags['artists'][0]['name']
@@ -81,7 +82,7 @@ def embed_mp3(music_file, meta_tags):
def embed_m4a(music_file, meta_tags):
"""Embed metadata to M4A files."""
""" Embed metadata to M4A files. """
# Apple has specific tags - see mutagen docs -
# http://mutagen.readthedocs.io/en/latest/api/mp4.html
tags = {'album': '\xa9alb',

View File

@@ -6,3 +6,4 @@ spotipy >= 2.4.4
mutagen >= 1.37
unicode-slugify >= 0.1.3
titlecase >= 0.10.0
logzero >= 1.3.1

279
spotdl.py
View File

@@ -1,9 +1,10 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from core import logger
from core import metadata
from core import convert
from core import misc
from core import internals
from bs4 import BeautifulSoup
from titlecase import titlecase
from slugify import slugify
@@ -13,21 +14,26 @@ import urllib.request
import os
import sys
import time
import sys
import platform
import pprint
def generate_songname(tags):
"""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. """
raw_song = u'{0} - {1}'.format(tags['artists'][0]['name'], tags['name'])
return raw_song
def generate_metadata(raw_song):
"""Fetch a song's metadata from Spotify."""
if misc.is_spotify(raw_song):
""" Fetch a song's metadata from Spotify. """
if internals.is_spotify(raw_song):
# fetch track information directly if it is spotify link
log.debug('Fetching metadata for given track URL')
meta_tags = spotify.track(raw_song)
else:
# otherwise search on spotify and fetch information from first result
log.debug('Searching for "{}" on Spotify'.format(raw_song))
try:
meta_tags = spotify.search(raw_song, limit=1)['tracks']['items'][0]
except IndexError:
@@ -52,49 +58,64 @@ def generate_metadata(raw_song):
meta_tags[u'publisher'] = album['label']
meta_tags[u'total_tracks'] = album['tracks']['total']
log.debug(pprint.pformat(meta_tags))
return meta_tags
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, tries_remaining=5):
"""Search for the song on YouTube and generate a URL to its video."""
""" Search for the song on YouTube and generate a URL to its video. """
# prevents an infinite loop but allows for a few retries
if tries_remaining == 0:
log.debug('No tries left. I quit.')
return
if meta_tags is None:
song = raw_song
search_url = misc.generate_search_url(song, viewsort=False)
search_url = internals.generate_search_url(song, viewsort=False)
else:
song = generate_songname(meta_tags)
search_url = misc.generate_search_url(song, viewsort=True)
search_url = internals.generate_search_url(song, viewsort=True)
log.debug('Opening URL: {0}'.format(search_url))
item = urllib.request.urlopen(search_url).read()
# item = unicode(item, 'utf-8')
items_parse = BeautifulSoup(item, "html.parser")
videos = []
for x in items_parse.find_all('div', {'class': 'yt-lockup-dismissable yt-uix-tile'}):
# ensure result is not a channel
if x.find('channel') is not None or 'yt-lockup-channel' in x.parent.attrs['class'] or 'yt-lockup-channel' in x.attrs['class']:
continue
# ensure result is not a mix/playlist
if 'yt-lockup-playlist' in x.parent.attrs['class']:
continue
# confirm the video result is not an advertisement
if x.find('googleads') is not None:
if not is_video(x):
continue
y = x.find('div', class_='yt-lockup-content')
link = y.find('a')['href']
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(raw_song, meta_tags, tries_remaining - 1)
youtubedetails = {'link': link, 'title': title, 'videotime': videotime, 'seconds':misc.get_sec(videotime)}
youtubedetails = {'link': link, 'title': title, 'videotime': videotime,
'seconds': internals.get_sec(videotime)}
videos.append(youtubedetails)
if meta_tags is None:
break
@@ -102,20 +123,26 @@ def generate_youtube_url(raw_song, meta_tags, tries_remaining=5):
if not videos:
return None
log.debug(pprint.pformat(videos))
if args.manual:
print(song)
print('')
print('0. Skip downloading this song')
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):
print(u'{0}. {1} {2} {3}'.format(i+1, v['title'], v['videotime'], "http://youtube.com"+v['link']))
print('')
log.info(u'{0}. {1} {2} {3}'.format(i+1, v['title'], v['videotime'],
"http://youtube.com"+v['link']))
# let user select the song to download
result = misc.input_link(videos)
result = internals.input_link(videos)
if result is None:
return None
else:
if meta_tags is not None:
if meta_tags is None:
# 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
@@ -130,24 +157,23 @@ def generate_youtube_url(raw_song, meta_tags, tries_remaining=5):
possible_videos_by_duration = list(filter(lambda x: abs(x['seconds'] - (int(meta_tags['duration_ms'])/1000)) <= duration_tolerance, videos))
duration_tolerance += 1
if duration_tolerance > max_duration_tolerance:
print(meta_tags['name'], 'by', meta_tags['artists'][0]['name'], 'was not found')
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]
else:
# if the metadata could not be acquired, take the first result from Youtube because the proper song length is unknown
result = videos[0]
full_link = None
if result:
full_link = u'youtube.com{0}'.format(result['link'])
full_link = u'http://youtube.com{0}'.format(result['link'])
else:
full_link = None
log.debug('Best matching video link: {}'.format(full_link))
return full_link
def go_pafy(raw_song, meta_tags):
"""Parse track from YouTube."""
if misc.is_youtube(raw_song):
def go_pafy(raw_song, meta_tags=None):
""" Parse track from YouTube. """
if internals.is_youtube(raw_song):
track_info = pafy.new(raw_song)
else:
track_url = generate_youtube_url(raw_song, meta_tags)
@@ -161,7 +187,7 @@ def go_pafy(raw_song, meta_tags):
def get_youtube_title(content, number=None):
"""Get the YouTube video's title."""
""" Get the YouTube video's title. """
title = content.title
if number is None:
return title
@@ -170,7 +196,7 @@ def get_youtube_title(content, number=None):
def feed_playlist(username):
"""Fetch user playlists when using the -u option."""
""" Fetch user playlists when using the -u option. """
playlists = spotify.user_playlists(username)
links = []
check = 1
@@ -180,9 +206,10 @@ def feed_playlist(username):
# in rare cases, playlists may not be found, so playlists['next']
# is None. Skip these. Also see Issue #91.
if playlist['name'] is not None:
print(u'{0:>5}. {1:<30} ({2} tracks)'.format(
log.info(u'{0:>5}. {1:<30} ({2} tracks)'.format(
check, playlist['name'],
playlist['tracks']['total']))
log.debug(playlist['external_urls']['spotify'])
links.append(playlist)
check += 1
if playlists['next']:
@@ -190,9 +217,7 @@ def feed_playlist(username):
else:
break
print('')
playlist = misc.input_link(links)
print('')
playlist = internals.input_link(links)
write_playlist(playlist['owner']['id'], playlist['id'])
@@ -205,9 +230,11 @@ def write_tracks(text_file, tracks):
else:
track = item
try:
file_out.write(track['external_urls']['spotify'] + '\n')
track_url = track['external_urls']['spotify']
file_out.write(track_url + '\n')
log.debug(track_url)
except KeyError:
print(u'Skipping track {0} by {1} (local only?)'.format(
log.warning(u'Skipping track {0} by {1} (local only?)'.format(
track['name'], track['artists'][0]['name']))
# 1 page = 50 results
# check if there are more pages
@@ -218,11 +245,11 @@ def write_tracks(text_file, tracks):
def write_playlist(username, playlist_id):
results = spotify.user_playlist(
username, playlist_id, fields='tracks,next,name')
results = spotify.user_playlist(username, playlist_id,
fields='tracks,next,name')
text_file = u'{0}.txt'.format(slugify(results['name'], ok='-_()[]{}'))
print(u'Feeding {0} tracks to {1}'.format(results['tracks']['total'], text_file))
log.info(u'Writing {0} tracks to {1}'.format(
results['tracks']['total'], text_file))
tracks = results['tracks']
write_tracks(text_file, tracks)
@@ -230,54 +257,61 @@ def write_playlist(username, playlist_id):
def write_album(album):
tracks = spotify.album_tracks(album['id'])
text_file = u'{0}.txt'.format(slugify(album['name'], ok='-_()[]{}'))
print(u'Feeding {0} tracks to {1}'.format(tracks['total'], text_file))
log.info(u'writing {0} tracks to {1}'.format(
tracks['total'], text_file))
write_tracks(text_file, tracks)
def download_song(file_name, content):
"""Download the audio file from YouTube."""
""" Download the audio file from YouTube. """
if args.input_ext in (".webm", ".m4a"):
link = content.getbestaudio(preftype=args.input_ext[1:])
else:
return False
log.debug('Downloading from URL: ' + link.url)
if link is None:
return False
else:
link.download(
filepath='{0}{1}'.format(os.path.join(args.folder, file_name), args.input_ext))
filepath = '{0}{1}'.format(os.path.join(args.folder, file_name),
args.input_ext)
link.download(filepath=filepath)
return True
def check_exists(music_file, raw_song, meta_tags, islist=True):
"""Check if the input song already exists in the given folder."""
""" Check if the input song already exists in the given folder. """
log.debug('Cleaning any temp files and checking '
'if "{}" already exists'.format(music_file))
songs = os.listdir(args.folder)
for song in songs:
if song.endswith('.temp'):
os.remove(os.path.join(args.folder, song))
continue
# check if any song with similar name is already present in the given folder
file_name = misc.sanitize_title(music_file)
file_name = internals.sanitize_title(music_file)
if song.startswith(file_name):
# check if the already downloaded song has correct metadata
already_tagged = metadata.compare(os.path.join(args.folder, song), meta_tags)
log.debug('Found an already existing song: "{}"'.format(song))
if internals.is_spotify(raw_song):
# check if the already downloaded song has correct metadata
# if not, remove it and download again without prompt
already_tagged = metadata.compare(os.path.join(args.folder, song),
meta_tags)
log.debug('Checking if it is already tagged correctly? {}',
already_tagged)
if not already_tagged:
os.remove(os.path.join(args.folder, song))
return False
# if not, remove it and download again without prompt
if misc.is_spotify(raw_song) and not already_tagged:
os.remove(os.path.join(args.folder, song))
return False
# do not prompt and skip the current song
# if already downloaded when using list
if islist:
print('Song already exists')
return True
# if downloading only single song, prompt to re-download
if islist:
log.warning('Song already exists')
return True
else:
prompt = input('Song with same name has already been downloaded. '
'Re-download? (y/n): ').lower()
if prompt == 'y':
log.info('Song with same name has already been downloaded. '
'Re-download? (y/N): ')
prompt = input('> ')
if prompt.lower() == 'y':
os.remove(os.path.join(args.folder, song))
return False
else:
@@ -286,7 +320,7 @@ def check_exists(music_file, raw_song, meta_tags, islist=True):
def grab_list(text_file):
"""Download all songs from the list."""
""" Download all songs from the list. """
with open(text_file, 'r') as listed:
lines = (listed.read()).splitlines()
# ignore blank lines in text_file (if any)
@@ -294,37 +328,36 @@ def grab_list(text_file):
lines.remove('')
except ValueError:
pass
print(u'Total songs in list: {0} songs'.format(len(lines)))
print('')
# nth input song
log.info(u'Preparing to download {} songs'.format(len(lines)))
number = 1
for raw_song in lines:
print('')
try:
grab_single(raw_song, number=number)
# token expires after 1 hour
except spotipy.client.SpotifyException:
# refresh token when it expires
new_token = misc.generate_token()
log.debug('Token expired, generating new one and authorizing')
new_token = internals.generate_token()
global spotify
spotify = spotipy.Spotify(auth=new_token)
grab_single(raw_song, number=number)
# detect network problems
except (urllib.request.URLError, TypeError, IOError):
lines.append(raw_song)
# remove the downloaded song from .txt
misc.trim_song(text_file)
# and append it to the last line in .txt
# remove the downloaded song from file
internals.trim_song(text_file)
# and append it at the end of file
with open(text_file, 'a') as myfile:
myfile.write(raw_song + '\n')
print('Failed to download song. Will retry after other songs.')
log.warning('Failed to download song. Will retry after other songs\n')
# wait 0.5 sec to avoid infinite looping
time.sleep(0.5)
continue
except KeyboardInterrupt:
misc.grace_quit()
finally:
print('')
misc.trim_song(text_file)
log.debug('Removing downloaded song from text file')
internals.trim_song(text_file)
number += 1
@@ -340,14 +373,14 @@ def grab_playlist(playlist):
username = splits[-3]
except IndexError:
# Wrong format, in either case
print('The provided playlist URL is not in a recognized format!')
log.error('The provided playlist URL is not in a recognized format!')
sys.exit(10)
playlist_id = splits[-1]
try:
write_playlist(username, playlist_id)
except spotipy.client.SpotifyException:
print('Unable to find playlist')
print('Make sure the playlist is set to publicly visible and then try again')
log.error('Unable to find playlist')
log.info('Make sure the playlist is set to publicly visible and then try again')
sys.exit(11)
@@ -366,74 +399,84 @@ def grab_album(album):
def grab_single(raw_song, number=None):
"""Logic behind downloading a song."""
""" Logic behind downloading a song. """
if number:
islist = True
else:
islist = False
if misc.is_youtube(raw_song):
if internals.is_youtube(raw_song):
log.debug('Input song is a YouTube URL')
content = go_pafy(raw_song, meta_tags=None)
raw_song = slugify(content.title).replace('-', ' ')
meta_tags = generate_metadata(raw_song)
else:
meta_tags = generate_metadata(raw_song)
content = go_pafy(raw_song, meta_tags)
meta_tags = generate_metadata(raw_song)
content = go_pafy(raw_song, meta_tags)
if content is None:
log.debug('Found no matching video')
return
# print '[number]. [artist] - [song]' if downloading from list
# otherwise print '[artist] - [song]'
print(get_youtube_title(content, number))
# log '[number]. [artist] - [song]' if downloading from list
# otherwise log '[artist] - [song]'
log.info(get_youtube_title(content, number))
# generate file name of the song to download
songname = content.title
if meta_tags is not None:
refined_songname = generate_songname(meta_tags)
log.debug('Refining songname from "{0}" to "{1}"'.format(songname, refined_songname))
if not refined_songname == ' - ':
songname = refined_songname
file_name = misc.sanitize_title(songname)
file_name = internals.sanitize_title(songname)
if not check_exists(file_name, raw_song, meta_tags, islist=islist):
if download_song(file_name, content):
print('')
input_song = file_name + args.input_ext
output_song = file_name + args.output_ext
print('')
convert.song(input_song, output_song, args.folder,
avconv=args.avconv, verbose=args.verbose)
avconv=args.avconv)
if not args.input_ext == args.output_ext:
os.remove(os.path.join(args.folder, input_song))
if not args.no_metadata:
metadata.embed(os.path.join(args.folder, output_song), meta_tags)
else:
print('No audio streams available')
log.error('No audio streams available')
class TestArgs(object):
manual = False
input_ext = '.m4a'
output_ext = '.mp3'
folder = 'Music/'
# token is mandatory when using Spotify's API
# https://developer.spotify.com/news-stories/2017/01/27/removing-unauthenticated-calls-to-the-web-api/
token = misc.generate_token()
token = internals.generate_token()
spotify = spotipy.Spotify(auth=token)
if __name__ == '__main__':
args = misc.get_arguments()
misc.filter_path(args.folder)
args = internals.get_arguments()
internals.filter_path(args.folder)
if args.song:
grab_single(raw_song=args.song)
elif args.list:
grab_list(text_file=args.list)
elif args.playlist:
grab_playlist(playlist=args.playlist)
elif args.album:
grab_album(album=args.album)
elif args.username:
feed_playlist(username=args.username)
else:
misc.filter_path('Music')
args = TestArgs()
logger.log = logger.logzero.setup_logger(formatter=logger.formatter,
level=args.log_level)
log = logger.log
log.debug('Python version: {}'.format(sys.version))
log.debug('Platform: {}'.format(platform.platform()))
log.debug(pprint.pformat(args.__dict__))
try:
if args.song:
grab_single(raw_song=args.song)
elif args.list:
grab_list(text_file=args.list)
elif args.playlist:
grab_playlist(playlist=args.playlist)
elif args.album:
grab_album(album=args.album)
elif args.username:
feed_playlist(username=args.username)
sys.exit(0)
except KeyboardInterrupt as e:
log.exception(e)
sys.exit(-1)

View File

@@ -1,31 +1,44 @@
# -*- coding: UTF-8 -*-
from spotdl import logger
import spotdl
import os
raw_song = "Tony's Videos VERY SHORT VIDEO 28.10.2016"
for x in os.listdir(spotdl.args.folder):
os.remove(os.path.join(spotdl.args.folder, x))
class TestArgs:
manual = False
input_ext = '.m4a'
output_ext = '.mp3'
folder = 'test'
log_level = logger.logging.DEBUG
test_args = TestArgs()
setattr(spotdl, "args", test_args)
spotdl.log = logger.logzero.setup_logger(formatter=logger.formatter,
level=spotdl.args.log_level)
def test_youtube_url():
expect_url = 'youtube.com/watch?v=qOOcy2-tmbk'
expect_url = 'http://youtube.com/watch?v=qOOcy2-tmbk'
url = spotdl.generate_youtube_url(raw_song, meta_tags=None)
assert url == expect_url
def test_youtube_title():
expect_title = "Tony's Videos VERY SHORT VIDEO 28.10.2016"
global content
content = spotdl.go_pafy(raw_song, meta_tags=None)
global title
expect_title = "Tony's Videos VERY SHORT VIDEO 28.10.2016"
content = spotdl.go_pafy(raw_song, meta_tags=None)
title = spotdl.get_youtube_title(content)
assert title == expect_title
def test_check_exists():
expect_check = False
# prerequisites for determining filename
file_name = spotdl.misc.sanitize_title(title)
file_name = spotdl.internals.sanitize_title(title)
check = spotdl.check_exists(file_name, raw_song, meta_tags=None, islist=True)
assert check == expect_check
@@ -33,7 +46,7 @@ def test_check_exists():
def test_download():
expect_download = True
# prerequisites for determining filename
file_name = spotdl.misc.sanitize_title(title)
file_name = spotdl.internals.sanitize_title(title)
download = spotdl.download_song(file_name, content)
assert download == expect_download
@@ -42,7 +55,9 @@ def test_convert():
# exit code 0 = success
expect_convert = 0
# prerequisites for determining filename
file_name = spotdl.misc.sanitize_title(title)
file_name = spotdl.internals.sanitize_title(title)
global input_song
global output_song
input_song = file_name + spotdl.args.input_ext
output_song = file_name + spotdl.args.output_ext
convert = spotdl.convert.song(input_song, output_song, spotdl.args.folder)
@@ -53,11 +68,8 @@ def test_metadata():
expect_metadata = None
# prerequisites for determining filename
meta_tags = spotdl.generate_metadata(raw_song)
meta_tags = spotdl.generate_metadata(raw_song)
file_name = spotdl.misc.sanitize_title(title)
output_song = file_name + spotdl.args.output_ext
file_name = spotdl.internals.sanitize_title(title)
metadata_output = spotdl.metadata.embed(os.path.join(spotdl.args.folder, output_song), meta_tags)
input_song = file_name + spotdl.args.input_ext
metadata_input = spotdl.metadata.embed(os.path.join(spotdl.args.folder, input_song), meta_tags)
assert (metadata_output == expect_metadata) and (metadata_input == expect_metadata)
@@ -65,8 +77,8 @@ def test_metadata():
def test_check_exists2():
expect_check = True
# prerequisites for determining filename
file_name = spotdl.misc.sanitize_title(title)
input_song = file_name + spotdl.args.input_ext
file_name = spotdl.internals.sanitize_title(title)
os.remove(os.path.join(spotdl.args.folder, input_song))
check = spotdl.check_exists(file_name, raw_song, meta_tags=None, islist=True)
os.remove(os.path.join(spotdl.args.folder, output_song))
assert check == expect_check

View File

@@ -1,12 +1,25 @@
# -*- coding: UTF-8 -*-
from spotdl import logger
import spotdl
import os
raw_song = 'http://open.spotify.com/track/0JlS7BXXD07hRmevDnbPDU'
for x in os.listdir(spotdl.args.folder):
os.remove(os.path.join(spotdl.args.folder, x))
class TestArgs:
manual = False
input_ext = '.m4a'
output_ext = '.mp3'
folder = 'test'
log_level = 'DEBUG'
test_args = TestArgs()
setattr(spotdl, "args", test_args)
spotdl.log = logger.logzero.setup_logger(formatter=logger.formatter,
level=spotdl.args.log_level)
spotdl.internals.filter_path(spotdl.args.folder)
def test_spotify_title():
@@ -35,7 +48,7 @@ def test_check_exists():
# prerequisites for determining filename
songname = spotdl.generate_songname(meta_tags)
global file_name
file_name = spotdl.misc.sanitize_title(songname)
file_name = spotdl.internals.sanitize_title(songname)
check = spotdl.check_exists(file_name, raw_song, meta_tags, islist=True)
assert check == expect_check
@@ -52,6 +65,8 @@ def test_convert():
# exit code 0 = success
expect_convert = 0
# prerequisites for determining filename
global input_song
global output_song
input_song = file_name + spotdl.args.input_ext
output_song = file_name + spotdl.args.output_ext
convert = spotdl.convert.song(input_song, output_song, spotdl.args.folder)
@@ -61,9 +76,7 @@ def test_convert():
def test_metadata():
expect_metadata = True
# prerequisites for determining filename
output_song = file_name + spotdl.args.output_ext
metadata_output = spotdl.metadata.embed(os.path.join(spotdl.args.folder, output_song), meta_tags)
input_song = file_name + spotdl.args.input_ext
metadata_input = spotdl.metadata.embed(os.path.join(spotdl.args.folder, input_song), meta_tags)
assert metadata_output == (metadata_input == expect_metadata)
@@ -71,7 +84,7 @@ def test_metadata():
def test_check_exists2():
expect_check = True
# prerequisites for determining filename
input_song = file_name + spotdl.args.input_ext
os.remove(os.path.join(spotdl.args.folder, input_song))
check = spotdl.check_exists(file_name, raw_song, meta_tags, islist=True)
os.remove(os.path.join(spotdl.args.folder, output_song))
assert check == expect_check

View File

@@ -33,8 +33,7 @@ def test_tracks():
try:
fout.write(track['external_urls']['spotify'] + '\n')
except KeyError:
title = track['name'] + ' by '+ track['artists'][0]['name']
print('Skipping track ' + title + ' (local only?)')
pass
# 1 page = 50 results
# check if there are more pages
if tracks['next']:
@@ -45,7 +44,7 @@ def test_tracks():
with open('list.txt', 'r') as listed:
expect_song = (listed.read()).splitlines()[0]
spotdl.misc.trim_song('list.txt')
spotdl.internals.trim_song('list.txt')
with open('list.txt', 'a') as myfile:
myfile.write(expect_song)