diff --git a/AUTHORS b/AUTHORS index 1787beb..4663485 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,6 +3,7 @@ Main Developers * @FlyinGrub * David Fischer @davidfischer-ch +* @7x11x13 Contributors ============ diff --git a/README.md b/README.md index 62c0839..d338ab9 100755 --- a/README.md +++ b/README.md @@ -36,34 +36,49 @@ scdl me -f ## 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 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 - -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 - --extract-artist Set artist tag from title instead of username - --flac Convert WAV files to FLAC - --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 +-h --help Show this screen +--version Show version +-l [url] URL can be track/playlist/user +-n [maxtracks] Download the n last tracks of a playlist according to the creation date +-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 +-r Download all reposts of user +-c Continue if a downloaded file already exists +--force-metadata This will set metadata on already downloaded track +-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 +--extract-artist Set artist tag from title instead of username +--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 +--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 +--original-art Download original cover art +--original-name Do not change name of original file downloads +--no-original Do not download original file; only mp3 or m4a +--only-original Only download songs with original file available +--name-format [format] Specify the downloaded file name format +--playlist-name-format [format] Specify the downloaded file name format, if it is being downloaded as part of a playlist +--client-id [id] Specify the client_id to use +--auth-token [token] Specify the auth token to use +--overwrite Overwrite file if it already exists ``` diff --git a/config/scdl.cfg b/config/scdl.cfg deleted file mode 100644 index 15c6137..0000000 --- a/config/scdl.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[scdl] -auth_token = -path = . diff --git a/scdl/__init__.py b/scdl/__init__.py index 0ec2189..16ed0b5 100644 --- a/scdl/__init__.py +++ b/scdl/__init__.py @@ -2,31 +2,4 @@ """Python Soundcloud Music Downloader.""" -import os - -__version__ = 'v1.6.12' -CLIENT_ID = 'a3e059563d7fd3372b49b37f00a00bcf' -ALT_CLIENT_ID = '2t9loNQH90kzJcsFCODdigxfp325aq4z' -ALT2_CLIENT_ID = 'NONE' - -USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36' - - -default_config = """[scdl] -auth_token = -path = . -""" - -if 'XDG_CONFIG_HOME' in os.environ: - config_dir = os.path.join(os.environ['XDG_CONFIG_HOME'], 'scdl') -else: - config_dir = os.path.join(os.path.expanduser('~'), '.config', 'scdl') - -config_file = os.path.join(config_dir, 'scdl.cfg') - -if not os.path.exists(config_file): - if not os.path.exists(config_dir): - os.makedirs(config_dir) - - with open(config_file, 'w') as f: - f.write(default_config) +__version__ = "v2.2.0" \ No newline at end of file diff --git a/scdl/client.py b/scdl/client.py deleted file mode 100644 index 04f2c7f..0000000 --- a/scdl/client.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- encoding: utf-8 -*- - -import requests -from scdl import CLIENT_ID, USER_AGENT - - -class Client(): - - def get_collection(self, url, token): - params = { - 'client_id': CLIENT_ID, - 'linked_partitioning': '1', - } - if token: - params['oauth_token'] = token - resources = list() - while url: - response = requests.get(url, params=params, headers={'User-Agent': USER_AGENT}) - response.raise_for_status() - json_data = response.json() - if 'collection' in json_data: - resources.extend(json_data['collection']) - else: - resources.extend(json_data) - if 'next_href' in json_data: - url = json_data['next_href'] - else: - url = None - return resources diff --git a/scdl/scdl.cfg b/scdl/scdl.cfg new file mode 100644 index 0000000..6b7c034 --- /dev/null +++ b/scdl/scdl.cfg @@ -0,0 +1,17 @@ +[scdl] +client_id = a3e059563d7fd3372b49b37f00a00bcf +auth_token = +path = . +name_format = {title} +playlist_name_format = {playlist[title]}_{playlist[tracknumber]}_{title} + +# example name_format values: +# {timestamp}_{user[username]}_{title} +# {id}_{user[username]}_{title} +# {id}_{user[id]}_{title} +# list of all BasicTrack attributes can be found at: https://github.com/7x11x13/soundcloud.py/blob/main/soundcloud/resource/track.py#L35 + +# playlist_name_format playlist attributes: +# playlist[author] - username of playlist author +# playlist[title] - name of playlist +# playlist[tracknumber] - tracknumber of track in playlist (zero-padded) \ No newline at end of file diff --git a/scdl/scdl.py b/scdl/scdl.py index 2e70fc9..2a2cac3 100755 --- a/scdl/scdl.py +++ b/scdl/scdl.py @@ -4,113 +4,98 @@ """scdl allows you to download music from Soundcloud Usage: - scdl -l [-a | -f | -C | -t | -p][-c | --force-metadata][-n ]\ + scdl -l [-a | -f | -C | -t | -p | -r][-c | --force-metadata][-n ] [-o ][--hidewarnings][--debug | --error][--path ][--addtofile][--addtimestamp] [--onlymp3][--hide-progress][--min-size ][--max-size ][--remove][--no-album-tag] -[--no-playlist-folder][--download-archive ][--extract-artist][--flac] - scdl me (-s | -a | -f | -t | -p | -m)[-c | --force-metadata][-n ]\ -[-o ][--hidewarnings][--debug | --error][--path ][--addtofile][--addtimestamp] -[--onlymp3][--hide-progress][--min-size ][--max-size ][--remove] -[--no-playlist-folder][--download-archive ][--extract-artist][--flac][--no-album-tag] +[--no-playlist-folder][--download-archive ][--extract-artist][--flac][--original-art] +[--original-name][--no-original][--only-original][--name-format ] +[--playlist-name-format ][--client-id ][--auth-token ][--overwrite] 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 - -n [maxtracks] Download the n last tracks of a playlist according to the creation date - -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 - -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 - --extract-artist Set artist tag from title instead of username - --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 - --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 + -h --help Show this screen + --version Show version + -l [url] URL can be track/playlist/user + -n [maxtracks] Download the n last tracks of a playlist according to the creation date + -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 + -r Download all reposts of user + -c Continue if a downloaded file already exists + --force-metadata This will set metadata on already downloaded track + -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 + --extract-artist Set artist tag from title instead of username + --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 + --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 + --original-art Download original cover art + --original-name Do not change name of original file downloads + --no-original Do not download original file; only mp3 or m4a + --only-original Only download songs with original file available + --name-format [format] Specify the downloaded file name format + --playlist-name-format [format] Specify the downloaded file name format, if it is being downloaded as part of a playlist + --client-id [id] Specify the client_id to use + --auth-token [token] Specify the auth token to use + --overwrite Overwrite file if it already exists """ +import configparser import logging +import mimetypes +import pathlib + +mimetypes.init() + import os +import re +import shutil import signal +import subprocess import sys +import tempfile import time import warnings -import math -import shutil -import requests -import re -import tempfile -import codecs -import shlex -import shutil +from dataclasses import asdict -import configparser import mutagen -from docopt import docopt +from mutagen.easymp4 import EasyMP4 + +EasyMP4.RegisterTextKey("website", "purl") +import requests from clint.textui import progress +from docopt import docopt +from pathvalidate import sanitize_filename +from soundcloud import BasicAlbumPlaylist, BasicTrack, MiniTrack, SoundCloud -from scdl import __version__, CLIENT_ID, ALT_CLIENT_ID, USER_AGENT -from scdl import client, utils +from scdl import __version__, utils -from datetime import datetime -import subprocess - -logging.basicConfig(level=logging.INFO, format='%(message)s') -logging.getLogger('requests').setLevel(logging.WARNING) +logging.basicConfig(level=logging.INFO, format="%(message)s") +logging.getLogger("requests").setLevel(logging.WARNING) logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) logger.addFilter(utils.ColorizeFilter()) -arguments = None -token = '' -path = '' -offset = 1 - -url = { - 'playlists-liked': ('https://api-v2.soundcloud.com/users/{0}/playlists' - '/liked_and_owned?limit=200'), - 'favorites': ('https://api-v2.soundcloud.com/users/{0}/track_likes?' - 'limit=200'), - 'commented': ('https://api-v2.soundcloud.com/users/{0}/comments'), - 'tracks': ('https://api-v2.soundcloud.com/users/{0}/tracks?' - 'limit=200'), - 'all': ('https://api-v2.soundcloud.com/profile/soundcloud:users:{0}?' - 'limit=200'), - 'playlists': ('https://api-v2.soundcloud.com/users/{0}/playlists?' - 'limit=5'), - 'resolve': ('https://api-v2.soundcloud.com/resolve?url={0}'), - 'trackinfo': ('https://api-v2.soundcloud.com/tracks/{0}'), - '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}') -} -client = client.Client() - fileToKeep = [] @@ -119,311 +104,259 @@ def main(): Main function, parses the URL from command line arguments """ signal.signal(signal.SIGINT, signal_handler) - global offset - global arguments - # Parse argument + # Parse arguments arguments = docopt(__doc__, version=__version__) + python_args = { + "offset": 1 + } - if arguments['--debug']: + if arguments["--debug"]: logger.level = logging.DEBUG - elif arguments['--error']: + elif arguments["--error"]: logger.level = logging.ERROR + + if "XDG_CONFIG_HOME" in os.environ: + config_file = pathlib.Path(os.environ["XDG_CONFIG_HOME"], "scdl", "scdl.cfg") + else: + config_file = pathlib.Path.home().joinpath(".config", "scdl", "scdl.cfg") # import conf file - get_config() - - logger.info('Soundcloud Downloader') - logger.debug(arguments) - - if arguments['-o'] is not None: - try: - offset = int(arguments['-o']) - if offset < 0: - raise - except: - logger.error('Offset should be a positive integer...') - sys.exit(-1) - logger.debug('offset: %d', offset) - - 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']) - - if arguments['--hidewarnings']: - warnings.filterwarnings('ignore') - - if arguments['--path'] is not None: - if os.path.exists(arguments['--path']): - os.chdir(arguments['--path']) - else: - logger.error('Invalid path in arguments...') - sys.exit(-1) - logger.debug('Downloading to ' + os.getcwd() + '...') - - if arguments['-l']: - parse_url(arguments['-l']) - elif arguments['me']: - if arguments['-f']: - download(who_am_i(), 'favorites', 'likes') - if arguments['-C']: - download(who_am_i(), 'commented', 'commented tracks') - elif arguments['-t']: - download(who_am_i(), 'tracks', 'uploaded tracks') - elif arguments['-a']: - download(who_am_i(), 'all', 'tracks and reposts') - elif arguments['-p']: - download(who_am_i(), 'playlists', 'playlists') - elif arguments['-m']: - download(who_am_i(), 'playlists-liked', 'my and liked playlists') - - if arguments['--remove']: - remove_files() - - -def get_config(): - """ - Reads the music download filepath from scdl.cfg - """ - 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') - try: - token = config['scdl']['auth_token'] - path = config['scdl']['path'] - except: - 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) + config = get_config(config_file) + + # change download path + path = config["scdl"]["path"] if os.path.exists(path): os.chdir(path) else: - logger.error('Invalid path in scdl.cfg...') + logger.error(f"Invalid download path '{path}' in {config_file}") sys.exit(-1) + + logger.info("Soundcloud Downloader") + logger.debug(arguments) + if not arguments["--client-id"]: + arguments["--client-id"] = config["scdl"]["client_id"] -def get_item(track_url, client_id=CLIENT_ID): - """ - Fetches metadata for a track or playlist - """ - try: - item_url = url['resolve'].format(track_url) + if not arguments["--auth-token"]: + arguments["--auth-token"] = config["scdl"]["auth_token"] + + client_id, token = arguments["--client-id"], arguments["--auth-token"] + + client = SoundCloud(client_id, token if token else None) + + if not client.is_client_id_valid(): + raise ValueError(f"client_id is not valid") + + if token and not client.is_auth_token_valid(): + raise ValueError(f"auth_token is not valid") - r = requests.get(item_url, params={'client_id': client_id}, headers={'User-Agent': USER_AGENT}) - logger.debug(r.url) - if r.status_code == 403: - return get_item(track_url, ALT_CLIENT_ID) - - item = r.json() - no_tracks = item['kind'] == 'playlist' and not item['tracks'] - if no_tracks and client_id != ALT_CLIENT_ID: - return get_item(track_url, ALT_CLIENT_ID) - except Exception: - if client_id == ALT_CLIENT_ID: - logger.error('Failed to get item...') - return - logger.error('Error resolving url, retrying...') - time.sleep(5) + if arguments["-o"] is not None: try: - return get_item(track_url, ALT_CLIENT_ID) - except Exception as e: - logger.error('Could not resolve url {0}'.format(track_url)) - logger.exception(e) + python_args["offset"] = int(arguments["-o"]) + if python_args["offset"] < 1: + raise ValueError() + except: + logger.error("Offset should be a positive integer...") sys.exit(-1) - return item + logger.debug("offset: %d", python_args["offset"]) + + 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"]) + + if arguments["--hidewarnings"]: + warnings.filterwarnings("ignore") + + if arguments["--path"] is not None: + if os.path.exists(arguments["--path"]): + os.chdir(arguments["--path"]) + else: + logger.error("Invalid path in arguments...") + sys.exit(-1) + logger.debug("Downloading to " + os.getcwd() + "...") + + if not arguments["--name-format"]: + arguments["--name-format"] = config["scdl"]["name_format"] + + if not arguments["--playlist-name-format"]: + arguments["--playlist-name-format"] = config["scdl"]["playlist_name_format"] + + # convert arguments dict to python_args (kwargs-friendly args) + for key, value in arguments.items(): + key = key.strip("-").replace("-", "_") + python_args[key] = value + + if arguments["-l"]: + download_url(client, **python_args) + + if arguments["--remove"]: + remove_files() + +def get_config(config_file: pathlib.Path) -> configparser.ConfigParser: + """ + Gets config from scdl.cfg + """ + config = configparser.ConfigParser() + + default_config_file = pathlib.Path(__file__).with_name("scdl.cfg") + + # load default config first + config.read(default_config_file) + + # load config file if it exists + if config_file.exists(): + config.read(config_file) + + # save config to disk + config_file.parent.mkdir(parents=True, exist_ok=True) + with open(config_file, "w", encoding="UTF-8") as f: + config.write(f) + + return config -def parse_url(track_url): +def download_url(client: SoundCloud, **kwargs): """ Detects if a URL is a track or a playlist, and parses the track(s) to the track downloader """ - global arguments - item = get_item(track_url) + url = kwargs.get("l") + item = client.resolve(url) logger.debug(item) if not item: return - elif item['kind'] == 'track': - logger.info('Found a track') - download_track(item) - elif item['kind'] == 'playlist': - logger.info('Found a playlist') - download_playlist(item) - elif item['kind'] == 'user': - logger.info('Found a user profile') - if arguments['-f']: - download(item, 'favorites', 'likes') - elif arguments['-C']: - download(item, 'commented', 'commented tracks') - elif arguments['-t']: - download(item, 'tracks', 'uploaded tracks') - elif arguments['-a']: - download(item, 'all', 'tracks and reposts') - elif arguments['-p']: - download(item, 'playlists', 'playlists') - elif arguments['-m']: - download(item, 'playlists-liked', 'my and liked playlists') + elif item.kind == "track": + logger.info("Found a track") + download_track(client, item, **kwargs) + elif item.kind == "playlist": + logger.info("Found a playlist") + download_playlist(client, item, **kwargs) + elif item.kind == "user": + user = item + logger.info("Found a user profile") + if kwargs.get("f"): + logger.info(f"Retrieving all likes of user {user.username}...") + resources = client.get_user_likes(user.id, limit=1000) + for i, like in enumerate(resources, 1): + logger.info(f"like n°{i} of {user.likes_count}") + if hasattr(like, "track"): + download_track(client, like.track, **kwargs) + elif hasattr(like, "playlist"): + download_playlist(client, client.get_playlist(like.playlist.id), **kwargs) + else: + raise ValueError(f"Unknown like type {like}") + logger.info(f"Downloaded all likes of user {user.username}!") + elif kwargs.get("C"): + logger.info(f"Retrieving all commented tracks of user {user.username}...") + resources = client.get_user_comments(user.id, limit=1000) + for i, comment in enumerate(resources, 1): + logger.info(f"comment n°{i} of {user.comments_count}") + download_track(client, client.get_track(comment.track.id), **kwargs) + logger.info(f"Downloaded all commented tracks of user {user.username}!") + elif kwargs.get("t"): + logger.info(f"Retrieving all tracks of user {user.username}...") + resources = client.get_user_tracks(user.id, limit=1000) + for i, track in enumerate(resources, 1): + logger.info(f"track n°{i} of {user.track_count}") + download_track(client, track, **kwargs) + logger.info(f"Downloaded all tracks of user {user.username}!") + elif kwargs.get("a"): + logger.info(f"Retrieving all tracks & reposts of user {user.username}...") + resources = client.get_user_stream(user.id, limit=1000) + for i, item in enumerate(resources, 1): + logger.info(f"item n°{i} of {user.track_count + user.reposts_count if user.reposts_count else '?'}") + if item.type in ("track", "track-repost"): + download_track(client, item.track, **kwargs) + elif item.type in ("playlist", "playlist-repost"): + download_playlist(client, item.playlist, **kwargs) + else: + raise ValueError(f"Unknown item type {item.type}") + logger.info(f"Downloaded all tracks & reposts of user {user.username}!") + elif kwargs.get("p"): + logger.info(f"Retrieving all playlists of user {user.username}...") + resources = client.get_user_playlists(user.id, limit=1000) + for i, playlist in enumerate(resources, 1): + logger.info(f"playlist n°{i} of {user.playlist_count}") + download_playlist(client, playlist, **kwargs) + logger.info(f"Downloaded all playlists of user {user.username}!") + elif kwargs.get("r"): + logger.info(f"Retrieving all reposts of user {user.username}...") + resources = client.get_user_reposts(user.id, limit=1000) + for i, item in enumerate(resources, 1): + logger.info(f"item n°{i} of {user.reposts_count or '?'}") + if item.type == "track-repost": + download_track(client, item.track, **kwargs) + elif item.type == "playlist-repost": + download_playlist(client, item.playlist, **kwargs) + else: + raise ValueError(f"Unknown item type {item.type}") + logger.info(f"Downloaded all reposts of user {user.username}!") else: - logger.error('Please provide a download type...') + logger.error("Please provide a download type...") else: - logger.error('Unknown item type {0}'.format(item['kind'])) - - -def who_am_i(): - """ - Display username from current token and check for validity - """ - me = url['me'].format(token) - r = requests.get(me, params={'client_id': CLIENT_ID}, headers={'User-Agent': USER_AGENT}) - r.raise_for_status() - current_user = r.json() - logger.debug(me) - - logger.info('Hello {0}!'.format(current_user['username'])) - return current_user - + logger.error("Unknown item type {0}".format(item.kind)) def remove_files(): """ Removes any pre-existing tracks that were not just downloaded """ logger.info("Removing local track files that were not downloaded...") - files = [f for f in os.listdir('.') if os.path.isfile(f)] + files = [f for f in os.listdir(".") if os.path.isfile(f)] for f in files: if f not in fileToKeep: os.remove(f) - -def get_track_info(track): - """ - Fetches track info from Soundcloud, given a track_id - """ - if 'media' in track: - return track - - logger.info('Retrieving more info on the track') - info_url = url["trackinfo"].format(track['id']) - r = requests.get(info_url, params={'client_id': CLIENT_ID}, stream=True, headers={'User-Agent': USER_AGENT}) - item = r.json() - logger.debug(item) - return item - - -def download(user, dl_type, name): - """ - Download user items of dl_type (ie. all, playlists, liked, commented, etc.) - """ - 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'] - user_id = user['id'] - logger.info( - 'Retrieving all {0} of user {1}...'.format(name, username) - ) - dl_url = url[dl_type].format(user_id) - logger.debug(dl_url) - 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)) - for counter, item in enumerate(resources, offset): - try: - logger.debug(item) - logger.info('{0} n°{1} of {2}'.format( - name.capitalize(), counter, 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']) - elif dl_type == 'tracks': - download_track(item) - else: - download_track(item['track']) - except Exception as e: - logger.exception(e) - logger.info('Downloaded all {0} {1} of user {2}!'.format( - total, name, username) - ) - - -def download_playlist(playlist): +def download_playlist(client: SoundCloud, playlist: BasicAlbumPlaylist, **kwargs): """ Downloads a playlist """ - global arguments - invalid_chars = '\/:*?|<>"' - playlist_name = playlist['title'].encode('utf-8', 'ignore') - playlist_name = playlist_name.decode('utf8') - playlist_name = ''.join(c for c in playlist_name if c not in invalid_chars) + playlist_name = playlist.title.encode("utf-8", "ignore") + playlist_name = playlist_name.decode("utf8") + playlist_name = sanitize_filename(playlist_name) - if not arguments['--no-playlist-folder']: + if not kwargs.get("no_playlist_folder"): if not os.path.exists(playlist_name): os.makedirs(playlist_name) os.chdir(playlist_name) 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) + if kwargs.get("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(kwargs.get("n"))] + else: + del playlist.tracks[: kwargs.get("offset") - 1] + tracknumber_digits = len(str(len(playlist.tracks))) + for counter, track in enumerate(playlist.tracks, kwargs.get("offset")): + logger.debug(track) + logger.info(f"Track n°{counter}") + playlist_info = { + "author": playlist.user.username, + "title": playlist.title, + "tracknumber": str(counter).zfill(tracknumber_digits), + } + if isinstance(track, MiniTrack): + track = client.get_track(track.id) + download_track(client, track, playlist_info, **kwargs) finally: - if not arguments['--no-playlist-folder']: - os.chdir('..') - - -def download_my_stream(): - """ - DONT WORK FOR NOW - Download the stream of the current user - """ - # TODO - # Use Token - + if not kwargs.get("no_playlist_folder"): + os.chdir("..") def try_utime(path, filetime): try: @@ -431,72 +364,88 @@ def try_utime(path, filetime): except: logger.error("Cannot update utime of file") +def get_filename(track: BasicTrack, original_filename=None, aac=False, playlist_info=None, **kwargs): + + if kwargs.get("original_name") and original_filename: + return original_filename + + username = track.user.username + title = track.title.encode("utf-8", "ignore").decode("utf-8") -def get_filename(track, original_filename=None): - invalid_chars = '\/:*?|<>"' - username = track['user']['username'] - title = track['title'].encode('utf-8', 'ignore').decode('utf8') - - if arguments['--addtofile']: - if username not in title and '-' not in title: - title = '{0} - {1}'.format(username, title) + if kwargs.get("addtofile"): + if username not in title and "-" not in title: + title = "{0} - {1}".format(username, title) logger.debug('Adding "{0}" to filename'.format(username)) - if arguments['--addtimestamp']: - # created_at sample: 2019-01-30T11:11:37Z - ts = datetime \ - .strptime(track['created_at'], "%Y-%m-%dT%H:%M:%SZ") \ - .timestamp() + timestamp = str(int(track.created_at.timestamp())) + if kwargs.get("addtimestamp"): + title = timestamp + "_" + title + + if not kwargs.get("addtofile") and not kwargs.get("addtimestamp"): + if playlist_info: + title = kwargs.get("playlist_name_format").format(**asdict(track), playlist=playlist_info, timestamp=timestamp) + else: + title = kwargs.get("name_format").format(**asdict(track), timestamp=timestamp) - title = str(int(ts)) + "_" + title - - ext = ".mp3" + ext = ".m4a" if aac else ".mp3" # contain aac in m4a to write metadata if original_filename is not None: - original_filename.encode('utf-8', 'ignore').decode('utf8') + original_filename.encode("utf-8", "ignore").decode("utf8") ext = os.path.splitext(original_filename)[1] - filename = title[:251] + ext.lower() - filename = ''.join(c for c in filename if c not in invalid_chars) + # get filename to 255 bytes + while len(title.encode("utf-8")) > 255 - len(ext.encode("utf-8")): + title = title[:-1] + filename = title + ext.lower() + filename = sanitize_filename(filename) return filename -def download_original_file(track, title): - logger.info('Downloading the original file.') - original_url = url['original_download'].format(track['id']) +def download_original_file(client: SoundCloud, track: BasicTrack, title: str, playlist_info=None, **kwargs): + logger.info("Downloading the original file.") # Get the requests stream - r = requests.get( - original_url, params={'client_id': CLIENT_ID}, headers={'User-Agent': USER_AGENT} - ) - r = requests.get(r.json()['redirectUri'], stream=True, headers={'User-Agent': USER_AGENT}) + url = client.get_track_original_download(track.id) + + if not url: + logger.info("Could not get original download link") + return (None, False) + + r = requests.get(url, stream=True) if r.status_code == 401: - logger.info('The original file has no download left.') + logger.info("The original file has no download left.") return (None, False) if r.status_code == 404: - logger.info('Could not get name from stream - using basic name') + logger.info("Could not get name from stream - using basic name") return (None, False) # Find filename - d = r.headers.get('content-disposition') + d = r.headers.get("content-disposition") filename = re.findall("filename=(.+)", d)[0] - filename = get_filename(track, filename) - logger.debug("filename : {0}".format(filename)) + filename, ext = os.path.splitext(filename) + + # Find file extension + mime = r.headers.get("content-type") + ext = mimetypes.guess_extension(mime) or ext + filename += ext + + filename = get_filename(track, filename, playlist_info=playlist_info, **kwargs) + logger.debug(f"filename : {filename}") # Skip if file ID or filename already exists - if already_downloaded(track, title, filename): - if arguments['--flac'] and can_convert(filename): + if already_downloaded(track, title, filename, **kwargs): + if kwargs.get("flac") and can_convert(filename): filename = filename[:-4] + ".flac" return (filename, True) # Write file - total_length = int(r.headers.get('content-length')) + total_length = int(r.headers.get("content-length")) temp = tempfile.NamedTemporaryFile(delete=False) received = 0 with temp as f: for chunk in progress.bar( - r.iter_content(chunk_size=1024), - expected_size=(total_length / 1024) + 1, - hide=True if arguments["--hide-progress"] else False + r.iter_content(chunk_size=1024), + expected_size=(total_length / 1024) + 1, + hide=True if kwargs.get("hide_progress") else False, ): if chunk: received += len(chunk) @@ -504,16 +453,16 @@ def download_original_file(track, title): f.flush() if received != total_length: - logger.error('connection closed prematurely, download incomplete') + logger.error("connection closed prematurely, download incomplete") sys.exit(-1) shutil.move(temp.name, os.path.join(os.getcwd(), filename)) - if arguments['--flac'] and can_convert(filename): - logger.info('Converting to .flac...') + if kwargs.get("flac") and can_convert(filename): + logger.info("Converting to .flac...") newfilename = filename[:-4] + ".flac" - commands = ['ffmpeg', '-i', filename, newfilename, '-loglevel', 'error'] - logger.debug("Commands: {}".format(commands)) + commands = ["ffmpeg", "-i", filename, newfilename, "-loglevel", "error"] + logger.debug(f"Commands: {commands}") subprocess.call(commands) os.remove(filename) filename = newfilename @@ -521,245 +470,296 @@ def download_original_file(track, title): return (filename, False) -def get_track_m3u8(track): +def get_track_m3u8(client: SoundCloud, track: BasicTrack, aac=False): url = None - for transcoding in track['media']['transcodings']: - if transcoding['format']['protocol'] == 'hls' \ - and transcoding['format']['mime_type'] == 'audio/mpeg': - url = transcoding['url'] + for transcoding in track.media.transcodings: + if transcoding.format.protocol == "hls": + if (not aac and transcoding.format.mime_type == "audio/mpeg") or ( + aac and transcoding.format.mime_type.startswith("audio/mp4") + ): + url = transcoding.url if url is not None: - r = requests.get(url, params={'client_id': CLIENT_ID}, headers={'User-Agent': USER_AGENT}) + headers = client.get_default_headers() + if client.auth_token: + headers["Authorization"] = f"OAuth {client.auth_token}" + r = requests.get(url, params={"client_id": client.client_id}, headers=headers) logger.debug(r.url) - return r.json()['url'] + return r.json()["url"] -def download_hls_mp3(track, title): - filename = get_filename(track) - logger.debug("filename : {0}".format(filename)) +def download_hls(client: SoundCloud, track: BasicTrack, title: str, playlist_info=None, **kwargs): + + if kwargs["onlymp3"]: + aac = False + else: + aac = any( + t.format.mime_type.startswith("audio/mp4") + for t in track.media.transcodings + ) + + filename = get_filename(track, None, aac, playlist_info, **kwargs) + logger.debug(f"filename : {filename}") # Skip if file ID or filename already exists - if already_downloaded(track, title, filename): + if already_downloaded(track, title, filename, **kwargs): return (filename, True) # Get the requests stream - url = get_track_m3u8(track) + url = get_track_m3u8(client, track, aac) filename_path = os.path.abspath(filename) - subprocess.call(['ffmpeg', '-i', url, '-c', 'copy', filename_path, '-loglevel', 'fatal']) + p = subprocess.run( + ["ffmpeg", "-i", url, "-c", "copy", filename_path, "-loglevel", "error"], + capture_output=True, + ) + if p.stderr: + logger.error(p.stderr.decode("utf-8")) return (filename, False) -def download_track(track, playlist_info=None): +def download_track(client: SoundCloud, track: BasicTrack, playlist_info=None, **kwargs): """ 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)) + title = track.title + title = title.encode("utf-8", "ignore").decode("utf8") + logger.info(f"Downloading {title}") # Not streamable - if not track['streamable']: - logger.error('{0} is not streamable...'.format(title)) + if not track.streamable: + logger.error(f"{title} is not streamable...") return # Geoblocked track - if track['policy'] == 'BLOCK': - logger.error('{0} is not available in your location...\n'.format(title)) + if track.policy == "BLOCK": + logger.error(f"{title} is not available in your location...\n") return # Downloadable track filename = None is_already_downloaded = False - if track['downloadable'] and track['has_downloads_left'] and not arguments['--onlymp3']: - filename, is_already_downloaded = download_original_file(track, title) + if ( + track.downloadable + and track.has_downloads_left + and not kwargs["onlymp3"] + and not kwargs.get("no_original") + ): + filename, is_already_downloaded = download_original_file(client, track, title, playlist_info, **kwargs) if filename is None: - filename, is_already_downloaded = download_hls_mp3(track, title) + if kwargs.get("only_original"): + logger.info(f'Track "{title}" does not have original file available. Skipping...') + return + filename, is_already_downloaded = download_hls(client, track, title, playlist_info, **kwargs) - # 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']: + if kwargs.get("remove"): fileToKeep.append(filename) - record_download_archive(track) + record_download_archive(track, **kwargs) # Skip if file ID or filename already exists - if is_already_downloaded and not arguments['--force-metadata']: - logger.info('Track "{0}" already downloaded.'.format(title)) + if is_already_downloaded and not kwargs.get("force_metadata"): + logger.info(f'Track "{title}" already downloaded.') return # If file does not exist an error occurred if not os.path.isfile(filename): - logger.error('An error occurred downloading {0}.\n'.format(filename)) - logger.error('Exiting...') + logger.error(f"An error occurred downloading {filename}.\n") + logger.error("Exiting...") sys.exit(-1) # Try to set the metadata - if filename.endswith('.mp3') or filename.endswith('.flac'): + if ( + filename.endswith(".mp3") + or filename.endswith(".flac") + or filename.endswith(".m4a") + ): try: - set_metadata(track, filename, playlist_info) - except Exception as e: - logger.error('Error trying to set the tags...') - logger.debug(e) + set_metadata(track, filename, playlist_info, **kwargs) + except: + logger.exception("Error trying to set the tags...") else: logger.error("This type of audio doesn't support tagging...") # Try to change the real creation date - created_at = track['created_at'] - timestamp = datetime.strptime(created_at, '%Y-%m-%dT%H:%M:%SZ') - filetime = int(time.mktime(timestamp.timetuple())) + filetime = int(time.mktime(track.created_at.timetuple())) try_utime(filename, filetime) - logger.info('{0} Downloaded.\n'.format(filename)) + logger.info(f"{filename} Downloaded.\n") def can_convert(filename): ext = os.path.splitext(filename)[1] - return 'wav' in ext or 'aif' in ext + return "wav" in ext or "aif" in ext -def already_downloaded(track, title, filename): +def already_downloaded(track: BasicTrack, title: str, filename: str, **kwargs): """ Returns True if the file has already been downloaded """ - global arguments already_downloaded = False if os.path.isfile(filename): already_downloaded = True - if arguments['--flac'] and can_convert(filename) \ - and os.path.isfile(filename[:-4] + ".flac"): + if kwargs.get("overwrite"): + os.remove(filename) + already_downloaded = False + if ( + kwargs.get("flac") + and can_convert(filename) + and os.path.isfile(filename[:-4] + ".flac") + ): already_downloaded = True - if arguments['--download-archive'] and in_download_archive(track): + if kwargs.get("overwrite"): + os.remove(filename[:-4] + ".flac") + already_downloaded = False + if kwargs.get("download_archive") and in_download_archive(track, **kwargs): already_downloaded = True - if arguments['--flac'] and can_convert(filename) and os.path.isfile(filename): + if kwargs.get("flac") and can_convert(filename) and os.path.isfile(filename): already_downloaded = False if already_downloaded: - if arguments['-c'] or arguments['--remove'] or arguments['--force-metadata']: + if kwargs.get("c") or kwargs.get("remove") or kwargs.get("force_metadata"): return True else: - logger.error('Track "{0}" already exists!'.format(title)) - logger.error('Exiting... (run again with -c to continue)') + logger.error(f'Track "{title}" already exists!') + logger.error("Exiting... (run again with -c to continue)") sys.exit(-1) return False -def in_download_archive(track): +def in_download_archive(track: BasicTrack, **kwargs): """ Returns True if a track_id exists in the download archive """ - global arguments - if not arguments['--download-archive']: + if not kwargs.get("download_archive"): return - archive_filename = arguments.get('--download-archive') + archive_filename = kwargs.get("download_archive") try: - with open(archive_filename, 'a+', encoding='utf-8') as file: + with open(archive_filename, "a+", encoding="utf-8") as file: file.seek(0) - track_id = '{0}'.format(track['id']) + track_id = str(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) + logger.error("Error trying to read download archive...") + logger.error(ioe) return False -def record_download_archive(track): +def record_download_archive(track: BasicTrack, **kwargs): """ Write the track_id in the download archive """ - global arguments - if not arguments['--download-archive']: + if not kwargs.get("download_archive"): return - archive_filename = arguments.get('--download-archive') + archive_filename = kwargs.get("download_archive") try: - with open(archive_filename, 'a', encoding='utf-8') as file: - file.write('{0}'.format(track['id']) + '\n') + with open(archive_filename, "a", encoding="utf-8") as file: + file.write(f"{track.id}\n") except IOError as ioe: - logger.error('Error trying to write to download archive...') - logger.debug(ioe) + logger.error("Error trying to write to download archive...") + logger.error(ioe) -def set_metadata(track, filename, playlist_info=None): +def set_metadata(track: BasicTrack, filename: str, playlist_info=None, **kwargs): """ Sets the mp3 file metadata using the Python module Mutagen """ - logger.info('Setting tags...') - global arguments - artwork_url = track['artwork_url'] - user = track['user'] + logger.info("Setting tags...") + artwork_url = track.artwork_url + user = track.user if not artwork_url: - artwork_url = user['avatar_url'] - artwork_url = artwork_url.replace('large', 't500x500') - response = requests.get(artwork_url, stream=True, headers={'User-Agent': USER_AGENT}) + artwork_url = user.avatar_url + response = None + if kwargs.get("original_art"): + new_artwork_url = artwork_url.replace("large", "original") + try: + response = requests.get(new_artwork_url, stream=True) + if response.headers["Content-Type"] not in ( + "image/png", + "image/jpeg", + "image/jpg", + ): + response = None + except: + pass + if response is None: + new_artwork_url = artwork_url.replace("large", "t500x500") + response = requests.get(new_artwork_url, stream=True) + if response.headers["Content-Type"] not in ( + "image/png", + "image/jpeg", + "image/jpg", + ): + response = None + if response is None: + logger.error(f"Could not get cover art at {new_artwork_url}") with tempfile.NamedTemporaryFile() as out_file: - shutil.copyfileobj(response.raw, out_file) - out_file.seek(0) + if response: + shutil.copyfileobj(response.raw, out_file) + out_file.seek(0) - track_created = track['created_at'] - track_date = datetime.strptime(track_created, "%Y-%m-%dT%H:%M:%SZ") - debug_extract_dates = '{0} {1}'.format(track_created, track_date) - logger.debug('Extracting date: {0}'.format(debug_extract_dates)) - track['date'] = track_date.strftime("%Y-%m-%d %H::%M::%S") + track.date = track.created_at.strftime("%Y-%m-%d %H::%M::%S") - 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() + track.artist = user.username + if kwargs.get("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 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'] + audio.delete() + 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']) + if not kwargs.get("no_album_tag"): + audio["album"] = playlist_info["title"] + audio["tracknumber"] = str(playlist_info["tracknumber"]) audio.save() a = mutagen.File(filename) - if track['description']: + if track.description: if a.__class__ == mutagen.flac.FLAC: - a['description'] = track['description'] + a["description"] = track.description elif a.__class__ == mutagen.mp3.MP3: - a['COMM'] = mutagen.id3.COMM( - encoding=3, lang=u'ENG', text=track['description'] + a["COMM"] = mutagen.id3.COMM( + encoding=3, lang="ENG", text=track.description ) - if artwork_url: + elif a.__class__ == mutagen.mp4.MP4: + a["\xa9cmt"] = track.description + if response: if a.__class__ == mutagen.flac.FLAC: p = mutagen.flac.Picture() p.data = out_file.read() - p.width = 500 - p.height = 500 + p.mime = "image/jpeg" p.type = mutagen.id3.PictureType.COVER_FRONT a.add_picture(p) elif a.__class__ == mutagen.mp3.MP3: - a['APIC'] = mutagen.id3.APIC( - encoding=3, mime='image/jpeg', type=3, - desc='Cover', data=out_file.read() + a["APIC"] = mutagen.id3.APIC( + encoding=3, + mime="image/jpeg", + type=3, + desc="Cover", + data=out_file.read(), ) + elif a.__class__ == mutagen.mp4.MP4: + a["covr"] = [mutagen.mp4.MP4Cover(out_file.read())] a.save() @@ -767,14 +767,16 @@ def signal_handler(signal, frame): """ Handle keyboard interrupt """ - logger.info('\nGood bye!') + logger.info("\nGood bye!") sys.exit(0) + def is_ffmpeg_available(): """ Returns true if ffmpeg is available in the operating system """ - return shutil.which('ffmpeg') is not None + return shutil.which("ffmpeg") is not None -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/scdl/utils.py b/scdl/utils.py index e561272..ab19aba 100644 --- a/scdl/utils.py +++ b/scdl/utils.py @@ -15,7 +15,8 @@ __all__ = ('ColorizeFilter', ) class ColorizeFilter(logging.Filter): color_by_level = { - logging.DEBUG: 'yellow', + logging.DEBUG: 'blue', + logging.WARNING: 'yellow', logging.ERROR: 'red', logging.INFO: 'white' } diff --git a/setup.py b/setup.py index dc7e930..703cc72 100755 --- a/setup.py +++ b/setup.py @@ -6,39 +6,46 @@ from setuptools import setup, find_packages import scdl from os import path + this_directory = path.abspath(path.dirname(__file__)) -with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: +with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: long_description = f.read() setup( - name='scdl', + name="scdl", version=scdl.__version__, packages=find_packages(), - author='FlyinGrub', - author_email='flyinggrub@gmail.com', - description='Download Music from Souncloud', + author="FlyinGrub", + author_email="flyinggrub@gmail.com", + description="Download Music from Souncloud", long_description=long_description, - long_description_content_type='text/markdown', + long_description_content_type="text/markdown", install_requires=[ - 'docopt', - 'mutagen', - 'termcolor', - 'requests', - 'clint' + "docopt", + "mutagen", + "termcolor", + "requests", + "clint", + "pathvalidate", + "soundcloud-v2>=1.1.4" ], - url='https://github.com/flyingrub/scdl', + url="https://github.com/flyingrub/scdl", classifiers=[ - 'Programming Language :: Python', - 'Development Status :: 5 - Production/Stable', - 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.4', - 'Topic :: Internet', - 'Topic :: Multimedia :: Sound/Audio', + "Programming Language :: Python", + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Internet", + "Topic :: Multimedia :: Sound/Audio", ], + python_requires = ">=3.6", entry_points={ - 'console_scripts': [ - 'scdl = scdl.scdl:main', + "console_scripts": [ + "scdl = scdl.scdl:main", ], }, )