Better error handling, return exit code 1 on errors
parent
d79d55ff89
commit
0f6991077b
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
"""Python Soundcloud Music Downloader."""
|
"""Python Soundcloud Music Downloader."""
|
||||||
|
|
||||||
__version__ = "v2.2.4"
|
__version__ = "v2.2.5"
|
177
scdl/scdl.py
177
scdl/scdl.py
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue