Better error handling, return exit code 1 on errors

master
7x11x13 2021-11-26 09:48:20 -05:00
parent d79d55ff89
commit 0f6991077b
2 changed files with 97 additions and 82 deletions

View File

@ -2,4 +2,4 @@
"""Python Soundcloud Music Downloader.""" """Python Soundcloud Music Downloader."""
__version__ = "v2.2.4" __version__ = "v2.2.5"

View File

@ -8,7 +8,7 @@ Usage:
[-o <offset>][--hidewarnings][--debug | --error][--path <path>][--addtofile][--addtimestamp] [-o <offset>][--hidewarnings][--debug | --error][--path <path>][--addtofile][--addtimestamp]
[--onlymp3][--hide-progress][--min-size <size>][--max-size <size>][--remove][--no-album-tag] [--onlymp3][--hide-progress][--min-size <size>][--max-size <size>][--remove][--no-album-tag]
[--no-playlist-folder][--download-archive <file>][--extract-artist][--flac][--original-art] [--no-playlist-folder][--download-archive <file>][--extract-artist][--flac][--original-art]
[--original-name][--no-original][--only-original][--name-format <format>][--strict] [--original-name][--no-original][--only-original][--name-format <format>][--strict-playlist]
[--playlist-name-format <format>][--client-id <id>][--auth-token <token>][--overwrite] [--playlist-name-format <format>][--client-id <id>][--auth-token <token>][--overwrite]
scdl -h | --help scdl -h | --help
scdl --version scdl --version
@ -58,7 +58,7 @@ Options:
--client-id [id] Specify the client_id to use --client-id [id] Specify the client_id to use
--auth-token [token] Specify the auth token to use --auth-token [token] Specify the auth token to use
--overwrite Overwrite file if it already exists --overwrite Overwrite file if it already exists
--strict Fail if setting metadata fails --strict-playlist Abort playlist downloading if one track fails to download
""" """
import configparser import configparser
@ -71,11 +71,11 @@ mimetypes.init()
import os import os
import re import re
import shutil import shutil
import signal
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import time import time
import traceback
import warnings import warnings
from dataclasses import asdict from dataclasses import asdict
@ -100,12 +100,22 @@ logger.addFilter(utils.ColorizeFilter())
fileToKeep = [] fileToKeep = []
class SoundCloudException(Exception):
pass
def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
logger.error("\nGoodbye!")
else:
logger.error("".join(traceback.format_exception(exc_type, exc_value, exc_traceback)))
sys.exit(1)
sys.excepthook = handle_exception
def main(): def main():
""" """
Main function, parses the URL from command line arguments Main function, parses the URL from command line arguments
""" """
signal.signal(signal.SIGINT, signal_handler)
# exit if ffmpeg not installed # exit if ffmpeg not installed
if not is_ffmpeg_available(): if not is_ffmpeg_available():
@ -153,10 +163,12 @@ def main():
client = SoundCloud(client_id, token if token else None) client = SoundCloud(client_id, token if token else None)
if not client.is_client_id_valid(): if not client.is_client_id_valid():
raise ValueError(f"client_id is not valid") logger.error(f"Invalid client_id in {config_file}")
sys.exit(1)
if (token or arguments["me"]) and not client.is_auth_token_valid(): if (token or arguments["me"]) and not client.is_auth_token_valid():
raise ValueError(f"auth_token is not valid") logger.error(f"Invalid auth_token in {config_file}")
sys.exit(1)
if arguments["-o"] is not None: if arguments["-o"] is not None:
try: try:
@ -249,7 +261,8 @@ def download_url(client: SoundCloud, **kwargs):
item = client.resolve(url) item = client.resolve(url)
logger.debug(item) logger.debug(item)
if not item: if not item:
return logger.error("URL is not valid")
sys.exit(1)
elif item.kind == "track": elif item.kind == "track":
logger.info("Found a track") logger.info("Found a track")
download_track(client, item, **kwargs) download_track(client, item, **kwargs)
@ -265,25 +278,27 @@ def download_url(client: SoundCloud, **kwargs):
for i, like in enumerate(resources, 1): for i, like in enumerate(resources, 1):
logger.info(f"like n°{i} of {user.likes_count}") logger.info(f"like n°{i} of {user.likes_count}")
if hasattr(like, "track"): if hasattr(like, "track"):
download_track(client, like.track, **kwargs) download_track(client, like.track, exit_on_fail=kwargs.get("strict_playlist"), **kwargs)
elif hasattr(like, "playlist"): elif hasattr(like, "playlist"):
download_playlist(client, client.get_playlist(like.playlist.id), **kwargs) download_playlist(client, client.get_playlist(like.playlist.id), **kwargs)
else: else:
raise ValueError(f"Unknown like type {like}") logger.error(f"Unknown like type {like}")
if kwargs.get("strict_playlist"):
sys.exit(1)
logger.info(f"Downloaded all likes of user {user.username}!") logger.info(f"Downloaded all likes of user {user.username}!")
elif kwargs.get("C"): elif kwargs.get("C"):
logger.info(f"Retrieving all commented tracks of user {user.username}...") logger.info(f"Retrieving all commented tracks of user {user.username}...")
resources = client.get_user_comments(user.id, limit=1000) resources = client.get_user_comments(user.id, limit=1000)
for i, comment in enumerate(resources, 1): for i, comment in enumerate(resources, 1):
logger.info(f"comment n°{i} of {user.comments_count}") logger.info(f"comment n°{i} of {user.comments_count}")
download_track(client, client.get_track(comment.track.id), **kwargs) download_track(client, client.get_track(comment.track.id), exit_on_fail=kwargs.get("strict_playlist"), **kwargs)
logger.info(f"Downloaded all commented tracks of user {user.username}!") logger.info(f"Downloaded all commented tracks of user {user.username}!")
elif kwargs.get("t"): elif kwargs.get("t"):
logger.info(f"Retrieving all tracks of user {user.username}...") logger.info(f"Retrieving all tracks of user {user.username}...")
resources = client.get_user_tracks(user.id, limit=1000) resources = client.get_user_tracks(user.id, limit=1000)
for i, track in enumerate(resources, 1): for i, track in enumerate(resources, 1):
logger.info(f"track n°{i} of {user.track_count}") logger.info(f"track n°{i} of {user.track_count}")
download_track(client, track, **kwargs) download_track(client, track, exit_on_fail=kwargs.get("strict_playlist"), **kwargs)
logger.info(f"Downloaded all tracks of user {user.username}!") logger.info(f"Downloaded all tracks of user {user.username}!")
elif kwargs.get("a"): elif kwargs.get("a"):
logger.info(f"Retrieving all tracks & reposts of user {user.username}...") logger.info(f"Retrieving all tracks & reposts of user {user.username}...")
@ -291,11 +306,13 @@ def download_url(client: SoundCloud, **kwargs):
for i, item in enumerate(resources, 1): 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 '?'}") 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"): if item.type in ("track", "track-repost"):
download_track(client, item.track, **kwargs) download_track(client, item.track, exit_on_fail=kwargs.get("strict_playlist"), **kwargs)
elif item.type in ("playlist", "playlist-repost"): elif item.type in ("playlist", "playlist-repost"):
download_playlist(client, item.playlist, **kwargs) download_playlist(client, item.playlist, **kwargs)
else: else:
raise ValueError(f"Unknown item type {item.type}") logger.error(f"Unknown item type {item.type}")
if kwargs.get("strict_playlist"):
sys.exit(1)
logger.info(f"Downloaded all tracks & reposts of user {user.username}!") logger.info(f"Downloaded all tracks & reposts of user {user.username}!")
elif kwargs.get("p"): elif kwargs.get("p"):
logger.info(f"Retrieving all playlists of user {user.username}...") logger.info(f"Retrieving all playlists of user {user.username}...")
@ -310,16 +327,20 @@ def download_url(client: SoundCloud, **kwargs):
for i, item in enumerate(resources, 1): for i, item in enumerate(resources, 1):
logger.info(f"item n°{i} of {user.reposts_count or '?'}") logger.info(f"item n°{i} of {user.reposts_count or '?'}")
if item.type == "track-repost": if item.type == "track-repost":
download_track(client, item.track, **kwargs) download_track(client, item.track, exit_on_fail=kwargs.get("strict_playlist"), **kwargs)
elif item.type == "playlist-repost": elif item.type == "playlist-repost":
download_playlist(client, item.playlist, **kwargs) download_playlist(client, item.playlist, **kwargs)
else: else:
raise ValueError(f"Unknown item type {item.type}") logger.error(f"Unknown item type {item.type}")
if kwargs.get("strict_playlist"):
sys.exit(1)
logger.info(f"Downloaded all reposts of user {user.username}!") logger.info(f"Downloaded all reposts of user {user.username}!")
else: else:
logger.error("Please provide a download type...") logger.error("Please provide a download type...")
sys.exit(1)
else: else:
logger.error("Unknown item type {0}".format(item.kind)) logger.error(f"Unknown item type {item.kind}")
sys.exit(1)
def remove_files(): def remove_files():
""" """
@ -363,7 +384,7 @@ def download_playlist(client: SoundCloud, playlist: BasicAlbumPlaylist, **kwargs
} }
if isinstance(track, MiniTrack): if isinstance(track, MiniTrack):
track = client.get_track(track.id) track = client.get_track(track.id)
download_track(client, track, playlist_info, **kwargs) download_track(client, track, playlist_info, kwargs.get("strict_playlist"), **kwargs)
finally: finally:
if not kwargs.get("no_playlist_folder"): if not kwargs.get("no_playlist_folder"):
os.chdir("..") os.chdir("..")
@ -497,6 +518,9 @@ def get_track_m3u8(client: SoundCloud, track: BasicTrack, aac=False):
def download_hls(client: SoundCloud, track: BasicTrack, title: str, playlist_info=None, **kwargs): def download_hls(client: SoundCloud, track: BasicTrack, title: str, playlist_info=None, **kwargs):
if not track.media.transcodings:
raise SoundCloudException(f"Track {track.permalink_url} has no transcodings available")
if kwargs["onlymp3"]: if kwargs["onlymp3"]:
aac = False aac = False
else: else:
@ -524,78 +548,76 @@ def download_hls(client: SoundCloud, track: BasicTrack, title: str, playlist_inf
return (filename, False) return (filename, False)
def download_track(client: SoundCloud, track: BasicTrack, playlist_info=None, **kwargs): def download_track(client: SoundCloud, track: BasicTrack, playlist_info=None, exit_on_fail=True, **kwargs):
""" """
Downloads a track Downloads a track
""" """
title = track.title try:
title = title.encode("utf-8", "ignore").decode("utf8") title = track.title
logger.info(f"Downloading {title}") title = title.encode("utf-8", "ignore").decode("utf8")
logger.info(f"Downloading {title}")
# Not streamable # Not streamable
if not track.streamable: if not track.streamable:
logger.error(f"{title} is not streamable...") raise SoundCloudException(f"{title} is not streamable...")
return
# Geoblocked track # Geoblocked track
if track.policy == "BLOCK": if track.policy == "BLOCK":
logger.error(f"{title} is not available in your location...\n") raise SoundCloudException(f"{title} is not available in your location...")
return
# Downloadable track # Downloadable track
filename = None filename = None
is_already_downloaded = False is_already_downloaded = False
if ( if (
track.downloadable track.downloadable
and track.has_downloads_left and track.has_downloads_left
and not kwargs["onlymp3"] and not kwargs["onlymp3"]
and not kwargs.get("no_original") and not kwargs.get("no_original")
): ):
filename, is_already_downloaded = download_original_file(client, track, title, playlist_info, **kwargs) filename, is_already_downloaded = download_original_file(client, track, title, playlist_info, **kwargs)
if filename is None: if filename is None:
if kwargs.get("only_original"): if kwargs.get("only_original"):
logger.info(f'Track "{title}" does not have original file available. Skipping...') raise SoundCloudException(f'Track "{track.permalink_url}" does not have original file available. Not downloading...')
return filename, is_already_downloaded = download_hls(client, track, title, playlist_info, **kwargs)
filename, is_already_downloaded = download_hls(client, track, title, playlist_info, **kwargs)
if kwargs.get("remove"): if kwargs.get("remove"):
fileToKeep.append(filename) fileToKeep.append(filename)
record_download_archive(track, **kwargs) record_download_archive(track, **kwargs)
# Skip if file ID or filename already exists # Skip if file ID or filename already exists
if is_already_downloaded and not kwargs.get("force_metadata"): if is_already_downloaded and not kwargs.get("force_metadata"):
logger.info(f'Track "{title}" already downloaded.') raise SoundCloudException(f"{filename} already downloaded.")
return
# If file does not exist an error occurred # If file does not exist an error occurred
if not os.path.isfile(filename): if not os.path.isfile(filename):
logger.error(f"An error occurred downloading {filename}.\n") raise SoundCloudException(f"An error occurred downloading {filename}.")
logger.error("Exiting...")
sys.exit(1)
# Try to set the metadata # Try to set the metadata
if ( if (
filename.endswith(".mp3") filename.endswith(".mp3")
or filename.endswith(".flac") or filename.endswith(".flac")
or filename.endswith(".m4a") or filename.endswith(".m4a")
): ):
try: try:
set_metadata(track, filename, playlist_info, **kwargs) set_metadata(track, filename, playlist_info, **kwargs)
except: except:
logger.exception("Error trying to set the tags...")
if kwargs.get("strict"):
os.remove(filename) os.remove(filename)
sys.exit(1) logger.exception("Error trying to set the tags...")
else: raise SoundCloudException("Error trying to set the tags...")
logger.error("This type of audio doesn't support tagging...") else:
logger.error("This type of audio doesn't support tagging...")
# Try to change the real creation date # Try to change the real creation date
filetime = int(time.mktime(track.created_at.timetuple())) filetime = int(time.mktime(track.created_at.timetuple()))
try_utime(filename, filetime) try_utime(filename, filetime)
logger.info(f"{filename} Downloaded.\n") logger.info(f"{filename} Downloaded.\n")
except SoundCloudException as err:
logger.error(err)
if exit_on_fail:
sys.exit(1)
def can_convert(filename): def can_convert(filename):
@ -777,13 +799,6 @@ def limit_filename_length(name: str, ext: str, max_bytes=255):
name = name[:-1] name = name[:-1]
return name + ext return name + ext
def signal_handler(signal, frame):
"""
Handle keyboard interrupt
"""
logger.info("\nGood bye!")
sys.exit(0)
def is_ffmpeg_available(): def is_ffmpeg_available():
""" """
Returns true if ffmpeg is available in the operating system Returns true if ffmpeg is available in the operating system