scdl/scdl/scdl.py

781 lines
26 KiB
Python
Raw Normal View History

2015-05-14 00:41:45 -07:00
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
2017-12-10 06:43:07 -08:00
"""scdl allows you to download music from Soundcloud
Usage:
scdl -l <track_url> [-a | -f | -C | -t | -p][-c | --force-metadata][-n <maxtracks>]\
[-o <offset>][--hidewarnings][--debug | --error][--path <path>][--addtofile][--addtimestamp]
[--onlymp3][--hide-progress][--min-size <size>][--max-size <size>][--remove][--no-album-tag]
2018-04-10 04:10:08 -07:00
[--no-playlist-folder][--download-archive <file>][--extract-artist][--flac]
scdl me (-s | -a | -f | -t | -p | -m)[-c | --force-metadata][-n <maxtracks>]\
[-o <offset>][--hidewarnings][--debug | --error][--path <path>][--addtofile][--addtimestamp]
2017-12-26 03:05:15 -08:00
[--onlymp3][--hide-progress][--min-size <size>][--max-size <size>][--remove]
[--no-playlist-folder][--download-archive <file>][--extract-artist][--flac][--no-album-tag]
2015-01-14 08:55:14 -08:00
scdl -h | --help
scdl --version
Options:
2017-12-26 03:05:15 -08:00
-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
-n [maxtracks] Download the n last tracks of a playlist according to the creation date
2017-12-26 03:05:15 -08:00
-s Download the stream of a user (token needed)
-a Download all tracks of user (including reposts)
-t Download all uploads of a user (no reposts)
-f Download all favorites of a user
-C Download all commented by a user
-p Download all playlists of a user
-m Download all liked and owned playlists of user
-c Continue if a downloaded file already exists
--force-metadata This will set metadata on already downloaded track
2017-12-26 03:05:15 -08:00
-o [offset] Begin with a custom offset
--addtimestamp Add track creation timestamp to filename,
which allows for chronological sorting
--addtofile Add artist to filename if missing
--debug Set log level to DEBUG
--download-archive [file] Keep track of track IDs in an archive file,
and skip already-downloaded files
--error Set log level to ERROR
2018-01-08 05:54:04 -08:00
--extract-artist Set artist tag from title instead of username
2017-12-26 03:05:15 -08:00
--hide-progress Hide the wget progress bar
--hidewarnings Hide Warnings. (use with precaution)
--max-size [max-size] Skip tracks larger than size (k/m/g)
--min-size [min-size] Skip tracks smaller than size (k/m/g)
--no-playlist-folder Download playlist tracks into main directory,
instead of making a playlist subfolder
--onlymp3 Download only the streamable mp3 file,
even if track has a Downloadable file
--path [path] Use a custom path for downloaded files
--remove Remove any files not downloaded from execution
2018-04-11 09:27:32 -07:00
--flac Convert original files to .flac
--no-album-tag On some player track get the same cover art if from the same album, this prevent it
"""
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
2018-04-10 04:10:08 -07:00
import shlex
import shutil
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
from datetime import datetime
2019-12-07 07:59:31 -08:00
import subprocess
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-01-14 08:55:14 -08:00
arguments = None
token = ''
2014-12-02 17:16:04 -08:00
path = ''
offset = 1
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'
2017-09-19 05:37:26 -07:00
'/liked_and_owned?limit=200'),
2020-03-30 10:17:00 -07:00
'favorites': ('https://api-v2.soundcloud.com/users/{0}/track_likes?'
2017-09-19 05:37:26 -07:00
'limit=200'),
2020-03-30 10:17:00 -07:00
'commented': ('https://api-v2.soundcloud.com/users/{0}/comments'),
'tracks': ('https://api-v2.soundcloud.com/users/{0}/tracks?'
2017-09-19 05:37:26 -07:00
'limit=200'),
2016-02-29 09:08:41 -08:00
'all': ('https://api-v2.soundcloud.com/profile/soundcloud:users:{0}?'
2017-09-19 05:37:26 -07:00
'limit=200'),
2020-03-30 10:17:00 -07:00
'playlists': ('https://api-v2.soundcloud.com/users/{0}/playlists?'
'limit=5'),
2020-03-30 10:17:00 -07:00
'resolve': ('https://api-v2.soundcloud.com/resolve?url={0}'),
'trackinfo': ('https://api-v2.soundcloud.com/tracks/{0}'),
2020-03-30 10:17:00 -07:00
'original_download' : ("https://api-v2.soundcloud.com/tracks/{0}/download"),
'user': ('https://api-v2.soundcloud.com/users/{0}'),
'me': ('https://api-v2.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
fileToKeep = []
2014-10-12 15:16:18 -07:00
2017-12-26 03:05:15 -08:00
def main():
2014-11-16 09:19:42 -08:00
"""
2017-12-10 06:43:07 -08:00
Main function, parses the URL from command line arguments
2014-11-16 09:19:42 -08:00
"""
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
# 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
2017-01-27 08:27:53 -08:00
# import conf file
get_config()
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:
offset = int(arguments['-o'])
if offset < 0:
raise
2014-11-16 09:19:42 -08:00
except:
logger.error('Offset should be a positive integer...')
sys.exit(-1)
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(-1)
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(-1)
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...')
sys.exit(-1)
2019-12-07 07:59:31 -08:00
logger.debug('Downloading to ' + os.getcwd() + '...')
2014-12-07 15:15:04 -08:00
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')
if arguments['-C']:
download(who_am_i(), 'commented', 'commented tracks')
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
if arguments['--remove']:
2017-12-10 06:43:07 -08:00
remove_files()
def get_config():
2014-11-16 09:19:42 -08:00
"""
2017-12-10 06:43:07 -08:00
Reads the music download filepath from scdl.cfg
2014-11-16 09:19:42 -08:00
"""
global token
config = configparser.ConfigParser()
if 'XDG_CONFIG_HOME' in os.environ:
config_file = os.path.join(
os.environ['XDG_CONFIG_HOME'], 'scdl', 'scdl.cfg',
)
else:
config_file = os.path.join(
os.path.expanduser('~'), '.config', 'scdl', 'scdl.cfg',
)
config.read(config_file, 'utf8')
2014-11-16 09:19:42 -08:00
try:
token = config['scdl']['auth_token']
path = config['scdl']['path']
except:
2017-12-26 03:05:15 -08:00
logger.error('Are you sure scdl.cfg is in $HOME/.config/scdl/ ?')
logger.error('Are both "auth_token" and "path" defined there?')
sys.exit(-1)
2014-11-16 09:19:42 -08:00
if os.path.exists(path):
os.chdir(path)
else:
logger.error('Invalid path in scdl.cfg...')
sys.exit(-1)
2014-11-16 09:19:42 -08:00
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
"""
2017-12-10 06:43:07 -08:00
Fetches metadata for a track or playlist
2014-11-16 09:19:42 -08:00
"""
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:
2017-12-10 06:43:07 -08:00
logger.error('Failed to get item...')
2016-04-23 09:16:53 -07:00
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(-1)
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
"""
2017-12-10 06:43:07 -08:00
Detects if a URL is a track or a playlist, and parses the track(s)
2016-02-08 05:32:59 -08:00
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')
elif arguments['-C']:
download(item, 'commented', 'commented tracks')
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:
2017-12-10 06:43:07 -08:00
logger.error('Unknown item type {0}'.format(item['kind']))
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
"""
2017-12-10 06:43:07 -08:00
Display username from current token and check for validity
2014-11-16 09:19:42 -08:00
"""
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']))
2014-11-16 09:19:42 -08:00
return current_user
2017-12-10 06:43:07 -08:00
def remove_files():
"""
2017-12-10 06:43:07 -08:00
Removes any pre-existing tracks that were not just downloaded
"""
2017-12-10 06:43:07 -08:00
logger.info("Removing local track files that were not downloaded...")
files = [f for f in os.listdir('.') if os.path.isfile(f)]
for f in files:
2017-12-26 03:05:15 -08:00
if f not in fileToKeep:
os.remove(f)
2017-12-26 03:05:15 -08:00
def get_track_info(track):
"""
2017-12-10 06:43:07 -08:00
Fetches track info from Soundcloud, given a track_id
"""
if 'media' in track:
return track
logger.info('Retrieving more info on the track')
2020-03-30 10:55:22 -07:00
info_url = url["trackinfo"].format(track['id'])
r = requests.get(info_url, params={'client_id': CLIENT_ID}, stream=True)
item = r.json()
logger.debug(item)
return item
2014-10-23 07:14:29 -07:00
2017-12-26 03:05:15 -08:00
2016-02-29 09:08:41 -08:00
def download(user, dl_type, name):
2014-11-16 09:19:42 -08:00
"""
2017-12-10 06:43:07 -08:00
Download user items of dl_type (ie. all, playlists, liked, commented, etc.)
2014-11-16 09:19:42 -08:00
"""
if not is_ffmpeg_available():
logger.error('ffmpeg is not available and download cannot continue. Please install ffmpeg and re-run the program.')
return
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)
)
2017-09-19 05:37:26 -07:00
dl_url = url[dl_type].format(user_id)
logger.debug(dl_url)
2017-12-10 06:43:07 -08:00
resources = client.get_collection(dl_url, token)
del resources[:offset - 1]
logger.debug(resources)
total = len(resources)
logger.info('Retrieved {0} {1}'.format(total, name))
2017-12-10 06:43:07 -08:00
for counter, item in enumerate(resources, offset):
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, total)
2016-02-08 05:32:59 -08:00
)
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'])
2020-03-30 10:17:00 -07:00
elif dl_type == 'tracks':
download_track(item)
else:
2020-03-30 10:17:00 -07:00
download_track(item['track'])
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
"""
2017-12-10 06:43:07 -08:00
Downloads a playlist
2014-11-16 09:19:42 -08:00
"""
global arguments
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 arguments['--no-playlist-folder']:
if not os.path.exists(playlist_name):
os.makedirs(playlist_name)
os.chdir(playlist_name)
2015-01-19 13:11:55 -08:00
try:
with codecs.open(playlist_name + '.m3u', 'w+', 'utf8') as playlist_file:
playlist_file.write('#EXTM3U' + os.linesep)
if arguments['-n']: # Order by creation date and get the n lasts tracks
playlist['tracks'].sort(key=lambda track: track['created_at'], reverse=True)
playlist['tracks'] = playlist['tracks'][:int(arguments['-n'])]
else:
del playlist['tracks'][:offset - 1]
for counter, track_raw in enumerate(playlist['tracks'], offset):
logger.debug(track_raw)
logger.info('Track n°{0}'.format(counter))
playlist_info = {'title': playlist['title'], 'file': playlist_file, 'tracknumber': counter}
download_track(track_raw, playlist_info)
finally:
if not arguments['--no-playlist-folder']:
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
2017-12-26 03:05:15 -08:00
def try_utime(path, filetime):
try:
os.utime(path, (time.time(), filetime))
except:
logger.error("Cannot update utime of file")
def get_filename(track, original_filename=None):
2017-11-21 08:38:14 -08:00
invalid_chars = '\/:*?|<>"'
2017-01-27 08:01:31 -08:00
username = track['user']['username']
title = track['title'].encode('utf-8', 'ignore').decode('utf8')
2017-12-24 02:35:27 -08:00
if arguments['--addtofile']:
2017-12-15 02:41:57 -08:00
if username not in title and '-' not in title:
title = '{0} - {1}'.format(username, title)
2017-12-24 02:35:27 -08:00
logger.debug('Adding "{0}" to filename'.format(username))
if arguments['--addtimestamp']:
# created_at sample: 2019-01-30T11:11:37Z
2019-12-07 07:59:31 -08:00
ts = datetime \
2020-01-14 12:00:20 -08:00
.strptime(track['created_at'], "%Y-%m-%dT%H:%M:%SZ") \
.timestamp()
title = str(int(ts)) + "_" + title
ext = ".mp3"
if original_filename is not None:
original_filename.encode('utf-8', 'ignore').decode('utf8')
ext = os.path.splitext(original_filename)[1]
filename = title[:251] + ext.lower()
2017-11-21 08:38:14 -08:00
filename = ''.join(c for c in filename if c not in invalid_chars)
return filename
2017-01-27 08:01:31 -08:00
2015-01-05 14:22:14 -08:00
def download_original_file(track, title):
logger.info('Downloading the original file.')
2020-03-30 10:17:00 -07:00
original_url = url['original_download'].format(track['id'])
# Get the requests stream
r = requests.get(
2020-04-08 04:30:19 -07:00
original_url, params={'client_id': CLIENT_ID}
)
2020-04-08 04:30:19 -07:00
r = requests.get(r.json()['redirectUri'], stream=True)
if r.status_code == 401:
logger.info('The original file has no download left.')
return (None, False)
2019-12-19 03:32:31 -08:00
if r.status_code == 404:
logger.info('Could not get name from stream - using basic name')
return (None, False)
# Find filename
d = r.headers.get('content-disposition')
2020-04-08 04:30:19 -07:00
filename = re.findall("filename=(.+)", d)[0]
filename = get_filename(track, filename)
2017-12-24 02:35:27 -08:00
logger.debug("filename : {0}".format(filename))
2014-11-16 09:19:42 -08:00
2017-12-24 02:35:27 -08:00
# Skip if file ID or filename already exists
2017-12-26 03:05:15 -08:00
if already_downloaded(track, title, filename):
if arguments['--flac'] and can_convert(filename):
filename = filename[:-4] + ".flac"
return (filename, True)
# Write file
2017-12-24 02:35:27 -08:00
total_length = int(r.headers.get('content-length'))
temp = tempfile.NamedTemporaryFile(delete=False)
2019-12-07 07:59:31 -08:00
received = 0
2017-12-24 02:35:27 -08:00
with temp as f:
for chunk in progress.bar(
2019-12-07 07:59:31 -08:00
r.iter_content(chunk_size=1024),
expected_size=(total_length / 1024) + 1,
hide=True if arguments["--hide-progress"] else False
2017-12-24 02:35:27 -08:00
):
if chunk:
2019-12-07 07:59:31 -08:00
received += len(chunk)
2017-12-24 02:35:27 -08:00
f.write(chunk)
f.flush()
if received != total_length:
logger.error('connection closed prematurely, download incomplete')
sys.exit(-1)
2017-12-24 02:35:27 -08:00
shutil.move(temp.name, os.path.join(os.getcwd(), filename))
2018-04-11 09:27:32 -07:00
if arguments['--flac'] and can_convert(filename):
logger.info('Converting to .flac...')
2018-04-10 04:10:08 -07:00
newfilename = filename[:-4] + ".flac"
commands = ['ffmpeg', '-i', filename, newfilename, '-loglevel', 'error']
2020-01-21 01:35:31 -08:00
logger.debug("Commands: {}".format(commands))
subprocess.call(commands)
2018-04-10 06:06:51 -07:00
os.remove(filename)
2018-04-10 04:10:08 -07:00
filename = newfilename
return (filename, False)
def get_track_m3u8(track):
url = None
for transcoding in track['media']['transcodings']:
if transcoding['format']['protocol'] == 'hls' \
2019-12-07 07:59:31 -08:00
and transcoding['format']['mime_type'] == 'audio/mpeg':
url = transcoding['url']
if url is not None:
r = requests.get(url, params={'client_id': CLIENT_ID})
logger.debug(r.url)
return r.json()['url']
def download_hls_mp3(track, title):
filename = get_filename(track)
logger.debug("filename : {0}".format(filename))
# Skip if file ID or filename already exists
if already_downloaded(track, title, filename):
return (filename, True)
# Get the requests stream
url = get_track_m3u8(track)
2020-01-21 01:35:31 -08:00
filename_path = os.path.abspath(filename)
subprocess.call(['ffmpeg', '-i', url, '-c', 'copy', filename_path, '-loglevel', 'fatal'])
return (filename, False)
def download_track(track, playlist_info=None):
"""
Downloads a track
"""
global arguments
track = get_track_info(track)
title = track['title']
title = title.encode('utf-8', 'ignore').decode('utf8')
logger.info('Downloading {0}'.format(title))
# Not streamable
if not track['streamable']:
logger.error('{0} is not streamable...'.format(title))
return
# Geoblocked track
if track['policy'] == 'BLOCK':
logger.error('{0} is not available in your location...\n'.format(title))
return
# Downloadable track
filename = None
is_already_downloaded = False
2020-03-30 10:17:00 -07:00
if track['downloadable'] and track['has_downloads_left'] and not arguments['--onlymp3']:
filename, is_already_downloaded = download_original_file(track, title)
if filename is None:
filename, is_already_downloaded = download_hls_mp3(track, title)
# Add the track to the generated m3u playlist file
if playlist_info:
duration = math.floor(track['duration'] / 1000)
playlist_info['file'].write(
'#EXTINF:{0},{1}{3}{2}{3}'.format(
duration, title, filename, os.linesep
)
)
if arguments['--remove']:
fileToKeep.append(filename)
record_download_archive(track)
2020-06-08 09:42:08 -07:00
# Skip if file ID or filename already exists
if is_already_downloaded and not arguments['--force-metadata']:
2020-06-08 09:42:08 -07:00
logger.info('Track "{0}" already downloaded.'.format(title))
return
# If file does not exist an error occured
if not os.path.isfile(filename):
logger.error('An error occured downloading {0}.\n'.format(filename))
logger.error('Exiting...')
sys.exit(-1)
# Try to set the metadata
2018-04-11 09:27:32 -07:00
if filename.endswith('.mp3') or filename.endswith('.flac'):
2017-12-24 02:35:27 -08:00
try:
set_metadata(track, filename, playlist_info)
2017-12-24 02:35:27 -08:00
except Exception as e:
logger.error('Error trying to set the tags...')
logger.debug(e)
else:
logger.error("This type of audio doesn't support tagging...")
2017-12-26 03:05:15 -08:00
# Try to change the real creation date
2017-12-24 02:35:27 -08:00
created_at = track['created_at']
timestamp = datetime.strptime(created_at, '%Y-%m-%dT%H:%M:%SZ')
2017-12-26 03:05:15 -08:00
filetime = int(time.mktime(timestamp.timetuple()))
try_utime(filename, filetime)
2017-12-24 02:35:27 -08:00
logger.info('{0} Downloaded.\n'.format(filename))
2018-04-11 09:27:32 -07:00
def can_convert(filename):
ext = os.path.splitext(filename)[1]
return 'wav' in ext or 'aif' in ext
2018-04-11 09:27:32 -07:00
2019-12-07 07:59:31 -08:00
2018-04-11 09:27:32 -07:00
def already_downloaded(track, title, filename):
2017-12-24 02:35:27 -08:00
"""
Returns True if the file has already been downloaded
"""
global arguments
already_downloaded = False
2018-04-11 09:27:32 -07:00
if os.path.isfile(filename):
2017-12-24 02:35:27 -08:00
already_downloaded = True
2018-04-11 09:27:32 -07:00
if arguments['--flac'] and can_convert(filename) \
2019-12-07 07:59:31 -08:00
and os.path.isfile(filename[:-4] + ".flac"):
2018-04-11 08:30:55 -07:00
already_downloaded = True
2017-12-24 02:35:27 -08:00
if arguments['--download-archive'] and in_download_archive(track):
already_downloaded = True
2018-04-11 09:27:32 -07:00
if arguments['--flac'] and can_convert(filename) and os.path.isfile(filename):
already_downloaded = False
2017-12-24 02:35:27 -08:00
if already_downloaded:
if arguments['-c'] or arguments['--remove'] or arguments['--force-metadata']:
2017-12-24 02:35:27 -08:00
return True
2014-11-16 09:19:42 -08:00
else:
2017-12-26 03:05:15 -08:00
logger.error('Track "{0}" already exists!'.format(title))
logger.error('Exiting... (run again with -c to continue)')
sys.exit(-1)
2017-12-24 02:35:27 -08:00
return False
2014-11-16 09:19:42 -08:00
2017-12-24 02:35:27 -08:00
def in_download_archive(track):
"""
Returns True if a track_id exists in the download archive
"""
global arguments
2017-12-26 03:05:15 -08:00
if not arguments['--download-archive']:
return
2017-12-24 02:35:27 -08:00
archive_filename = arguments.get('--download-archive')
try:
with open(archive_filename, 'a+', encoding='utf-8') as file:
file.seek(0)
track_id = '{0}'.format(track['id'])
for line in file:
if line.strip() == track_id:
return True
except IOError as ioe:
logger.error('Error trying to read download archive...')
logger.debug(ioe)
return False
def record_download_archive(track):
"""
Write the track_id in the download archive
"""
global arguments
2017-12-26 03:05:15 -08:00
if not arguments['--download-archive']:
return
2017-12-24 02:35:27 -08:00
archive_filename = arguments.get('--download-archive')
try:
with open(archive_filename, 'a', encoding='utf-8') as file:
2019-12-07 07:59:31 -08:00
file.write('{0}'.format(track['id']) + '\n')
2017-12-24 02:35:27 -08:00
except IOError as ioe:
logger.error('Error trying to write to download archive...')
logger.debug(ioe)
2014-11-16 09:19:42 -08:00
def set_metadata(track, filename, playlist_info=None):
2014-11-16 09:19:42 -08:00
"""
2017-12-10 06:43:07 -08:00
Sets the mp3 file metadata using the Python module Mutagen
2014-11-16 09:19:42 -08:00
"""
2017-12-10 06:43:07 -08:00
logger.info('Setting tags...')
2018-01-08 05:54:04 -08:00
global arguments
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)
2017-12-26 03:05:15 -08:00
track_created = track['created_at']
track_date = datetime.strptime(track_created, "%Y-%m-%dT%H:%M:%SZ")
2017-12-26 03:05:15 -08:00
debug_extract_dates = '{0} {1}'.format(track_created, track_date)
logger.debug('Extracting date: {0}'.format(debug_extract_dates))
2018-04-10 06:06:51 -07:00
track['date'] = track_date.strftime("%Y-%m-%d %H::%M::%S")
2018-01-08 05:54:04 -08:00
track['artist'] = user['username']
if arguments['--extract-artist']:
for dash in [' - ', ' ', ' ', '', '']:
if dash in track['title']:
artist_title = track['title'].split(dash)
track['artist'] = artist_title[0].strip()
track['title'] = artist_title[1].strip()
break
2018-01-08 05:54:04 -08:00
2018-04-10 06:06:51 -07:00
audio = mutagen.File(filename, easy=True)
audio['title'] = track['title']
audio['artist'] = track['artist']
if track['genre']: audio['genre'] = track['genre']
if track['permalink_url']: audio['website'] = track['permalink_url']
if track['date']: audio['date'] = track['date']
if playlist_info:
if not arguments['--no-album-tag']:
audio['album'] = playlist_info['title']
audio['tracknumber'] = str(playlist_info['tracknumber'])
2018-04-11 08:30:55 -07:00
audio.save()
a = mutagen.File(filename)
2018-01-02 07:02:37 -08:00
if track['description']:
if a.__class__ == mutagen.flac.FLAC:
2018-04-11 08:30:55 -07:00
a['description'] = track['description']
elif a.__class__ == mutagen.mp3.MP3:
2018-04-10 06:06:51 -07:00
a['COMM'] = mutagen.id3.COMM(
encoding=3, lang=u'ENG', text=track['description']
)
2016-03-01 14:12:34 -08:00
if artwork_url:
if a.__class__ == mutagen.flac.FLAC:
2018-04-11 08:30:55 -07:00
p = mutagen.flac.Picture()
p.data = out_file.read()
p.width = 500
p.height = 500
p.type = mutagen.id3.PictureType.COVER_FRONT
a.add_picture(p)
elif a.__class__ == mutagen.mp3.MP3:
2018-04-10 06:06:51 -07:00
a['APIC'] = mutagen.id3.APIC(
2018-04-11 08:30:55 -07:00
encoding=3, mime='image/jpeg', type=3,
desc='Cover', data=out_file.read()
2016-03-01 14:12:34 -08:00
)
2018-04-11 08:30:55 -07:00
a.save()
2014-11-16 09:19:42 -08:00
2014-10-23 07:14:29 -07:00
def signal_handler(signal, frame):
2014-11-16 09:19:42 -08:00
"""
2017-12-10 06:43:07 -08:00
Handle keyboard interrupt
2014-11-16 09:19:42 -08:00
"""
2017-01-27 08:27:53 -08:00
logger.info('\nGood bye!')
2014-11-16 09:19:42 -08:00
sys.exit(0)
2014-10-12 15:16:18 -07:00
def is_ffmpeg_available():
"""
Returns true if ffmpeg is available in the operating system
"""
return shutil.which('ffmpeg') is not None
2019-12-07 07:59:31 -08:00
2015-05-09 15:13:11 -07:00
if __name__ == '__main__':
2014-11-16 09:19:42 -08:00
main()