scdl/scdl/scdl.py

507 lines
16 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>]\
2016-02-08 05:32:59 -08:00
[--hidewarnings][--debug | --error][--path <path>][--addtofile][--onlymp3]
[--hide-progress][--min-size <size>][--max-size <size>]
scdl me (-s | -a | -f | -t | -p | -m)[-c][-o <offset>]\
2016-02-08 05:32:59 -08:00
[--hidewarnings][--debug | --error][--path <path>][--addtofile][--onlymp3]
[--hide-progress][--min-size <size>][--max-size <size>]
2015-01-14 08:55:14 -08:00
scdl -h | --help
scdl --version
Options:
-h --help Show this screen
--version Show version
me Use the user profile from the auth_token
-l [url] URL can be track/playlist/user
-s Download the stream of a user (token needed)
-a Download all tracks of a user (including repost)
-t Download all uploads of a user
-f Download all favorites of a user
-p Download all playlists of a user
-m Download all liked and owned playlists of a user
-c Continue if a music already exist
-o [offset] Begin with a custom offset
--path [path] Use a custom path for this time
--min-size [min-size] Skip tracks smaller than size (k/m/g)
--max-size [max-size] Skip tracks larger than size (k/m/g)
--hidewarnings Hide Warnings. (use with precaution)
--addtofile Add the artist name to the filename if it isn't in the filename already
--onlymp3 Download only the mp3 file even if the track is Downloadable
--error Only print debug information (Error/Warning)
--debug Print every information and
--hide-progress Hide the wget progress bar
"""
import logging
import os
import signal
import sys
import time
2015-05-09 04:10:15 -07:00
import warnings
import math
2016-02-07 17:12:43 -08:00
import shutil
import requests
2016-02-07 17:35:51 -08:00
import re
2016-03-01 14:12:34 -08:00
import tempfile
2016-04-24 07:31:45 -07:00
import codecs
2015-01-19 13:11:55 -08:00
2015-05-09 04:10:15 -07:00
import configparser
import mutagen
from docopt import docopt
2016-02-07 17:12:43 -08:00
from clint.textui import progress
2014-11-12 08:00:27 -08:00
2016-08-27 07:33:16 -07:00
from scdl import __version__, CLIENT_ID, ALT_CLIENT_ID
from scdl import client, utils
2016-08-27 07:33:16 -07:00
2015-06-28 13:24:38 -07:00
logging.basicConfig(level=logging.INFO, format='%(message)s')
logging.getLogger('requests').setLevel(logging.WARNING)
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-01-19 13:11:55 -08:00
2016-02-29 09:08:41 -08:00
url = {
'playlists-liked': ('https://api-v2.soundcloud.com/users/{0}/playlists'
'/liked_and_owned?limit=200&offset={1}'),
2016-02-29 09:08:41 -08:00
'favorites': ('https://api.soundcloud.com/users/{0}/favorites?'
'limit=200&offset={1}'),
'tracks': ('https://api.soundcloud.com/users/{0}/tracks?'
'limit=200&offset={1}'),
'all': ('https://api-v2.soundcloud.com/profile/soundcloud:users:{0}?'
'limit=200&offset={1}'),
'playlists': ('https://api.soundcloud.com/users/{0}/playlists?'
2016-03-01 14:58:59 -08:00
'limit=200&offset={1}'),
'resolve': ('https://api.soundcloud.com/resolve?url={0}'),
'user': ('https://api.soundcloud.com/users/{0}'),
2016-03-01 14:58:59 -08:00
'me': ('https://api.soundcloud.com/me?oauth_token={0}')
2016-02-29 09:08:41 -08:00
}
client = client.Client()
2014-10-22 10:29:56 -07:00
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
2015-05-09 15:13:11 -07:00
if arguments['--debug']:
logger.level = logging.DEBUG
2015-05-09 15:13:11 -07:00
elif arguments['--error']:
logger.level = logging.ERROR
2015-01-14 08:55:14 -08:00
2015-05-09 15:13:11 -07:00
logger.info('Soundcloud Downloader')
logger.debug(arguments)
2015-05-09 15:13:11 -07:00
if arguments['-o'] is not None:
2014-11-16 09:19:42 -08:00
try:
2015-08-24 18:19:28 -07:00
offset = int(arguments['-o']) - 1
2014-11-16 09:19:42 -08:00
except:
2015-08-24 18:19:28 -07:00
logger.error('Offset should be an integer...')
2014-11-16 09:19:42 -08:00
sys.exit()
2015-08-24 18:19:28 -07:00
logger.debug('offset: %d', offset)
2014-11-16 09:19:42 -08:00
if arguments['--min-size'] is not None:
try:
arguments['--min-size'] = utils.size_in_bytes(
arguments['--min-size']
)
except:
logger.exception(
'Min size should be an integer with a possible unit suffix'
)
sys.exit()
logger.debug('min-size: %d', arguments['--min-size'])
if arguments['--max-size'] is not None:
try:
arguments['--max-size'] = utils.size_in_bytes(
arguments['--max-size']
)
except:
logger.error(
'Max size should be an integer with a possible unit suffix'
)
sys.exit()
logger.debug('max-size: %d', arguments['--max-size'])
2015-05-09 15:13:11 -07:00
if arguments['--hidewarnings']:
warnings.filterwarnings('ignore')
2014-11-16 09:19:42 -08:00
2015-05-09 15:13:11 -07:00
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()
2015-05-09 15:13:11 -07:00
if arguments['-l']:
parse_url(arguments['-l'])
elif arguments['me']:
2016-02-29 09:08:41 -08:00
if arguments['-f']:
download(who_am_i(), 'favorites', 'likes')
2015-05-09 15:13:11 -07:00
elif arguments['-t']:
2016-02-29 09:08:41 -08:00
download(who_am_i(), 'tracks', 'uploaded tracks')
elif arguments['-a']:
download(who_am_i(), 'all', 'tracks and reposts')
2015-05-09 15:13:11 -07:00
elif arguments['-p']:
2016-02-29 09:08:41 -08:00
download(who_am_i(), 'playlists', 'playlists')
elif arguments['-m']:
download(who_am_i(), 'playlists-liked', 'my and liked playlists')
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()
2016-08-27 07:33:16 -07:00
def get_item(track_url, client_id=CLIENT_ID):
2014-11-16 09:19:42 -08:00
"""
Fetches metadata for an track or playlist
"""
try:
2016-03-01 14:58:59 -08:00
item_url = url['resolve'].format(track_url)
2016-04-17 05:32:04 -07:00
r = requests.get(item_url, params={'client_id': client_id})
logger.debug(r.url)
2016-04-17 05:32:04 -07:00
if r.status_code == 403:
2016-08-27 07:33:16 -07:00
return get_item(track_url, ALT_CLIENT_ID)
2016-04-17 05:32:04 -07:00
2016-03-01 14:58:59 -08:00
item = r.json()
2016-04-17 05:32:04 -07:00
no_tracks = item['kind'] == 'playlist' and not item['tracks']
2016-08-27 07:33:16 -07:00
if no_tracks and client_id != ALT_CLIENT_ID:
return get_item(track_url, ALT_CLIENT_ID)
2014-11-16 09:19:42 -08:00
except Exception:
2016-08-27 07:33:16 -07:00
if client_id == ALT_CLIENT_ID:
2016-04-23 09:16:53 -07:00
logger.error('Get item failed...')
return
logger.error('Error resolving url, retrying...')
time.sleep(5)
try:
2016-08-27 07:33:16 -07:00
return get_item(track_url, ALT_CLIENT_ID)
except Exception as e:
2015-05-09 15:13:11 -07:00
logger.error('Could not resolve url {0}'.format(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
"""
2016-02-08 05:32:59 -08:00
Detects if the URL is a track or playlists, and parses the track(s)
to the track downloader
2014-11-16 09:19:42 -08:00
"""
2015-01-14 08:55:14 -08:00
global arguments
2014-11-16 09:19:42 -08:00
item = get_item(track_url)
2016-03-01 14:58:59 -08:00
logger.debug(item)
2014-11-16 09:19:42 -08:00
if not item:
return
2016-03-01 14:58:59 -08:00
elif item['kind'] == 'track':
2015-05-09 15:13:11 -07:00
logger.info('Found a track')
2016-03-01 14:58:59 -08:00
download_track(item)
elif item['kind'] == 'playlist':
2015-05-09 15:13:11 -07:00
logger.info('Found a playlist')
2016-03-01 14:58:59 -08:00
download_playlist(item)
elif item['kind'] == 'user':
logger.info('Found a user profile')
2015-05-09 15:13:11 -07:00
if arguments['-f']:
2016-03-01 13:27:47 -08:00
download(item, 'favorites', 'likes')
2015-05-09 15:13:11 -07:00
elif arguments['-t']:
2016-03-01 13:27:47 -08:00
download(item, 'tracks', 'uploaded tracks')
2015-05-09 15:13:11 -07:00
elif arguments['-a']:
2016-03-01 13:27:47 -08:00
download(item, 'all', 'tracks and reposts')
2015-05-09 15:13:11 -07:00
elif arguments['-p']:
2016-03-01 13:27:47 -08:00
download(item, 'playlists', 'playlists')
elif arguments['-m']:
download(item, 'playlists-liked', 'my and liked playlists')
2014-11-16 09:19:42 -08:00
else:
logger.error('Please provide a download type...')
2014-11-16 09:19:42 -08:00
else:
2015-05-09 15:13:11 -07:00
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
"""
2016-03-01 14:58:59 -08:00
me = url['me'].format(token)
r = requests.get(me, params={'client_id': CLIENT_ID})
2016-09-27 11:06:15 -07:00
r.raise_for_status()
2016-03-01 14:58:59 -08:00
current_user = r.json()
logger.debug(me)
2014-11-16 09:19:42 -08:00
2016-03-01 14:58:59 -08:00
logger.info('Hello {0}!'.format(current_user['username']))
logger.newline()
2014-11-16 09:19:42 -08:00
return current_user
2014-10-23 07:14:29 -07:00
2016-02-29 09:08:41 -08:00
def download(user, dl_type, name):
2014-11-16 09:19:42 -08:00
"""
Download all items of a user
2014-11-16 09:19:42 -08:00
"""
username = user['username']
2016-02-29 09:08:41 -08:00
user_id = user['id']
2016-02-08 05:32:59 -08:00
logger.info(
'Retrieving all {0} of user {1}...'.format(name, username)
)
2016-02-29 09:08:41 -08:00
dl_url = url[dl_type].format(user_id, offset)
logger.debug(dl_url)
ressources = client.get_collection(dl_url, token)
logger.debug(ressources)
total = len(ressources)
logger.info('Retrieved {0} {1}'.format(total, name))
for counter, item in enumerate(ressources, 1):
2015-08-24 18:19:28 -07:00
try:
logger.debug(item)
2016-02-08 05:32:59 -08:00
logger.info('{0}{1} of {2}'.format(
name.capitalize(), counter + offset, total)
)
if dl_type == 'all':
item_name = item['type'].split('-')[0] # remove the '-repost'
uri = item[item_name]['uri']
parse_url(uri)
elif dl_type == 'playlists':
download_playlist(item)
elif dl_type == 'playlists-liked':
parse_url(item['playlist']['uri'])
else:
download_track(item)
except Exception as e:
logger.exception(e)
logger.info('Downloaded all {0} {1} of user {2}!'.format(
total, name, username)
2016-02-08 05:32:59 -08:00
)
2014-11-16 09:19:42 -08:00
2014-10-12 15:16:18 -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
"""
2015-01-19 13:11:55 -08:00
invalid_chars = '\/:*?|<>"'
2016-04-23 08:25:19 -07:00
playlist_name = playlist['title'].encode('utf-8', 'ignore')
2016-04-24 07:31:45 -07:00
playlist_name = playlist_name.decode('utf8')
2015-01-19 13:11:55 -08:00
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)
2016-04-24 07:31:45 -07:00
with codecs.open(playlist_name + '.m3u', 'w+', 'utf8') as playlist_file:
playlist_file.write('#EXTM3U' + os.linesep)
2016-02-08 05:01:13 -08:00
for counter, track_raw in enumerate(playlist['tracks'], 1):
2016-02-07 17:12:43 -08:00
logger.debug(track_raw)
logger.info('Track n°{0}'.format(counter))
2016-02-08 05:01:13 -08:00
download_track(track_raw, playlist['title'], playlist_file)
2015-01-19 13:11:55 -08:00
os.chdir('..')
2014-11-16 09:19:42 -08:00
2014-10-23 07:14:29 -07:00
def download_my_stream():
2015-01-05 14:22:14 -08:00
"""
DONT WORK FOR NOW
Download the stream of the current user
"""
# TODO
# Use Token
def download_all_of_a_page(tracks):
"""
NOT RECOMMENDED
2015-01-05 14:22:14 -08:00
Download all song of a page
"""
2016-02-08 05:32:59 -08:00
logger.error(
'NOTE: This will only download the songs of the page.(49 max)'
)
logger.error('I recommend you to provide a user link and a download type.')
2015-05-16 05:31:19 -07:00
for counter, track in enumerate(tracks, 1):
logger.newline()
2015-05-16 05:31:19 -07:00
logger.info('Track n°{0}'.format(counter))
2015-01-05 14:22:14 -08:00
download_track(track)
def download_track(track, playlist_name=None, playlist_file=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
2016-03-01 14:58:59 -08:00
title = track['title']
2016-04-24 07:31:45 -07:00
title = title.encode('utf-8', 'ignore').decode('utf8')
2016-02-07 16:04:16 -08:00
if track['streamable']:
url = track['stream_url']
2014-11-16 09:19:42 -08:00
else:
2016-02-07 17:35:51 -08:00
logger.error('{0} is not streamable...'.format(title))
logger.newline()
2014-11-16 09:19:42 -08:00
return
2015-05-09 15:13:11 -07:00
logger.info('Downloading {0}'.format(title))
2014-11-16 09:19:42 -08:00
r = None
2016-01-31 05:15:45 -08:00
# filename
2016-02-07 16:04:16 -08:00
if track['downloadable'] and not arguments['--onlymp3']:
2016-04-11 12:17:17 -07:00
logger.info('Downloading the original file.')
original_url = track['download_url']
r = requests.get(original_url, params={'client_id': CLIENT_ID}, stream=True)
if r.status_code == 401:
logger.info('The original file has no download left.')
username = track['user']['username']
if username not in title and arguments['--addtofile']:
title = '{0} - {1}'.format(username, title)
filename = title + '.mp3'
else:
d = r.headers['content-disposition']
filename = re.findall("filename=(.+)", d)[0][1:-1]
2014-11-16 09:19:42 -08:00
else:
2016-02-07 16:04:16 -08:00
username = track['user']['username']
if username not in title and arguments['--addtofile']:
title = '{0} - {1}'.format(username, title)
2014-11-16 09:19:42 -08:00
filename = title + '.mp3'
2016-04-24 09:42:41 -07:00
invalid_chars = '\/:*?|<>"'
filename = ''.join(c for c in filename if c not in invalid_chars)
2016-02-07 17:35:51 -08:00
logger.debug("filename : {0}".format(filename))
# Add the track to the generated m3u playlist file
if playlist_file:
2016-02-07 16:04:16 -08:00
duration = math.floor(track['duration'] / 1000)
2016-02-08 05:32:59 -08:00
playlist_file.write(
'#EXTINF:{0},{1}{3}{2}{3}'.format(
duration, title, filename, os.linesep
)
)
2014-11-16 09:19:42 -08:00
# Download
if not os.path.isfile(filename):
2016-12-07 13:25:22 -08:00
if r is None or r.status_code == 401:
r = requests.get(url, params={'client_id': CLIENT_ID}, stream=True)
2016-12-07 13:25:22 -08:00
logger.debug(r.url)
if r.status_code == 401:
r = requests.get(url, params={'client_id': ALT_CLIENT_ID}, stream=True)
logger.debug(r.url)
r.raise_for_status()
2016-03-01 14:12:34 -08:00
temp = tempfile.NamedTemporaryFile(delete=False)
total_length = int(r.headers.get('content-length'))
min_size = arguments.get('--min-size')
max_size = arguments.get('--max-size')
if min_size is not None and total_length < min_size:
logging.info('{0} not large enough, skipping'.format(title))
return
if max_size is not None and total_length > max_size:
logging.info('{0} too large, skipping'.format(title))
return
2016-03-01 14:12:34 -08:00
with temp as f:
2016-02-08 05:32:59 -08:00
for chunk in progress.bar(
r.iter_content(chunk_size=1024),
2016-04-23 09:11:26 -07:00
expected_size=(total_length/1024) + 1,
hide=True if arguments["--hide-progress"] else False
2016-02-08 05:32:59 -08:00
):
2016-02-07 17:12:43 -08:00
if chunk:
f.write(chunk)
f.flush()
2016-03-01 14:12:34 -08:00
shutil.move(temp.name, os.path.join(os.getcwd(), filename))
if filename.endswith('.mp3') or filename.endswith('.m4a'):
2014-11-16 09:19:42 -08:00
try:
settags(track, filename, playlist_name)
2016-02-07 16:04:16 -08:00
except Exception as e:
logger.error('Error trying to set the tags...')
2016-02-07 16:04:16 -08:00
logger.debug(e)
2014-11-16 09:19:42 -08:00
else:
2015-05-09 15:13:11 -07:00
logger.error("This type of audio doesn't support tagging...")
2014-11-16 09:19:42 -08:00
else:
2015-05-09 15:13:11 -07:00
if arguments['-c']:
logger.info('{0} already Downloaded'.format(title))
logger.newline()
2014-11-16 09:19:42 -08:00
return
else:
logger.newline()
2015-05-09 15:13:11 -07:00
logger.error('Music already exists ! (exiting)')
2014-11-16 09:19:42 -08:00
sys.exit(0)
logger.newline()
2015-05-09 15:13:11 -07:00
logger.info('{0} Downloaded.'.format(filename))
logger.newline()
2014-11-16 09:19:42 -08:00
2015-12-21 10:51:23 -08:00
def settags(track, filename, album=None):
2014-11-16 09:19:42 -08:00
"""
Set the tags to the mp3
"""
2015-05-09 15:13:11 -07:00
logger.info('Settings tags...')
2016-02-07 16:04:16 -08:00
artwork_url = track['artwork_url']
user = track['user']
2016-02-08 05:32:59 -08:00
if not artwork_url:
artwork_url = user['avatar_url']
2014-11-16 09:19:42 -08:00
artwork_url = artwork_url.replace('large', 't500x500')
2016-02-07 17:12:43 -08:00
response = requests.get(artwork_url, stream=True)
2016-03-01 14:12:34 -08:00
with tempfile.NamedTemporaryFile() as out_file:
2016-02-07 17:12:43 -08:00
shutil.copyfileobj(response.raw, out_file)
2016-03-01 14:12:34 -08:00
out_file.seek(0)
audio = mutagen.File(filename)
audio['TIT2'] = mutagen.id3.TIT2(encoding=3, text=track['title'])
audio['TPE1'] = mutagen.id3.TPE1(encoding=3, text=user['username'])
2016-03-01 14:12:34 -08:00
audio['TCON'] = mutagen.id3.TCON(encoding=3, text=track['genre'])
if album:
audio['TALB'] = mutagen.id3.TALB(encoding=3, text=album)
if artwork_url:
audio['APIC'] = mutagen.id3.APIC(
encoding=3, mime='image/jpeg', type=3, desc='Cover',
data=out_file.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
2014-11-16 09:19:42 -08:00
"""
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
2015-05-09 15:13:11 -07:00
if __name__ == '__main__':
2014-11-16 09:19:42 -08:00
main()