summaryrefslogtreecommitdiff
path: root/tidal_dl_ng/cli.py
diff options
context:
space:
mode:
authorexislow <exislow@users.noreply.github.com>2025-07-03 09:57:26 +0200
committerGitHub <noreply@github.com>2025-07-03 09:57:26 +0200
commit7da0d4c41e4abb6ec03ee1b0f64474ee970a230e (patch)
treea5e625565e2439ad92c590f7f9d2aeaf49824fe9 /tidal_dl_ng/cli.py
parent29ea5c51825628dbe96ce4404488b87d5fadb80d (diff)
parent4662d18fdfc25d42d496cf6abb1c8d44be0c76dd (diff)
downloadtidal-dl-ng-7da0d4c41e4abb6ec03ee1b0f64474ee970a230e.tar.gz
Merge pull request #478 from exislow/ai-refactoring
Diffstat (limited to 'tidal_dl_ng/cli.py')
-rw-r--r--tidal_dl_ng/cli.py363
1 files changed, 232 insertions, 131 deletions
diff --git a/tidal_dl_ng/cli.py b/tidal_dl_ng/cli.py
index 75deba4..85e9f6e 100644
--- a/tidal_dl_ng/cli.py
+++ b/tidal_dl_ng/cli.py
@@ -22,12 +22,6 @@ from tidal_dl_ng.constants import CTX_TIDAL, MediaType
from tidal_dl_ng.download import Download
from tidal_dl_ng.helper.path import get_format_template, path_file_settings
from tidal_dl_ng.helper.tidal import (
- Album,
- Artist,
- Mix,
- Playlist,
- Track,
- Video,
all_artist_album_ids,
get_tidal_media_id,
get_tidal_media_type,
@@ -47,30 +41,167 @@ app.add_typer(dl_fav_group, name="dl_fav")
def version_callback(value: bool):
+ """Callback to print version and exit if version flag is set.
+
+ Args:
+ value (bool): If True, prints version and exits.
+ """
if value:
print(f"{__version__}")
+
raise typer.Exit()
+def _handle_track_or_video(
+ dl: Download, ctx: typer.Context, item: str, media: object, file_template: str, idx: int, urls_pos_last: int
+) -> None:
+ """Handle downloading a track or video item.
+
+ Args:
+ dl (Download): The Download instance.
+ ctx (typer.Context): Typer context object.
+ item (str): The URL or identifier of the item.
+ media: The media object to download.
+ file_template (str): The file template for saving the media.
+ idx (int): The index of the item in the list.
+ urls_pos_last (int): The last index in the URLs list.
+ """
+ settings = ctx.obj[CTX_TIDAL].settings
+ download_delay: bool = bool(settings.data.download_delay and idx < urls_pos_last)
+
+ dl.item(
+ media=media,
+ file_template=file_template,
+ download_delay=download_delay,
+ quality_audio=settings.data.quality_audio,
+ quality_video=settings.data.quality_video,
+ )
+
+
+def _handle_album_playlist_mix_artist(
+ ctx: typer.Context,
+ dl: Download,
+ handling_app: HandlingApp,
+ media_type: MediaType,
+ media: object,
+ item_id: str,
+ file_template: str,
+) -> bool:
+ """Handle downloading albums, playlists, mixes, or artist collections.
+
+ Args:
+ ctx (typer.Context): Typer context object.
+ dl (Download): The Download instance.
+ handling_app (HandlingApp): The HandlingApp instance.
+ media_type (MediaType): The type of media (album, playlist, mix, or artist).
+ media: The media object to download.
+ item_id (str): The ID of the media item.
+ file_template (str): The file template for saving the media.
+
+ Returns:
+ bool: False if aborted, True otherwise.
+ """
+ item_ids: list[str] = []
+ settings = ctx.obj[CTX_TIDAL].settings
+
+ if media_type == MediaType.ARTIST:
+ media_type = MediaType.ALBUM
+ item_ids += all_artist_album_ids(media)
+ else:
+ item_ids.append(item_id)
+
+ for _item_id in item_ids:
+ if handling_app.event_abort.is_set():
+ return False
+
+ dl.items(
+ media_id=_item_id,
+ media_type=media_type,
+ file_template=file_template,
+ video_download=settings.data.video_download,
+ download_delay=settings.data.download_delay,
+ quality_audio=settings.data.quality_audio,
+ quality_video=settings.data.quality_video,
+ )
+
+ return True
+
+
+def _process_url(
+ dl: Download,
+ ctx: typer.Context,
+ handling_app: HandlingApp,
+ item: str,
+ idx: int,
+ urls_pos_last: int,
+) -> bool:
+ """Process a single URL or ID for download.
+
+ Args:
+ dl (Download): The Download instance.
+ ctx (typer.Context): Typer context object.
+ handling_app (HandlingApp): The HandlingApp instance.
+ item (str): The URL or identifier to process.
+ idx (int): The index of the item in the list.
+ urls_pos_last (int): The last index in the URLs list.
+
+ Returns:
+ bool: False if aborted, True otherwise.
+ """
+ settings = ctx.obj[CTX_TIDAL].settings
+
+ if handling_app.event_abort.is_set():
+ return False
+
+ if "http" not in item:
+ print(f"It seems like that you have supplied an invalid URL: {item}")
+ return True
+
+ media_type = get_tidal_media_type(item)
+ if not isinstance(media_type, MediaType):
+ print(f"Could not determine media type for: {item}")
+ return True
+
+ item_id = get_tidal_media_id(item)
+ if not isinstance(item_id, str):
+ print(f"Could not determine media id for: {item}")
+ return True
+
+ file_template = get_format_template(media_type, settings)
+ if not isinstance(file_template, str):
+ print(f"Could not determine file template for: {item}")
+ return True
+
+ try:
+ media = instantiate_media(ctx.obj[CTX_TIDAL].session, media_type, item_id)
+ except Exception:
+ print(f"Media not found (ID: {item_id}). Maybe it is not available anymore.")
+ return True
+
+ if media_type in [MediaType.TRACK, MediaType.VIDEO]:
+ _handle_track_or_video(dl, ctx, item, media, file_template, idx, urls_pos_last)
+ elif media_type in [MediaType.ALBUM, MediaType.PLAYLIST, MediaType.MIX, MediaType.ARTIST]:
+ return _handle_album_playlist_mix_artist(ctx, dl, handling_app, media_type, media, item_id, file_template)
+ return True
+
+
def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bool:
"""Invokes download function and tracks progress.
- :param ctx: The typer context object.
- :type ctx: typer.Context
- :param urls: The list of URLs to download.
- :type urls: list[str]
- :param try_login: If true, attempts to login to TIDAL.
- :type try_login: bool
- :return: True if ran successfully.
- :rtype: bool
+ Args:
+ ctx (typer.Context): The typer context object.
+ urls (list[str]): The list of URLs to download.
+ try_login (bool, optional): If true, attempts to login to TIDAL. Defaults to True.
+
+ Returns:
+ bool: True if ran successfully.
"""
if try_login:
- # Call login method to validate the token.
ctx.invoke(login, ctx)
- # Create initial objects.
- settings: Settings = Settings()
+ settings: Settings = ctx.obj[CTX_TIDAL].settings
handling_app: HandlingApp = HandlingApp()
+
progress: Progress = Progress(
TextColumn("[progress.description]{task.description}"),
SpinnerColumn(),
@@ -79,8 +210,9 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
refresh_per_second=20,
auto_refresh=True,
expand=True,
- transient=False, # Prevent progress from disappearing
+ transient=False,
)
+
progress_overall = Progress(
TextColumn("[progress.description]{task.description}"),
SpinnerColumn(),
@@ -89,12 +221,14 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
refresh_per_second=20,
auto_refresh=True,
expand=True,
- transient=False, # Prevent progress from disappearing
+ transient=False,
)
+
fn_logger = LoggerWrapped(progress.print)
+
dl = Download(
session=ctx.obj[CTX_TIDAL].session,
- skip_existing=ctx.obj[CTX_TIDAL].settings.data.skip_existing,
+ skip_existing=settings.data.skip_existing,
path_base=settings.data.download_base_path,
fn_logger=fn_logger,
progress=progress,
@@ -102,80 +236,20 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
event_abort=handling_app.event_abort,
event_run=handling_app.event_run,
)
- progress_table = Table.grid()
- # Style Progress display.
+ progress_table = Table.grid()
progress_table.add_row(progress)
progress_table.add_row(progress_overall)
-
- progress_group = Group(
- progress_table,
- )
+ progress_group = Group(progress_table)
urls_pos_last = len(urls) - 1
- # Use a single Live display for both progress and table
with Live(progress_group, refresh_per_second=20, vertical_overflow="visible"):
try:
- for item in urls:
- media_type: MediaType | bool = False
-
- # Exit loop if abort signal is set.
- if handling_app.event_abort.is_set():
+ for idx, item in enumerate(urls):
+ if _process_url(dl, ctx, handling_app, item, idx, urls_pos_last) is False:
return False
-
- # Extract media name and id from link.
- if "http" in item:
- media_type = get_tidal_media_type(item)
- item_id = get_tidal_media_id(item)
- file_template = get_format_template(media_type, settings)
- else:
- print(f"It seems like that you have supplied an invalid URL: {item}")
-
- continue
-
- try:
- media: Track | Video | Album | Playlist | Mix | Artist = instantiate_media(
- ctx.obj[CTX_TIDAL].session, media_type, item_id
- )
- except Exception:
- print(f"Media not found (ID: {item_id}). Maybe it is not available anymore.")
- continue
-
- # Download media.
- if media_type in [MediaType.TRACK, MediaType.VIDEO]:
- download_delay: bool = bool(settings.data.download_delay and urls.index(item) < urls_pos_last)
-
- dl.item(
- media=media,
- file_template=file_template,
- download_delay=download_delay,
- quality_audio=settings.data.quality_audio,
- quality_video=settings.data.quality_video,
- )
- elif media_type in [MediaType.ALBUM, MediaType.PLAYLIST, MediaType.MIX, MediaType.ARTIST]:
- item_ids: [int] = []
-
- if media_type == MediaType.ARTIST:
- media_type = MediaType.ALBUM
- item_ids = item_ids + all_artist_album_ids(media)
- else:
- item_ids.append(item_id)
-
- for item_id in item_ids:
- # Exit loop if abort signal is set.
- if handling_app.event_abort.is_set():
- return False
-
- dl.items(
- media_id=item_id,
- media_type=media_type,
- file_template=file_template,
- video_download=ctx.obj[CTX_TIDAL].settings.data.video_download,
- download_delay=settings.data.download_delay,
- )
finally:
- # Clear and stop progress display
progress.refresh()
progress.stop()
@@ -187,6 +261,12 @@ def callback_app(
ctx: typer.Context,
version: Annotated[bool | None, typer.Option("--version", "-v", callback=version_callback, is_eager=True)] = None,
):
+ """App callback to initialize context and handle version option.
+
+ Args:
+ ctx (typer.Context): Typer context object.
+ version (bool | None, optional): Version flag. Defaults to None.
+ """
ctx.obj = {"tidal": None}
@@ -196,16 +276,12 @@ def settings_management(
editor: Annotated[
bool, typer.Option("--editor", "-e", help="Open the settings file in your default editor.")
] = False,
-):
- """
- Print or set an option.
- If no arguments are given, all options will be listed.
- If only one argument is given, the value will be printed for this option.
- To set a value for an option simply pass the value as the second argument
+) -> None:
+ """Print or set an option, or open the settings file in an editor.
- :param editor: If set, your favorite system editor will be opened.
- :param names: (Optional) None (list all options), one (list the value only for this option) or two arguments
- (set the value for the option).
+ Args:
+ names (list[str] | None, optional): None (list all options), one (list the value only for this option) or two arguments (set the value for the option). Defaults to None.
+ editor (bool, optional): If set, your favorite system editor will be opened. Defaults to False.
"""
if editor:
config_path: Path = Path(path_file_settings())
@@ -223,12 +299,11 @@ def settings_management(
if names:
if names[0] not in d_settings:
print(f'Option "{names[0]}" is not valid!')
- else:
- if len(names) == 1:
- print(f'{names[0]}: "{d_settings[names[0]]}"')
- elif len(names) > 1:
- settings.set_option(names[0], names[1])
- settings.save()
+ elif len(names) == 1:
+ print(f'{names[0]}: "{d_settings[names[0]]}"')
+ elif len(names) > 1:
+ settings.set_option(names[0], names[1])
+ settings.save()
else:
help_settings: dict = HelpSettings().to_dict()
table = Table(title=f"Config: {path_file_settings()}")
@@ -236,7 +311,6 @@ def settings_management(
table.add_column("Value", style="magenta")
table.add_column("Description", style="green")
- # Iterate over the attributes of the dataclass
for key, value in sorted(d_settings.items()):
table.add_row(key, str(value), help_settings[key])
@@ -246,6 +320,14 @@ def settings_management(
@app.command(name="login")
def login(ctx: typer.Context) -> bool:
+ """Login to TIDAL and update context object.
+
+ Args:
+ ctx (typer.Context): Typer context object.
+
+ Returns:
+ bool: True if login was successful, False otherwise.
+ """
print("Let us check, if you are already logged in... ", end="")
settings = Settings()
@@ -258,6 +340,11 @@ def login(ctx: typer.Context) -> bool:
@app.command(name="logout")
def logout() -> bool:
+ """Logout from TIDAL.
+
+ Returns:
+ bool: True if logout was successful, False otherwise.
+ """
settings = Settings()
tidal = Tidal(settings)
result = tidal.logout()
@@ -287,6 +374,16 @@ def download(
),
] = None,
) -> bool:
+ """Download media from provided URLs or a file containing URLs.
+
+ Args:
+ ctx (typer.Context): Typer context object.
+ urls (list[str] | None, optional): List of URLs to download. Defaults to None.
+ file_urls (Path | None, optional): Path to file containing URLs. Defaults to None.
+
+ Returns:
+ bool: True if download was successful, False otherwise.
+ """
if not urls:
# Read the text file provided.
if file_urls:
@@ -307,10 +404,11 @@ def download(
def download_fav_tracks(ctx: typer.Context) -> bool:
"""Download your favorite track collection.
- :param ctx: Typer context object.
- :type ctx: typer.Context
- :return: Download result.
- :rtype: bool
+ Args:
+ ctx (typer.Context): Typer context object.
+
+ Returns:
+ bool: Download result.
"""
# Method name
func_name_favorites: str = "tracks"
@@ -325,10 +423,11 @@ def download_fav_tracks(ctx: typer.Context) -> bool:
def download_fav_artists(ctx: typer.Context) -> bool:
"""Download your favorite artist collection.
- :param ctx: Typer context object.
- :type ctx: typer.Context
- :return: Download result.
- :rtype: bool
+ Args:
+ ctx (typer.Context): Typer context object.
+
+ Returns:
+ bool: Download result.
"""
# Method name
func_name_favorites: str = "artists"
@@ -343,10 +442,11 @@ def download_fav_artists(ctx: typer.Context) -> bool:
def download_fav_albums(ctx: typer.Context) -> bool:
"""Download your favorite album collection.
- :param ctx: Typer context object.
- :type ctx: typer.Context
- :return: Download result.
- :rtype: bool
+ Args:
+ ctx (typer.Context): Typer context object.
+
+ Returns:
+ bool: Download result.
"""
# Method name
func_name_favorites: str = "albums"
@@ -361,10 +461,11 @@ def download_fav_albums(ctx: typer.Context) -> bool:
def download_fav_videos(ctx: typer.Context) -> bool:
"""Download your favorite video collection.
- :param ctx: Typer context object.
- :type ctx: typer.Context
- :return: Download result.
- :rtype: bool
+ Args:
+ ctx (typer.Context): Typer context object.
+
+ Returns:
+ bool: Download result.
"""
# Method name
func_name_favorites: str = "videos"
@@ -375,26 +476,26 @@ def download_fav_videos(ctx: typer.Context) -> bool:
def _download_fav_factory(ctx: typer.Context, func_name_favorites: str) -> bool:
"""Factory which helps to download items from the favorites collections.
- :param ctx: Typer context object.
- :type ctx: typer.Context
- :param func_name_favorites: Method name to call from `tidalapi` favorites object.
- :type func_name_favorites: str
- :return: Download result.
- :rtype: bool
+ Args:
+ ctx (typer.Context): Typer context object.
+ func_name_favorites (str): Method name to call from `tidalapi` favorites object.
+
+ Returns:
+ bool: Download result.
"""
- # Call login method to validate the token.
ctx.invoke(login, ctx)
-
- # Get the method from the module
func_favorites: Callable = getattr(ctx.obj[CTX_TIDAL].session.user.favorites, func_name_favorites)
- # Get favorite videos
- media_urls: [str] = [media.share_url for media in func_favorites()]
-
+ media_urls: list[str] = [media.share_url for media in func_favorites()]
return _download(ctx, media_urls, try_login=False)
@app.command()
def gui(ctx: typer.Context):
+ """Launch the GUI for the application.
+
+ Args:
+ ctx (typer.Context): Typer context object.
+ """
from tidal_dl_ng.gui import gui_activate
ctx.invoke(login, ctx)
@@ -404,9 +505,9 @@ def gui(ctx: typer.Context):
def handle_sigint_term(signum, frame):
"""Set app abort event, so threads can check it and shutdown.
- :param signum:
- :param frame:
- :return:
+ Args:
+ signum: Signal number.
+ frame: Current stack frame.
"""
handling_app: HandlingApp = HandlingApp()