Files
spotify-downloader/spotdl.py
2017-06-15 03:58:53 +05:30

386 lines
13 KiB
Python

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
from core.misc import getInputLink
from core.misc import trimSong
from core.misc import getArgs
from core.misc import isSpotify
from core.misc import generateSearchURL
from core.misc import fixEncoding
from core.misc import graceQuit
from bs4 import BeautifulSoup
from shutil import copyfileobj
import sys
from slugify import slugify
from titlecase import titlecase
from mutagen.id3 import ID3, APIC
from mutagen.easyid3 import EasyID3
from mutagen.mp4 import MP4, MP4Cover
import spotipy
import spotipy.oauth2 as oauth2
import urllib2
import pafy
import os
def generateSongName(raw_song):
if isSpotify(raw_song):
tags = generateMetaTags(raw_song)
raw_song = tags['artists'][0]['name'] + ' - ' + tags['name']
return raw_song
def generateMetaTags(raw_song):
try:
if isSpotify(raw_song):
meta_tags = spotify.track(raw_song)
else:
meta_tags = spotify.search(raw_song, limit=1)['tracks']['items'][0]
artist_id = spotify.artist(meta_tags['artists'][0]['id'])
try:
meta_tags['genre'] = titlecase(artist_id['genres'][0])
except IndexError:
meta_tags['genre'] = None
meta_tags['release_date'] = spotify.album(meta_tags['album']['id'])['release_date']
return meta_tags
except BaseException:
return None
def generateYouTubeURL(raw_song):
song = generateSongName(raw_song)
searchURL = generateSearchURL(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')
for x in items_parse.find_all('h3', {'class': 'yt-lockup-title'}):
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)
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']
check += 1
full_link = "youtube.com" + result
return full_link
def goPafy(raw_song):
trackURL = generateYouTubeURL(raw_song)
if trackURL is None:
return None
else:
return pafy.new(trackURL)
def getYouTubeTitle(content, number):
title = 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):
playlists = spotify.user_playlists(username)
links = []
check = 1
for playlist in playlists['items']:
print(str(check) + '. ' + fixEncoding(playlist['name']) + ' (' + str(playlist['tracks']['total']) + ' tracks)')
links.append(playlist)
check += 1
print('')
playlist = getInputLink(links)
results = spotify.user_playlist(playlist['owner']['id'], playlist['id'], fields="tracks,next")
print('')
file = slugify(playlist['name'], ok='-_()[]{}') + '.txt'
print('Feeding ' + str(playlist['tracks']['total']) + ' tracks to ' + file)
tracks = results['tracks']
feedTracks(file, tracks)
while tracks['next']:
tracks = spotify.next(tracks)
feedTracks(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, input_ext):
music_file = generateFileName(content)
if input_ext == '.webm':
link = content.getbestaudio(preftype='webm')
if link is not None:
link.download(filepath='Music/' + music_file + input_ext)
else:
link = content.getbestaudio(preftype='m4a')
if link is not None:
link.download(filepath='Music/' + music_file + input_ext)
def convertWithAvconv(music_file, input_ext, output_ext, verbose):
if os.name == 'nt':
avconv_path = 'Scripts\\avconv.exe'
else:
avconv_path = 'avconv'
os.system(
avconv_path + ' -loglevel 0 -i "' +
'Music/' +
music_file +
input_ext + '" -ab 192k "' +
'Music/' +
music_file +
output_ext + '"')
os.remove('Music/' + music_file + input_ext)
def convertWithFfmpeg(music_file, input_ext, output_ext, 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
#
print(music_file, input_ext, output_ext, verbose)
if verbose:
ffmpeg_pre = 'ffmpeg -y '
else:
ffmpeg_pre = 'ffmpeg -hide_banner -nostats -v panic -y '
if input_ext == '.m4a':
if 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
if verbose:
print(ffmpeg_pre +
'-i "Music/' + music_file + input_ext + '" ' +
ffmpeg_params +
'"Music/' + music_file + output_ext + '" ')
os.system(
ffmpeg_pre +
'-i "Music/' + music_file + input_ext + '" ' +
ffmpeg_params +
'"Music/' + music_file + output_ext + '" ')
os.remove('Music/' + music_file + input_ext)
def checkExists(music_file, raw_song, islist):
files = os.listdir("Music")
for file in files:
if file.endswith(".temp"):
os.remove("Music/" + file)
continue
if file.startswith(music_file):
# FIXME
#audiofile = mutagen.load("Music/" + music_file + output_ext)
#if isSpotify(raw_song) and not audiofile.tag.title == (
# generateMetaTags(raw_song))['name']:
# os.remove("Music/" + music_file + output_ext)
# return False
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/" + file)
return False
else:
return True
# Remove song from file once downloaded
def fixSong(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')
fixSongM4A(music_file, meta_tags, output_ext)
elif output_ext == '.mp3':
print('Fixing meta-tags')
fixSongMP3(music_file, meta_tags, output_ext)
else:
print('Cannot embed meta-tags into given output extension')
def fixSongMP3(music_file, meta_tags, output_ext):
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']
if meta_tags['genre']:
audiofile['genre'] = meta_tags['genre']
audiofile['tracknumber'] = [meta_tags['track_number'], 0]
audiofile['discnumber'] = [meta_tags['disc_number'], 0]
audiofile['date'] = meta_tags['release_date']
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 fixSongM4A(music_file, meta_tags, output_ext):
# 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']
if meta_tags['genre']:
audiofile[tags['genre']] = meta_tags['genre']
audiofile[tags['year']] = meta_tags['release_date']
audiofile[tags['track']] = [(meta_tags['track_number'], 0)]
audiofile[tags['disk']] = [(meta_tags['disc_number'], 0)]
albumart = urllib2.urlopen(meta_tags['album']['images'][0]['url'])
audiofile["covr"] = [ MP4Cover(albumart.read(), imageformat=MP4Cover.FORMAT_JPEG) ]
albumart.close()
audiofile.save()
def convertSong(music_file, input_ext, output_ext, ffmpeg, verbose):
print(music_file, input_ext, output_ext, ffmpeg, verbose)
if not input_ext == output_ext:
print('Converting ' + music_file + input_ext + ' to ' + output_ext[1:])
if ffmpeg:
convertWithFfmpeg(music_file, input_ext, output_ext, verbose)
else:
convertWithAvconv(music_file, input_ext, output_ext, verbose)
else:
print('Skipping conversion since input_ext = output_ext')
# 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, args.input_ext)
print('')
if not args.no_convert:
convertSong(music_file, args.input_ext, args.output_ext, args.ffmpeg, args.verbose)
meta_tags = generateMetaTags(raw_song)
fixSong(music_file, meta_tags, args.output_ext)
# Fix python2 encoding issues
def grabList(file):
lines = open(file, 'r').read()
lines = lines.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
number = 1
for raw_song in lines:
try:
grabSingle(raw_song, number=number)
trimSong(file)
number += 1
print('')
except KeyboardInterrupt:
graceQuit()
except ConnectionError:
lines.append(raw_song)
trimSong(file)
with open(file, 'a') as myfile:
myfile.write(raw_song)
print('Failed to download song. Will retry after other songs.')
if __name__ == '__main__':
# Python 3 compatibility
if sys.version_info > (3, 0):
raw_input = input
os.chdir(sys.path[0])
if not os.path.exists("Music"):
os.makedirs("Music")
# Please respect this user token :)
oauth2 = oauth2.SpotifyClientCredentials(
client_id='4fe3fecfe5334023a1472516cc99d805',
client_secret='0f02b7c483c04257984695007a4a8d5c')
token = oauth2.get_access_token()
spotify = spotipy.Spotify(auth=token)
# Set up arguments
args = getArgs()
print(args)
#if args.ffmpeg:
# input_ext = args.input_ext
# output_ext = args.output_ext
#else:
# input_ext = '.m4a'
# output_ext = '.mp3'
if args.song:
grabSingle(raw_song=args.song)
elif args.list:
grabList(file=args.list)
elif args.username:
feedPlaylist(username=args.username)