scdl/scdl/scdl.py

466 lines
14 KiB
Python
Raw Normal View History

2015-05-14 00:41:45 -07:00
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
"""scdl allow you to download music from soundcloud
Usage:
2015-01-14 08:55:14 -08:00
scdl -l <track_url> [-a | -f | -t | -p][-c][-o <offset>]\
2015-01-19 10:25:04 -08:00
[--hidewarnings][--debug | --error][--path <path>][--addtofile][--onlymp3]
2015-01-14 08:55:14 -08:00
scdl me (-s | -a | -f | -t | -p)[-c][-o <offset>]\
2015-01-19 10:25:04 -08:00
[--hidewarnings][--debug | --error][--path <path>][--addtofile][--onlymp3]
2015-01-14 08:55:14 -08:00
scdl -h | --help
scdl --version
Options:
-h --help Show this screen
--version Show version
2014-11-16 09:19:42 -08:00
me Use the user profile from the auth_token
-l [url] URL can be track/playlist/user
2014-11-16 09:19:42 -08:00
-s Download the stream of an user (token needed)
-a Download all track of an user (including repost)
-t Download all upload of an user
-f Download all favorite of an user
-p Download all playlist of an user
-c Continue if a music already exist
-o [offset] Begin with a custom offset
--path [path] Use a custom path for this time
2014-11-16 09:19:42 -08:00
--hidewarnings Hide Warnings. (use with precaution)
--addtofile Add the artist name to the filename if it isn't in the filename already
2015-01-19 11:23:46 -08:00
--onlymp3 Download only the mp3 file even if the track is Downloadable
--error Only print debug information (Error/Warning)
--debug Print every information and
"""
from __future__ import absolute_import, division, print_function, unicode_literals
from docopt import docopt
import configparser
import logging
import warnings
import os
import signal
import sys
import time
2015-01-19 13:11:55 -08:00
2014-10-12 15:16:18 -07:00
import soundcloud
import wget
import urllib.request
import json
2015-04-08 14:51:08 -07:00
from requests.exceptions import HTTPError
2014-11-12 08:00:27 -08:00
2014-11-16 09:00:41 -08:00
import mutagen
2014-10-12 15:16:18 -07:00
from scdl import __version__
2015-05-14 00:36:19 -07:00
from scdl import utils
logging.basicConfig(level=logging.INFO, format='%(asctime)-15s %(name)-5s %(levelname)-8s %(message)s')
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
2015-05-14 00:36:19 -07:00
logger.addFilter(utils.ColorizeFilter())
2015-05-09 06:35:18 -07:00
logger.newline = print
2015-01-14 08:55:14 -08:00
arguments = None
token = ''
2014-12-02 17:16:04 -08:00
path = ''
2014-11-16 09:19:42 -08:00
offset = 0
2015-04-28 06:47:17 -07:00
scdl_client_id = '95a4c0ef214f2a4a0852142807b54b35'
2015-01-19 13:11:55 -08:00
2014-10-22 10:29:56 -07:00
client = soundcloud.Client(client_id=scdl_client_id)
2014-10-12 15:16:18 -07:00
def main():
2014-11-16 09:19:42 -08:00
"""
Main function, call parse_url
"""
signal.signal(signal.SIGINT, signal_handler)
global offset
2015-01-14 08:55:14 -08:00
global arguments
2014-11-16 09:19:42 -08:00
# import conf file
get_config()
# Parse argument
2015-01-19 11:23:46 -08:00
arguments = docopt(__doc__, version=__version__)
2015-01-14 08:55:14 -08:00
if arguments["--debug"]:
logger.level = logging.DEBUG
2015-01-14 08:55:14 -08:00
elif arguments["--error"]:
logger.level = logging.ERROR
2015-01-14 08:55:14 -08:00
logger.info("Soundcloud Downloader")
logger.debug(arguments)
if arguments["-o"] is not None:
2014-11-16 09:19:42 -08:00
try:
offset = int(arguments["-o"])
2014-11-16 09:19:42 -08:00
except:
logger.error('Offset should be an Integer...')
2014-11-16 09:19:42 -08:00
sys.exit()
if arguments["--hidewarnings"]:
warnings.filterwarnings("ignore")
if arguments["--path"] is not None:
if os.path.exists(arguments["--path"]):
os.chdir(arguments["--path"])
2014-12-02 17:16:04 -08:00
else:
logger.error('Invalid path in arguments...')
2014-12-07 15:15:04 -08:00
sys.exit()
logger.debug('Downloading to '+os.getcwd()+'...')
2014-12-07 15:15:04 -08:00
logger.newline()
2014-11-16 09:19:42 -08:00
if arguments["-l"]:
2014-12-02 17:16:04 -08:00
parse_url(arguments["-l"])
2014-11-16 09:19:42 -08:00
elif arguments["me"]:
if arguments["-a"]:
download_all_user_tracks(who_am_i())
elif arguments["-f"]:
download_user_favorites(who_am_i())
elif arguments["-t"]:
download_user_tracks(who_am_i())
elif arguments["-p"]:
download_user_playlists(who_am_i())
2014-10-22 10:29:56 -07:00
def get_config():
2014-11-16 09:19:42 -08:00
"""
read the path where to store music
"""
global token
config = configparser.ConfigParser()
config.read(os.path.join(os.path.expanduser('~'), '.config/scdl/scdl.cfg'))
try:
token = config['scdl']['auth_token']
path = config['scdl']['path']
except:
logger.error('Are you sure scdl.cfg is in $HOME/.config/scdl/ ?')
2014-11-16 09:19:42 -08:00
sys.exit()
if os.path.exists(path):
os.chdir(path)
else:
logger.error('Invalid path in scdl.cfg...')
2014-11-16 09:19:42 -08:00
sys.exit()
2014-10-23 08:22:58 -07:00
def get_item(track_url):
2014-11-16 09:19:42 -08:00
"""
Fetches metadata for an track or playlist
"""
try:
item = client.get('/resolve', url=track_url)
except Exception:
logger.error('Error resolving url, retrying...')
time.sleep(5)
try:
item = client.get('/resolve', url=track_url)
except Exception as e:
logger.error("Could not resolve url " + track_url)
logger.exception(e)
sys.exit(0)
2014-11-16 09:19:42 -08:00
return item
2014-10-23 08:22:58 -07:00
def parse_url(track_url):
2014-11-16 09:19:42 -08:00
"""
Detects if the URL is a track or playlists, and parses the track(s) to the track downloader
"""
2015-01-14 08:55:14 -08:00
global arguments
2014-11-16 09:19:42 -08:00
item = get_item(track_url)
2015-01-05 14:22:14 -08:00
2014-11-16 09:19:42 -08:00
if not item:
return
2015-01-05 14:22:14 -08:00
elif isinstance(item, soundcloud.resource.ResourceList):
download_all(item)
2014-11-16 09:19:42 -08:00
elif item.kind == 'track':
logger.info("Found a track")
2014-11-16 09:19:42 -08:00
download_track(item)
elif item.kind == "playlist":
logger.info("Found a playlist")
2014-11-16 09:19:42 -08:00
download_playlist(item)
elif item.kind == 'user':
logger.info("Found an user profile")
2014-11-16 09:19:42 -08:00
if arguments["-f"]:
download_user_favorites(item)
elif arguments["-t"]:
download_user_tracks(item)
elif arguments["-a"]:
download_all_user_tracks(item)
elif arguments["-p"]:
download_user_playlists(item)
else:
logger.error('Please provide a download type...')
2014-11-16 09:19:42 -08:00
else:
logger.error("Unknown item type")
2014-11-16 09:19:42 -08:00
2014-10-23 08:22:58 -07:00
2014-10-23 07:14:29 -07:00
def who_am_i():
2014-11-16 09:19:42 -08:00
"""
display to who the current token correspond, check if the token is valid
"""
global client
client = soundcloud.Client(access_token=token, client_id=scdl_client_id)
try:
current_user = client.get('/me')
except:
logger.error('Invalid token...')
2014-11-16 09:19:42 -08:00
sys.exit(0)
logger.info('Hello ' + current_user.username + '!')
logger.newline()
2014-11-16 09:19:42 -08:00
return current_user
2014-10-23 07:14:29 -07:00
2014-10-23 08:22:58 -07:00
def download_all_user_tracks(user):
2014-11-16 09:19:42 -08:00
"""
Find track & repost of the user
"""
global offset
user_id = user.id
2015-04-28 07:13:34 -07:00
url = "https://api.sndcdn.com/e1/users/%s/sounds.json?limit=1&offset=%d&client_id=%s" % (user_id, offset, scdl_client_id)
2014-11-16 09:19:42 -08:00
response = urllib.request.urlopen(url)
data = response.read()
text = data.decode('utf-8')
json_data = json.loads(text)
while str(json_data) != '[]':
2014-11-16 09:19:42 -08:00
offset += 1
try:
this_url = json_data[0]['track']['uri']
except:
this_url = json_data[0]['playlist']['uri']
logger.info('Track n°%d' % (offset))
2014-11-16 09:19:42 -08:00
parse_url(this_url)
2015-04-28 07:13:34 -07:00
url = "https://api.sndcdn.com/e1/users/%s/sounds.json?limit=1&offset=%d&client_id=%s" % (user_id, offset, scdl_client_id)
2014-11-16 09:19:42 -08:00
response = urllib.request.urlopen(url)
data = response.read()
text = data.decode('utf-8')
json_data = json.loads(text)
2014-10-22 10:29:56 -07:00
2014-10-23 08:22:58 -07:00
def download_user_tracks(user):
2014-11-16 09:19:42 -08:00
"""
Find track in user upload --> no repost
"""
global offset
count = 0
tracks = client.get('/users/' + str(user.id) + '/tracks', limit=10, offset=offset)
for track in tracks:
for track in tracks:
count += 1
logger.newline()
logger.info('Track n°%d' % (count))
2014-11-16 09:19:42 -08:00
download_track(track)
offset += 10
tracks = client.get('/users/' + str(user.id) + '/tracks', limit=10, offset=offset)
logger.info('All users track downloaded!')
2014-10-23 08:22:58 -07:00
def download_user_playlists(user):
2014-11-16 09:19:42 -08:00
"""
Find playlists of the user
"""
global offset
count = 0
playlists = client.get('/users/' + str(user.id) + '/playlists', limit=10, offset=offset)
for playlist in playlists:
for playlist in playlists:
count += 1
logger.newline()
logger.info('Playlist n°%d' % (count))
2014-11-16 09:19:42 -08:00
download_playlist(playlist)
offset += 10
playlists = client.get('/users/' + str(user.id) + '/playlists', limit=10, offset=offset)
logger.info('All users playlists downloaded!')
2014-10-23 07:14:29 -07:00
def download_user_favorites(user):
2014-11-16 09:19:42 -08:00
"""
Find tracks in user favorites
"""
global offset
count = 0
favorites = client.get('/users/' + str(user.id) + '/favorites', limit=10, offset=offset)
for track in favorites:
for track in favorites:
count += 1
logger.newline()
logger.info('Favorite n°%d' % (count))
2014-11-16 09:19:42 -08:00
download_track(track)
offset += 10
favorites = client.get('/users/' + str(user.id) + '/favorites', limit=10, offset=offset)
logger.info('All users favorites downloaded!')
2014-11-16 09:19:42 -08:00
2014-10-12 15:16:18 -07:00
2014-10-23 08:22:58 -07:00
def download_my_stream():
2014-11-16 09:19:42 -08:00
"""
DONT WORK FOR NOW
Download the stream of the current user
"""
client = soundcloud.Client(access_token=token, client_id=scdl_client_id)
activities = client.get('/me/activities')
logger.debug(activities)
2014-10-23 08:22:58 -07:00
2014-10-23 07:14:29 -07:00
def download_playlist(playlist):
2014-11-16 09:19:42 -08:00
"""
Download a playlist
"""
count = 0
2015-01-19 13:11:55 -08:00
invalid_chars = '\/:*?|<>"'
playlist_name = playlist.title.encode('utf-8', 'ignore').decode('utf-8')
playlist_name = ''.join(c for c in playlist_name if c not in invalid_chars)
if not os.path.exists(playlist_name):
os.makedirs(playlist_name)
os.chdir(playlist_name)
2014-11-16 09:19:42 -08:00
for track_raw in playlist.tracks:
count += 1
mp3_url = get_item(track_raw["permalink_url"])
logger.info('Track n°%d' % (count))
2015-01-19 13:11:55 -08:00
download_track(mp3_url, playlist.title)
os.chdir('..')
2014-11-16 09:19:42 -08:00
2014-10-23 07:14:29 -07:00
2015-01-05 14:22:14 -08:00
def download_all(tracks):
"""
Download all song of a page
Not recommended
"""
logger.error("NOTE: This will only download the songs of the page.(49 max)")
logger.error("I recommend you to provide an user link and a download type.")
2015-01-05 14:22:14 -08:00
count = 0
for track in tracks:
count += 1
logger.newline()
logger.info('Track n°%d' % (count))
2015-01-05 14:22:14 -08:00
download_track(track)
2015-04-08 14:51:08 -07:00
def alternative_download(track):
logger.debug('alternative_download used')
2015-04-08 14:51:08 -07:00
track_id = str(track.id)
url = 'http://api.soundcloud.com/i1/tracks/' + track_id + '/streams?client_id=a3e059563d7fd3372b49b37f00a00bcf'
res = urllib.request.urlopen(url)
data = res.read().decode('utf-8')
json_data = json.loads(data)
try:
mp3_url = json_data['http_mp3_128_url']
except KeyError:
logger.error('http_mp3_128_url not found in json response, report to developer.')
2015-04-08 14:51:08 -07:00
mp3_url = None
return mp3_url
2015-01-19 13:11:55 -08:00
def download_track(track, playlist_name=None):
2014-11-16 09:19:42 -08:00
"""
Downloads a track
"""
2015-01-14 08:55:14 -08:00
global arguments
2014-11-16 09:19:42 -08:00
if track.streamable:
2015-04-08 14:51:08 -07:00
try:
stream_url = client.get(track.stream_url, allow_redirects=False)
url = stream_url.location
except HTTPError:
url = alternative_download(track)
2014-11-16 09:19:42 -08:00
else:
logger.error('%s is not streamable...' % (track.title))
logger.newline()
2014-11-16 09:19:42 -08:00
return
title = track.title
2015-01-28 09:14:35 -08:00
title = title.encode('utf-8', 'ignore').decode(sys.stdout.encoding)
logger.info("Downloading " + title)
2014-11-16 09:19:42 -08:00
#filename
2015-01-19 10:25:04 -08:00
if track.downloadable and not arguments["--onlymp3"]:
logger.info('Downloading the orginal file.')
2014-11-16 09:19:42 -08:00
url = track.download_url + '?client_id=' + scdl_client_id
filename = urllib.request.urlopen(url).info()['Content-Disposition'].split('filename=')[1]
if filename[0] == '"' or filename[0] == "'":
filename = filename[1:-1]
else:
2015-01-05 02:05:41 -08:00
invalid_chars = '\/:*?|<>"'
2014-11-16 09:19:42 -08:00
if track.user['username'] not in title and arguments["--addtofile"]:
title = track.user['username'] + ' - ' + title
title = ''.join(c for c in title if c not in invalid_chars)
filename = title + '.mp3'
# Download
if not os.path.isfile(filename):
wget.download(url, filename)
logger.newline()
2014-11-16 09:19:42 -08:00
if '.mp3' in filename:
try:
2015-01-19 13:11:55 -08:00
if playlist_name is None:
settags(track, filename)
else:
settags(track, filename, playlist_name)
2014-11-16 09:19:42 -08:00
except:
logger.error('Error trying to set the tags...')
2014-11-16 09:19:42 -08:00
else:
logger.error('This type of audio doesn\'t support tagging...')
2014-11-16 09:19:42 -08:00
else:
if arguments["-c"]:
logger.info(title + " already Downloaded")
logger.newline()
2014-11-16 09:19:42 -08:00
return
else:
logger.newline()
logger.error("Music already exists ! (exiting)")
2014-11-16 09:19:42 -08:00
sys.exit(0)
logger.newline()
logger.info(filename + ' Downloaded.')
logger.newline()
2014-11-16 09:19:42 -08:00
2015-01-19 13:11:55 -08:00
def settags(track, filename, album='Soundcloud'):
2014-11-16 09:19:42 -08:00
"""
Set the tags to the mp3
"""
logger.info("Settings tags...")
2014-11-16 09:19:42 -08:00
user = client.get('/users/' + str(track.user_id), allow_redirects=False)
artwork_url = track.artwork_url
if artwork_url is None:
artwork_url = user.avatar_url
artwork_url = artwork_url.replace('large', 't500x500')
urllib.request.urlretrieve(artwork_url, '/tmp/scdl.jpg')
audio = mutagen.File(filename)
audio["TIT2"] = mutagen.id3.TIT2(encoding=3, text=track.title)
2015-01-19 13:11:55 -08:00
audio["TALB"] = mutagen.id3.TALB(encoding=3, text=album)
2014-11-16 09:19:42 -08:00
audio["TPE1"] = mutagen.id3.TPE1(encoding=3, text=user.username)
audio["TCON"] = mutagen.id3.TCON(encoding=3, text=track.genre)
if artwork_url is not None:
audio["APIC"] = mutagen.id3.APIC(encoding=3, mime='image/jpeg', type=3, desc='Cover', data=open('/tmp/scdl.jpg', 'rb').read())
else:
logger.error("Artwork can not be set.")
2014-11-16 09:19:42 -08:00
audio.save()
2014-10-23 07:14:29 -07:00
def signal_handler(signal, frame):
2014-11-16 09:19:42 -08:00
"""
handle keyboardinterrupt
"""
time.sleep(1)
files = os.listdir()
for f in files:
if not os.path.isdir(f) and ".tmp" in f:
os.remove(f)
logger.newline()
logger.info('Good bye!')
2014-11-16 09:19:42 -08:00
sys.exit(0)
2014-10-12 15:16:18 -07:00
if __name__ == "__main__":
2014-11-16 09:19:42 -08:00
main()