Downloading of spotify playlists is a go.

This commit is contained in:
Kepoor Hampond 2017-01-25 20:57:56 -08:00
parent a05d7dfdc8
commit fbe3edf1ef
7 changed files with 217 additions and 283 deletions

View file

@ -2,97 +2,85 @@
[![License: GNU](https://img.shields.io/badge/License-GNU-yellow.svg)](http://www.gnu.org/licenses/gpl.html)
[![PyPI](https://img.shields.io/badge/PyPi-Python_3.x-blue.svg)](https://pypi.python.org/pypi/irs)
<em>Now with working album art!</em>
<em>Downloading of Spotify playlists coming soon!</em>
<em>Spotify playlists are now downloadable!</em>
An ironically named program to download audio from youtube and then parse metadata for the downloaded file.
___
### Why the name?
As an acronym, it spells IRS. I think this is breathtakingly hilarious because the Internal Revenue Service (also the IRS) takes away, while my program gives. I'm so funny. You can tell that I'll get laid in college.
### Usage and Examples
To download a specific song, you'll want to use the `-s` flag:
To download Spotify playlists, you need to supply client_ids. To do this, you'll want to create an application [here](https://developer.spotify.com/my-applications/#!/applications/create). Once you've done that, you'll want to copy your 'client id' and your 'client secret' into the config file and their corresponding lines. To find the config file run this command: `irs -C`. If that's all working, enter the name of the playlist you would like to download like this:
```bash
irs -p "Brain Food"
```
If you download a specific song, you'll want to use the `-s` and `-a` flag.
```bash
irs -a "David Bowie" -s "Ziggy Stardust"
```
To download an entire album, you'll want to use the `-A` flag:
To download an entire album, you'll want to use the `-A` flag. If the album you want can't be found, run it with the `-a` flag for some more specification.
```bash
irs -A "Sadnecessary" # -a "Milky Chance"
```
irs -a "Milky Chance" -A "Sadnecessary"
```
If you want to download a playlist, you'll need to make a text file, such as `Awesome Mix.txt`. In the file, you'll want to write songs into it like this:
```
Hot Blood - Kaleo
Work Song - Hozier
Drop The Game - Flume & Chet Faker
```
Then, run a command like this where the playlist file resides:
```
irs -p "Awesome Mix.txt"
```
[![asciicast](https://asciinema.org/a/bcs7i0sjmka052wsdyxg5xrug.png)](https://asciinema.org/a/bcs7i0sjmka052wsdyxg5xrug?speed=3&autoplay=true)
[![asciicast](https://asciinema.org/a/8kb9882j4cbtd4hwbsbb7h0ia.png)](https://asciinema.org/a/8kb9882j4cbtd4hwbsbb7h0ia?speed=3)
Full usage:
```
usage:
usage:
irs (-h | -v)
irs [-l]
irs -p PLAYLIST [-ng] [-c COMMAND] [-l]
irs -a ARTIST (-s SONG | -A ALBUM [-st SEARCH_TERMS]) [-c COMMAND] [-l]
irs -p PLAYLIST [-c COMMAND] [-l]
irs -A ALBUM [-c COMMAND] [-l]
irs -a ARTIST -s SONG [-c COMMAND] [-l]
Options:
-h, --help show this help message and exit
-v, --version Display the version and exit.
-A ALBUM, --album ALBUM
Search spotify for an album.
-p PLAYLIST, --playlist PLAYLIST
Search spotify for a playlist.
-c COMMAND, --command COMMAND
Run a background command with each song's location.
Example: `-c "rhythmbox %(loc)s"`
-a ARTIST, --artist ARTIST
Specify the artist name.
-p PLAYLIST, --playlist PLAYLIST
Specify playlist filename. Each line in the file
should be formatted like so: `SONGNAME - ARTIST`
-s SONG, --song SONG Specify song name of the artist.
-A ALBUM, --album ALBUM
Specify album name of the artist.
-st SEARCH_TERMS, --search-terms SEARCH_TERMS
Only use if calling -A/--album. Acts as extra search
terms when looking for the album.
Specify the artist name. Only needed for -s/--song
-s SONG, --song SONG Specify song name of the artist. Must be used with
-a/--artist
-l, --choose-link If supplied, will bring up a console choice for what
link you want to download based off a list of titles.
-ng, --no-organize Only use if calling -p/--playlist. Forces all files
downloaded to be organized normally.
```
___
### Installation
Please note that it currently is only usable in `Python 3.x`. Almost all dependencies are automatically installed by pip, but `youtube_dl` still needs `ffmpeg` to convert video to audio, so for Windows, you can install [`Scoop`](http://scoop.sh/) and then just do:
```
$ scoop install ffmpeg
scoop install ffmpeg
```
For OSX, you can use [`Brew`](http://brew.sh/) to install `ffmpeg`:
```
$ brew install ffmpeg
brew install ffmpeg
```
And then for Ubuntu:
```
$ sudo apt-get install ffmpeg
sudo apt-get install ffmpeg
```
Most other linux distros have `ffmpeg` or `libav-tools` in their package manager repos, so you can install one or the other for other distros.
Finally, install it!
```
$ pip install irs
pip install irs
```
### Why the name?
As an acronym, it spells IRS. I think this is breathtakingly hilarious because the Internal Revenue Service (also the IRS) takes away, while my program gives. I'm so funny. You can tell that I'll get laid in college.
### Wishlist
- [x] Finds album based off of song name and artist
- [x] Full album downloading
- [x] Album art metadata correctly displayed
- [x] Playlist downloading
- [ ] Spotify playlist downloading - <em>Coming soon</em>
- [x] Spotify playlist downloading
- [ ] GUI/Console interactive version - <em>In progress</em>
- [ ] 100% success rate for automatic song choosing

View file

@ -2,29 +2,37 @@
HELP = \
"""
usage:
irs (-h | -v)
irs (-h | -v | -C)
irs [-l]
irs -p PLAYLIST [-ng] [-c COMMAND] [-l]
irs -a ARTIST (-s SONG | -A ALBUM [-st SEARCH_TERMS]) [-c COMMAND] [-l]
irs -p PLAYLIST [-c COMMAND] [-l]
irs -A ALBUM [-c COMMAND] [-l]
irs -a ARTIST -s SONG [-c COMMAND] [-l]
Options:
-h, --help show this help message and exit
-v, --version Display the version and exit.
-C, --config Return location of configuration file.
-A ALBUM, --album ALBUM
Search spotify for an album.
-p PLAYLIST, --playlist PLAYLIST
Search spotify for a playlist.
-c COMMAND, --command COMMAND
Run a background command with each song's location.
Example: `-c "rhythmbox %(loc)s"`
-a ARTIST, --artist ARTIST
Specify the artist name.
-p PLAYLIST, --playlist PLAYLIST
Specify playlist filename. Each line in the file
should be formatted like so: `SONGNAME - ARTIST`
-s SONG, --song SONG Specify song name of the artist.
-A ALBUM, --album ALBUM
Specify album name of the artist.
Specify the artist name. Only needed for -s/--song
-s SONG, --song SONG Specify song name of the artist. Must be used with
-a/--artist
-l, --choose-link If supplied, will bring up a console choice for what
link you want to download based off a list of titles.
-ng, --no-organize Only use if calling -p/--playlist. Forces all files
downloaded to be organized normally.
"""
# For exiting
@ -36,11 +44,14 @@ import argparse
# Import the manager
from .manager import Manager
from .utils import *
from .config import CONFIG
def main():
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('-h', '--help', action='store_true', dest='help')
parser.add_argument('-v', '--version', dest="version", action='store_true', help="Display the version and exit.")
parser.add_argument('-C', '--config', dest="config", action='store_true', help="Return config file location.")
parser.add_argument('-c', '--command', dest="command", help="Run a background command with each song's location.")
parser.add_argument('-a', '--artist', dest="artist", help="Specify the artist name.")
@ -57,11 +68,8 @@ def main():
media.add_argument('-A', '--album', dest="album", help="Specify album name of the artist.")
parser.add_argument('-o', '--order-files', action='store_true', dest="order_files",\
help="Only use if callign with -p/--playlist or -A/--album. Adds a digit to front of each file specifying order.")
args = parser.parse_args()
args = parser.parse_args(sys.argv[1:] + CONFIG["default_flags"].split(" ")[:-1])
if args.organize == None:
args.organize = True
@ -82,23 +90,25 @@ def main():
print ("\n")
exit(0)
elif args.config:
print (get_config_file_path())
elif not args.organize and not args.playlist:
parser.error("error: must supply -p/--playlist if specifying -ng/--no-organize")
exit(1)
elif args.artist and not (args.album or args.song):
parser.error("error: must supply -A/--album or -s/--song if specifying -a/--artist")
exit(1)
#elif args.artist and not (args.album or args.song):
# parser.error("error: must supply -A/--album or -s/--song if specifying -a/--artist")
# exit(1)
elif args.playlist:
manager.rip_playlist()
manager.rip_spotify_list("playlist")
elif args.artist:
if args.album:
manager.rip_album()
elif args.album:
manager.rip_spotify_list("album")
elif args.song:
manager.rip_mp3()
elif args.song:
manager.rip_mp3()
else:
manager.console()

19
irs/config.py Normal file
View file

@ -0,0 +1,19 @@
CONFIG = dict(
# To autostart rhythmbox with a new song:
# default_flags = '-c rhythmbox %(loc)s',
# To make choosing of links default:
# default_flags = '-l',
default_flags = '',
# These are necessary to download Spotify playlists
client_id = '',
client_secret = '',
# For a custom directory. Note that `~` will not work as a shortcut.
directory = '',
# If you want numbered file names
numbered_file_names = True,
)

View file

@ -1,5 +1,6 @@
# Powered by:
import youtube_dl
from spotipy.oauth2 import SpotifyClientCredentials
import spotipy
# Info getting
@ -14,6 +15,7 @@ from bs4 import BeautifulSoup
# Local utils
from .utils import *
from .metadata import *
from .config import CONFIG
class Manager:
def __init__(self, args):
@ -40,31 +42,18 @@ class Manager:
except ValueError:
print (bc.FAIL + "\nPlease enter a valid number." + bc.ENDC)
if media in (1, 2):
self.args.artist = color_input("Artist of song/album")
if media == 1:
self.args.song = color_input("Song you would like to download")
self.rip_mp3()
if media == 1:
self.args.song = color_input("Song you would like to download")
self.rip_mp3()
elif media == 2:
self.args.album = color_input("Album you would like to download")
self.rip_album()
elif media == 2:
self.args.album = color_input("Album you would like to download")
self.rip_spotify_list("album")
elif media == 3:
self.args.playlist = color_input("Playlist file name")
self.args.playlist = color_input("Playlist name to search for")
organize = ""
while organize not in ("y", "n", "yes", "no", ""):
print (bc.HEADER + "Would you like to place all songs into a single folder? (Y/n)", end="")
organize = input(bc.BOLD + bc.YELLOW + ": " + bc.ENDC).lower()
if organize in ("y", "yes", ""):
self.args.organize = True
elif organize in ("n", "no"):
self.args.organize = False
self.rip_playlist()
self.rip_spotify_list("playlist")
def find_mp3(self, song=None, artist=None):
if not song:
@ -131,85 +120,6 @@ class Manager:
return search_results[i]
def rip_playlist(self):
file_name = self.args.playlist
organize = self.args.organize
try:
file = open(file_name, 'r')
except Exception:
print (file_name + bc.FAIL + " could not be found." + bc.ENDC)
exit(1)
errors = []
song_number = 0
for line in file:
if line.strip() == "":
pass
#try:
arr = line.strip("\n").split(" - ")
self.args.song = arr[0]
self.args.artist = arr[1]
if os.path.isdir(self.args.artist):
remove = False
else:
remove = True
location = self.rip_mp3()
locations = location.split("/")
song_number += 1
# Enter... the reorganizing...
if organize:
folder_name = ("playlist - " + file_name)[:40]
if not os.path.isdir(folder_name):
os.makedirs(folder_name)
os.rename(location, "%s/%s - %s" % (folder_name, song_number, locations[-1]))
if remove:
import shutil # Only import this if I have to.
shutil.rmtree(locations[0])
if organize:
os.rename(file_name, folder_name + "/" + file_name)
os.rename(folder_name, folder_name.replace("playlist - ", ""))
#except Exception as e:
# errors.append(line + color(" : ", ["YELLOW"]) + bc.FAIL + str(e) + bc.ENDC)
if len(errors) > 0:
print (bc.FAIL + "Something was wrong with the formatting of the following lines:" + bc.ENDC)
for i in errors:
print ("\t%s" % i)
def get_album_contents(self, search):
spotify = spotipy.Spotify()
results = spotify.search(q=search, type='album')
items = results['albums']['items']
if len(items) > 0:
album = choose_from_list(items)
album_id = (album['uri'])
contents = spotify.album_tracks(album_id)["items"]
contents = contents[0:-1]
names = []
for song in contents:
song = song["name"]
names.append(song)
return names, album_id
else:
print (bc.FAIL + "There was no results." + bc.ENDC)
exit(1)
def get_album_art(self, artist, album, id=None):
spotify = spotipy.Spotify()
@ -223,48 +133,92 @@ class Manager:
album = items[0]['images'][0]['url']
return album
def rip_spotify_list(self, search, type, id=None):
spotify = spotipy.Spotify()
def rip_spotify_list(self, type):
if type == "playlist":
search = self.args.playlist
elif type == "album":
search = self.args.album
if self.args.artist:
search += self.args.artist
try:
client_credentials_manager = SpotifyClientCredentials(CONFIG["client_id"], CONFIG["client_secret"])
spotify = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
except spotipy.oauth2.SpotifyOauthError:
spotify = spotipy.Spotify()
results = spotify.search(q=search, type=type)
items = results[type + "s"]['items']
songs = []
if len(items) > 0:
spotify_list = choose_from_spotify_list(items)
list_type = spotify_list["type"]
if list_type != "playlist":
spotify_list = eval("spotify.%s" % list_type)(spotify_list["uri"])
else:
try:
spotify_list = spotify.user_playlist(spotify_list["owner"]["id"], \
playlist_id=spotify_list["uri"], fields="tracks,next")
except spotipy.client.SpotifyException:
fail_oauth()
print (bc.YELLOW + "\nFetching tracks and their metadata: " + bc.ENDC)
increment = 0
for song in spotify_list["tracks"]["items"]:
increment += 1
list_size = increment / len(spotify_list["tracks"]["items"])
drawProgressBar(list_size)
if list_type == "playlist":
song = song["track"]
artist = spotify.artist(song["artists"][0]["id"])
if list_type == "playlist":
album = (spotify.track(song["uri"])["album"])
else:
album = spotify_list
songs.append({
"name": song["name"],
"artist": artist["name"],
"album": album["name"],
"tracknum": song["track_number"],
"album_cover": album["images"][0]["url"]
})
print (bc.OKGREEN + "\nFound tracks:" + bc.ENDC)
print (bc.HEADER)
for song in songs:
print ("\t" + song["name"] + " - " + song["artist"])
print (bc.ENDC + "\n")
for song in songs:
self.rip_mp3(song["name"], song["artist"], album=song["album"], \
tracknum=song["tracknum"], album_art_url=song["album_cover"])
else:
print (bc.FAIL + "No results were found." + bc.ENDC)
def rip_album(self):
search = self.args.artist + " " + self.args.album
songs, album_uri = self.get_album_contents(search)
if not songs:
print (bc.FAIL + "Could not find album." + bc.ENDC)
print (bc.FAIL + "No results were found. Make sure to use proper spelling and capitalization." + bc.ENDC)
exit(1)
print ("")
print (bc.HEADER + "Album Contents:" + bc.ENDC)
for song in songs:
print (bc.OKBLUE + " - " + song + bc.ENDC)
print (bc.YELLOW + "\nFinding album cover ... " + bc.ENDC, end="\r")
album_art_url = self.get_album_art(self.args.artist, self.args.album, id=album_uri)
print (bc.OKGREEN + "Album cover found: " + bc.ENDC + album_art_url)
for track_number, song in enumerate(songs):
print (color("\n%s/%s - " % (track_number + 1, len(songs)), ["UNDERLINE"]), end="")
self.rip_mp3(song, album=self.args.album, tracknum=track_number + 1, album_art_url=album_art_url)
else:
print (bc.BOLD + bc.UNDERLINE + self.args.album + bc.ENDC + bc.OKGREEN + " downloaded successfully!\n")
def rip_mp3(self, song=None, artist=None,
album=None, # if you want to specify an album and save a bit of time.
tracknum=None, # to specify the tracknumber in the album.
album_art_url=None, # if you want to save a lot of time trying to find album cover.
organize=True
):
if not song:
song = self.args.song
@ -273,7 +227,12 @@ class Manager:
audio_code = self.find_mp3(song=song, artist=artist)
filename = strip_special_chars(song) + ".mp3"
if CONFIG["numbered_file_names"] and tracknum:
track = str(tracknum) + " - "
else:
track = ""
filename = track + strip_special_chars(song) + ".mp3"
ydl_opts = {
'format': 'bestaudio/best',
@ -288,13 +247,13 @@ class Manager:
ydl.download(["http://www.youtube.com/watch?v=" + audio_code])
artist_folder = artist
artist_folder = CONFIG["directory"] + "/" + artist
if not os.path.isdir(artist_folder):
os.makedirs(artist_folder)
if album:
album_folder = artist + "/" + album
album_folder = CONFIG["directory"] + "/" + artist + "/" + album
if not os.path.isdir(album_folder):
os.makedirs(album_folder)
location = album_folder

View file

@ -1,4 +1,7 @@
import sys, os
import sys, os, spotipy, irs
def get_config_file_path():
return os.path.dirname(irs.__file__) + "/config.py"
def strip_special_chars(string):
special_chars = "\ / : * ? \" < > | - ( )".split(" ")
@ -111,19 +114,50 @@ def finish_unorganize(file_name):
os.rename(folder_name, folder_name.replace("playlist - ", ""))
def fail_oauth():
print (bc.FAIL + "To download Spotify playlists, you need to supply client_ids." + bc.ENDC)
print ("To do this, you'll want to create an application here:")
print ("https://developer.spotify.com/my-applications/#!/applications/create")
print ("Once you've done that, you'll want to copy your 'client id' and your 'client secret'")
print ("into the config file and their corresponding locations:")
print (get_config_file_path())
exit(1)
def choose_from_spotify_list(thelist):
spotify = spotipy.Spotify()
thelist = list(thelist)
print ("Results:")
choice = ""
while choice not in tuple(range(0, len(thelist[:5]))):
for index, album in enumerate(thelist[:5]):
info = spotify.user(album["owner"]["id"])
try:
display_info = " (" + str(info["followers"]["total"]) + " followers)" + " - " + info["display_name"]
except Exception:
display_info = " - info couldn't be found"
while choice not in tuple(range(0, len(thelist[:10]))):
for index, result in enumerate(thelist[:10]):
type = result["type"]
print ("\t" + str(index) + ") " + album["name"] + display_info)
if type == "playlist":
info = spotify.user(result["owner"]["id"])
try:
display_info = " (" + str(info["followers"]["total"]) + " followers)"
display_info += " - " + info["display_name"]
except Exception:
display_info = " - info couldn't be found"
elif type == "album":
info = spotify.album(result["id"])
display_info = " - " + info["artists"][0]["name"]
print ("\t" + str(index) + ") " + result["name"] + display_info)
choice = int(input(bc.YELLOW + "\nEnter result number: " + bc.ENDC))
return thelist[choice]
def drawProgressBar(percent, barLen = 40):
import sys
sys.stdout.write("\r")
progress = ""
for i in range(barLen):
if i < int(barLen * percent):
progress += "#"
else:
progress += "-"
sys.stdout.write("[%s] %.2f%%" % (progress, percent * 100))
sys.stdout.flush()

View file

@ -2,7 +2,7 @@ from setuptools import setup
setup(
name='irs',
version='2.7.20',
version='3.7.20',
description='A music downloader that just gets metadata.',
url='https://github.com/kepoorhampond/irs',
author='Kepoor Hampond',

View file

@ -1,76 +0,0 @@
class bc:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKGREEN = '\033[32m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
GRAY = '\033[30m'
YELLOW = '\033[33m'
def choose_from_spotify_list(thelist):
thelist = list(thelist)
print ("Results:")
choice = ""
while choice not in tuple(range(0, len(thelist[:5]))):
for index, result in enumerate(thelist[:5]):
type = result["type"]
if type == "playlist":
info = spotify.user(result["owner"]["id"])
try:
display_info = " (" + str(info["followers"]["total"]) + " followers)"
display_info += " - " + info["display_name"]
except Exception:
display_info = " - info couldn't be found"
elif type == "album":
info = spotify.album(result["id"])
display_info = " - " + info["artists"][0]["name"]
print ("\t" + str(index) + ") " + result["name"] + display_info)
choice = int(input(bc.YELLOW + "\nEnter result number: " + bc.ENDC))
return thelist[choice]
def rip_spotify_list(search, type, id=None):
spotify = spotipy.Spotify()
results = spotify.search(q=search, type=type)
items = results[type + "s"]['items']
songs = []
if len(items) > 0:
spotify_list = choose_from_spotify_list(items)
list_type = spotify_list["type"]
if list_type != "playlist":
spotify_list = eval("spotify.%s" % list_type)(spotify_list["uri"])
else:
try:
spotify_list = spotify.user_playlist(spotify_list["owner"]["id"], \
playlist_id=spotify_list["uri"], fields="tracks,next")
except spotipy.client.SpotifyException:
print (bc.FAIL + "To download Spotify playlists, you need to supply client_id's" + bc.ENDC)
exit(1)
print (bc.YELLOW + "\nFetching tracks ..." + bc.ENDC, end="\r")
for song in spotify_list["tracks"]["items"]:
artist = spotify.artist(song["artists"][0]["id"])
songs.append([song["name"], artist["name"]])
print (bc.OKGREEN + "Found tracks!" + bc.ENDC)
return songs
else:
print (bc.FAIL + "No results were found." + bc.ENDC)
exit(1)
import spotipy
spotify = spotipy.Spotify()
#results = spotify.search(q="Star Wars Headspace", type="album")
#results = results["albums"]["items"]
print (rip_spotify_list("Brain Food", "playlist"))