diff --git a/core/convert.py b/core/convert.py index cf7f522..bef2714 100644 --- a/core/convert.py +++ b/core/convert.py @@ -2,21 +2,25 @@ import subprocess import os import sys + def song(input_song, output_song, avconv=False, verbose=False): + """Do the audio format conversion.""" if not input_song == output_song: if sys.version_info < (3, 0): input_song = input_song.encode('utf-8') output_song = output_song.encode('utf-8') - print('Converting ' + input_song + ' to ' + output_song.split('.')[-1]) + print('Converting {0} to {1}'.format( + input_song, output_song.split('.')[-1])) 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 + return 0 + def convert_with_avconv(input_song, output_song, verbose): - # different path for windows + """Convert the audio file using avconv.""" if os.name == 'nt': avconv_path = 'Scripts\\avconv.exe' else: @@ -33,17 +37,20 @@ 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): - # 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 convert_with_ffmpeg(input_song, output_song, verbose): + """Convert the audio file using FFMpeg. + + 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 + """ if os.name == "nt": ffmpeg_pre = 'Scripts\\ffmpeg.exe ' @@ -54,6 +61,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 +77,7 @@ 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(' ') - - subprocess.call(command) + command = '{0}-i Music/{1} {2}Music/{3}'.format( + ffmpeg_pre, input_song, ffmpeg_params, output_song).split(' ') + return subprocess.call(command) diff --git a/core/metadata.py b/core/metadata.py index a751c59..9fb00f1 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): + """Check if the input file title matches the expected title.""" + already_tagged = False try: if file.endswith('.mp3'): audiofile = EasyID3('Music/' + file) @@ -22,10 +24,12 @@ 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): + """Embed metadata.""" if sys.version_info < (3, 0): music_file = music_file.encode('utf-8') if meta_tags is None: @@ -41,14 +45,17 @@ def embed(music_file, meta_tags): print('Cannot embed meta-tags into given output extension') return False + def embed_mp3(music_file, meta_tags): + """Embed metadata to MP3 files.""" # EasyID3 is fun to use ;) audiofile = EasyID3('Music/' + music_file) audiofile['artist'] = meta_tags['artists'][0]['name'] 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,12 +75,15 @@ 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): + """Embed metadata to M4A files.""" # Apple has specific tags - see mutagen docs - # http://mutagen.readthedocs.io/en/latest/api/mp4.html tags = {'album': '\xa9alb', @@ -98,7 +108,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 +118,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..ecbbc93 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): + """Let the user input a number.""" 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,99 +24,120 @@ def input_link(links): except ValueError: print('Choose a valid number!') -# take input correctly for both python2 & 3 + def user_input(string=''): + """Take input correctly for both Python 2 & 3.""" if sys.version_info > (3, 0): return 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:]) + """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: + 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): + """Check if the input song is a Spotify link.""" + if (len(raw_song) == 22 and raw_song.replace(" ", "%20") == raw_song) or \ + (raw_song.find('spotify') > -1): return True else: return False -# generate filename of the song to be downloaded + def generate_filename(title): + """Generate filename of the song to be downloaded.""" # IMO python2 sucks dealing with unicode title = fix_encoding(title) title = fix_decoding(title) title = title.replace(' ', '_') + # slugify removes any special characters filename = slugify(title, ok='-_()[]{}', lower=False) return fix_encoding(filename) -# please respect these credentials :) + def generate_token(): - creds = oauth2.SpotifyClientCredentials( + """Generate the token. Please respect these credentials :)""" + 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): + """Generate YouTube search URL for the given song.""" # urllib2.quote() encodes URL with special characters - url = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q=" + quote(song) + url = u"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): + """Fix encoding issues in Python 2.""" if sys.version_info < (3, 0): query = query.encode('utf-8') return query + def fix_decoding(query): + """Fix decoding issues in Python 2.""" 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..5a92563 100755 --- a/spotdl.py +++ b/spotdl.py @@ -1,5 +1,4 @@ #!/usr/bin/env python - # -*- coding: UTF-8 -*- from core import metadata @@ -19,15 +18,17 @@ try: except ImportError: import urllib.request as urllib2 -# "[artist] - [song]" + def generate_songname(raw_song): + """Generate a string of the format '[artist] - [song]' for the given song.""" if misc.is_spotify(raw_song): tags = generate_metadata(raw_song) - raw_song = tags['artists'][0]['name'] + ' - ' + tags['name'] + raw_song = u'{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): + """Fetch a song's metadata from Spotify.""" if misc.is_spotify(raw_song): # fetch track information directly if it is spotify link meta_tags = spotify.track(raw_song) @@ -52,18 +53,18 @@ 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]" + """Search for the song on YouTube and generate an URL to its video.""" 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: @@ -74,8 +75,8 @@ def generate_youtube_url(raw_song): # fetch all video links on first page on YouTube 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()) + if x.find('channel') is None and x.find('googleads') is None: + print(u'{0}. {1}'.format(check, x.get_text())) links.append(x.find('a')['href']) check += 1 print('') @@ -85,46 +86,52 @@ 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') > -1 or result.find('googleads') > -1: + result = items_parse.find_all( + attrs={'class': 'yt-uix-tile-link'})[check]['href'] check += 1 - full_link = "youtube.com" + result + + full_link = u'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 + """Parse track from YouTube.""" track_url = generate_youtube_url(raw_song) if track_url is None: return None else: - # parse the YouTube video return pafy.new(track_url) -# title of the YouTube video + def get_youtube_title(content, number=None): + """Get the YouTube video's title.""" 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): - # fetch all user playlists + """Fetch user playlists when using the -u option.""" 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(u'{0}. {1} ({2} tracks)'.format( + check, misc.fix_encoding(playlist['name']), + playlist['tracks']['total'])) links.append(playlist) check += 1 if playlists['next']: @@ -133,25 +140,23 @@ def feed_playlist(username): break print('') - # 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 = u'{0}.txt'.format(slugify(playlist['name'], ok='-_()[]{}')) + print(u'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(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 if tracks['next']: @@ -159,12 +164,12 @@ def feed_playlist(username): else: break + def download_song(content): + """Download the audio file from YouTube.""" if args.input_ext == '.webm': - # best available audio in .webm link = content.getbestaudio(preftype='webm') elif args.input_ext == '.m4a': - # best available audio in .webm link = content.getbestaudio(preftype='m4a') else: return False @@ -173,16 +178,17 @@ def download_song(content): return False 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") + """Check if the input song already exists in the 'Music' folder.""" + files = os.listdir('Music') for file in files: - if file.endswith(".temp"): - os.remove("Music/" + file) + if file.endswith('.temp'): + os.remove(u'Music/{0}'.format(file)) continue # check if any file with similar name is already present in Music/ dfile = misc.fix_decoding(file) @@ -190,25 +196,31 @@ 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): + """Download all songs from the list.""" with open(file, 'r') as listed: lines = (listed.read()).splitlines() # ignore blank lines in file (if any) @@ -216,7 +228,7 @@ def grab_list(file): lines.remove('') except ValueError: pass - print('Total songs in list = ' + str(len(lines)) + ' songs') + print(u'Total songs in list: {0} songs'.format(len(lines))) print('') # nth input song number = 1 @@ -226,9 +238,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,9 +259,9 @@ 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 + """Logic behind downloading a song.""" if number: islist = True else: @@ -257,9 +269,10 @@ 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) music_file = misc.fix_decoding(music_file) @@ -268,17 +281,16 @@ def grab_single(raw_song, number=None): print('') input_song = music_file + args.input_ext output_song = music_file + args.output_ext - convert.song(input_song, - output_song, - avconv=args.avconv, + convert.song(input_song, output_song, avconv=args.avconv, verbose=args.verbose) - os.remove('Music/' + input_song) + os.remove('Music/{0}'.format(misc.fix_encoding(input_song))) 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 +305,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..d4f288c 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,15 +32,17 @@ 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 + # exit code 0 = success + expect_convert = 0 content = spotdl.go_pafy(raw_song) music_file = spotdl.misc.generate_filename(content.title) music_file = spotdl.misc.fix_decoding(music_file) @@ -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..537a596 100644 --- a/test/test_username.py +++ b/test/test_username.py @@ -4,22 +4,26 @@ 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'] - result = spotdl.spotify.user_playlist(playlist['owner']['id'], playlist['id'], fields='tracks,next') + result = spotdl.spotify.user_playlist( + playlist['owner']['id'], playlist['id'], fields='tracks,next') tracks = result['tracks'] with open('list.txt', 'a') as fout: