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/
last_albumart.jpg
list.txt
*.pyc
__pycache__/
/Music/
/*.txt

View File

@@ -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:

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
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

566
spotdl.py
View File

@@ -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
def getInputLink(links):
#for i in range(len(links)):
# links[i] = str(i + 1) + '. ' + links[i]
while True:
# urllib2 is urllib.request in python3
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!')
import urllib2
except ImportError:
import urllib.request as urllib2
# 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)
def generateMetaTags(raw_song):
try:
if isSpotify(raw_song):
return spotify.track(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:
return spotify.search(raw_song, limit=1)['tracks']['items'][0]
except BaseException:
return None
# 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 generateSearchURL(song):
URL = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q=" + \
urllib.quote(song)
return URL
try:
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 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':
elif args.input_ext == '.webm':
if args.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.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 + '" ' +
command = (ffmpeg_pre +
'-i Music/' + music_file + args.input_ext + ' ' +
ffmpeg_params +
'"Music/' + music_file + output_ext + '" ')
'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
# 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()
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)
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.')
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 <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():
continue
except KeyboardInterrupt:
misc.grace_quit()
finally:
print('')
print('')
print('Exitting..')
exit()
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
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)