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 - [ ] [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*? #### What is the purpose of your *issue*?
- [ ] Script won't run - [ ] Bug
- [ ] Encountered bug - [ ] Feature Request
- [ ] Feature request
- [ ] Question - [ ] Question
- [ ] Other - [ ] Other
#### System information
- Your `python` version: `python 3.x`
- Your operating system: `Ubuntu 16.04`
### Description ### 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 --> <!-- Give your issue a relevant title and you are good to go -->

View File

@@ -17,7 +17,7 @@
- Track number - Track number
- Disc number - Disc number
- Release date - 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. - 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`. - For all available options, run `python3 spotdl.py --help`.
``` ```
usage: spotdl.py [-h] (-s SONG | -l LIST | -p PLAYLIST | -u USERNAME) [-m] usage: spotdl.py [-h]
[-nm] [-a] [-f FOLDER] [-v] [-i INPUT_EXT] [-o OUTPUT_EXT] (-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. Download and convert songs from Spotify, Youtube etc.
@@ -103,13 +105,15 @@ optional arguments:
-f FOLDER, --folder FOLDER -f FOLDER, --folder FOLDER
path to folder where files will be stored in (default: path to folder where files will be stored in (default:
Music/) 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: prefered input format .m4a or .webm (Opus) (default:
.m4a) .m4a)
-o OUTPUT_EXT, --output_ext OUTPUT_EXT -o OUTPUT_EXT, --output-ext OUTPUT_EXT
prefered output extension .mp3 or .m4a (AAC) (default: prefered output extension .mp3 or .m4a (AAC) (default:
.mp3) .mp3)
-ll {INFO,WARNING,ERROR,DEBUG}, --log-level {INFO,WARNING,ERROR,DEBUG}
possible values - ['INFO', 'WARNING', 'ERROR',
'DEBUG'] (default: INFO)
``` ```
#### Download by Name #### 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. Just make sure your working directory is the one you have the music files in.
## Return codes ## Exit codes
- `0` - Success - `0` - Success
- `1` - Unknown error - `1` - Unknown error
- `2` - Command line error (e.g. invalid args) - `2` - Command line error (e.g. invalid args)
- `-1` - KeyboardInterrupt
- `10` - Invalid playlist URL - `10` - Invalid playlist URL
- `11` - Playlist not found - `11` - Playlist not found

View File

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

View File

@@ -1,28 +1,31 @@
import sys
import os
import argparse import argparse
import spotipy.oauth2 as oauth2 import spotipy.oauth2 as oauth2
from urllib.request import quote 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): def input_link(links):
"""Let the user input a number.""" """ Let the user input a choice. """
while True: while True:
try: 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): if 1 <= the_chosen_one <= len(links):
return links[the_chosen_one - 1] return links[the_chosen_one - 1]
elif the_chosen_one == 0: elif the_chosen_one == 0:
return None return None
else: else:
print('Choose a valid number!') log.warning('Choose a valid number!')
except ValueError: except ValueError:
print('Choose a valid number!') log.warning('Choose a valid number!')
def trim_song(file): def trim_song(file):
"""Remove the first song from file.""" """ Remove the first song from file. """
with open(file, 'r') as file_in: with open(file, 'r') as file_in:
data = file_in.read().splitlines(True) data = file_in.read().splitlines(True)
with open(file, 'w') as file_out: with open(file, 'w') as file_out:
@@ -60,26 +63,32 @@ def get_arguments():
'-f', '--folder', default=(os.path.join(sys.path[0], 'Music')), '-f', '--folder', default=(os.path.join(sys.path[0], 'Music')),
help='path to folder where files will be stored in') help='path to folder where files will be stored in')
parser.add_argument( parser.add_argument(
'-v', '--verbose', default=False, help='show debug output', '-i', '--input-ext', default='.m4a',
action='store_true')
parser.add_argument(
'-i', '--input_ext', default='.m4a',
help='prefered input format .m4a or .webm (Opus)') help='prefered input format .m4a or .webm (Opus)')
parser.add_argument( parser.add_argument(
'-o', '--output_ext', default='.mp3', '-o', '--output-ext', default='.mp3',
help='prefered output extension .mp3 or .m4a (AAC)') 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): 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 = len(raw_song) == 22 and raw_song.replace(" ", "%20") == raw_song
status = status or raw_song.find('spotify') > -1 status = status or raw_song.find('spotify') > -1
return status return status
def is_youtube(raw_song): 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 = len(raw_song) == 11 and raw_song.replace(" ", "%20") == raw_song
status = status and not raw_song.lower() == raw_song status = status and not raw_song.lower() == raw_song
status = status or 'youtube.com/watch?v=' in 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): 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(' ', '_')
title = title.replace('/', '_') title = title.replace('/', '_')
@@ -97,7 +106,7 @@ def sanitize_title(title):
def generate_token(): def generate_token():
"""Generate the token. Please respect these credentials :)""" """ Generate the token. Please respect these credentials :) """
credentials = oauth2.SpotifyClientCredentials( credentials = oauth2.SpotifyClientCredentials(
client_id='4fe3fecfe5334023a1472516cc99d805', client_id='4fe3fecfe5334023a1472516cc99d805',
client_secret='0f02b7c483c04257984695007a4a8d5c') client_secret='0f02b7c483c04257984695007a4a8d5c')
@@ -106,7 +115,7 @@ def generate_token():
def generate_search_url(song, viewsort=False): 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 # urllib.request.quote() encodes URL with special characters
song = quote(song) song = quote(song)
if viewsort: if viewsort:
@@ -125,15 +134,11 @@ def filter_path(path):
os.remove(os.path.join(path, temp)) os.remove(os.path.join(path, temp))
def grace_quit():
print('\n\nExiting.')
sys.exit(0)
def get_sec(time_str): def get_sec(time_str):
v = time_str.split(':', 3) v = time_str.split(':', 3)
v.reverse() v.reverse()
sec = 0 sec = 0
if len(v) > 0: #seconds if len(v) > 0: # seconds
sec += int(v[0]) sec += int(v[0])
if len(v) > 1: # minutes if len(v) > 1: # minutes
sec += int(v[1]) * 60 sec += int(v[1]) * 60

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

View File

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

249
spotdl.py
View File

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

View File

@@ -1,31 +1,44 @@
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
from spotdl import logger
import spotdl import spotdl
import os import os
raw_song = "Tony's Videos VERY SHORT VIDEO 28.10.2016" 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(): 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) url = spotdl.generate_youtube_url(raw_song, meta_tags=None)
assert url == expect_url assert url == expect_url
def test_youtube_title(): def test_youtube_title():
expect_title = "Tony's Videos VERY SHORT VIDEO 28.10.2016"
global content global content
content = spotdl.go_pafy(raw_song, meta_tags=None)
global title 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) title = spotdl.get_youtube_title(content)
assert title == expect_title assert title == expect_title
def test_check_exists(): def test_check_exists():
expect_check = False expect_check = False
# prerequisites for determining filename # 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) check = spotdl.check_exists(file_name, raw_song, meta_tags=None, islist=True)
assert check == expect_check assert check == expect_check
@@ -33,7 +46,7 @@ def test_check_exists():
def test_download(): def test_download():
expect_download = True expect_download = True
# prerequisites for determining filename # 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) download = spotdl.download_song(file_name, content)
assert download == expect_download assert download == expect_download
@@ -42,7 +55,9 @@ def test_convert():
# exit code 0 = success # exit code 0 = success
expect_convert = 0 expect_convert = 0
# prerequisites for determining filename # 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 input_song = file_name + spotdl.args.input_ext
output_song = file_name + spotdl.args.output_ext output_song = file_name + spotdl.args.output_ext
convert = spotdl.convert.song(input_song, output_song, spotdl.args.folder) convert = spotdl.convert.song(input_song, output_song, spotdl.args.folder)
@@ -53,11 +68,8 @@ def test_metadata():
expect_metadata = None expect_metadata = None
# prerequisites for determining filename # prerequisites for determining filename
meta_tags = spotdl.generate_metadata(raw_song) meta_tags = spotdl.generate_metadata(raw_song)
meta_tags = spotdl.generate_metadata(raw_song) file_name = spotdl.internals.sanitize_title(title)
file_name = spotdl.misc.sanitize_title(title)
output_song = file_name + spotdl.args.output_ext
metadata_output = spotdl.metadata.embed(os.path.join(spotdl.args.folder, output_song), meta_tags) 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) 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) assert (metadata_output == expect_metadata) and (metadata_input == expect_metadata)
@@ -65,8 +77,8 @@ def test_metadata():
def test_check_exists2(): def test_check_exists2():
expect_check = True expect_check = True
# prerequisites for determining filename # prerequisites for determining filename
file_name = spotdl.misc.sanitize_title(title) file_name = spotdl.internals.sanitize_title(title)
input_song = file_name + spotdl.args.input_ext
os.remove(os.path.join(spotdl.args.folder, input_song)) os.remove(os.path.join(spotdl.args.folder, input_song))
check = spotdl.check_exists(file_name, raw_song, meta_tags=None, islist=True) 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 assert check == expect_check

View File

@@ -1,12 +1,25 @@
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
from spotdl import logger
import spotdl import spotdl
import os import os
raw_song = 'http://open.spotify.com/track/0JlS7BXXD07hRmevDnbPDU' 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(): def test_spotify_title():
@@ -35,7 +48,7 @@ def test_check_exists():
# prerequisites for determining filename # prerequisites for determining filename
songname = spotdl.generate_songname(meta_tags) songname = spotdl.generate_songname(meta_tags)
global file_name 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) check = spotdl.check_exists(file_name, raw_song, meta_tags, islist=True)
assert check == expect_check assert check == expect_check
@@ -52,6 +65,8 @@ def test_convert():
# exit code 0 = success # exit code 0 = success
expect_convert = 0 expect_convert = 0
# prerequisites for determining filename # prerequisites for determining filename
global input_song
global output_song
input_song = file_name + spotdl.args.input_ext input_song = file_name + spotdl.args.input_ext
output_song = file_name + spotdl.args.output_ext output_song = file_name + spotdl.args.output_ext
convert = spotdl.convert.song(input_song, output_song, spotdl.args.folder) convert = spotdl.convert.song(input_song, output_song, spotdl.args.folder)
@@ -61,9 +76,7 @@ def test_convert():
def test_metadata(): def test_metadata():
expect_metadata = True expect_metadata = True
# prerequisites for determining filename # 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) 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) metadata_input = spotdl.metadata.embed(os.path.join(spotdl.args.folder, input_song), meta_tags)
assert metadata_output == (metadata_input == expect_metadata) assert metadata_output == (metadata_input == expect_metadata)
@@ -71,7 +84,7 @@ def test_metadata():
def test_check_exists2(): def test_check_exists2():
expect_check = True expect_check = True
# prerequisites for determining filename # prerequisites for determining filename
input_song = file_name + spotdl.args.input_ext
os.remove(os.path.join(spotdl.args.folder, input_song)) os.remove(os.path.join(spotdl.args.folder, input_song))
check = spotdl.check_exists(file_name, raw_song, meta_tags, islist=True) 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 assert check == expect_check

View File

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