Merge pull request #72 from ritiek/develop

Change Notes
This commit is contained in:
Ritiek Malhotra
2017-06-21 01:23:00 +05:30
committed by GitHub
7 changed files with 449 additions and 390 deletions

7
.gitignore vendored
View File

@@ -1,3 +1,4 @@
Music/ *.pyc
last_albumart.jpg __pycache__/
list.txt /Music/
/*.txt

View File

@@ -15,6 +15,7 @@
- Track number - Track number
- Disc number - Disc number
- Release date - Release date
- and some more..
- Works straight out of the box and does not require to generate or mess with your API keys. - 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 ## 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 us 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 you and me some time :D.
- Provide as much information possible when opening your ticket. - Provide as much information possible when opening your ticket.
@@ -48,25 +47,26 @@ git clone https://github.com/Ritiek/Spotify-Downloader
cd Spotify-Downloader cd Spotify-Downloader
pip install -U -r requirements.txt 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 ### Windows
Assuming you have Python already installed.. 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` `"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. - 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 ## Instructions for Downloading Songs
@@ -88,8 +88,8 @@ optional arguments:
-n, --no-convert skip the conversion process and meta-tags (default: -n, --no-convert skip the conversion process and meta-tags (default:
False) False)
-m, --manual choose the song to download manually (default: False) -m, --manual choose the song to download manually (default: False)
-f, --ffmpeg Use ffmpeg instead of libav for conversion. If not set -a, --avconv Use avconv for conversion. If not set
defaults to libav (default: False) defaults to FFmpeg (default: False)
-v, --verbose show debug output (default: False) -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: prefered input format .m4a or .webm (Opus) (default:

1
core/__init__.py Normal file
View File

@@ -0,0 +1 @@

104
core/metadata.py Normal file
View File

@@ -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()

113
core/misc.py Normal file
View File

@@ -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 <playlist_name>.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()

View File

@@ -1,10 +1,8 @@
pathlib >= 1.0.1 pathlib >= 1.0.1
requests >= 2.17.3
BeautifulSoup4 >= 0.4.13 BeautifulSoup4 >= 0.4.13
youtube_dl >= 2017.5.1 youtube_dl >= 2017.5.1
pafy >= 0.5.3.1 pafy >= 0.5.3.1
spotipy >= 2.4.4 spotipy >= 2.4.4
eyeD3 >= 0.8
mutagen >= 1.37 mutagen >= 1.37
unicode-slugify >= 0.1.3 unicode-slugify >= 0.1.3
titlecase >= 0.10.0 titlecase >= 0.10.0

570
spotdl.py
View File

@@ -1,189 +1,186 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
# Usual import stuff from core import metadata
from core import misc
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from shutil import copyfileobj
from sys import path, version_info
from slugify import slugify
from titlecase import titlecase from titlecase import titlecase
from mutagen.mp4 import MP4, MP4Cover from slugify import slugify
import spotipy import spotipy
import spotipy.oauth2 as oauth2
import eyed3
import requests
import pafy import pafy
import sys
import os import os
import argparse import subprocess
import urllib
# urllib2 is urllib.request in python3
try:
import urllib2
except ImportError:
import urllib.request as urllib2
def getInputLink(links): # decode spotify link to "[artist] - [song]"
#for i in range(len(links)): def generate_songname(raw_song):
# links[i] = str(i + 1) + '. ' + links[i] if misc.is_spotify(raw_song):
while True: tags = generate_metadata(raw_song)
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)
raw_song = tags['artists'][0]['name'] + ' - ' + tags['name'] raw_song = tags['artists'][0]['name'] + ' - ' + tags['name']
return raw_song return misc.fix_encoding(raw_song)
# fetch song's metadata from spotify
def generateMetaTags(raw_song): def generate_metadata(raw_song):
try: if misc.is_spotify(raw_song):
if isSpotify(raw_song): # fetch track information directly if it is spotify link
return spotify.track(raw_song) meta_tags = spotify.track(raw_song)
else: else:
return spotify.search(raw_song, limit=1)['tracks']['items'][0] # otherwise search on spotify and fetch information from first result
except BaseException: meta_tags = spotify.search(raw_song, limit=1)['tracks']['items'][0]
return None
artist = spotify.artist(meta_tags['artists'][0]['id'])
album = spotify.album(meta_tags['album']['id'])
def generateSearchURL(song): try:
URL = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q=" + \ meta_tags[u'genre'] = titlecase(artist['genres'][0])
urllib.quote(song) except IndexError:
return URL 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 generateYouTubeURL(raw_song): def generate_YouTube_URL(raw_song):
song = generateSongName(raw_song) # decode spotify http link to "[artist] - [song]"
searchURL = generateSearchURL(song) song = generate_songname(raw_song)
items = requests.get(searchURL).text # generate direct search YouTube URL
items_parse = BeautifulSoup(items, "html.parser") searchURL = misc.generate_search_URL(song)
item = urllib2.urlopen(searchURL).read()
items_parse = BeautifulSoup(item, "html.parser")
check = 1 check = 1
if args.manual: if args.manual:
links = [] links = []
print(song) print(song)
print('') print('')
print('0. Skip downloading this song') 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'}): 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: if not x.find('channel') == -1 or not x.find('googleads') == -1:
print(str(check) + '. ' + x.get_text()) print(str(check) + '. ' + x.get_text())
links.append(x.find('a')['href']) links.append(x.find('a')['href'])
check += 1 check += 1
print('') print('')
result = getInputLink(links) # let user select the song to download
result = misc.input_link(links)
if result is None: if result is None:
return None return None
else: else:
result = items_parse.find_all( # get video link of the first YouTube result
attrs={'class': 'yt-uix-tile-link'})[0]['href'] result = items_parse.find_all(attrs={'class': 'yt-uix-tile-link'})[0]['href']
while not result.find('channel') == - \ # confirm the video result is not an advertisement
1 or not result.find('googleads') == -1: # otherwise keep iterating until it is not
result = items_parse.find_all( while not result.find('channel') == -1 or not result.find('googleads') == -1:
attrs={'class': 'yt-uix-tile-link'})[check]['href'] result = items_parse.find_all(attrs={'class': 'yt-uix-tile-link'})[check]['href']
check += 1 check += 1
full_link = "youtube.com" + result full_link = "youtube.com" + result
return full_link return full_link
# parse track from YouTube
def goPafy(raw_song): def go_pafy(raw_song):
trackURL = generateYouTubeURL(raw_song) # video link of the video to extract audio from
trackURL = generate_YouTube_URL(raw_song)
if trackURL is None: if trackURL is None:
return None return None
else: else:
# parse the YouTube video
return pafy.new(trackURL) return pafy.new(trackURL)
# title of the YouTube video
def getYouTubeTitle(content, number): def get_YouTube_title(content, number):
title = content.title title = misc.fix_encoding(content.title)
if number is None: if number is None:
return title return title
else: else:
return str(number) + '. ' + title return str(number) + '. ' + title
# fetch user playlists when using -u option
def feedTracks(file, tracks): def feed_playlist(username):
with open(file, 'a') as fout: # fetch all user playlists
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) playlists = spotify.user_playlists(username)
links = [] links = []
check = 1 check = 1
# iterate over user playlists
for playlist in playlists['items']: 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) links.append(playlist)
check += 1 check += 1
print('') 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") results = spotify.user_playlist(playlist['owner']['id'], playlist['id'], fields="tracks,next")
print('') print('')
# slugify removes any special characters
file = slugify(playlist['name'], ok='-_()[]{}') + '.txt' file = slugify(playlist['name'], ok='-_()[]{}') + '.txt'
print('Feeding ' + str(playlist['tracks']['total']) + ' tracks to ' + file) print('Feeding ' + str(playlist['tracks']['total']) + ' tracks to ' + file)
tracks = results['tracks'] 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']: while tracks['next']:
tracks = spotify.next(tracks) tracks = spotify.next(tracks)
feedTracks(file, tracks) misc.feed_tracks(file, tracks)
# Generate name for the song to be downloaded def download_song(content):
music_file = misc.generate_filename(content.title)
if args.input_ext == '.webm':
def generateFileName(content): # download best available audio in .webm
title = (content.title).replace(' ', '_')
title = slugify(title, ok='-_()[]{}', lower=False)
return fixEncoding(title)
def downloadSong(content):
music_file = generateFileName(content)
if input_ext == '.webm':
link = content.getbestaudio(preftype='webm') link = content.getbestaudio(preftype='webm')
if link is not None: if link is not None:
link.download(filepath='Music/' + music_file + input_ext) link.download(filepath='Music/' + music_file + args.input_ext)
else: else:
link = content.getbestaudio(preftype="m4a") # download best available audio in .webm
link = content.getbestaudio(preftype='m4a')
if link is not None: 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': if os.name == 'nt':
avconv_path = 'Scripts\\avconv.exe' avconv_path = 'Scripts\\avconv.exe'
else: else:
avconv_path = 'avconv' avconv_path = 'avconv'
os.system(
avconv_path + ' -loglevel 0 -i "' + if args.verbose:
'Music/' + level = 'debug'
music_file + else:
'.m4a" -ab 192k "' + level = '0'
'Music/' +
music_file + command = [avconv_path,
'.mp3"') '-loglevel', level,
os.remove('Music/' + music_file + '.m4a') '-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? # What are the differences and similarities between ffmpeg, libav, and avconv?
# https://stackoverflow.com/questions/9477115 # https://stackoverflow.com/questions/9477115
# ffmeg encoders high to lower quality # 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 # 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? # on MacOS brew install ffmpeg --with-fdk-aac will do just that. Other OS?
# https://trac.ffmpeg.org/wiki/Encode/AAC # 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 os.name == "nt":
if output_ext == '.mp3': 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 ' ffmpeg_params = '-codec:v copy -codec:a libmp3lame -q:a 2 '
elif output_ext == '.webm': elif output_ext == '.webm':
ffmpeg_params = '-c:a libopus -vbr on -b:a 192k -vn ' ffmpeg_params = '-c:a libopus -vbr on -b:a 192k -vn '
else: elif args.input_ext == '.webm':
return if args.output_ext == '.mp3':
elif input_ext == '.webm': ffmpeg_params = ' -ab 192k -ar 44100 -vn '
if output_ext == '.mp3': elif args.output_ext == '.m4a':
ffmpeg_params = '-ab 192k -ar 44100 -vn ' ffmpeg_params = '-cutoff 20000 -c:a libfdk_aac -b:a 192k -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 args.verbose: command = (ffmpeg_pre +
print(ffmpeg_pre + '-i Music/' + music_file + args.input_ext + ' ' +
'-i "Music/' + music_file + input_ext + '" ' +
ffmpeg_params + ffmpeg_params +
'"Music/' + music_file + output_ext + '" ') 'Music/' + music_file + args.output_ext + '').split(' ')
os.system( subprocess.call(command)
ffmpeg_pre +
'-i "Music/' + music_file + input_ext + '" ' +
ffmpeg_params +
'"Music/' + music_file + output_ext + '" ')
os.remove('Music/' + music_file + input_ext)
# check if input song already exists in Music folder
def checkExists(music_file, raw_song, islist): def check_exists(music_file, raw_song, islist):
if os.path.exists("Music/" + music_file + input_ext + ".temp"): files = os.listdir("Music")
os.remove("Music/" + music_file + input_ext + ".temp") for file in files:
if args.no_convert: if file.endswith(".temp"):
extension = input_ext os.remove("Music/" + file)
else: continue
extension = output_ext # check if any file with similar name is already present in Music/
if os.path.isfile("Music/" + music_file + extension): if file.startswith(misc.generate_filename(music_file)):
if extension == '.mp3': # check if the already downloaded song has correct metadata
audiofile = eyed3.load("Music/" + music_file + extension) already_tagged = metadata.compare(file, generate_metadata(raw_song))
if isSpotify(raw_song) and not audiofile.tag.title == ( # if not, remove it and download again without prompt
generateMetaTags(raw_song))['name']: if misc.is_spotify(raw_song) and not already_tagged:
os.remove("Music/" + music_file + extension) os.remove("Music/" + file)
return False return False
# do not prompt and skip the current song if already downloaded when using list
if islist: if islist:
return True return True
# if downloading only single song, prompt to re-download
else: else:
prompt = raw_input( prompt = raw_input('Song with same name has already been downloaded. Re-download? (y/n): ').lower()
'Song with same name has already been downloaded. Re-download? (y/n): ').lower()
if prompt == "y": if prompt == "y":
os.remove("Music/" + music_file + extension) os.remove("Music/" + file)
return False return False
else: else:
return True return True
# Remove song from file once downloaded # download songs from list
def grab_list(file):
with open(file, 'r') as listed:
def trimSong(file): lines = (listed.read()).splitlines()
with open(file, 'r') as fin: # ignore blank lines in file (if any)
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)
try: try:
lines.remove('') lines.remove('')
except ValueError: except ValueError:
pass pass
print('Total songs in list = ' + str(len(lines)) + ' songs') print('Total songs in list = ' + str(len(lines)) + ' songs')
print('') print('')
# Count the number of song being downloaded # nth input song
number = 1 number = 1
for raw_song in lines: for raw_song in lines:
try: try:
grabSingle(raw_song, number=number) grab_single(raw_song, number=number)
trimSong(file) # token expires after 1 hour
number += 1 except spotipy.oauth2.SpotifyOauthError:
print('') # refresh token when it expires
except KeyboardInterrupt: token = misc.generate_token()
graceQuit() global spotify
except requests.exceptions.ConnectionError: spotify = spotipy.Spotify(auth=token)
grab_single(raw_song, number=number)
# detect network problems
except (urllib2.URLError, TypeError, IOError):
lines.append(raw_song) 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: with open(file, 'a') as myfile:
myfile.write(raw_song) myfile.write(raw_song)
print('Failed to download song. Will retry after other songs.') print('Failed to download song. Will retry after other songs.')
continue
except KeyboardInterrupt:
def getArgs(argv=None): misc.grace_quit()
parser = argparse.ArgumentParser(description='Download and convert songs \ finally:
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 <playlist_name>.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('') misc.trim_song(file)
print('Exitting..') number += 1
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__': if __name__ == '__main__':
# Python 3 compatibility # python 3 compatibility
if version_info > (3, 0): if sys.version_info > (3, 0):
raw_input = input raw_input = input
os.chdir(path[0]) os.chdir(sys.path[0])
if not os.path.exists("Music"): if not os.path.exists("Music"):
os.makedirs("Music") os.makedirs("Music")
for temp in os.listdir('Music/'): # token is mandatory when using Spotify's API
if temp.endswith('.m4a.temp'): # https://developer.spotify.com/news-stories/2017/01/27/removing-unauthenticated-calls-to-the-web-api/
os.remove('Music/' + temp) token = misc.generate_token()
# Please respect this user token :)
oauth2 = oauth2.SpotifyClientCredentials(
client_id='4fe3fecfe5334023a1472516cc99d805',
client_secret='0f02b7c483c04257984695007a4a8d5c')
token = oauth2.get_access_token()
spotify = spotipy.Spotify(auth=token) spotify = spotipy.Spotify(auth=token)
# Set up arguments # set up arguments
args = getArgs() args = misc.get_arguments()
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'
if args.song: if args.song:
grabSingle(raw_song=args.song) grab_single(raw_song=args.song)
elif args.list: elif args.list:
grabList(file=args.list) grab_list(file=args.list)
elif args.username: elif args.username:
feedPlaylist(username=args.username) feed_playlist(username=args.username)