diff --git a/core/convert.py b/core/convert.py index cf7f522..8f26dfa 100644 --- a/core/convert.py +++ b/core/convert.py @@ -2,6 +2,7 @@ import subprocess import os import sys + def song(input_song, output_song, avconv=False, verbose=False): if not input_song == output_song: if sys.version_info < (3, 0): @@ -11,10 +12,11 @@ def song(input_song, output_song, avconv=False, verbose=False): if avconv: exit_code = convert_with_avconv(input_song, output_song, verbose) else: - exit_code = convert_with_FFmpeg(input_song, output_song, verbose) + exit_code = convert_with_ffmpeg(input_song, output_song, verbose) return exit_code return None + def convert_with_avconv(input_song, output_song, verbose): # different path for windows if os.name == 'nt': @@ -33,10 +35,10 @@ def convert_with_avconv(input_song, output_song, verbose): '-ab', '192k', 'Music/' + output_song] - subprocess.call(command) + return subprocess.call(command) -def convert_with_FFmpeg(input_song, output_song, verbose): +def convert_with_ffmpeg(input_song, output_song, verbose): # What are the differences and similarities between ffmpeg, libav, and avconv? # https://stackoverflow.com/questions/9477115 # ffmeg encoders high to lower quality @@ -54,6 +56,7 @@ def convert_with_FFmpeg(input_song, output_song, verbose): if not verbose: ffmpeg_pre += '-hide_banner -nostats -v panic ' + ffmpeg_params = '' input_ext = input_song.split('.')[-1] output_ext = output_song.split('.')[-1] @@ -69,10 +72,8 @@ def convert_with_FFmpeg(input_song, output_song, verbose): elif output_ext == 'm4a': ffmpeg_params = '-cutoff 20000 -c:a libfdk_aac -b:a 192k -vn ' - command = (ffmpeg_pre + - '-i Music/' + input_song + ' ' + - ffmpeg_params + - 'Music/' + output_song + '').split(' ') + command = '{0}-i Music/{1} {2}Music/{4}'.format( + ffmpeg_pre, input_song, ffmpeg_params, output_song).split(' ') - subprocess.call(command) + return subprocess.call(command) diff --git a/core/metadata.py b/core/metadata.py index a751c59..6b78ae5 100755 --- a/core/metadata.py +++ b/core/metadata.py @@ -9,8 +9,10 @@ try: except ImportError: import urllib.request as urllib2 + # check if input file title matches with expected title def compare(file, metadata): + already_tagged = False try: if file.endswith('.mp3'): audiofile = EasyID3('Music/' + file) @@ -22,9 +24,10 @@ def compare(file, metadata): # fetch track title metadata already_tagged = audiofile[tags['title']] == metadata['name'] except KeyError: - already_tagged = False + pass return already_tagged + def embed(music_file, meta_tags): if sys.version_info < (3, 0): music_file = music_file.encode('utf-8') @@ -41,6 +44,7 @@ def embed(music_file, meta_tags): print('Cannot embed meta-tags into given output extension') return False + def embed_mp3(music_file, meta_tags): # EasyID3 is fun to use ;) audiofile = EasyID3('Music/' + music_file) @@ -48,7 +52,8 @@ def embed_mp3(music_file, meta_tags): audiofile['albumartist'] = meta_tags['artists'][0]['name'] audiofile['album'] = meta_tags['album']['name'] audiofile['title'] = meta_tags['name'] - audiofile['tracknumber'] = [meta_tags['track_number'], meta_tags['total_tracks']] + audiofile['tracknumber'] = [meta_tags['track_number'], + meta_tags['total_tracks']] audiofile['discnumber'] = [meta_tags['disc_number'], 0] audiofile['date'] = meta_tags['release_date'] audiofile['originaldate'] = meta_tags['release_date'] @@ -68,11 +73,13 @@ def embed_mp3(music_file, meta_tags): audiofile.save(v2_version=3) audiofile = ID3('Music/' + music_file) albumart = urllib2.urlopen(meta_tags['album']['images'][0]['url']) - audiofile["APIC"] = APIC(encoding=3, mime='image/jpeg', type=3, desc=u'Cover', data=albumart.read()) + audiofile["APIC"] = APIC(encoding=3, mime='image/jpeg', type=3, + desc=u'Cover', data=albumart.read()) albumart.close() audiofile.save(v2_version=3) return True + def embed_m4a(music_file, meta_tags): # Apple has specific tags - see mutagen docs - # http://mutagen.readthedocs.io/en/latest/api/mp4.html @@ -98,7 +105,8 @@ def embed_m4a(music_file, meta_tags): audiofile[tags['albumartist']] = meta_tags['artists'][0]['name'] audiofile[tags['album']] = meta_tags['album']['name'] audiofile[tags['title']] = meta_tags['name'] - audiofile[tags['tracknumber']] = [(meta_tags['track_number'], meta_tags['total_tracks'])] + audiofile[tags['tracknumber']] = [(meta_tags['track_number'], + meta_tags['total_tracks'])] audiofile[tags['disknumber']] = [(meta_tags['disc_number'], 0)] audiofile[tags['date']] = meta_tags['release_date'] audiofile[tags['originaldate']] = meta_tags['release_date'] @@ -107,7 +115,8 @@ def embed_m4a(music_file, meta_tags): if meta_tags['copyright']: audiofile[tags['copyright']] = meta_tags['copyright'] albumart = urllib2.urlopen(meta_tags['album']['images'][0]['url']) - audiofile[tags['albumart']] = [ MP4Cover(albumart.read(), imageformat=MP4Cover.FORMAT_JPEG) ] + audiofile[tags['albumart']] = [MP4Cover( + albumart.read(), imageformat=MP4Cover.FORMAT_JPEG)] albumart.close() audiofile.save() return True diff --git a/core/misc.py b/core/misc.py index 3fdea63..1bdc997 100755 --- a/core/misc.py +++ b/core/misc.py @@ -6,15 +6,16 @@ import spotipy.oauth2 as oauth2 try: from urllib2 import quote -except: +except ImportError: from urllib.request import quote + # method to input (user playlists) and (track when using manual mode) def input_link(links): while True: try: the_chosen_one = int(user_input('>> Choose your number: ')) - if the_chosen_one >= 1 and the_chosen_one <= len(links): + if 1 <= the_chosen_one <= len(links): return links[the_chosen_one - 1] elif the_chosen_one == 0: return None @@ -23,6 +24,7 @@ def input_link(links): except ValueError: print('Choose a valid number!') + # take input correctly for both python2 & 3 def user_input(string=''): if sys.version_info > (3, 0): @@ -30,41 +32,51 @@ def user_input(string=''): else: return raw_input(string) + # remove first song from .txt def trim_song(file): - with open(file, 'r') as fin: - data = fin.read().splitlines(True) - with open(file, 'w') as fout: - fout.writelines(data[1:]) + with open(file, 'r') as file_in: + data = file_in.read().splitlines(True) + with open(file, 'w') as file_out: + file_out.writelines(data[1:]) + def get_arguments(): - parser = argparse.ArgumentParser(description='Download and convert songs \ - from Spotify, Youtube etc.', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser = argparse.ArgumentParser( + description='Download and convert songs from Spotify, Youtube etc.', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) group = parser.add_mutually_exclusive_group(required=True) - group.add_argument('-s', '--song', - help='download song by spotify link or name') - group.add_argument('-l', '--list', - help='download songs from a file') - group.add_argument('-u', '--username', - help="load user's playlists into .txt") - parser.add_argument('-m', '--manual', default=False, - help='choose the song to download manually', action='store_true') - parser.add_argument('-nm', '--no-metadata', default=False, - help='do not embed metadata in songs', action='store_true') - parser.add_argument('-a', '--avconv', default=False, - help='Use avconv for conversion otherwise set defaults to ffmpeg', - action='store_true') - parser.add_argument('-v', '--verbose', default=False, - help='show debug output', action='store_true') - parser.add_argument('-i', '--input_ext', default='.m4a', - help='prefered input format .m4a or .webm (Opus)') - parser.add_argument('-o', '--output_ext', default='.mp3', - help='prefered output extension .mp3 or .m4a (AAC)') + group.add_argument( + '-s', '--song', help='download song by spotify link or name') + group.add_argument( + '-l', '--list', help='download songs from a file') + group.add_argument( + '-u', '--username', + help="load user's playlists into .txt") + parser.add_argument( + '-m', '--manual', default=False, + help='choose the song to download manually', action='store_true') + parser.add_argument( + '-nm', '--no-metadata', default=False, + help='do not embed metadata in songs', action='store_true') + parser.add_argument( + '-a', '--avconv', default=False, + help='Use avconv for conversion otherwise set defaults to ffmpeg', + action='store_true') + parser.add_argument( + '-v', '--verbose', default=False, help='show debug output', + action='store_true') + parser.add_argument( + '-i', '--input_ext', default='.m4a', + help='prefered input format .m4a or .webm (Opus)') + parser.add_argument( + '-o', '--output_ext', default='.mp3', + help='prefered output extension .mp3 or .m4a (AAC)') return parser.parse_args() + # check if input song is spotify link def is_spotify(raw_song): if (len(raw_song) == 22 and raw_song.replace(" ", "%20") == raw_song) or (raw_song.find('spotify') > -1): @@ -72,6 +84,7 @@ def is_spotify(raw_song): else: return False + # generate filename of the song to be downloaded def generate_filename(title): # IMO python2 sucks dealing with unicode @@ -82,40 +95,45 @@ def generate_filename(title): filename = slugify(title, ok='-_()[]{}', lower=False) return fix_encoding(filename) + # please respect these credentials :) def generate_token(): - creds = oauth2.SpotifyClientCredentials( + credentials = oauth2.SpotifyClientCredentials( client_id='4fe3fecfe5334023a1472516cc99d805', client_secret='0f02b7c483c04257984695007a4a8d5c') - token = creds.get_access_token() + token = credentials.get_access_token() return token + def generate_search_url(song): # urllib2.quote() encodes URL with special characters - url = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q=" + quote(song) + url = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q={0}".format( + quote(song)) return url + # fix encoding issues in python2 def fix_encoding(query): if sys.version_info < (3, 0): query = query.encode('utf-8') return query + def fix_decoding(query): if sys.version_info < (3, 0): query = query.decode('utf-8') return query + def filter_path(path): os.chdir(sys.path[0]) if not os.path.exists(path): os.makedirs(path) for temp in os.listdir(path): if temp.endswith('.temp'): - os.remove(path + '/' + temp) + os.remove('{0}/{1}'.format(path, temp)) + def grace_quit(): - print('') - print('') - print('Exitting..') + print('\n\nExiting.') sys.exit() diff --git a/spotdl.py b/spotdl.py index 812925a..661fb4a 100755 --- a/spotdl.py +++ b/spotdl.py @@ -1,5 +1,4 @@ #!/usr/bin/env python - # -*- coding: UTF-8 -*- from core import metadata @@ -19,13 +18,15 @@ try: except ImportError: import urllib.request as urllib2 + # "[artist] - [song]" def generate_songname(raw_song): if misc.is_spotify(raw_song): tags = generate_metadata(raw_song) - raw_song = tags['artists'][0]['name'] + ' - ' + tags['name'] + raw_song = '{0} - {1}'.format(tags['artists'][0]['name'], tags['name']) return misc.fix_encoding(raw_song) + # fetch song's metadata from spotify def generate_metadata(raw_song): if misc.is_spotify(raw_song): @@ -52,18 +53,19 @@ def generate_metadata(raw_song): meta_tags[u'release_date'] = album['release_date'] meta_tags[u'publisher'] = album['label'] meta_tags[u'total_tracks'] = album['tracks']['total'] - #import pprint - #pprint.pprint(meta_tags) - #pprint.pprint(spotify.album(meta_tags['album']['id'])) + # import pprint + # pprint.pprint(meta_tags) + # pprint.pprint(spotify.album(meta_tags['album']['id'])) return meta_tags + def generate_youtube_url(raw_song): # decode spotify http link to "[artist] - [song]" song = generate_songname(raw_song) # generate direct search YouTube URL search_url = misc.generate_search_url(song) item = urllib2.urlopen(search_url).read() - #item = unicode(item, 'utf-8') + # item = unicode(item, 'utf-8') items_parse = BeautifulSoup(item, "html.parser") check = 1 if args.manual: @@ -75,7 +77,7 @@ def generate_youtube_url(raw_song): for x in items_parse.find_all('h3', {'class': 'yt-lockup-title'}): # confirm the video result is not an advertisement if not x.find('channel') == -1 or not x.find('googleads') == -1: - print(str(check) + '. ' + x.get_text()) + print('{0}. {1}'.format(check, x.get_text())) links.append(x.find('a')['href']) check += 1 print('') @@ -85,15 +87,20 @@ def generate_youtube_url(raw_song): return None else: # get video link of the first YouTube result - result = items_parse.find_all(attrs={'class': 'yt-uix-tile-link'})[0]['href'] + result = items_parse.find_all( + attrs={'class': 'yt-uix-tile-link'})[0]['href'] + # confirm the video result is not an advertisement # otherwise keep iterating until it is not - while not result.find('channel') == -1 or not result.find('googleads') == -1: - result = items_parse.find_all(attrs={'class': 'yt-uix-tile-link'})[check]['href'] + while result.find('channel') < 0 or result.find('googleads') < 0: + result = items_parse.find_all( + attrs={'class': 'yt-uix-tile-link'})[check]['href'] check += 1 - full_link = "youtube.com" + result + + full_link = "youtube.com{0}'.format(result) return full_link + # parse track from YouTube def go_pafy(raw_song): # video link of the video to extract audio from @@ -104,13 +111,15 @@ def go_pafy(raw_song): # parse the YouTube video return pafy.new(track_url) + # title of the YouTube video def get_youtube_title(content, number=None): title = misc.fix_encoding(content.title) if number is None: return title else: - return str(number) + '. ' + title + return '{0}. {1}'.format(number, title) + # fetch user playlists when using -u option def feed_playlist(username): @@ -118,13 +127,16 @@ def feed_playlist(username): playlists = spotify.user_playlists(username) links = [] check = 1 + # iterate over user playlists while True: for playlist in playlists['items']: - # In rare cases, playlists may not be found, so playlists['next'] is - # None. Skip these. Also see Issue #91. + # 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(str(check) + '. ' + misc.fix_encoding(playlist['name']) + ' (' + str(playlist['tracks']['total']) + ' tracks)') + print('{0}. {1} ({2} tracks)'.format( + check, misc.fix_encoding(playlist['name'])), + playlist['tracks']['total']) links.append(playlist) check += 1 if playlists['next']: @@ -136,22 +148,23 @@ def feed_playlist(username): # let user select playlist playlist = misc.input_link(links) # fetch detailed information for playlist - results = spotify.user_playlist(playlist['owner']['id'], playlist['id'], fields="tracks,next") + results = spotify.user_playlist( + playlist['owner']['id'], playlist['id'], fields='tracks,next') print('') # slugify removes any special characters - file = slugify(playlist['name'], ok='-_()[]{}') + '.txt' - print('Feeding ' + str(playlist['tracks']['total']) + ' tracks to ' + file) + file = '{0}.txt'.format(slugify(playlist['name'], ok='-_()[]{}')) + print('Feeding {0} tracks to {1}'.format(playlist['tracks']['total'], file)) tracks = results['tracks'] - with open(file, 'a') as fout: + with open(file, 'a') as file_out: while True: for item in tracks['items']: track = item['track'] try: - fout.write(track['external_urls']['spotify'] + '\n') + file_out.write(track['external_urls']['spotify'] + '\n') except KeyError: - title = track['name'] + ' by '+ track['artists'][0]['name'] - print('Skipping track ' + title + ' (local only?)') + print('Skipping track {0} by {1} (local only?)'.format( + track['name'], track['artists'][0]['name'])) # 1 page = 50 results # check if there are more pages if tracks['next']: @@ -159,6 +172,7 @@ def feed_playlist(username): else: break + def download_song(content): if args.input_ext == '.webm': # best available audio in .webm @@ -174,15 +188,17 @@ def download_song(content): else: music_file = misc.generate_filename(content.title) # download link - link.download(filepath='Music/' + music_file + args.input_ext) + link.download( + filepath='Music/{0}{1}'.format(music_file, args.input_ext)) return True + # check if input song already exists in Music folder def check_exists(music_file, raw_song, islist=True): - files = os.listdir("Music") + files = os.listdir('Music') for file in files: - if file.endswith(".temp"): - os.remove("Music/" + file) + if file.endswith('.temp'): + os.remove('Music/{0}'.format(file)) continue # check if any file with similar name is already present in Music/ dfile = misc.fix_decoding(file) @@ -190,23 +206,29 @@ def check_exists(music_file, raw_song, islist=True): if dfile.startswith(umfile): # check if the already downloaded song has correct metadata already_tagged = metadata.compare(file, generate_metadata(raw_song)) + # if not, remove it and download again without prompt if misc.is_spotify(raw_song) and not already_tagged: - os.remove("Music/" + file) + os.remove('Music/{0}'.format(file)) return False - # do not prompt and skip the current song if already downloaded when using list + + # do not prompt and skip the current song + # if already downloaded when using list if islist: return True # if downloading only single song, prompt to re-download else: - prompt = misc.user_input('Song with same name has already been downloaded. Re-download? (y/n): ').lower() - if prompt == "y": - os.remove("Music/" + file) + prompt = misc.user_input( + 'Song with same name has already been downloaded. ' + 'Re-download? (y/n): ').lower() + if prompt == 'y': + os.remove('Music/{0}'.format(file)) return False else: return True return False + # download songs from list def grab_list(file): with open(file, 'r') as listed: @@ -216,7 +238,7 @@ def grab_list(file): lines.remove('') except ValueError: pass - print('Total songs in list = ' + str(len(lines)) + ' songs') + print('Total songs in list: {0} songs'.format(len(lines))) print('') # nth input song number = 1 @@ -226,9 +248,9 @@ def grab_list(file): # token expires after 1 hour except spotipy.oauth2.SpotifyOauthError: # refresh token when it expires - token = misc.generate_token() + new_token = misc.generate_token() global spotify - spotify = spotipy.Spotify(auth=token) + spotify = spotipy.Spotify(auth=new_token) grab_single(raw_song, number=number) # detect network problems except (urllib2.URLError, TypeError, IOError): @@ -247,6 +269,7 @@ def grab_list(file): misc.trim_song(file) number += 1 + # logic behind downloading some song def grab_single(raw_song, number=None): # check if song is being downloaded from list @@ -257,8 +280,8 @@ def grab_single(raw_song, number=None): content = go_pafy(raw_song) if content is None: return - # print "[number]. [artist] - [song]" if downloading from list - # otherwise print "[artist] - [song]" + # print '[number]. [artist] - [song]' if downloading from list + # otherwise print '[artist] - [song]' print(get_youtube_title(content, number)) # generate file name of the song to download music_file = misc.generate_filename(content.title) @@ -272,13 +295,14 @@ def grab_single(raw_song, number=None): output_song, avconv=args.avconv, verbose=args.verbose) - os.remove('Music/' + input_song) + os.remove('Music/{0}'.format(file)) meta_tags = generate_metadata(raw_song) if not args.no_metadata: metadata.embed(output_song, meta_tags) else: print('No audio streams available') + class Args(object): manual = False input_ext = '.m4a' @@ -293,9 +317,7 @@ spotify = spotipy.Spotify(auth=token) misc.filter_path('Music') if __name__ == '__main__': - os.chdir(sys.path[0]) - args = misc.get_arguments() if args.song: diff --git a/test/test_single.py b/test/test_single.py index 5b55f1e..d1ac20a 100644 --- a/test/test_single.py +++ b/test/test_single.py @@ -4,22 +4,26 @@ import spotdl raw_song = 'http://open.spotify.com/track/0JlS7BXXD07hRmevDnbPDU' + def test_spotify_title(): expect_title = 'David André Østby - Intro' title = spotdl.generate_songname(raw_song) assert title == expect_title + def test_youtube_url(): expect_url = 'youtube.com/watch?v=rg1wfcty0BA' url = spotdl.generate_youtube_url(raw_song) assert url == expect_url + def test_youtube_title(): expect_title = 'Intro - David André Østby' content = spotdl.go_pafy(raw_song) title = spotdl.get_youtube_title(content) assert title == expect_title + def test_check_exists(): expect_check = False content = spotdl.go_pafy(raw_song) @@ -28,12 +32,14 @@ def test_check_exists(): check = spotdl.check_exists(music_file, raw_song) assert check == expect_check + def test_download(): expect_download = True content = spotdl.go_pafy(raw_song) download = spotdl.download_song(content) assert download == expect_download + def test_convert(): # exit code None = success expect_convert = None @@ -45,6 +51,7 @@ def test_convert(): convert = spotdl.convert.song(input_song, output_song) assert convert == expect_convert + def test_metadata(): expect_metadata = True content = spotdl.go_pafy(raw_song) @@ -60,6 +67,7 @@ def test_metadata(): assert metadata_output == (metadata_input == expect_metadata) + def check_exists2(): expect_check = True content = spotdl.go_pafy(raw_song) diff --git a/test/test_username.py b/test/test_username.py index eb996fb..1ce287c 100644 --- a/test/test_username.py +++ b/test/test_username.py @@ -4,18 +4,21 @@ import spotdl username = 'alex' + def test_user(): expect_playlists = 7 playlists = spotdl.spotify.user_playlists(username) playlists = len(playlists['items']) assert playlists == expect_playlists + def test_playlist(): expect_tracks = 14 playlist = spotdl.spotify.user_playlists(username)['items'][0] tracks = playlist['tracks']['total'] assert tracks == expect_tracks + def test_tracks(): playlist = spotdl.spotify.user_playlists(username)['items'][0] expect_lines = playlist['tracks']['total']