♻️ Refactored download for CLI.

This commit is contained in:
Robert Honz
2024-01-14 23:09:43 +01:00
parent affaae101e
commit ec728d4dcd
9 changed files with 134 additions and 90 deletions
+2
View File
@@ -4,5 +4,7 @@ import importlib.metadata
try: try:
# __package__ allows for the case where __name__ is "__main__" # __package__ allows for the case where __name__ is "__main__"
__version__ = importlib.metadata.version(__package__ or __name__) __version__ = importlib.metadata.version(__package__ or __name__)
__name_display__ = importlib.metadata.metadata(__package__ or __name__).json["name"]
except importlib.metadata.PackageNotFoundError: except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0" __version__ = "0.0.0"
__name_display__ = __package__ or __name__
+28 -40
View File
@@ -3,6 +3,7 @@ from pathlib import Path
from typing import Annotated, Optional from typing import Annotated, Optional
import typer import typer
from helper.tidal import get_tidal_media_id, get_tidal_media_type
from rich.live import Live from rich.live import Live
from rich.panel import Panel from rich.panel import Panel
from rich.progress import BarColumn, Console, Progress, SpinnerColumn, TextColumn from rich.progress import BarColumn, Console, Progress, SpinnerColumn, TextColumn
@@ -12,8 +13,7 @@ from tidal_dl_ng import __version__
from tidal_dl_ng.config import Settings, Tidal from tidal_dl_ng.config import Settings, Tidal
from tidal_dl_ng.constants import CTX_TIDAL, MediaType 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 path_file_settings from tidal_dl_ng.helper.path import get_format_template, path_file_settings
from tidal_dl_ng.helper.url import get_tidal_media_id, get_tidal_media_type
from tidal_dl_ng.helper.wrapper import WrapperLogger from tidal_dl_ng.helper.wrapper import WrapperLogger
from tidal_dl_ng.model.cfg import HelpSettings from tidal_dl_ng.model.cfg import HelpSettings
@@ -104,7 +104,7 @@ def login(ctx: typer.Context) -> bool:
@app.command(name="dl") @app.command(name="dl")
def download( def download(
ctx: typer.Context, ctx: typer.Context,
urls_or_ids: Annotated[Optional[list[str]], typer.Argument()] = None, urls: Annotated[Optional[list[str]], typer.Argument()] = None,
list_urls: Annotated[ list_urls: Annotated[
Optional[Path], Optional[Path],
typer.Option( typer.Option(
@@ -120,77 +120,64 @@ def download(
), ),
] = None, ] = None,
): ):
if not urls_or_ids: if not urls:
# Read the text file provided.
if list_urls: if list_urls:
text = list_urls.read_text() text = list_urls.read_text()
urls_or_ids = text.splitlines() urls = text.splitlines()
else: else:
print("Provide either URLs, IDs or a file containing URLs (one per line).") print("Provide either URLs, IDs or a file containing URLs (one per line).")
raise typer.Abort() raise typer.Abort()
# Call login method to validate the token.
ctx.invoke(login, ctx) ctx.invoke(login, ctx)
dl = Download(ctx.obj[CTX_TIDAL].session, ctx.obj[CTX_TIDAL].settings.data.skip_existing) # Create initial objects.
settings: Settings = Settings() settings: Settings = Settings()
media_type: MediaType = None dl = Download(ctx.obj[CTX_TIDAL].session, ctx.obj[CTX_TIDAL].settings.data.skip_existing)
progress: Progress = Progress( progress: Progress = Progress(
"{task.description}", "{task.description}",
SpinnerColumn(), SpinnerColumn(),
BarColumn(), BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
) )
progress_table = Table.grid()
fn_logger = WrapperLogger(progress.print) fn_logger = WrapperLogger(progress.print)
progress_table = Table.grid()
# Style Progress display.
progress_table.add_row(Panel.fit(progress, title="Download Progress", border_style="green", padding=(2, 2))) progress_table.add_row(Panel.fit(progress, title="Download Progress", border_style="green", padding=(2, 2)))
for item in urls_or_ids: for item in urls:
if "http" in item: media_type: str | bool = False
media_name = get_tidal_media_type(item)
id_item = get_tidal_media_id(item)
else:
media_name = False
id_item = item
if not media_name: # 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)
# If url is invalid skip to next url in list.
if not media_type:
print(f"It seems like that you have supplied an invalid URL: {item}") print(f"It seems like that you have supplied an invalid URL: {item}")
continue continue
# Create Live display for Progress.
with Live(progress_table, refresh_per_second=10): with Live(progress_table, refresh_per_second=10):
# TODO: Fix media type # Download media.
if media_name in ["track", "video"]: if media_type in [MediaType.Track, MediaType.Video]:
if media_name == "track":
file_template = settings.data.format_track
media_type = MediaType.Track
elif media_name == "video":
file_template = settings.data.format_video
media_type = MediaType.Video
dl.item( dl.item(
media_id=id_item, media_id=item_id,
media_type=media_type, media_type=media_type,
path_base=settings.data.download_base_path, path_base=settings.data.download_base_path,
file_template=file_template, file_template=file_template,
progress=progress, progress=progress,
fn_logger=fn_logger, fn_logger=fn_logger,
) )
elif media_name in ["album", "playlist", "mix"]: elif media_type in [MediaType.Album, MediaType.Playlist, MediaType.Mix]:
file_template: str | bool = False
if media_name == "album":
file_template = settings.data.format_album
media_type = MediaType.Album
elif media_name == "playlist":
file_template = settings.data.format_playlist
media_type = MediaType.Playlist
elif media_name == "mix":
file_template = settings.data.format_mix
media_type = MediaType.Mix
dl.items( dl.items(
media_id=id_item, media_id=item_id,
media_type=media_type, media_type=media_type,
path_base=settings.data.download_base_path, path_base=settings.data.download_base_path,
file_template=file_template, file_template=file_template,
@@ -200,6 +187,7 @@ def download(
fn_logger=fn_logger, fn_logger=fn_logger,
) )
# Stop Progress display.
progress.stop() progress.stop()
return True return True
+7 -3
View File
@@ -8,7 +8,7 @@ import tidalapi
from requests import HTTPError from requests import HTTPError
from tidal_dl_ng.helper.decorator import SingletonMeta from tidal_dl_ng.helper.decorator import SingletonMeta
from tidal_dl_ng.helper.path import path_file_settings, path_file_token from tidal_dl_ng.helper.path import path_base, path_file_settings, path_file_token
from tidal_dl_ng.model.cfg import Settings as ModelSettings from tidal_dl_ng.model.cfg import Settings as ModelSettings
from tidal_dl_ng.model.cfg import Token as ModelToken from tidal_dl_ng.model.cfg import Token as ModelToken
@@ -17,10 +17,14 @@ class BaseConfig:
data: ModelSettings | ModelToken = None data: ModelSettings | ModelToken = None
file_path: str = None file_path: str = None
cls_model: object = None cls_model: object = None
path_base: str = path_base()
def save(self) -> None: def save(self) -> None:
data_json = self.data.to_json() data_json = self.data.to_json()
# Try to create the base folder.
os.makedirs(self.path_base, exist_ok=True)
with open(self.file_path, encoding="utf-8", mode="w") as f: with open(self.file_path, encoding="utf-8", mode="w") as f:
f.write(data_json) f.write(data_json)
@@ -79,8 +83,8 @@ class Tidal(BaseConfig, metaclass=SingletonMeta):
def __init__(self, settings: Settings = None): def __init__(self, settings: Settings = None):
self.session = tidalapi.Session() self.session = tidalapi.Session()
self.session.config.client_id = "6BDSRdpK9hqEBTgU" # self.session.config.client_id = "km8T1xS355y7dd3H"
self.session.config.client_secret = "xeuPmY7nbpZ9IIbLAcQ93shka1VNheUAqN6IcszjTG8=" # self.session.config.client_secret = "vcmeGW1OuZ0fWYMCSZ6vNvSLJlT3XEpW0ambgYt5ZuI="
self.session.video_quality = tidalapi.VideoQuality.high self.session.video_quality = tidalapi.VideoQuality.high
self.file_path = path_file_token() self.file_path = path_file_token()
self.token_from_storage = self.read(self.file_path) self.token_from_storage = self.read(self.file_path)
+3 -1
View File
@@ -267,7 +267,9 @@ class Download:
totaltrack=track.album.num_tracks if track.album and track.album.num_tracks else 1, totaltrack=track.album.num_tracks if track.album and track.album.num_tracks else 1,
totaldisc=track.album.num_volumes if track.album and track.album.num_volumes else 1, totaldisc=track.album.num_volumes if track.album and track.album.num_volumes else 1,
discnumber=track.volume_num if track.volume_num else 1, discnumber=track.volume_num if track.volume_num else 1,
url_cover=self.cover_url(track.album.cover, self.settings.data.metadata_cover_dimension) if track.album else "", url_cover=(
self.cover_url(track.album.cover, self.settings.data.metadata_cover_dimension) if track.album else ""
),
) )
m.save() m.save()
+4 -18
View File
@@ -2,6 +2,8 @@ import math
import sys import sys
from collections.abc import Callable from collections.abc import Callable
from helper.path import get_format_template
try: try:
import qdarktheme import qdarktheme
from PySide6 import QtCore, QtGui, QtWidgets from PySide6 import QtCore, QtGui, QtWidgets
@@ -409,13 +411,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
item=self.s_item_advance, list_item=self.s_list_advance, item_name=self.s_item_name item=self.s_item_advance, list_item=self.s_list_advance, item_name=self.s_item_name
) )
progress: Progress = Progress() progress: Progress = Progress()
file_template = get_format_template(media, self.settings)
if isinstance(media, Track | Video): if isinstance(media, Track | Video):
if isinstance(media, Track):
file_template: str = self.settings.data.format_track
elif isinstance(media, Video):
file_template: str = self.settings.data.format_video
result_download, download_path_file = dl.item( result_download, download_path_file = dl.item(
media=media, media=media,
path_base=self.settings.data.download_base_path, path_base=self.settings.data.download_base_path,
@@ -430,19 +428,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
else: else:
logger_gui.info(f"Download skipped (file exists): {download_path_file}") logger_gui.info(f"Download skipped (file exists): {download_path_file}")
elif isinstance(media, Album | Playlist | Mix): elif isinstance(media, Album | Playlist | Mix):
file_template: str | bool = False progress_name = media.name if media.name else media.title if media.title else "List N/A"
if isinstance(media, Album):
file_template: str = self.settings.data.format_album
progress_name = media.name
elif isinstance(media, Playlist):
file_template: str = self.settings.data.format_playlist
progress_name = media.name
elif isinstance(media, Mix):
file_template: str = self.settings.data.format_mix
progress_name = media.title
else:
progress_name = "List N/A"
self.progress_list_name(progress_name) self.progress_list_name(progress_name)
+56 -9
View File
@@ -8,8 +8,11 @@ from pathvalidate import sanitize_filename, sanitize_filepath
from pathvalidate.error import ValidationError from pathvalidate.error import ValidationError
from tidalapi import Album, Mix, Playlist, Track, UserPlaylist, Video from tidalapi import Album, Mix, Playlist, Track, UserPlaylist, Video
from tidal_dl_ng import __name_display__
from tidal_dl_ng.constants import MediaType
def path_base():
def path_home() -> str:
if "XDG_CONFIG_HOME" in os.environ: if "XDG_CONFIG_HOME" in os.environ:
return os.environ["XDG_CONFIG_HOME"] return os.environ["XDG_CONFIG_HOME"]
elif "HOME" in os.environ: elif "HOME" in os.environ:
@@ -20,16 +23,41 @@ def path_base():
return os.path.abspath("./") return os.path.abspath("./")
def path_file_log(): def path_base() -> str:
return os.path.join(path_base(), ".tidal-dl-ng.log") path_config: str = ".config"
path_base: str = os.path.join(path_home(), path_config, __name_display__)
return path_base
def path_file_token(): def path_file_log() -> str:
return os.path.join(path_base(), ".tidal-dl-ng_token.json") # TODO: Remove this soon. Only for migration to new dir.
old = os.path.join(path_home(), ".tidal-dl-ng.log")
if os.path.isfile(old):
os.makedirs(path_base(), exist_ok=True)
os.rename(old, os.path.join(path_base(), "app.log"))
return os.path.join(path_base(), "app.log")
def path_file_settings(): def path_file_token() -> str:
return os.path.join(path_base(), ".tidal-dl-ng_settings.json") # TODO: Remove this soon. Only for migration to new dir.
old = os.path.join(path_home(), ".tidal-dl-ng_token.json")
if os.path.isfile(old):
os.makedirs(path_base(), exist_ok=True)
os.rename(old, os.path.join(path_base(), "token.json"))
return os.path.join(path_base(), "token.json")
def path_file_settings() -> str:
# TODO: Remove this soon. Only for migration to new dir.
old = os.path.join(path_home(), ".tidal-dl-ng_settings.json")
if os.path.isfile(old):
os.makedirs(path_base(), exist_ok=True)
os.rename(old, os.path.join(path_base(), "settings.json"))
return os.path.join(path_base(), "settings.json")
def format_path_media(fmt_template: str, media: Track | Album | Playlist | UserPlaylist | Video | Mix) -> str: def format_path_media(fmt_template: str, media: Track | Album | Playlist | UserPlaylist | Video | Mix) -> str:
@@ -38,7 +66,7 @@ def format_path_media(fmt_template: str, media: Track | Album | Playlist | UserP
# Search track format template for placeholder. # Search track format template for placeholder.
regex = r"\{(.+?)\}" regex = r"\{(.+?)\}"
matches = re.finditer(regex, fmt_template, re.MULTILINE) matches = re.finditer(regex, fmt_template, re.MULTILINE)
fn_format = get_fn_format(media) fn_format = get_format_fn(media)
for _matchNum, match in enumerate(matches, start=1): for _matchNum, match in enumerate(matches, start=1):
template_str = match.group() template_str = match.group()
@@ -104,7 +132,7 @@ def format_str_video(name: str, media: Video) -> str | bool:
return result return result
def get_fn_format(media: Track | Album | Playlist | UserPlaylist | Video | Mix) -> Callable: def get_format_fn(media: Track | Album | Playlist | UserPlaylist | Video | Mix) -> Callable:
result = None result = None
if isinstance(media, Track): if isinstance(media, Track):
@@ -121,6 +149,25 @@ def get_fn_format(media: Track | Album | Playlist | UserPlaylist | Video | Mix)
return result return result
def get_format_template(
media: Track | Album | Playlist | UserPlaylist | Video | Mix | MediaType, settings
) -> str | bool:
result = False
if isinstance(media, Track) or media == MediaType.Track:
result = settings.data.format_track
elif isinstance(media, Album) or media == MediaType.Album:
result = settings.data.format_album
elif isinstance(media, Playlist | UserPlaylist) or media == MediaType.Playlist:
result = settings.data.format_playlist
elif isinstance(media, Mix) or media == MediaType.Mix:
result = settings.data.format_mix
elif isinstance(media, Video) or media == MediaType.Video:
result = settings.data.format_video
return result
def path_file_sanitize(path_file: str, adapt: bool = False) -> (bool, str): def path_file_sanitize(path_file: str, adapt: bool = False) -> (bool, str):
# Split into path and filename # Split into path and filename
pathname, filename = os.path.split(path_file) pathname, filename = os.path.split(path_file)
+30
View File
@@ -1,5 +1,7 @@
from tidalapi import Track from tidalapi import Track
from tidal_dl_ng.constants import MediaType
def name_builder_artist(media: Track) -> str: def name_builder_artist(media: Track) -> str:
return ", ".join(artist.name for artist in media.artists) return ", ".join(artist.name for artist in media.artists)
@@ -11,3 +13,31 @@ def name_builder_title(media: Track) -> str:
def name_builder_item(media: Track) -> str: def name_builder_item(media: Track) -> str:
return f"{name_builder_artist(media)} - {name_builder_title(media)}" return f"{name_builder_artist(media)} - {name_builder_title(media)}"
def get_tidal_media_id(url_or_id_media: str) -> str:
id_dirty = url_or_id_media.rsplit("/", 1)[-1]
id_media = id_dirty.rsplit("?", 1)[0]
return id_media
def get_tidal_media_type(url_media: str) -> MediaType | bool:
result: MediaType | bool = False
url_split = url_media.split("/")[-2]
if len(url_split) > 1:
media_name = url_media.split("/")[-2]
if media_name == "track":
result = MediaType.Track
elif media_name == "video":
result = MediaType.Video
elif media_name == "album":
result = MediaType.Album
elif media_name == "playlist":
result = MediaType.Playlist
elif media_name == "mix":
result = MediaType.Mix
return result
-15
View File
@@ -1,15 +0,0 @@
def get_tidal_media_id(url_or_id_media: str) -> str:
id_dirty = url_or_id_media.rsplit("/", 1)[-1]
id_media = id_dirty.rsplit("?", 1)[0]
return id_media
def get_tidal_media_type(url_media: str) -> str | bool:
result = False
url_split = url_media.split("/")[-2]
if len(url_split) > 1:
result = url_media.split("/")[-2]
return result
+4 -4
View File
@@ -11,15 +11,15 @@ from tidal_dl_ng.constants import CoverDimensions, QualityVideo, SkipExisting
class Settings: class Settings:
skip_existing: SkipExisting = SkipExisting.Disabled skip_existing: SkipExisting = SkipExisting.Disabled
# TODO: Implement cover download to a separate file. # TODO: Implement cover download to a separate file.
#album_cover_save: bool = True # album_cover_save: bool = True
lyrics_save: bool = False lyrics_save: bool = False
# TODO: Implement API KEY selection. # TODO: Implement API KEY selection.
#api_key_index: bool = 0 # api_key_index: bool = 0
# TODO: Implement album info download to separate file. # TODO: Implement album info download to separate file.
#album_info_save: bool = False # album_info_save: bool = False
video_download: bool = True video_download: bool = True
# TODO: Implement multi threading for downloads. # TODO: Implement multi threading for downloads.
#multi_thread: bool = False # multi_thread: bool = False
download_delay: bool = True download_delay: bool = True
download_base_path: str = "./download" download_base_path: str = "./download"
quality_audio: Quality = Quality.low_320k quality_audio: Quality = Quality.low_320k