diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index bb83018..fc717d6 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -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 - - + +### Log + +
+``` +paste the output over here +``` +
diff --git a/LICENSE.txt b/LICENSE similarity index 100% rename from LICENSE.txt rename to LICENSE diff --git a/README.md b/README.md index 29a2ad9..c81451d 100755 --- a/README.md +++ b/README.md @@ -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 diff --git a/core/convert.py b/core/convert.py index ea8a953..95a4704 100644 --- a/core/convert.py +++ b/core/convert.py @@ -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) diff --git a/core/misc.py b/core/internals.py similarity index 73% rename from core/misc.py rename to core/internals.py index 57fc005..ccae89b 100755 --- a/core/misc.py +++ b/core/internals.py @@ -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 diff --git a/core/logger.py b/core/logger.py new file mode 100644 index 0000000..435480f --- /dev/null +++ b/core/logger.py @@ -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) diff --git a/core/metadata.py b/core/metadata.py index 77137c4..969f2e0 100755 --- a/core/metadata.py +++ b/core/metadata.py @@ -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', diff --git a/requirements.txt b/requirements.txt index ef67238..324b7dc 100755 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ spotipy >= 2.4.4 mutagen >= 1.37 unicode-slugify >= 0.1.3 titlecase >= 0.10.0 +logzero >= 1.3.1 \ No newline at end of file diff --git a/spotdl.py b/spotdl.py index 3b6b497..4e970d3 100755 --- a/spotdl.py +++ b/spotdl.py @@ -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) diff --git a/test/test_simple.py b/test/test_simple.py index 72bb3de..0fbd402 100644 --- a/test/test_simple.py +++ b/test/test_simple.py @@ -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 diff --git a/test/test_spotify.py b/test/test_spotify.py index 9b1e624..2e2abfe 100644 --- a/test/test_spotify.py +++ b/test/test_spotify.py @@ -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 diff --git a/test/test_username.py b/test/test_username.py index 8025cfc..4e78f3d 100644 --- a/test/test_username.py +++ b/test/test_username.py @@ -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)