diff --git a/.gitignore b/.gitignore index 74459fb..ffeaad5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -Music/ -last_albumart.jpg -list.txt +*.pyc +__pycache__/ +/Music/ +/*.txt diff --git a/README.md b/README.md index 07be5f8..924b473 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ - Track number - Disc number - Release date + - and some more.. - Works straight out of the box and does not require to generate or mess with your API keys. @@ -26,9 +27,7 @@ That's how your Music library will look like! ## Reporting Issues -- **Spotify made it mandatory to use a token to fetch track information. So, if you get rate limited or face any token problems, please let me know in [#58](https://github.com/Ritiek/Spotify-Downloader/issues/58).** - -- Search for your problem in the [issues section](https://github.com/Ritiek/Spotify-Downloader/issues?utf8=%E2%9C%93&q=) before opening a new ticket. It might be already answered and save you and me some time :D. +- Search for your problem in the [issues section](https://github.com/Ritiek/Spotify-Downloader/issues?utf8=%E2%9C%93&q=) before opening a new ticket. It might be already answered and save us time :D. - Provide as much information possible when opening your ticket. @@ -48,25 +47,26 @@ git clone https://github.com/Ritiek/Spotify-Downloader cd Spotify-Downloader pip install -U -r requirements.txt ``` -You'll also need to install avconv (use `--ffmpeg` option when using the script if you want `ffmpeg`): -`sudo apt-get install libav-tools` (`brew install libav` for Mac) +You'll also need to install FFmpeg for conversion (use `--avconv` if you'd like to use that instead): + +Linux: `sudo apt-get install ffmpeg` + +Mac: `brew install ffmpeg --with-libass --with-opus --with-fdk-aac` ### Windows Assuming you have Python already installed.. -- Download Libav-Tools for windows: https://builds.libav.org/windows/release-gpl/libav-x86_64-w64-mingw32-11.7.7z. Copy all the contents of bin folder (of libav) to Scripts folder (in your python's installation directory). +- Download FFmpeg for windows from [here](http://ffmpeg.zeranoe.com/builds/). Copy `ffmpeg.exe` from bin folder (of FFmpeg) to Scripts folder (in your python's installation directory). -- Download the zip file of this repository and extract its contents in your python's installation folder as well. +- Download the zip file of this repository and extract its contents in your python's installation folder. -Shift+right-click on empty area and open cmd and type: +- Change your current working directory to python's installation directory. Shift+right-click on empty area and open cmd and type: `"Scripts/pip.exe" install -U -r requirements.txt` - If you do not want to naviagte to your python folder from the command-line everytime you want to run the script, you can have your python 'PATH' environment variables set and then you can run the script from any directory. - - python install folder: like (C:\Program Files\Python36) - - python scripts folder: like (C:\Program Files\Python36\Scripts) ## Instructions for Downloading Songs @@ -88,8 +88,8 @@ optional arguments: -n, --no-convert skip the conversion process and meta-tags (default: False) -m, --manual choose the song to download manually (default: False) - -f, --ffmpeg Use ffmpeg instead of libav for conversion. If not set - defaults to libav (default: False) + -a, --avconv Use avconv for conversion. If not set + defaults to FFmpeg (default: False) -v, --verbose show debug output (default: False) -i INPUT_EXT, --input_ext INPUT_EXT prefered input format .m4a or .webm (Opus) (default: diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/core/__init__.py @@ -0,0 +1 @@ + diff --git a/core/metadata.py b/core/metadata.py new file mode 100644 index 0000000..db36e81 --- /dev/null +++ b/core/metadata.py @@ -0,0 +1,104 @@ +from mutagen.easyid3 import EasyID3 +from mutagen.id3 import ID3, APIC +from mutagen.mp4 import MP4, MP4Cover + +# urllib2 is urllib.request in python3 +try: + import urllib2 +except ImportError: + import urllib.request as urllib2 + +# check if input file title matches with expected title +def compare(file, metadata): + try: + if file.endswith('.mp3'): + audiofile = EasyID3('Music/' + file) + # fetch track title metadata + already_tagged = audiofile['title'][0] == metadata['name'] + elif file.endswith('.m4a'): + tags = {'title': '\xa9nam'} + audiofile = MP4('Music/' + file) + # fetch track title metadata + already_tagged = audiofile[tags['title']] == metadata['name'] + except KeyError: + already_tagged = False + return already_tagged + +def embed(music_file, meta_tags, output_ext): + if meta_tags is None: + print('Could not find meta-tags') + elif output_ext == '.m4a': + print('Fixing meta-tags') + embed_m4a(music_file, meta_tags, output_ext) + elif output_ext == '.mp3': + print('Fixing meta-tags') + embed_mp3(music_file, meta_tags, output_ext) + else: + print('Cannot embed meta-tags into given output extension') + +def embed_mp3(music_file, meta_tags, output_ext): + # EasyID3 is fun to use ;) + audiofile = EasyID3('Music/' + music_file + output_ext) + 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['discnumber'] = [meta_tags['disc_number'], 0] + audiofile['date'] = meta_tags['release_date'] + audiofile['originaldate'] = meta_tags['release_date'] + audiofile['media'] = meta_tags['type'] + audiofile['copyright'] = meta_tags['copyright'] + audiofile['author'] = meta_tags['artists'][0]['name'] + audiofile['lyricist'] = meta_tags['artists'][0]['name'] + audiofile['arranger'] = meta_tags['artists'][0]['name'] + audiofile['performer'] = meta_tags['artists'][0]['name'] + audiofile['encodedby'] = meta_tags['publisher'] + audiofile['isrc'] = meta_tags['external_ids']['isrc'] + audiofile['website'] = meta_tags['external_urls']['spotify'] + audiofile['length'] = str(meta_tags['duration_ms'] / 1000) + if meta_tags['genre']: + audiofile['genre'] = meta_tags['genre'] + audiofile.save(v2_version=3) + audiofile = ID3('Music/' + music_file + output_ext) + 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()) + albumart.close() + audiofile.save(v2_version=3) + +def embed_m4a(music_file, meta_tags, output_ext): + # Apple has specific tags - see mutagen docs - + # http://mutagen.readthedocs.io/en/latest/api/mp4.html + tags = {'album': '\xa9alb', + 'artist': '\xa9ART', + 'date': '\xa9day', + 'title': '\xa9nam', + 'originaldate': 'purd', + 'comment': '\xa9cmt', + 'group': '\xa9grp', + 'writer': '\xa9wrt', + 'genre': '\xa9gen', + 'tracknumber': 'trkn', + 'albumartist': 'aART', + 'disknumber': 'disk', + 'cpil': 'cpil', + 'albumart': 'covr', + 'copyright': 'cprt', + 'tempo': 'tmpo'} + + audiofile = MP4('Music/' + music_file + output_ext) + audiofile[tags['artist']] = meta_tags['artists'][0]['name'] + 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['disknumber']] = [(meta_tags['disc_number'], 0)] + audiofile[tags['date']] = meta_tags['release_date'] + audiofile[tags['originaldate']] = meta_tags['release_date'] + audiofile[tags['copyright']] = meta_tags['copyright'] + if meta_tags['genre']: + audiofile[tags['genre']] = meta_tags['genre'] + albumart = urllib2.urlopen(meta_tags['album']['images'][0]['url']) + audiofile[tags['albumart']] = [ MP4Cover(albumart.read(), imageformat=MP4Cover.FORMAT_JPEG) ] + albumart.close() + audiofile.save() diff --git a/core/misc.py b/core/misc.py new file mode 100644 index 0000000..ba41320 --- /dev/null +++ b/core/misc.py @@ -0,0 +1,113 @@ +import argparse +import sys +import os +from slugify import slugify +import spotipy.oauth2 as oauth2 + +try: + from urllib2 import quote +except: + 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(raw_input('>> Choose your number: ')) + if the_chosen_one >= 1 and the_chosen_one <= len(links): + return links[the_chosen_one - 1] + elif the_chosen_one == 0: + return None + else: + print('Choose a valid number!') + except ValueError: + print('Choose a valid number!') + +# 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:]) + +def get_arguments(): + 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)') + + 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): + return True + else: + return False + +# write tracks into list file +def feed_tracks(file, tracks): + with open(file, 'a') as fout: + for item in tracks['items']: + track = item['track'] + try: + fout.write(track['external_urls']['spotify'] + '\n') + except KeyError: + title = track['name'] + ' by '+ track['artists'][0]['name'] + print('Skipping track ' + title + ' (local only?)') + +# generate filename of the song to be downloaded +def generate_filename(title): + # IMO python2 sucks dealing with unicode + title = fix_encoding(title, decode=True) + 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( + client_id='4fe3fecfe5334023a1472516cc99d805', + client_secret='0f02b7c483c04257984695007a4a8d5c') + token = creds.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) + return URL + +# fix encoding issues in python2 +def fix_encoding(query, decode=False): + if sys.version_info < (3, 0): + query = query.encode('utf-8') + if decode: + query = query.decode('utf-8') + return query + +def grace_quit(): + print('') + print('') + print('Exitting..') + sys.exit() diff --git a/requirements.txt b/requirements.txt index 903eeb1..ef67238 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,8 @@ pathlib >= 1.0.1 -requests >= 2.17.3 BeautifulSoup4 >= 0.4.13 youtube_dl >= 2017.5.1 pafy >= 0.5.3.1 spotipy >= 2.4.4 -eyeD3 >= 0.8 mutagen >= 1.37 unicode-slugify >= 0.1.3 titlecase >= 0.10.0 diff --git a/spotdl.py b/spotdl.py index 6a9270f..b9eed58 100644 --- a/spotdl.py +++ b/spotdl.py @@ -1,189 +1,186 @@ #!/usr/bin/env python + # -*- coding: UTF-8 -*- -# Usual import stuff +from core import metadata +from core import misc from bs4 import BeautifulSoup -from shutil import copyfileobj -from sys import path, version_info -from slugify import slugify from titlecase import titlecase -from mutagen.mp4 import MP4, MP4Cover +from slugify import slugify import spotipy -import spotipy.oauth2 as oauth2 -import eyed3 -import requests import pafy +import sys import os -import argparse -import urllib +import subprocess +# urllib2 is urllib.request in python3 +try: + import urllib2 +except ImportError: + import urllib.request as urllib2 -def getInputLink(links): - #for i in range(len(links)): - # links[i] = str(i + 1) + '. ' + links[i] - while True: - try: - the_chosen_one = int(raw_input('>> Choose your number: ')) - if the_chosen_one >= 1 and the_chosen_one <= len(links): - return links[the_chosen_one - 1] - elif the_chosen_one == 0: - return None - else: - print('Choose a valid number!') - except ValueError: - print('Choose a valid number!') - -# Check if input song is Spotify URL or just a song name - - -def isSpotify(raw_song): - if (len(raw_song) == 22 and raw_song.replace(" ", "%20") == raw_song) or (raw_song.find('spotify') > -1): - return True - else: - return False - -# [Artist] - [Song Name] - - -def generateSongName(raw_song): - if isSpotify(raw_song): - tags = generateMetaTags(raw_song) +# decode spotify link to "[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'] - return raw_song + return misc.fix_encoding(raw_song) +# fetch song's metadata from spotify +def generate_metadata(raw_song): + if misc.is_spotify(raw_song): + # fetch track information directly if it is spotify link + meta_tags = spotify.track(raw_song) + else: + # otherwise search on spotify and fetch information from first result + meta_tags = spotify.search(raw_song, limit=1)['tracks']['items'][0] + + artist = spotify.artist(meta_tags['artists'][0]['id']) + album = spotify.album(meta_tags['album']['id']) -def generateMetaTags(raw_song): try: - if isSpotify(raw_song): - return spotify.track(raw_song) - else: - return spotify.search(raw_song, limit=1)['tracks']['items'][0] - except BaseException: - return None + meta_tags[u'genre'] = titlecase(artist['genres'][0]) + except IndexError: + meta_tags[u'genre'] = None + meta_tags[u'release_date'] = album['release_date'] + meta_tags[u'copyright'] = album['copyrights'][0]['text'] + 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'])) + return meta_tags -def generateSearchURL(song): - URL = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q=" + \ - urllib.quote(song) - return URL - - -def generateYouTubeURL(raw_song): - song = generateSongName(raw_song) - searchURL = generateSearchURL(song) - items = requests.get(searchURL).text - items_parse = BeautifulSoup(items, "html.parser") +def generate_YouTube_URL(raw_song): + # decode spotify http link to "[artist] - [song]" + song = generate_songname(raw_song) + # generate direct search YouTube URL + searchURL = misc.generate_search_URL(song) + item = urllib2.urlopen(searchURL).read() + items_parse = BeautifulSoup(item, "html.parser") check = 1 if args.manual: links = [] print(song) print('') print('0. Skip downloading this 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()) links.append(x.find('a')['href']) check += 1 print('') - result = getInputLink(links) + # let user select the song to download + result = misc.input_link(links) if result is None: return None else: - result = items_parse.find_all( - attrs={'class': 'yt-uix-tile-link'})[0]['href'] - 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'] + # get video link of the first YouTube result + 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'] check += 1 full_link = "youtube.com" + result return full_link - -def goPafy(raw_song): - trackURL = generateYouTubeURL(raw_song) +# parse track from YouTube +def go_pafy(raw_song): + # video link of the video to extract audio from + trackURL = generate_YouTube_URL(raw_song) if trackURL is None: return None else: + # parse the YouTube video return pafy.new(trackURL) - -def getYouTubeTitle(content, number): - title = content.title +# title of the YouTube video +def get_YouTube_title(content, number): + title = misc.fix_encoding(content.title) if number is None: return title else: return str(number) + '. ' + title - -def feedTracks(file, tracks): - with open(file, 'a') as fout: - for item in tracks['items']: - track = item['track'] - try: - fout.write(track['external_urls']['spotify'] + '\n') - except KeyError: - pass - - -def feedPlaylist(username): +# fetch user playlists when using -u option +def feed_playlist(username): + # fetch all user playlists playlists = spotify.user_playlists(username) links = [] check = 1 + # iterate over user playlists for playlist in playlists['items']: - print(str(check) + '. ' + fixEncoding(playlist['name']) + ' (' + str(playlist['tracks']['total']) + ' tracks)') + print(str(check) + '. ' + misc.fix_encoding(playlist['name']) + ' (' + str(playlist['tracks']['total']) + ' tracks)') links.append(playlist) check += 1 print('') - playlist = getInputLink(links) + # 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") print('') + # slugify removes any special characters file = slugify(playlist['name'], ok='-_()[]{}') + '.txt' print('Feeding ' + str(playlist['tracks']['total']) + ' tracks to ' + file) tracks = results['tracks'] - feedTracks(file, tracks) + # write tracks to file + misc.feed_tracks(file, tracks) + # check if there are more pages + # 1 page = 50 results while tracks['next']: tracks = spotify.next(tracks) - feedTracks(file, tracks) + misc.feed_tracks(file, tracks) -# Generate name for the song to be downloaded - - -def generateFileName(content): - title = (content.title).replace(' ', '_') - title = slugify(title, ok='-_()[]{}', lower=False) - return fixEncoding(title) - - -def downloadSong(content): - music_file = generateFileName(content) - if input_ext == '.webm': +def download_song(content): + music_file = misc.generate_filename(content.title) + if args.input_ext == '.webm': + # download best available audio in .webm link = content.getbestaudio(preftype='webm') if link is not None: - link.download(filepath='Music/' + music_file + input_ext) + link.download(filepath='Music/' + music_file + args.input_ext) else: - link = content.getbestaudio(preftype="m4a") + # download best available audio in .webm + link = content.getbestaudio(preftype='m4a') if link is not None: - link.download(filepath='Music/' + music_file + input_ext) + link.download(filepath='Music/' + music_file + args.input_ext) +# convert song from input_ext to output_ext +def convert_song(music_file): + # skip conversion if input_ext == output_ext + if not args.input_ext == args.output_ext: + print('Converting ' + music_file + args.input_ext + ' to ' + args.output_ext[1:]) + if args.avconv: + convert_with_avconv(music_file) + else: + convert_with_FFmpeg(music_file) + os.remove('Music/' + music_file + args.input_ext) -def convertWithAvconv(music_file): +def convert_with_avconv(music_file): + # different path for windows if os.name == 'nt': avconv_path = 'Scripts\\avconv.exe' else: avconv_path = 'avconv' - os.system( - avconv_path + ' -loglevel 0 -i "' + - 'Music/' + - music_file + - '.m4a" -ab 192k "' + - 'Music/' + - music_file + - '.mp3"') - os.remove('Music/' + music_file + '.m4a') + + if args.verbose: + level = 'debug' + else: + level = '0' + + command = [avconv_path, + '-loglevel', level, + '-i', 'Music/' + music_file + args.input_ext, + '-ab', '192k', + 'Music/' + music_file + args.output_ext] + + subprocess.call(command) -def convertWithFfmpeg(music_file): +def convert_with_FFmpeg(music_file): # What are the differences and similarities between ffmpeg, libav, and avconv? # https://stackoverflow.com/questions/9477115 # ffmeg encoders high to lower quality @@ -191,300 +188,145 @@ def convertWithFfmpeg(music_file): # 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 args.verbose: - ffmpeg_pre = 'ffmpeg -y ' - else: - ffmpeg_pre = 'ffmpeg -hide_banner -nostats -v panic -y ' - if input_ext == '.m4a': - if output_ext == '.mp3': + if os.name == "nt": + ffmpeg_pre = 'Scripts\\ffmpeg.exe ' + else: + ffmpeg_pre = 'ffmpeg ' + + ffmpeg_pre += '-y ' + if not args.verbose: + ffmpeg_pre += '-hide_banner -nostats -v panic ' + + if args.input_ext == '.m4a': + if args.output_ext == '.mp3': ffmpeg_params = '-codec:v copy -codec:a libmp3lame -q:a 2 ' elif output_ext == '.webm': ffmpeg_params = '-c:a libopus -vbr on -b:a 192k -vn ' - else: - return - elif input_ext == '.webm': - if output_ext == '.mp3': - ffmpeg_params = '-ab 192k -ar 44100 -vn ' - elif output_ext == '.m4a': - ffmpeg_params = '-cutoff 20000 -c:a libfdk_aac -b:a 256k -vn ' - else: - return - else: - print('Unknown formats. Unable to convert.', input_ext, output_ext) - return + elif args.input_ext == '.webm': + if args.output_ext == '.mp3': + ffmpeg_params = ' -ab 192k -ar 44100 -vn ' + elif args.output_ext == '.m4a': + ffmpeg_params = '-cutoff 20000 -c:a libfdk_aac -b:a 192k -vn ' - if args.verbose: - print(ffmpeg_pre + - '-i "Music/' + music_file + input_ext + '" ' + - ffmpeg_params + - '"Music/' + music_file + output_ext + '" ') + command = (ffmpeg_pre + + '-i Music/' + music_file + args.input_ext + ' ' + + ffmpeg_params + + 'Music/' + music_file + args.output_ext + '').split(' ') - os.system( - ffmpeg_pre + - '-i "Music/' + music_file + input_ext + '" ' + - ffmpeg_params + - '"Music/' + music_file + output_ext + '" ') - os.remove('Music/' + music_file + input_ext) + subprocess.call(command) - -def checkExists(music_file, raw_song, islist): - if os.path.exists("Music/" + music_file + input_ext + ".temp"): - os.remove("Music/" + music_file + input_ext + ".temp") - if args.no_convert: - extension = input_ext - else: - extension = output_ext - if os.path.isfile("Music/" + music_file + extension): - if extension == '.mp3': - audiofile = eyed3.load("Music/" + music_file + extension) - if isSpotify(raw_song) and not audiofile.tag.title == ( - generateMetaTags(raw_song))['name']: - os.remove("Music/" + music_file + extension) +# check if input song already exists in Music folder +def check_exists(music_file, raw_song, islist): + files = os.listdir("Music") + for file in files: + if file.endswith(".temp"): + os.remove("Music/" + file) + continue + # check if any file with similar name is already present in Music/ + if file.startswith(misc.generate_filename(music_file)): + # 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) return False - if islist: - return True - else: - prompt = raw_input( - 'Song with same name has already been downloaded. Re-download? (y/n): ').lower() - if prompt == "y": - os.remove("Music/" + music_file + extension) - return False - else: + # 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 = raw_input('Song with same name has already been downloaded. Re-download? (y/n): ').lower() + if prompt == "y": + os.remove("Music/" + file) + return False + else: + return True -# Remove song from file once downloaded - - -def trimSong(file): - with open(file, 'r') as fin: - data = fin.read().splitlines(True) - with open(file, 'w') as fout: - fout.writelines(data[1:]) - - -def fixSongMP3(music_file, meta_tags): - audiofile = eyed3.load("Music/" + music_file + '.mp3') - audiofile.tag.artist = meta_tags['artists'][0]['name'] - audiofile.tag.album_artist = meta_tags['artists'][0]['name'] - audiofile.tag.album = meta_tags['album']['name'] - audiofile.tag.title = meta_tags['name'] - artist = spotify.artist(meta_tags['artists'][0]['id']) - try: - audiofile.tag.genre = titlecase(artist['genres'][0]) - except IndexError: - pass - audiofile.tag.track_num = meta_tags['track_number'] - audiofile.tag.disc_num = meta_tags['disc_number'] - audiofile.tag.release_date = spotify.album( - meta_tags['album']['id'])['release_date'] - albumart = ( - requests.get( - meta_tags['album']['images'][0]['url'], - stream=True)).raw - with open('last_albumart.jpg', 'wb') as out_file: - copyfileobj(albumart, out_file) - albumart = open("last_albumart.jpg", "rb").read() - audiofile.tag.images.set(3, albumart, "image/jpeg") - audiofile.tag.save(version=(2, 3, 0)) - - -def fixSongM4A(music_file, meta_tags): - # eyed serves only mp3 not aac so using mutagen - # Apple has specific tags - see mutagen docs - - # http://mutagen.readthedocs.io/en/latest/api/mp4.html - tags = {'album': '\xa9alb', - 'artist': '\xa9ART', - 'year': '\xa9day', - 'title': '\xa9nam', - 'comment': '\xa9cmt', - 'group': '\xa9grp', - 'writer': '\xa9wrt', - 'genre': '\xa9gen', - 'track': 'trkn', - 'aart': 'aART', - 'disk': 'disk', - 'cpil': 'cpil', - 'tempo': 'tmpo'} - audiofile = MP4('Music/' + music_file + output_ext) - audiofile[tags['artist']] = meta_tags['artists'][0]['name'] - audiofile[tags['album']] = meta_tags['album']['name'] - audiofile[tags['title']] = meta_tags['name'] - artist = spotify.artist(meta_tags['artists'][0]['id']) - try: - audiofile[tags['genre']] = titlecase(artist['genres'][0]) - except IndexError: - pass - album = spotify.album(meta_tags['album']['id']) - audiofile[tags['year']] = album['release_date'] - audiofile[tags['track']] = [(meta_tags['track_number'], 0)] - audiofile[tags['disk']] = [(meta_tags['disc_number'], 0)] - albumart = ( - requests.get(meta_tags['album']['images'][0]['url'], stream=True)).raw - with open('last_albumart.jpg', 'wb') as out_file: - copyfileobj(albumart, out_file) - with open("last_albumart.jpg", "rb") as f: - audiofile["covr"] = [ - MP4Cover( - f.read(), - imageformat=MP4Cover.FORMAT_JPEG)] - audiofile.save() - - -def convertSong(music_file): - print('Converting ' + music_file + input_ext + ' to ' + output_ext[1:]) - if args.ffmpeg: - convertWithFfmpeg(music_file) - else: - convertWithAvconv(music_file) - - -def fixSong(music_file, meta_tags): - if meta_tags is None: - print('Could not find meta-tags') - elif output_ext == '.m4a': - print('Fixing meta-tags') - fixSongM4A(music_file, meta_tags) - elif output_ext == '.mp3': - print('Fixing meta-tags') - fixSongMP3(music_file, meta_tags) - else: - print('Cannot embed meta-tags into given output extension') - -# Logic behind preparing the song to download to finishing meta-tags - - -def grabSingle(raw_song, number=None): - if number: - islist = True - else: - islist = False - content = goPafy(raw_song) - if content is None: - return - print(getYouTubeTitle(content, number)) - music_file = generateFileName(content) - if not checkExists(music_file, raw_song, islist=islist): - downloadSong(content) - print('') - if not args.no_convert: - convertSong(music_file) - meta_tags = generateMetaTags(raw_song) - fixSong(music_file, meta_tags) - -# Fix python2 encoding issues - - -def fixEncoding(query): - if version_info > (3, 0): - return query - else: - return query.encode('utf-8') - - -def grabList(file): - lines = open(file, 'r').read() - lines = lines.splitlines() - # Ignore blank lines in file (if any) +# download songs from list +def grab_list(file): + with open(file, 'r') as listed: + lines = (listed.read()).splitlines() + # ignore blank lines in file (if any) try: lines.remove('') except ValueError: pass print('Total songs in list = ' + str(len(lines)) + ' songs') print('') - # Count the number of song being downloaded + # nth input song number = 1 for raw_song in lines: try: - grabSingle(raw_song, number=number) - trimSong(file) - number += 1 - print('') - except KeyboardInterrupt: - graceQuit() - except requests.exceptions.ConnectionError: + grab_single(raw_song, number=number) + # token expires after 1 hour + except spotipy.oauth2.SpotifyOauthError: + # refresh token when it expires + token = misc.generate_token() + global spotify + spotify = spotipy.Spotify(auth=token) + grab_single(raw_song, number=number) + # detect network problems + except (urllib2.URLError, TypeError, IOError): lines.append(raw_song) - trimSong(file) + # remove the downloaded song from .txt + misc.trim_song(file) + # and append it to the last line in .txt with open(file, 'a') as myfile: myfile.write(raw_song) print('Failed to download song. Will retry after other songs.') + continue + except KeyboardInterrupt: + misc.grace_quit() + finally: + print('') + misc.trim_song(file) + number += 1 - -def getArgs(argv=None): - 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('-n', '--no-convert', default=False, - help='skip the conversion process and meta-tags', action='store_true') - parser.add_argument('-m', '--manual', default=False, - help='choose the song to download manually', action='store_true') - parser.add_argument('-f', '--ffmpeg', default=False, - help='Use ffmpeg instead of libav for conversion. If not set defaults to libav', - 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(argv) - - -def graceQuit(): - print('') - print('') - print('Exitting..') - exit() - +# logic behind downloading some song +def grab_single(raw_song, number=None): + # check if song is being downloaded from list + if number: + islist = True + else: + islist = False + content = go_pafy(raw_song) + if content is None: + return + # 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) + if not check_exists(music_file, raw_song, islist=islist): + download_song(content) + print('') + convert_song(music_file) + meta_tags = generate_metadata(raw_song) + if not args.no_metadata: + metadata.embed(music_file, meta_tags, args.output_ext) if __name__ == '__main__': - # Python 3 compatibility - if version_info > (3, 0): + # python 3 compatibility + if sys.version_info > (3, 0): raw_input = input - os.chdir(path[0]) + os.chdir(sys.path[0]) if not os.path.exists("Music"): os.makedirs("Music") - for temp in os.listdir('Music/'): - if temp.endswith('.m4a.temp'): - os.remove('Music/' + temp) - - # Please respect this user token :) - oauth2 = oauth2.SpotifyClientCredentials( - client_id='4fe3fecfe5334023a1472516cc99d805', - client_secret='0f02b7c483c04257984695007a4a8d5c') - token = oauth2.get_access_token() + # 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() spotify = spotipy.Spotify(auth=token) - # Set up arguments - args = getArgs() - - if not args.verbose: - eyed3.log.setLevel("ERROR") - - if args.ffmpeg: - input_ext = args.input_ext - output_ext = args.output_ext - else: - input_ext = '.m4a' - output_ext = '.mp3' + # set up arguments + args = misc.get_arguments() if args.song: - grabSingle(raw_song=args.song) + grab_single(raw_song=args.song) elif args.list: - grabList(file=args.list) + grab_list(file=args.list) elif args.username: - feedPlaylist(username=args.username) + feed_playlist(username=args.username)