Merge pull request #87 from ritiek/develop

Add tests and some other minor improvements
This commit is contained in:
Ritiek Malhotra
2017-06-26 10:57:55 +05:30
committed by GitHub
10 changed files with 315 additions and 123 deletions

5
.gitignore vendored
View File

@@ -1,4 +1,5 @@
*.pyc *.pyc
__pycache__/ __pycache__/
/Music/ .cache/
/*.txt Music/
*.txt

23
.travis.yml Normal file
View File

@@ -0,0 +1,23 @@
language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
before_install:
- sudo apt-get -qq update
- sudo apt-get -y install autoconf automake build-essential libass-dev libfreetype6-dev libtheora-dev libtool libva-dev libvdpau-dev libvorbis-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev pkg-config texinfo wget zlib1g-dev
- sudo apt-get -y install yasm nasm libmp3lame-dev
- mkdir ~/ffmpeg_sources
- cd ~/ffmpeg_sources
- wget http://ffmpeg.org/releases/ffmpeg-snapshot.tar.bz2
- tar jxf ffmpeg-snapshot.tar.bz2
- cd ffmpeg
- PATH="$HOME/bin:$PATH" PKG_CONFIG_PATH="$HOME/ffmpeg_build/lib/pkgconfig" ./configure --prefix="$HOME/ffmpeg_build" --pkg-config-flags="--static" --extra-cflags="-I$HOME/ffmpeg_build/include" --extra-ldflags="-L$HOME/ffmpeg_build/lib" --bindir="$HOME/bin" --enable-libmp3lame
- PATH="$HOME/bin:$PATH" make -s -j
- make install
- hash -r
install:
- cd $TRAVIS_BUILD_DIR
- pip install -r requirements.txt
script: python -m pytest test

31
ISSUE_TEMPLATE.md Normal file
View File

@@ -0,0 +1,31 @@
<!--
Please follow the guide below
- You will be asked some questions and requested to provide some information, please read them CAREFULLY and answer honestly
- Put an `x` into all the boxes [ ] relevant to your *issue* (like that [x])
- Use *Preview* tab to see how your issue will actually look like
-->
- [ ] Using latest version as provided on the [master branch](https://github.com/ritiek/spotify-downloader/tree/master)
- [ ] [Searched](https://github.com/ritiek/spotify-downloader/issues?utf8=%E2%9C%93&q=is%3Aissue) for similar issues including closed ones
#### What is the purpose of your *issue*?
- [ ] Script won't run
- [ ] Encountered bug
- [ ] Feature request
- [ ] Question
- [ ] Other
#### System information
- Your `python` version: `python 2.7`
- Your operating system: `Ubuntu 16.04`
### Description
<!-- Provide as much information possible with relevant examples and whatever you have tried below -->
<!-- Give your issue a relevant title and you are good to go -->

View File

@@ -52,7 +52,9 @@ You'll also need to install FFmpeg for conversion (use `--avconv` if you'd like
Linux: `sudo apt-get install ffmpeg` Linux: `sudo apt-get install ffmpeg`
Mac: `brew install ffmpeg --with-libass --with-opus --with-fdk-aac` Mac: `brew install ffmpeg --with-libmp3lame --with-libass --with-opus --with-fdk-aac`
If it does not install correctly, you may have to build it from source. For more info see https://trac.ffmpeg.org/wiki/CompilationGuide.
### Windows ### Windows
@@ -153,20 +155,9 @@ http://open.spotify.com/track/64yrDBpcdwEdNY9loyEGbX
- Then you can simply run `python spotdl.py --list=<playlist_name>.txt` to download them all! - Then you can simply run `python spotdl.py --list=<playlist_name>.txt` to download them all!
## FAQ ## Running tests
#### I get system cannot find the specified file when downloading? `python -m pytest test`
Check out these issues [#22](https://github.com/Ritiek/Spotify-Downloader/issues/22), [#35](https://github.com/Ritiek/Spotify-Downloader/issues/35), [#36](https://github.com/Ritiek/Spotify-Downloader/issues/36).
#### How can I download whole playlist with its URI?
~~Currently this is not possible without generating unique tokens from Spotify but you can copy all the songs from a playlist and paste them in `list.txt`. I am avoiding tokens as much possible to retain the portability of this tool but if you would like to add it as an optional feature to this tool, PR's welcome!~~
This feature has been added!
#### You write horrible code. What's wrong with you?
I'm trying...
## Disclaimer ## Disclaimer

78
core/convert.py Normal file
View File

@@ -0,0 +1,78 @@
import subprocess
import os
import sys
def song(input_song, output_song, avconv=False, verbose=False):
if not input_song == output_song:
if sys.version_info < (3, 0):
input_song = input_song.encode('utf-8')
output_song = output_song.encode('utf-8')
print('Converting ' + input_song + ' to ' + output_song.split('.')[-1])
if avconv:
exit_code = convert_with_avconv(input_song, output_song, verbose)
else:
exit_code = convert_with_FFmpeg(input_song, output_song, verbose)
return exit_code
return None
def convert_with_avconv(input_song, output_song, verbose):
# different path for windows
if os.name == 'nt':
avconv_path = 'Scripts\\avconv.exe'
else:
avconv_path = 'avconv'
if verbose:
level = 'debug'
else:
level = '0'
command = [avconv_path,
'-loglevel', level,
'-i', 'Music/' + input_song,
'-ab', '192k',
'Music/' + output_song]
subprocess.call(command)
def convert_with_FFmpeg(input_song, output_song, verbose):
# What are the differences and similarities between ffmpeg, libav, and avconv?
# https://stackoverflow.com/questions/9477115
# ffmeg encoders high to lower quality
# libopus > libvorbis >= libfdk_aac > aac > libmp3lame
# libfdk_aac due to copyrights needs to be compiled by end user
# on MacOS brew install ffmpeg --with-fdk-aac will do just that. Other OS?
# https://trac.ffmpeg.org/wiki/Encode/AAC
if os.name == "nt":
ffmpeg_pre = 'Scripts\\ffmpeg.exe '
else:
ffmpeg_pre = 'ffmpeg '
ffmpeg_pre += '-y '
if not verbose:
ffmpeg_pre += '-hide_banner -nostats -v panic '
input_ext = input_song.split('.')[-1]
output_ext = output_song.split('.')[-1]
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 '
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 192k -vn '
command = (ffmpeg_pre +
'-i Music/' + input_song + ' ' +
ffmpeg_params +
'Music/' + output_song + '').split(' ')
subprocess.call(command)

View File

@@ -25,23 +25,25 @@ def compare(file, metadata):
already_tagged = False already_tagged = False
return already_tagged return already_tagged
def embed(music_file, meta_tags, output_ext): def embed(music_file, meta_tags):
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
music_file = misc.encode('utf-8') music_file = music_file.encode('utf-8')
if meta_tags is None: if meta_tags is None:
print('Could not find meta-tags') print('Could not find meta-tags')
elif output_ext == '.m4a': return None
elif music_file.endswith('.m4a'):
print('Fixing meta-tags') print('Fixing meta-tags')
embed_m4a(music_file, meta_tags, output_ext) return embed_m4a(music_file, meta_tags)
elif output_ext == '.mp3': elif music_file.endswith('.mp3'):
print('Fixing meta-tags') print('Fixing meta-tags')
embed_mp3(music_file, meta_tags, output_ext) return embed_mp3(music_file, meta_tags)
else: else:
print('Cannot embed meta-tags into given output extension') print('Cannot embed meta-tags into given output extension')
return False
def embed_mp3(music_file, meta_tags, output_ext): def embed_mp3(music_file, meta_tags):
# EasyID3 is fun to use ;) # EasyID3 is fun to use ;)
audiofile = EasyID3('Music/' + music_file + output_ext) audiofile = EasyID3('Music/' + music_file)
audiofile['artist'] = meta_tags['artists'][0]['name'] audiofile['artist'] = meta_tags['artists'][0]['name']
audiofile['albumartist'] = meta_tags['artists'][0]['name'] audiofile['albumartist'] = meta_tags['artists'][0]['name']
audiofile['album'] = meta_tags['album']['name'] audiofile['album'] = meta_tags['album']['name']
@@ -64,13 +66,14 @@ def embed_mp3(music_file, meta_tags, output_ext):
if meta_tags['copyright']: if meta_tags['copyright']:
audiofile['copyright'] = meta_tags['copyright'] audiofile['copyright'] = meta_tags['copyright']
audiofile.save(v2_version=3) audiofile.save(v2_version=3)
audiofile = ID3('Music/' + music_file + output_ext) audiofile = ID3('Music/' + music_file)
albumart = urllib2.urlopen(meta_tags['album']['images'][0]['url']) albumart = urllib2.urlopen(meta_tags['album']['images'][0]['url'])
audiofile["APIC"] = APIC(encoding=3, mime='image/jpeg', type=3, desc=u'Cover', data=albumart.read()) audiofile["APIC"] = APIC(encoding=3, mime='image/jpeg', type=3, desc=u'Cover', data=albumart.read())
albumart.close() albumart.close()
audiofile.save(v2_version=3) audiofile.save(v2_version=3)
return True
def embed_m4a(music_file, meta_tags, output_ext): def embed_m4a(music_file, meta_tags):
# Apple has specific tags - see mutagen docs - # Apple has specific tags - see mutagen docs -
# http://mutagen.readthedocs.io/en/latest/api/mp4.html # http://mutagen.readthedocs.io/en/latest/api/mp4.html
tags = {'album': '\xa9alb', tags = {'album': '\xa9alb',
@@ -90,7 +93,7 @@ def embed_m4a(music_file, meta_tags, output_ext):
'copyright': 'cprt', 'copyright': 'cprt',
'tempo': 'tmpo'} 'tempo': 'tmpo'}
audiofile = MP4('Music/' + music_file + output_ext) audiofile = MP4('Music/' + music_file)
audiofile[tags['artist']] = meta_tags['artists'][0]['name'] audiofile[tags['artist']] = meta_tags['artists'][0]['name']
audiofile[tags['albumartist']] = meta_tags['artists'][0]['name'] audiofile[tags['albumartist']] = meta_tags['artists'][0]['name']
audiofile[tags['album']] = meta_tags['album']['name'] audiofile[tags['album']] = meta_tags['album']['name']
@@ -107,3 +110,4 @@ def embed_m4a(music_file, meta_tags, output_ext):
audiofile[tags['albumart']] = [ MP4Cover(albumart.read(), imageformat=MP4Cover.FORMAT_JPEG) ] audiofile[tags['albumart']] = [ MP4Cover(albumart.read(), imageformat=MP4Cover.FORMAT_JPEG) ]
albumart.close() albumart.close()
audiofile.save() audiofile.save()
return True

View File

@@ -101,10 +101,10 @@ def generate_token():
token = creds.get_access_token() token = creds.get_access_token()
return token return token
def generate_search_URL(song): def generate_search_url(song):
# urllib2.quote() encodes URL with special characters # urllib2.quote() encodes URL with special characters
URL = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q=" + quote(song) url = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q=" + quote(song)
return URL return url
# fix encoding issues in python2 # fix encoding issues in python2
def fix_encoding(query): def fix_encoding(query):
@@ -117,6 +117,14 @@ def fix_decoding(query):
query = query.decode('utf-8') query = query.decode('utf-8')
return query return query
def filter_path(path):
os.chdir(sys.path[0])
if not os.path.exists(path):
os.makedirs(path)
for temp in os.listdir(path):
if temp.endswith('.temp'):
os.remove(path + '/' + temp)
def grace_quit(): def grace_quit():
print('') print('')
print('') print('')

123
spotdl.py
View File

@@ -3,6 +3,7 @@
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
from core import metadata from core import metadata
from core import convert
from core import misc from core import misc
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from titlecase import titlecase from titlecase import titlecase
@@ -11,7 +12,6 @@ import spotipy
import pafy import pafy
import sys import sys
import os import os
import subprocess
# urllib2 is urllib.request in python3 # urllib2 is urllib.request in python3
try: try:
@@ -19,7 +19,7 @@ try:
except ImportError: except ImportError:
import urllib.request as urllib2 import urllib.request as urllib2
# decode spotify link to "[artist] - [song]" # "[artist] - [song]"
def generate_songname(raw_song): def generate_songname(raw_song):
if misc.is_spotify(raw_song): if misc.is_spotify(raw_song):
tags = generate_metadata(raw_song) tags = generate_metadata(raw_song)
@@ -33,8 +33,10 @@ def generate_metadata(raw_song):
meta_tags = spotify.track(raw_song) meta_tags = spotify.track(raw_song)
else: else:
# otherwise search on spotify and fetch information from first result # otherwise search on spotify and fetch information from first result
try:
meta_tags = spotify.search(raw_song, limit=1)['tracks']['items'][0] meta_tags = spotify.search(raw_song, limit=1)['tracks']['items'][0]
except:
return None
artist = spotify.artist(meta_tags['artists'][0]['id']) artist = spotify.artist(meta_tags['artists'][0]['id'])
album = spotify.album(meta_tags['album']['id']) album = spotify.album(meta_tags['album']['id'])
@@ -55,12 +57,12 @@ def generate_metadata(raw_song):
#pprint.pprint(spotify.album(meta_tags['album']['id'])) #pprint.pprint(spotify.album(meta_tags['album']['id']))
return meta_tags return meta_tags
def generate_YouTube_URL(raw_song): def generate_youtube_url(raw_song):
# decode spotify http link to "[artist] - [song]" # decode spotify http link to "[artist] - [song]"
song = generate_songname(raw_song) song = generate_songname(raw_song)
# generate direct search YouTube URL # generate direct search YouTube URL
searchURL = misc.generate_search_URL(song) search_url = misc.generate_search_url(song)
item = urllib2.urlopen(searchURL).read() item = urllib2.urlopen(search_url).read()
#item = unicode(item, 'utf-8') #item = unicode(item, 'utf-8')
items_parse = BeautifulSoup(item, "html.parser") items_parse = BeautifulSoup(item, "html.parser")
check = 1 check = 1
@@ -95,15 +97,15 @@ def generate_YouTube_URL(raw_song):
# parse track from YouTube # parse track from YouTube
def go_pafy(raw_song): def go_pafy(raw_song):
# video link of the video to extract audio from # video link of the video to extract audio from
trackURL = generate_YouTube_URL(raw_song) track_url = generate_youtube_url(raw_song)
if trackURL is None: if track_url is None:
return None return None
else: else:
# parse the YouTube video # parse the YouTube video
return pafy.new(trackURL) return pafy.new(track_url)
# title of the YouTube video # title of the YouTube video
def get_YouTube_title(content, number): def get_youtube_title(content, number=None):
title = misc.fix_encoding(content.title) title = misc.fix_encoding(content.title)
if number is None: if number is None:
return title return title
@@ -157,77 +159,8 @@ def download_song(content):
link.download(filepath='Music/' + music_file + args.input_ext) link.download(filepath='Music/' + music_file + args.input_ext)
return True return True
# 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:
music_file = misc.fix_encoding(music_file)
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 convert_with_avconv(music_file):
# different path for windows
if os.name == 'nt':
avconv_path = 'Scripts\\avconv.exe'
else:
avconv_path = 'avconv'
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 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
# libopus > libvorbis >= libfdk_aac > aac > libmp3lame
# libfdk_aac due to copyrights needs to be compiled by end user
# on MacOS brew install ffmpeg --with-fdk-aac will do just that. Other OS?
# https://trac.ffmpeg.org/wiki/Encode/AAC
if os.name == "nt":
ffmpeg_pre = 'Scripts\\ffmpeg.exe '
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 '
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 '
command = (ffmpeg_pre +
'-i Music/' + music_file + args.input_ext + ' ' +
ffmpeg_params +
'Music/' + music_file + args.output_ext + '').split(' ')
subprocess.call(command)
# check if input song already exists in Music folder # check if input song already exists in Music folder
def check_exists(music_file, raw_song, islist): def check_exists(music_file, raw_song, islist=True):
files = os.listdir("Music") files = os.listdir("Music")
for file in files: for file in files:
if file.endswith(".temp"): if file.endswith(".temp"):
@@ -254,6 +187,7 @@ def check_exists(music_file, raw_song, islist):
return False return False
else: else:
return True return True
return False
# download songs from list # download songs from list
def grab_list(file): def grab_list(file):
@@ -307,32 +241,43 @@ def grab_single(raw_song, number=None):
return return
# print "[number]. [artist] - [song]" if downloading from list # print "[number]. [artist] - [song]" if downloading from list
# otherwise print "[artist] - [song]" # otherwise print "[artist] - [song]"
print(get_YouTube_title(content, number)) print(get_youtube_title(content, number))
# generate file name of the song to download # generate file name of the song to download
music_file = misc.generate_filename(content.title) music_file = misc.generate_filename(content.title)
music_file = misc.fix_decoding(music_file) music_file = misc.fix_decoding(music_file)
if not check_exists(music_file, raw_song, islist=islist): if not check_exists(music_file, raw_song, islist=islist):
if download_song(content): if download_song(content):
print('') print('')
convert_song(music_file) input_song = music_file + args.input_ext
output_song = music_file + args.output_ext
convert.song(input_song,
output_song,
avconv=args.avconv,
verbose=args.verbose)
os.remove('Music/' + input_song)
meta_tags = generate_metadata(raw_song) meta_tags = generate_metadata(raw_song)
if not args.no_metadata: if not args.no_metadata:
metadata.embed(music_file, meta_tags, args.output_ext) metadata.embed(output_song, meta_tags)
else: else:
print('No audio streams available') print('No audio streams available')
if __name__ == '__main__': class Args(object):
manual = False
os.chdir(sys.path[0]) input_ext = '.m4a'
if not os.path.exists("Music"): output_ext = '.mp3'
os.makedirs("Music")
args = Args()
# token is mandatory when using Spotify's API # token is mandatory when using Spotify's API
# https://developer.spotify.com/news-stories/2017/01/27/removing-unauthenticated-calls-to-the-web-api/ # https://developer.spotify.com/news-stories/2017/01/27/removing-unauthenticated-calls-to-the-web-api/
token = misc.generate_token() token = misc.generate_token()
spotify = spotipy.Spotify(auth=token) spotify = spotipy.Spotify(auth=token)
# set up arguments misc.filter_path('Music')
if __name__ == '__main__':
os.chdir(sys.path[0])
args = misc.get_arguments() args = misc.get_arguments()
if args.song: if args.song:

69
test/test_single.py Normal file
View File

@@ -0,0 +1,69 @@
# -*- coding: UTF-8 -*-
import spotdl
raw_song = 'http://open.spotify.com/track/0JlS7BXXD07hRmevDnbPDU'
def test_spotify_title():
expect_title = 'David André Østby - Intro'
title = spotdl.generate_songname(raw_song)
assert title == expect_title
def test_youtube_url():
expect_url = 'youtube.com/watch?v=rg1wfcty0BA'
url = spotdl.generate_youtube_url(raw_song)
assert url == expect_url
def test_youtube_title():
expect_title = 'Intro - David André Østby'
content = spotdl.go_pafy(raw_song)
title = spotdl.get_youtube_title(content)
assert title == expect_title
def test_check_exists():
expect_check = False
content = spotdl.go_pafy(raw_song)
music_file = spotdl.misc.generate_filename(content.title)
music_file = spotdl.misc.fix_decoding(music_file)
check = spotdl.check_exists(music_file, raw_song)
assert check == expect_check
def test_download():
expect_download = True
content = spotdl.go_pafy(raw_song)
download = spotdl.download_song(content)
assert download == expect_download
def test_convert():
# exit code None = success
expect_convert = None
content = spotdl.go_pafy(raw_song)
music_file = spotdl.misc.generate_filename(content.title)
music_file = spotdl.misc.fix_decoding(music_file)
input_song = music_file + spotdl.args.input_ext
output_song = music_file + spotdl.args.output_ext
convert = spotdl.convert.song(input_song, output_song)
assert convert == expect_convert
def test_metadata():
expect_metadata = True
content = spotdl.go_pafy(raw_song)
music_file = spotdl.misc.generate_filename(content.title)
music_file = spotdl.misc.fix_decoding(music_file)
meta_tags = spotdl.generate_metadata(raw_song)
output_song = music_file + spotdl.args.output_ext
metadata_output = spotdl.metadata.embed(output_song, meta_tags)
input_song = music_file + spotdl.args.input_ext
metadata_input = spotdl.metadata.embed(input_song, meta_tags)
assert metadata_output == (metadata_input == expect_metadata)
def check_exists2():
expect_check = True
content = spotdl.go_pafy(raw_song)
music_file = spotdl.misc.generate_filename(content.title)
music_file = spotdl.misc.fix_decoding(music_file)
check = spotdl.check_exists(music_file, raw_song)
assert check == expect_check

42
test/test_username.py Normal file
View File

@@ -0,0 +1,42 @@
# -*- coding: UTF-8 -*-
import spotdl
username = 'alex'
def test_user():
expect_playlists = 7
playlists = spotdl.spotify.user_playlists(username)
playlists = len(playlists['items'])
assert playlists == expect_playlists
def test_playlist():
expect_tracks = 14
playlist = spotdl.spotify.user_playlists(username)['items'][0]
tracks = playlist['tracks']['total']
assert tracks == expect_tracks
def test_list():
playlist = spotdl.spotify.user_playlists(username)['items'][0]
expect_lines = playlist['tracks']['total']
result = spotdl.spotify.user_playlist(playlist['owner']['id'], playlist['id'], fields='tracks,next')
tracks = result['tracks']
spotdl.misc.feed_tracks('list.txt', tracks)
while tracks['next']:
tracks = spotify.next(tracks)
misc.feed_tracks('list.txt', tracks)
with open('list.txt', 'r') as listed:
expect_song = (listed.read()).splitlines()[0]
spotdl.misc.trim_song('list.txt')
with open('list.txt', 'a') as myfile:
myfile.write(expect_song)
with open('list.txt', 'r') as listed:
songs = (listed.read()).splitlines()
lines = len(songs)
song = songs[-1]
assert (expect_lines == lines and expect_song == song)