refactor(cli): ✨ Improve function documentation and remove unused imports

* Enhanced docstrings for clarity and consistency across functions.
* Removed unused imports to clean up the codebase.
* Improved the structure of the `_download` function for better readability.
This commit is contained in:
Robert Honz
2025-07-01 11:42:07 +02:00
parent 4dde8c819a
commit 38a97357dd
+211 -115
View File
@@ -22,12 +22,6 @@ from tidal_dl_ng.constants import CTX_TIDAL, MediaType
from tidal_dl_ng.download import Download 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.path import get_format_template, path_file_settings
from tidal_dl_ng.helper.tidal import ( from tidal_dl_ng.helper.tidal import (
Album,
Artist,
Mix,
Playlist,
Track,
Video,
all_artist_album_ids, all_artist_album_ids,
get_tidal_media_id, get_tidal_media_id,
get_tidal_media_type, get_tidal_media_type,
@@ -47,30 +41,155 @@ app.add_typer(dl_fav_group, name="dl_fav")
def version_callback(value: bool): 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: if value:
print(f"{__version__}") print(f"{__version__}")
raise typer.Exit() raise typer.Exit()
def _handle_track_or_video(
dl: Download, settings: Settings, 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.
settings (Settings): The Settings instance.
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.
"""
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(
dl: Download,
ctx: typer.Context,
handling_app: HandlingApp,
settings: Settings,
media_type: MediaType,
media: object,
item_id: int,
file_template: str,
) -> bool:
"""Handle downloading albums, playlists, mixes, or artist collections.
Args:
dl (Download): The Download instance.
ctx (typer.Context): Typer context object.
handling_app (HandlingApp): The HandlingApp instance.
settings (Settings): The Settings instance.
media_type (MediaType): The type of media (album, playlist, mix, or artist).
media: The media object to download.
item_id (int): 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[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:
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,
)
return True
def _process_url(
dl: Download,
ctx: typer.Context,
handling_app: HandlingApp,
settings: Settings,
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.
settings (Settings): The Settings 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.
"""
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: MediaType = get_tidal_media_type(item)
item_id: int = get_tidal_media_id(item)
file_template: str = get_format_template(media_type, settings)
try:
media: object = 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, settings, 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(
dl, ctx, handling_app, settings, media_type, media, item_id, file_template
)
return True
def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bool: def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bool:
"""Invokes download function and tracks progress. """Invokes download function and tracks progress.
:param ctx: The typer context object. Args:
:type ctx: typer.Context ctx (typer.Context): The typer context object.
:param urls: The list of URLs to download. urls (list[str]): The list of URLs to download.
:type urls: list[str] try_login (bool, optional): If true, attempts to login to TIDAL. Defaults to True.
:param try_login: If true, attempts to login to TIDAL.
:type try_login: bool Returns:
:return: True if ran successfully. bool: True if ran successfully.
:rtype: bool
""" """
if try_login: if try_login:
# Call login method to validate the token.
ctx.invoke(login, ctx) ctx.invoke(login, ctx)
# Create initial objects.
settings: Settings = Settings() settings: Settings = Settings()
handling_app: HandlingApp = HandlingApp() handling_app: HandlingApp = HandlingApp()
progress: Progress = Progress( progress: Progress = Progress(
TextColumn("[progress.description]{task.description}"), TextColumn("[progress.description]{task.description}"),
SpinnerColumn(), SpinnerColumn(),
@@ -79,8 +198,9 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
refresh_per_second=20, refresh_per_second=20,
auto_refresh=True, auto_refresh=True,
expand=True, expand=True,
transient=False, # Prevent progress from disappearing transient=False,
) )
progress_overall = Progress( progress_overall = Progress(
TextColumn("[progress.description]{task.description}"), TextColumn("[progress.description]{task.description}"),
SpinnerColumn(), SpinnerColumn(),
@@ -89,9 +209,11 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
refresh_per_second=20, refresh_per_second=20,
auto_refresh=True, auto_refresh=True,
expand=True, expand=True,
transient=False, # Prevent progress from disappearing transient=False,
) )
fn_logger = LoggerWrapped(progress.print) fn_logger = LoggerWrapped(progress.print)
dl = Download( dl = Download(
session=ctx.obj[CTX_TIDAL].session, session=ctx.obj[CTX_TIDAL].session,
skip_existing=ctx.obj[CTX_TIDAL].settings.data.skip_existing, skip_existing=ctx.obj[CTX_TIDAL].settings.data.skip_existing,
@@ -102,80 +224,20 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
event_abort=handling_app.event_abort, event_abort=handling_app.event_abort,
event_run=handling_app.event_run, 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)
progress_table.add_row(progress_overall) progress_table.add_row(progress_overall)
progress_group = Group(progress_table)
progress_group = Group(
progress_table,
)
urls_pos_last = len(urls) - 1 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"): with Live(progress_group, refresh_per_second=20, vertical_overflow="visible"):
try: try:
for item in urls: for idx, item in enumerate(urls):
media_type: MediaType | bool = False if _process_url(dl, ctx, handling_app, settings, item, idx, urls_pos_last) is False:
# Exit loop if abort signal is set.
if handling_app.event_abort.is_set():
return 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: finally:
# Clear and stop progress display
progress.refresh() progress.refresh()
progress.stop() progress.stop()
@@ -187,6 +249,12 @@ def callback_app(
ctx: typer.Context, ctx: typer.Context,
version: Annotated[bool | None, typer.Option("--version", "-v", callback=version_callback, is_eager=True)] = None, 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} ctx.obj = {"tidal": None}
@@ -197,15 +265,11 @@ def settings_management(
bool, typer.Option("--editor", "-e", help="Open the settings file in your default editor.") bool, typer.Option("--editor", "-e", help="Open the settings file in your default editor.")
] = False, ] = False,
): ):
""" """Print or set an option, or open the settings file in an editor.
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
:param editor: If set, your favorite system editor will be opened. Args:
:param names: (Optional) None (list all options), one (list the value only for this option) or two arguments 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.
(set the value for the option). editor (bool, optional): If set, your favorite system editor will be opened. Defaults to False.
""" """
if editor: if editor:
config_path: Path = Path(path_file_settings()) config_path: Path = Path(path_file_settings())
@@ -246,6 +310,14 @@ def settings_management(
@app.command(name="login") @app.command(name="login")
def login(ctx: typer.Context) -> bool: 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="") print("Let us check, if you are already logged in... ", end="")
settings = Settings() settings = Settings()
@@ -258,6 +330,11 @@ def login(ctx: typer.Context) -> bool:
@app.command(name="logout") @app.command(name="logout")
def logout() -> bool: def logout() -> bool:
"""Logout from TIDAL.
Returns:
bool: True if logout was successful, False otherwise.
"""
settings = Settings() settings = Settings()
tidal = Tidal(settings) tidal = Tidal(settings)
result = tidal.logout() result = tidal.logout()
@@ -287,6 +364,16 @@ def download(
), ),
] = None, ] = None,
) -> bool: ) -> 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: if not urls:
# Read the text file provided. # Read the text file provided.
if file_urls: if file_urls:
@@ -307,10 +394,11 @@ def download(
def download_fav_tracks(ctx: typer.Context) -> bool: def download_fav_tracks(ctx: typer.Context) -> bool:
"""Download your favorite track collection. """Download your favorite track collection.
:param ctx: Typer context object. Args:
:type ctx: typer.Context ctx (typer.Context): Typer context object.
:return: Download result.
:rtype: bool Returns:
bool: Download result.
""" """
# Method name # Method name
func_name_favorites: str = "tracks" func_name_favorites: str = "tracks"
@@ -325,10 +413,11 @@ def download_fav_tracks(ctx: typer.Context) -> bool:
def download_fav_artists(ctx: typer.Context) -> bool: def download_fav_artists(ctx: typer.Context) -> bool:
"""Download your favorite artist collection. """Download your favorite artist collection.
:param ctx: Typer context object. Args:
:type ctx: typer.Context ctx (typer.Context): Typer context object.
:return: Download result.
:rtype: bool Returns:
bool: Download result.
""" """
# Method name # Method name
func_name_favorites: str = "artists" func_name_favorites: str = "artists"
@@ -343,10 +432,11 @@ def download_fav_artists(ctx: typer.Context) -> bool:
def download_fav_albums(ctx: typer.Context) -> bool: def download_fav_albums(ctx: typer.Context) -> bool:
"""Download your favorite album collection. """Download your favorite album collection.
:param ctx: Typer context object. Args:
:type ctx: typer.Context ctx (typer.Context): Typer context object.
:return: Download result.
:rtype: bool Returns:
bool: Download result.
""" """
# Method name # Method name
func_name_favorites: str = "albums" func_name_favorites: str = "albums"
@@ -361,10 +451,11 @@ def download_fav_albums(ctx: typer.Context) -> bool:
def download_fav_videos(ctx: typer.Context) -> bool: def download_fav_videos(ctx: typer.Context) -> bool:
"""Download your favorite video collection. """Download your favorite video collection.
:param ctx: Typer context object. Args:
:type ctx: typer.Context ctx (typer.Context): Typer context object.
:return: Download result.
:rtype: bool Returns:
bool: Download result.
""" """
# Method name # Method name
func_name_favorites: str = "videos" func_name_favorites: str = "videos"
@@ -375,12 +466,12 @@ def download_fav_videos(ctx: typer.Context) -> bool:
def _download_fav_factory(ctx: typer.Context, func_name_favorites: str) -> bool: def _download_fav_factory(ctx: typer.Context, func_name_favorites: str) -> bool:
"""Factory which helps to download items from the favorites collections. """Factory which helps to download items from the favorites collections.
:param ctx: Typer context object. Args:
:type ctx: typer.Context ctx (typer.Context): Typer context object.
:param func_name_favorites: Method name to call from `tidalapi` favorites object. func_name_favorites (str): Method name to call from `tidalapi` favorites object.
:type func_name_favorites: str
:return: Download result. Returns:
:rtype: bool bool: Download result.
""" """
# Call login method to validate the token. # Call login method to validate the token.
ctx.invoke(login, ctx) ctx.invoke(login, ctx)
@@ -395,6 +486,11 @@ def _download_fav_factory(ctx: typer.Context, func_name_favorites: str) -> bool:
@app.command() @app.command()
def gui(ctx: typer.Context): 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 from tidal_dl_ng.gui import gui_activate
ctx.invoke(login, ctx) ctx.invoke(login, ctx)
@@ -404,9 +500,9 @@ def gui(ctx: typer.Context):
def handle_sigint_term(signum, frame): def handle_sigint_term(signum, frame):
"""Set app abort event, so threads can check it and shutdown. """Set app abort event, so threads can check it and shutdown.
:param signum: Args:
:param frame: signum: Signal number.
:return: frame: Current stack frame.
""" """
handling_app: HandlingApp = HandlingApp() handling_app: HandlingApp = HandlingApp()