From 2353324570a50cea94aa3d84efd537a92388a2e7 Mon Sep 17 00:00:00 2001 From: Dongho Kim Date: Fri, 27 Dec 2024 22:00:28 +0900 Subject: [PATCH] test --- main.py | 52 + requirements.txt | 55 + templates/base.html | 43 + templates/pages/index.html | 20 + tidal_dl_ng/__init__.py | 134 ++ tidal_dl_ng/api.py | 114 ++ tidal_dl_ng/cli.py | 234 ++++ tidal_dl_ng/config.py | 191 +++ tidal_dl_ng/constants.py | 60 + tidal_dl_ng/dialog.py | 307 +++++ tidal_dl_ng/download.py | 805 ++++++++++++ tidal_dl_ng/gui.py | 1103 +++++++++++++++++ tidal_dl_ng/helper/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 174 bytes .../__pycache__/decorator.cpython-311.pyc | Bin 0 -> 1350 bytes .../__pycache__/decryption.cpython-311.pyc | Bin 0 -> 2646 bytes .../__pycache__/exceptions.cpython-311.pyc | Bin 0 -> 963 bytes .../helper/__pycache__/path.cpython-311.pyc | Bin 0 -> 17732 bytes .../helper/__pycache__/tidal.cpython-311.pyc | Bin 0 -> 9391 bytes .../__pycache__/wrapper.cpython-311.pyc | Bin 0 -> 1792 bytes tidal_dl_ng/helper/decorator.py | 22 + tidal_dl_ng/helper/decryption.py | 55 + tidal_dl_ng/helper/exceptions.py | 14 + tidal_dl_ng/helper/gui.py | 201 +++ tidal_dl_ng/helper/path.py | 324 +++++ tidal_dl_ng/helper/tidal.py | 205 +++ tidal_dl_ng/helper/wrapper.py | 26 + tidal_dl_ng/logger.py | 65 + tidal_dl_ng/metadata.py | 164 +++ tidal_dl_ng/model/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 173 bytes .../model/__pycache__/cfg.cpython-311.pyc | Bin 0 -> 6823 bytes .../__pycache__/downloader.cpython-311.pyc | Bin 0 -> 821 bytes .../__pycache__/gui_data.cpython-311.pyc | Bin 0 -> 2355 bytes .../model/__pycache__/meta.cpython-311.pyc | Bin 0 -> 894 bytes tidal_dl_ng/model/cfg.py | 108 ++ tidal_dl_ng/model/downloader.py | 13 + tidal_dl_ng/model/gui_data.py | 46 + tidal_dl_ng/model/meta.py | 14 + tidal_dl_ng/ui/__init__.py | 0 tidal_dl_ng/ui/default_album_image.png | Bin 0 -> 4960 bytes tidal_dl_ng/ui/dialog_login.py | 119 ++ tidal_dl_ng/ui/dialog_login.ui | 195 +++ tidal_dl_ng/ui/dialog_settings.py | 631 ++++++++++ tidal_dl_ng/ui/dialog_settings.ui | 812 ++++++++++++ tidal_dl_ng/ui/dialog_version.py | 161 +++ tidal_dl_ng/ui/dialog_version.ui | 227 ++++ tidal_dl_ng/ui/dummy_register.py | 33 + tidal_dl_ng/ui/dummy_wiggly.py | 68 + tidal_dl_ng/ui/icon.icns | Bin 0 -> 91254 bytes tidal_dl_ng/ui/icon.ico | Bin 0 -> 220222 bytes tidal_dl_ng/ui/icon.png | Bin 0 -> 54166 bytes tidal_dl_ng/ui/main.py | 588 +++++++++ tidal_dl_ng/ui/main.ui | 801 ++++++++++++ tidal_dl_ng/ui/spinner.py | 221 ++++ tidal_dl_ng/worker.py | 34 + 56 files changed, 8265 insertions(+) create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 templates/base.html create mode 100644 templates/pages/index.html create mode 100644 tidal_dl_ng/__init__.py create mode 100644 tidal_dl_ng/api.py create mode 100644 tidal_dl_ng/cli.py create mode 100644 tidal_dl_ng/config.py create mode 100644 tidal_dl_ng/constants.py create mode 100644 tidal_dl_ng/dialog.py create mode 100644 tidal_dl_ng/download.py create mode 100644 tidal_dl_ng/gui.py create mode 100644 tidal_dl_ng/helper/__init__.py create mode 100644 tidal_dl_ng/helper/__pycache__/__init__.cpython-311.pyc create mode 100644 tidal_dl_ng/helper/__pycache__/decorator.cpython-311.pyc create mode 100644 tidal_dl_ng/helper/__pycache__/decryption.cpython-311.pyc create mode 100644 tidal_dl_ng/helper/__pycache__/exceptions.cpython-311.pyc create mode 100644 tidal_dl_ng/helper/__pycache__/path.cpython-311.pyc create mode 100644 tidal_dl_ng/helper/__pycache__/tidal.cpython-311.pyc create mode 100644 tidal_dl_ng/helper/__pycache__/wrapper.cpython-311.pyc create mode 100644 tidal_dl_ng/helper/decorator.py create mode 100644 tidal_dl_ng/helper/decryption.py create mode 100644 tidal_dl_ng/helper/exceptions.py create mode 100644 tidal_dl_ng/helper/gui.py create mode 100644 tidal_dl_ng/helper/path.py create mode 100644 tidal_dl_ng/helper/tidal.py create mode 100644 tidal_dl_ng/helper/wrapper.py create mode 100644 tidal_dl_ng/logger.py create mode 100644 tidal_dl_ng/metadata.py create mode 100644 tidal_dl_ng/model/__init__.py create mode 100644 tidal_dl_ng/model/__pycache__/__init__.cpython-311.pyc create mode 100644 tidal_dl_ng/model/__pycache__/cfg.cpython-311.pyc create mode 100644 tidal_dl_ng/model/__pycache__/downloader.cpython-311.pyc create mode 100644 tidal_dl_ng/model/__pycache__/gui_data.cpython-311.pyc create mode 100644 tidal_dl_ng/model/__pycache__/meta.cpython-311.pyc create mode 100644 tidal_dl_ng/model/cfg.py create mode 100644 tidal_dl_ng/model/downloader.py create mode 100644 tidal_dl_ng/model/gui_data.py create mode 100644 tidal_dl_ng/model/meta.py create mode 100644 tidal_dl_ng/ui/__init__.py create mode 100644 tidal_dl_ng/ui/default_album_image.png create mode 100644 tidal_dl_ng/ui/dialog_login.py create mode 100644 tidal_dl_ng/ui/dialog_login.ui create mode 100644 tidal_dl_ng/ui/dialog_settings.py create mode 100644 tidal_dl_ng/ui/dialog_settings.ui create mode 100644 tidal_dl_ng/ui/dialog_version.py create mode 100644 tidal_dl_ng/ui/dialog_version.ui create mode 100644 tidal_dl_ng/ui/dummy_register.py create mode 100644 tidal_dl_ng/ui/dummy_wiggly.py create mode 100644 tidal_dl_ng/ui/icon.icns create mode 100644 tidal_dl_ng/ui/icon.ico create mode 100644 tidal_dl_ng/ui/icon.png create mode 100644 tidal_dl_ng/ui/main.py create mode 100644 tidal_dl_ng/ui/main.ui create mode 100644 tidal_dl_ng/ui/spinner.py create mode 100644 tidal_dl_ng/worker.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..48eadb3 --- /dev/null +++ b/main.py @@ -0,0 +1,52 @@ +from tidal_dl_ng.config import Settings, Tidal +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 ( + all_artist_album_ids, + get_tidal_media_id, + get_tidal_media_type, + instantiate_media, +) +from tidal_dl_ng.helper.wrapper import LoggerWrapped +from tidal_dl_ng.model.cfg import HelpSettings + +from rich.live import Live +from rich.panel import Panel +from rich.progress import BarColumn, Console, Progress, SpinnerColumn, TextColumn +from rich.table import Table + +from pathlib import Path +from typing import Annotated, Optional +from fastapi import FastAPI, HTTPException, Depends, Request +from fastapi.templating import Jinja2Templates +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel +from typing import List, Optional +import uvicorn + +# Initialize FastAPI app +app = FastAPI( + title="FastAPI Template", description="A simple FastAPI template with basic CRUD operations", version="1.0.0" +) + +app.mount("/static", StaticFiles(directory="static"), name="static") + +# Initialize Jinja2 templates +templates = Jinja2Templates(directory="templates") + + +# Main Landing Page +@app.get("/") +async def main(request: Request): + return templates.TemplateResponse("pages/index.html", {"request": request, "title": "Home Page"}) + + +# Health check endpoint +@app.get("/health") +async def health_check(): + return {"status": "healthy"} + + +if __name__ == "__main__": + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ad2c0f3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,55 @@ +annotated-types==0.7.0 +anyio==4.7.0 +certifi==2024.12.14 +charset-normalizer==3.4.1 +click==8.1.8 +dataclasses-json==0.6.7 +dnspython==2.7.0 +email_validator==2.2.0 +fastapi==0.115.6 +fastapi-cli==0.0.7 +ffmpeg-python==0.2.0 +future==1.0.0 +h11==0.14.0 +httpcore==1.0.7 +httptools==0.6.4 +httpx==0.28.1 +idna==3.10 +isodate==0.7.2 +Jinja2==3.1.5 +m3u8==6.0.0 +markdown-it-py==3.0.0 +MarkupSafe==3.0.2 +marshmallow==3.23.2 +mdurl==0.1.2 +mpegdash==0.4.0 +mutagen==1.47.0 +mypy-extensions==1.0.0 +packaging==24.2 +pathvalidate==3.2.1 +pycryptodome==3.21.0 +pydantic==2.10.4 +pydantic_core==2.27.2 +Pygments==2.18.0 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +python-multipart==0.0.20 +PyYAML==6.0.2 +ratelimit==2.2.1 +requests==2.32.3 +rich==13.9.4 +rich-toolkit==0.12.0 +shellingham==1.5.4 +six==1.17.0 +sniffio==1.3.1 +starlette==0.41.3 +tidalapi==0.8.2 +toml==0.10.2 +typer==0.15.1 +typing-inspect==0.9.0 +typing_extensions==4.12.2 +urllib3==2.3.0 +uvicorn==0.34.0 +uvloop==0.21.0 +watchfiles==1.0.3 +websockets==14.1 diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..93fe594 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,43 @@ +{# templates/base.html #} + + + + + + {% block title %}{{ title }}{% endblock %} - My Site + + {# CSS Block #} + {% block css %} + + {% endblock %} + + + {# Header Block #} + {% block header %} +
+ +
+ {% endblock %} + + {# Main Content Block #} +
+ {% block content %} + {% endblock %} +
+ + {# Footer Block #} + {% block footer %} + + {% endblock %} + + {# JavaScript Block #} + {% block javascript %} + + {% endblock %} + + \ No newline at end of file diff --git a/templates/pages/index.html b/templates/pages/index.html new file mode 100644 index 0000000..7375b00 --- /dev/null +++ b/templates/pages/index.html @@ -0,0 +1,20 @@ +{# templates/pages/home.html #} +{% extends "base.html" %} + +{% block content %} +
+

Welcome to the Home Page

+

This is the home page content.

+
+{% endblock %} + +{% block css %} +{{ super() }} + +{% endblock %} \ No newline at end of file diff --git a/tidal_dl_ng/__init__.py b/tidal_dl_ng/__init__.py new file mode 100644 index 0000000..5fb660b --- /dev/null +++ b/tidal_dl_ng/__init__.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +import importlib.metadata +import sys +from pathlib import Path +from urllib.parse import urlparse + +import requests +import toml + +from tidal_dl_ng.constants import REQUESTS_TIMEOUT_SEC +from tidal_dl_ng.model.meta import ProjectInformation, ReleaseLatest + + +def metadata_project() -> ProjectInformation: + result: ProjectInformation + file_path: Path = Path(__file__) + tmp_result: dict = {} + + paths: [Path] = [ + file_path.parent, + file_path.parent.parent, + file_path.parent.parent.parent, + ] + + for pyproject_toml_dir in paths: + pyproject_toml_file: Path = pyproject_toml_dir / "pyproject.toml" + + if pyproject_toml_file.exists() and pyproject_toml_file.is_file(): + tmp_result = toml.load(pyproject_toml_file) + + break + + if tmp_result: + result = ProjectInformation( + version=tmp_result["tool"]["poetry"]["version"], repository_url=tmp_result["tool"]["poetry"]["repository"] + ) + else: + try: + meta_info = importlib.metadata.metadata(name_package()) + result = ProjectInformation(version=meta_info["Version"], repository_url=meta_info["Home-page"]) + except: + result = ProjectInformation(version="0.0.0", repository_url="https://anerroroccur.ed/sorry/for/that") + + return result + + +def version_app() -> str: + metadata: ProjectInformation = metadata_project() + version: str = metadata.version + + return version + + +def repository_url() -> str: + metadata: ProjectInformation = metadata_project() + url_repo: str = metadata.repository_url + + return url_repo + + +def repository_path() -> str: + url_repo: str = repository_url() + url_path: str = urlparse(url_repo).path + + return url_path + + +def latest_version_information() -> ReleaseLatest: + release_info: ReleaseLatest + repo_path: str = repository_path() + url: str = f"https://api.github.com/repos{repo_path}/releases/latest" + + try: + response = requests.get(url, timeout=REQUESTS_TIMEOUT_SEC) + release_info: str = response.json() + + release_info = ReleaseLatest( + version=release_info["tag_name"], url=release_info["html_url"], release_info=release_info["body"] + ) + except: + release_info = ReleaseLatest( + version="v0.0.0", + url=url, + release_info=f"Something went wrong calling {url}. Check your internet connection.", + ) + + return release_info + + +def name_package() -> str: + package_name: str = __package__ or __name__ + + return package_name + + +def is_dev_env() -> bool: + package_name: str = name_package() + result: bool = False + + # Check if package is running from source code == dev mode + # If package is not running in PyInstaller environment. + if not getattr(sys, "frozen", False) and not hasattr(sys, "_MEIPASS"): + try: + importlib.metadata.version(package_name) + except: + # If package is not installed + result = True + + return result + + +def name_app() -> str: + app_name: str = name_package() + is_dev: bool = is_dev_env() + + if is_dev: + app_name = app_name + "-dev" + + return app_name + + +__name_display__ = name_app() +__version__ = version_app() + + +def update_available() -> (bool, ReleaseLatest): + latest_info: ReleaseLatest = latest_version_information() + result: bool = False + version_current: str = "v" + __version__ + + if version_current != latest_info.version and version_current != "v0.0.0": + result = True + + return result, latest_info diff --git a/tidal_dl_ng/api.py b/tidal_dl_ng/api.py new file mode 100644 index 0000000..5923159 --- /dev/null +++ b/tidal_dl_ng/api.py @@ -0,0 +1,114 @@ +import json + +import requests + +# See also +# https://github.com/yaronzz/Tidal-Media-Downloader/commit/1d5b8cd8f65fd1def45d6406778248249d6dfbdf +# https://github.com/yaronzz/Tidal-Media-Downloader/pull/840 +# https://github.com/nathom/streamrip/tree/main/streamrip +# https://github.com/arnesongit/plugin.audio.tidal2/blob/e9429d601d0c303d775d05a19a66415b57479d87/resources/lib/tidal2/tidalapi/__init__.py#L86 + +# TODO: Implement this into `Download`: Session should randomize the usage. +__KEYS_JSON__ = """ +{ + "version": "1.0.1", + "keys": [ + { + "platform": "Fire TV", + "formats": "Normal/High/HiFi(No Master)", + "clientId": "OmDtrzFgyVVL6uW56OnFA2COiabqm", + "clientSecret": "zxen1r3pO0hgtOC7j6twMo9UAqngGrmRiWpV7QC1zJ8=", + "valid": "False", + "from": "Fokka-Engineering (https://github.com/Fokka-Engineering/libopenTIDAL/blob/655528e26e4f3ee2c426c06ea5b8440cf27abc4a/README.md#example)" + }, + { + "platform": "Fire TV", + "formats": "Master-Only(Else Error)", + "clientId": "7m7Ap0JC9j1cOM3n", + "clientSecret": "vRAdA108tlvkJpTsGZS8rGZ7xTlbJ0qaZ2K9saEzsgY=", + "valid": "True", + "from": "Dniel97 (https://github.com/Dniel97/RedSea/blob/4ba02b88cee33aeb735725cb854be6c66ff372d4/config/settings.example.py#L68)" + }, + { + "platform": "Android TV", + "formats": "Normal/High/HiFi(No Master)", + "clientId": "Pzd0ExNVHkyZLiYN", + "clientSecret": "W7X6UvBaho+XOi1MUeCX6ewv2zTdSOV3Y7qC3p3675I=", + "valid": "False", + "from": "" + }, + { + "platform": "TV", + "formats": "Normal/High/HiFi/Master", + "clientId": "8SEZWa4J1NVC5U5Y", + "clientSecret": "owUYDkxddz+9FpvGX24DlxECNtFEMBxipU0lBfrbq60=", + "valid": "False", + "from": "morguldir (https://github.com/morguldir/python-tidal/commit/50f1afcd2079efb2b4cf694ef5a7d67fdf619d09)" + }, + { + "platform": "Android Auto", + "formats": "Normal/High/HiFi/Master", + "clientId": "zU4XHVVkc2tDPo4t", + "clientSecret": "VJKhDFqJPqvsPVNBV6ukXTJmwlvbttP7wlMlrc72se4=", + "valid": "True", + "from": "1nikolas (https://github.com/yaronzz/Tidal-Media-Downloader/pull/840)" + } + ] +} +""" +__API_KEYS__ = json.loads(__KEYS_JSON__) +__ERROR_KEY__ = ( + { + "platform": "None", + "formats": "", + "clientId": "", + "clientSecret": "", + "valid": "False", + }, +) + +from tidal_dl_ng.constants import REQUESTS_TIMEOUT_SEC + + +def getNum(): + return len(__API_KEYS__["keys"]) + + +def getItem(index: int): + if index < 0 or index >= len(__API_KEYS__["keys"]): + return __ERROR_KEY__ + return __API_KEYS__["keys"][index] + + +def isItemValid(index: int): + item = getItem(index) + return item["valid"] == "True" + + +def getItems(): + return __API_KEYS__["keys"] + + +def getLimitIndexs(): + array = [] + for i in range(len(__API_KEYS__["keys"])): + array.append(str(i)) + return array + + +def getVersion(): + return __API_KEYS__["version"] + + +# Load from gist +try: + respond = requests.get( + "https://api.github.com/gists/48d01f5a24b4b7b37f19443977c22cd6", timeout=REQUESTS_TIMEOUT_SEC + ) + if respond.status_code == 200: + content = respond.json()["files"]["tidal-api-key.json"]["content"] + __API_KEYS__ = json.loads(content) +except Exception as e: + # TODO: Implement proper logging. + print(e) + pass diff --git a/tidal_dl_ng/cli.py b/tidal_dl_ng/cli.py new file mode 100644 index 0000000..5abda11 --- /dev/null +++ b/tidal_dl_ng/cli.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python +from pathlib import Path +from typing import Annotated, Optional + +import typer +from rich.live import Live +from rich.panel import Panel +from rich.progress import BarColumn, Console, Progress, SpinnerColumn, TextColumn +from rich.table import Table + +from tidal_dl_ng import __version__ +from tidal_dl_ng.config import Settings, Tidal +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 ( + all_artist_album_ids, + get_tidal_media_id, + get_tidal_media_type, + instantiate_media, +) +from tidal_dl_ng.helper.wrapper import LoggerWrapped +from tidal_dl_ng.model.cfg import HelpSettings + +app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]}, add_completion=False) + + +def version_callback(value: bool): + if value: + print(f"{__version__}") + raise typer.Exit() + + +@app.callback() +def callback_app( + ctx: typer.Context, + version: Annotated[ + Optional[bool], typer.Option("--version", "-v", callback=version_callback, is_eager=True) + ] = None, +): + ctx.obj = {"tidal": None} + + +@app.command(name="cfg") +def settings_management( + names: Annotated[Optional[list[str]], typer.Argument()] = None, + 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 + + :param names: (Optional) None (list all options), one (list the value only for this option) or two arguments + (set the value for the option). + """ + if editor: + config_path: Path = Path(path_file_settings()) + + if not config_path.is_file(): + config_path.write_text('{"version": "1.0.0"}') + + config_file_str = str(config_path) + + typer.launch(config_file_str) + else: + settings = Settings() + d_settings = settings.data.to_dict() + + 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() + else: + help_settings: dict = HelpSettings().to_dict() + table = Table(title=f"Config: {path_file_settings()}") + table.add_column("Key", style="cyan", no_wrap=True) + 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]) + + console = Console() + console.print(table) + + +@app.command(name="login") +def login(ctx: typer.Context) -> bool: + print("Let us check, if you are already logged in... ", end="") + + settings = Settings() + tidal = Tidal(settings) + result = tidal.login(fn_print=print) + ctx.obj[CTX_TIDAL] = tidal + + return result + + +@app.command(name="logout") +def logout() -> bool: + settings = Settings() + tidal = Tidal(settings) + result = tidal.logout() + + if result: + print("You have been successfully logged out.") + + return result + + +@app.command(name="dl") +def download( + ctx: typer.Context, + urls: Annotated[Optional[list[str]], typer.Argument()] = None, + file_urls: Annotated[ + Optional[Path], + typer.Option( + "--list", + "-l", + exists=True, + file_okay=True, + dir_okay=False, + writable=False, + readable=True, + resolve_path=True, + help="List with URLs to download. One per line", + ), + ] = None, +): + if not urls: + # Read the text file provided. + if file_urls: + text = file_urls.read_text() + urls = text.splitlines() + else: + print("Provide either URLs, IDs or a file containing URLs (one per line).") + + raise typer.Abort() + + # Call login method to validate the token. + ctx.invoke(login, ctx) + + # Create initial objects. + settings: Settings = Settings() + progress: Progress = Progress( + "{task.description}", + SpinnerColumn(), + BarColumn(), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), + ) + fn_logger = LoggerWrapped(progress.print) + dl = Download( + session=ctx.obj[CTX_TIDAL].session, + skip_existing=ctx.obj[CTX_TIDAL].settings.data.skip_existing, + path_base=settings.data.download_base_path, + fn_logger=fn_logger, + progress=progress, + ) + progress_table = Table.grid() + + # Style Progress display. + progress_table.add_row(Panel.fit(progress, title="Download Progress", border_style="green", padding=(2, 2))) + + urls_pos_last = len(urls) - 1 + + for item in urls: + media_type: MediaType | bool = 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) + + # 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}") + + continue + + # Create Live display for Progress. + with Live(progress_table, refresh_per_second=10): + # 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_id=item_id, media_type=media_type, file_template=file_template, download_delay=download_delay + ) + elif media_type in [MediaType.ALBUM, MediaType.PLAYLIST, MediaType.MIX, MediaType.ARTIST]: + item_ids: [int] = [] + + if media_type == MediaType.ARTIST: + media = instantiate_media(ctx.obj[CTX_TIDAL].session, media_type, item_id) + 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: + 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, + ) + + # Stop Progress display. + progress.stop() + + return True + + +@app.command() +def gui(ctx: typer.Context): + from tidal_dl_ng.gui import gui_activate + + ctx.invoke(login, ctx) + gui_activate(ctx.obj[CTX_TIDAL]) + + +if __name__ == "__main__": + app() diff --git a/tidal_dl_ng/config.py b/tidal_dl_ng/config.py new file mode 100644 index 0000000..c6fdaee --- /dev/null +++ b/tidal_dl_ng/config.py @@ -0,0 +1,191 @@ +import json +import os +import shutil +from collections.abc import Callable +from json import JSONDecodeError +from pathlib import Path +from typing import Any + +import tidalapi +from requests import HTTPError + +from tidal_dl_ng.helper.decorator import SingletonMeta +from tidal_dl_ng.helper.path import path_config_base, path_file_settings, path_file_token +from tidal_dl_ng.model.cfg import Settings as ModelSettings +from tidal_dl_ng.model.cfg import Token as ModelToken + + +class BaseConfig: + data: ModelSettings | ModelToken + file_path: str + cls_model: ModelSettings | ModelToken + path_base: str = path_config_base() + + def save(self, config_to_compare: str = None) -> None: + data_json = self.data.to_json() + + # If old and current config is equal, skip the write operation. + if config_to_compare == data_json: + return + + # 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: + # Save it in a pretty format + obj_json_config = json.loads(data_json) + json.dump(obj_json_config, f, indent=4) + + def set_option(self, key: str, value: Any) -> None: + value_old: Any = getattr(self.data, key) + + if type(value_old) == bool: # noqa: E721 + value = True if value.lower() in ("true", "1", "yes", "y") else False # noqa: SIM210 + elif type(value_old) == int and type(value) != int: # noqa: E721 + value = int(value) + + setattr(self.data, key, value) + + def read(self, path: str) -> bool: + result: bool = False + settings_json: str = "" + + try: + with open(path, encoding="utf-8") as f: + settings_json = f.read() + + self.data = self.cls_model.from_json(settings_json) + result = True + except (JSONDecodeError, TypeError, FileNotFoundError, ValueError) as e: + if isinstance(e, ValueError): + path_bak = path + ".bak" + + # First check if a backup file already exists. If yes, remove it. + if os.path.exists(path_bak): + os.remove(path_bak) + + # Move the invalid config file to the backup location. + shutil.move(path, path_bak) + # TODO: Implement better global logger. + print( + "Something is wrong with your config. Maybe it is not compatible anymore due to a new app version." + f" You can find a backup of your old config here: '{path_bak}'. A new default config was created." + ) + + self.data = self.cls_model() + + # Call save in case of we need to update the saved config, due to changes in code. + self.save(settings_json) + + return result + + +class Settings(BaseConfig, metaclass=SingletonMeta): + def __init__(self): + self.cls_model = ModelSettings + self.file_path = path_file_settings() + self.read(self.file_path) + + +class Tidal(BaseConfig, metaclass=SingletonMeta): + session: tidalapi.Session + token_from_storage: bool = False + settings: Settings + is_pkce: bool + + def __init__(self, settings: Settings = None): + self.cls_model = ModelToken + tidal_config: tidalapi.Config = tidalapi.Config(item_limit=10000) + self.session = tidalapi.Session(tidal_config) + # self.session.config.client_id = "km8T1xS355y7dd3H" + # self.session.config.client_secret = "vcmeGW1OuZ0fWYMCSZ6vNvSLJlT3XEpW0ambgYt5ZuI=" + self.file_path = path_file_token() + self.token_from_storage = self.read(self.file_path) + + if settings: + self.settings = settings + self.settings_apply() + + def settings_apply(self, settings: Settings = None) -> bool: + if settings: + self.settings = settings + + self.session.audio_quality = self.settings.data.quality_audio + self.session.video_quality = tidalapi.VideoQuality.high + + return True + + def login_token(self, do_pkce: bool = False) -> bool: + result = False + self.is_pkce = do_pkce + + if self.token_from_storage: + try: + result = self.session.load_oauth_session( + self.data.token_type, + self.data.access_token, + self.data.refresh_token, + self.data.expiry_time, + is_pkce=do_pkce, + ) + except (HTTPError, JSONDecodeError): + result = False + # Remove token file. Probably corrupt or invalid. + if os.path.exists(self.file_path): + os.remove(self.file_path) + + print( + "Either there is something wrong with your credentials / account or some server problems on TIDALs " + "side. Anyway... Try to login again by re-starting this app." + ) + + return result + + def login_finalize(self) -> bool: + result = self.session.check_login() + + if result: + self.token_persist() + + return result + + def token_persist(self) -> None: + self.set_option("token_type", self.session.token_type) + self.set_option("access_token", self.session.access_token) + self.set_option("refresh_token", self.session.refresh_token) + self.set_option("expiry_time", self.session.expiry_time) + self.save() + + def login(self, fn_print: Callable) -> bool: + is_token = self.login_token() + result = False + + if is_token: + fn_print("Yep, looks good! You are logged in.") + + result = True + elif not is_token: + fn_print("You either do not have a token or your token is invalid.") + fn_print("No worries, we will handle this...") + # Login method: Device linking + self.session.login_oauth_simple(fn_print) + # Login method: PKCE authorization (was necessary for HI_RES_LOSSLESS streaming earlier) + # self.session.login_pkce(fn_print) + + is_login = self.login_finalize() + + if is_login: + fn_print("The login was successful. I have stored your credentials (token).") + + result = True + else: + fn_print("Something went wrong. Did you login using your browser correctly? May try again...") + + return result + + def logout(self): + Path(self.file_path).unlink(missing_ok=True) + self.token_from_storage = False + del self.session + + return True diff --git a/tidal_dl_ng/constants.py b/tidal_dl_ng/constants.py new file mode 100644 index 0000000..7da2711 --- /dev/null +++ b/tidal_dl_ng/constants.py @@ -0,0 +1,60 @@ +from enum import StrEnum + +CTX_TIDAL: str = "tidal" +REQUESTS_TIMEOUT_SEC: int = 45 +EXTENSION_LYRICS: str = ".lrc" +UNIQUIFY_THRESHOLD: int = 99 +FILENAME_SANITIZE_PLACEHOLDER: str = "_" +COVER_NAME: str = "cover.jpg" +BLOCK_SIZE: int = 4096 +BLOCKS: int = 1024 +CHUNK_SIZE: int = BLOCK_SIZE * BLOCKS +PLAYLIST_EXTENSION: str = ".m3u" +PLAYLIST_PREFIX: str = "_" + + +class QualityVideo(StrEnum): + P360: str = "360" + P480: str = "480" + P720: str = "720" + P1080: str = "1080" + + +class MediaType(StrEnum): + TRACK: str = "track" + VIDEO: str = "video" + PLAYLIST: str = "playlist" + ALBUM: str = "album" + MIX: str = "mix" + ARTIST: str = "artist" + + +class CoverDimensions(StrEnum): + Px80: str = "80" + Px160: str = "160" + Px320: str = "320" + Px640: str = "640" + Px1280: str = "1280" + + +class TidalLists(StrEnum): + Playlists: str = "Playlists" + Favorites: str = "Favorites" + Mixes: str = "Mixes" + + +class QueueDownloadStatus(StrEnum): + Waiting: str = "⏳️" + Downloading: str = "▶️" + Finished: str = "✅" + Failed: str = "❌" + Skipped: str = "↪️" + + +FAVORITES: {} = { + "fav_videos": {"name": "Videos", "function_name": "videos"}, + "fav_tracks": {"name": "Tracks", "function_name": "tracks"}, + "fav_mixes": {"name": "Mixes & Radio", "function_name": "mixes"}, + "fav_artists": {"name": "Artists", "function_name": "artists"}, + "fav_albums": {"name": "Albums", "function_name": "albums"}, +} diff --git a/tidal_dl_ng/dialog.py b/tidal_dl_ng/dialog.py new file mode 100644 index 0000000..b17c151 --- /dev/null +++ b/tidal_dl_ng/dialog.py @@ -0,0 +1,307 @@ +import datetime +import os.path +import shutil +import webbrowser +from enum import Enum, StrEnum +from pathlib import Path + +from PySide6 import QtCore, QtGui, QtWidgets +from tidalapi import Quality as QualityAudio + +from tidal_dl_ng import __version__ +from tidal_dl_ng.config import Settings +from tidal_dl_ng.constants import CoverDimensions, QualityVideo +from tidal_dl_ng.model.cfg import HelpSettings +from tidal_dl_ng.model.cfg import Settings as ModelSettings +from tidal_dl_ng.model.meta import ReleaseLatest +from tidal_dl_ng.ui.dialog_login import Ui_DialogLogin +from tidal_dl_ng.ui.dialog_settings import Ui_DialogSettings +from tidal_dl_ng.ui.dialog_version import Ui_DialogVersion + + +class DialogVersion(QtWidgets.QDialog): + """Version dialog.""" + + ui: Ui_DialogVersion + + def __init__( + self, parent=None, update_check: bool = False, update_available: bool = False, update_info: ReleaseLatest = None + ): + super().__init__(parent) + + # Create an instance of the GUI + self.ui = Ui_DialogVersion() + + # Run the .setupUi() method to show the GUI + self.ui.setupUi(self) + # Set the version. + self.ui.l_version.setText("v" + __version__) + + if not update_check: + self.update_info_hide() + self.error_hide() + else: + self.update_info(update_available, update_info) + + # Show + self.exec() + + def update_info(self, update_available: bool, update_info: ReleaseLatest): + if not update_available and update_info.version == "v0.0.0": + self.update_info_hide() + self.ui.l_error_details.setText( + "Cannot retrieve update information. Maybe something is wrong with your internet connection." + ) + else: + self.error_hide() + + if not update_available: + self.ui.l_h_version_new.setText("Latest available version:") + self.changelog_hide() + else: + self.ui.l_changelog_details.setText(update_info.release_info) + self.ui.pb_download.clicked.connect(lambda: webbrowser.open(update_info.url)) + + self.ui.l_version_new.setText(update_info.version) + + def error_hide(self): + self.ui.l_error.setHidden(True) + self.ui.l_error_details.setHidden(True) + + def update_info_hide(self): + self.ui.l_h_version_new.setHidden(True) + self.ui.l_version_new.setHidden(True) + self.changelog_hide() + + def changelog_hide(self): + self.ui.l_changelog.setHidden(True) + self.ui.l_changelog_details.setHidden(True) + self.ui.pb_download.setHidden(True) + + +class DialogLogin(QtWidgets.QDialog): + """Version dialog.""" + + ui: Ui_DialogLogin + url_redirect: str + + def __init__(self, url_login: str, hint: str, expires_in: int, parent=None): + super().__init__(parent) + + datetime_current: datetime.datetime = datetime.datetime.now() + datetime_expires: datetime.datetime = datetime_current + datetime.timedelta(0, expires_in) + + # Create an instance of the GUI + self.ui = Ui_DialogLogin() + + # Run the .setupUi() method to show the GUI + self.ui.setupUi(self) + # Set data. + self.ui.tb_url_login.setText(f'https://{url_login}') + self.ui.l_hint.setText(hint) + self.ui.l_expires_date_time.setText(datetime_expires.strftime("%Y-%m-%d %H:%M")) + # Show + self.return_code = self.exec() + + +class DialogPreferences(QtWidgets.QDialog): + """Preferences dialog.""" + + ui: Ui_DialogSettings + settings: Settings + data: ModelSettings + s_settings_save: QtCore.Signal + icon: QtGui.QIcon + help_settings: HelpSettings + parameters_checkboxes: [str] + parameters_combo: [(str, StrEnum)] + parameters_line_edit: [str] + parameters_spin_box: [str] + prefix_checkbox: str = "cb_" + prefix_label: str = "l_" + prefix_icon: str = "icon_" + prefix_line_edit: str = "le_" + prefix_combo: str = "c_" + prefix_spin_box: str = "sb_" + + def __init__(self, settings: Settings, settings_save: QtCore.Signal, parent=None): + super().__init__(parent) + + self.settings = settings + self.data = settings.data + self.s_settings_save = settings_save + self.help_settings = HelpSettings() + pixmapi: QtWidgets.QStyle.StandardPixmap = QtWidgets.QStyle.SP_MessageBoxQuestion + self.icon = self.style().standardIcon(pixmapi) + + self._init_checkboxes() + self._init_comboboxes() + self._init_line_edit() + self._init_spin_box() + + # Create an instance of the GUI + self.ui = Ui_DialogSettings() + + # Run the .setupUi() method to show the GUI + self.ui.setupUi(self) + # Set data. + self.gui_populate() + # Post setup + + self.exec() + + def _init_line_edit(self): + self.parameters_line_edit = [ + "download_base_path", + "format_album", + "format_playlist", + "format_mix", + "format_track", + "format_video", + "path_binary_ffmpeg", + ] + + def _init_spin_box(self): + self.parameters_spin_box = ["album_track_num_pad_min", "downloads_concurrent_max"] + + def _init_comboboxes(self): + self.parameters_combo = [ + ("quality_audio", QualityAudio), + ("quality_video", QualityVideo), + ("metadata_cover_dimension", CoverDimensions), + ] + + def _init_checkboxes(self): + self.parameters_checkboxes = [ + "lyrics_embed", + "lyrics_file", + "video_download", + "download_delay", + "video_convert_mp4", + "extract_flac", + "metadata_cover_embed", + "cover_album_file", + "skip_existing", + "symlink_to_track", + "playlist_create", + ] + + def gui_populate(self): + self.populate_checkboxes() + self.populate_combo() + self.populate_line_edit() + self.populate_spin_box() + + def dialog_chose_file( + self, + obj_line_edit: QtWidgets.QLineEdit, + file_mode: QtWidgets.QFileDialog | QtWidgets.QFileDialog.FileMode = QtWidgets.QFileDialog.Directory, + path_default: str = None, + ): + # If a path is set, use it otherwise the users home directory. + path_settings: str = os.path.expanduser(obj_line_edit.text()) if obj_line_edit.text() else "" + # Check if obj_line_edit is empty but path_default can be usd instead + path_settings = ( + path_settings if path_settings else os.path.expanduser(path_default) if path_default else path_settings + ) + dir_current: str = path_settings if path_settings and os.path.exists(path_settings) else str(Path.home()) + dialog: QtWidgets.QFileDialog = QtWidgets.QFileDialog() + + # Set to directory mode only but show files. + dialog.setFileMode(file_mode) + dialog.setViewMode(QtWidgets.QFileDialog.Detail) + dialog.setOption(QtWidgets.QFileDialog.ShowDirsOnly, False) + dialog.setOption(QtWidgets.QFileDialog.DontResolveSymlinks, True) + + # There is a bug in the PyQt implementation, which hides files in Directory mode. + # Thus, we need to use the PyQt dialog instead of the native dialog. + if os.name == "nt" and file_mode == QtWidgets.QFileDialog.Directory: + dialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True) + + dialog.setDirectory(dir_current) + + # Execute dialog and set path is something is choosen. + if dialog.exec(): + dir_name: str = dialog.selectedFiles()[0] + path: Path = Path(dir_name) + obj_line_edit.setText(str(path)) + + def populate_line_edit(self): + for pn in self.parameters_line_edit: + label_icon: QtWidgets.QLabel = getattr(self.ui, self.prefix_label + self.prefix_icon + pn) + label: QtWidgets.QLabel = getattr(self.ui, self.prefix_label + pn) + line_edit: QtWidgets.QLineEdit = getattr(self.ui, self.prefix_line_edit + pn) + + label_icon.setPixmap(QtGui.QPixmap(self.icon.pixmap(QtCore.QSize(16, 16)))) + label_icon.setToolTip(getattr(self.help_settings, pn)) + label.setText(pn) + line_edit.setText(str(getattr(self.data, pn))) + + # Base Path File Dialog + self.ui.pb_download_base_path.clicked.connect(lambda x: self.dialog_chose_file(self.ui.le_download_base_path)) + self.ui.pb_path_binary_ffmpeg.clicked.connect( + lambda x: self.dialog_chose_file( + self.ui.le_path_binary_ffmpeg, + file_mode=QtWidgets.QFileDialog.FileMode.ExistingFiles, + path_default=shutil.which("ffmpeg"), + ) + ) + + def populate_combo(self): + for p in self.parameters_combo: + pn: str = p[0] + values: Enum = p[1] + label_icon: QtWidgets.QLabel = getattr(self.ui, self.prefix_label + self.prefix_icon + pn) + label: QtWidgets.QLabel = getattr(self.ui, self.prefix_label + pn) + combo: QtWidgets.QComboBox = getattr(self.ui, self.prefix_combo + pn) + setting_current = getattr(self.data, pn) + + label_icon.setPixmap(QtGui.QPixmap(self.icon.pixmap(QtCore.QSize(16, 16)))) + label_icon.setToolTip(getattr(self.help_settings, pn)) + label.setText(pn) + + for index, v in enumerate(values): + combo.addItem(v.name, v) + + if v == setting_current: + combo.setCurrentIndex(index) + + def populate_checkboxes(self): + for pn in self.parameters_checkboxes: + checkbox: QtWidgets.QCheckBox = getattr(self.ui, self.prefix_checkbox + pn) + + checkbox.setText(pn) + checkbox.setToolTip(getattr(self.help_settings, pn)) + checkbox.setIcon(self.icon) + checkbox.setChecked(getattr(self.data, pn)) + + def populate_spin_box(self): + for pn in self.parameters_spin_box: + label_icon: QtWidgets.QLabel = getattr(self.ui, self.prefix_label + self.prefix_icon + pn) + label: QtWidgets.QLabel = getattr(self.ui, self.prefix_label + pn) + spin_box: QtWidgets.QSpinBox = getattr(self.ui, self.prefix_spin_box + pn) + + label_icon.setPixmap(QtGui.QPixmap(self.icon.pixmap(QtCore.QSize(16, 16)))) + label_icon.setToolTip(getattr(self.help_settings, pn)) + label.setText(pn) + spin_box.setValue(getattr(self.data, pn)) + + def accept(self): + # Get settings. + self.to_settings() + self.done(1) + + def to_settings(self): + for item in self.parameters_checkboxes: + setattr(self.settings.data, item, getattr(self.ui, self.prefix_checkbox + item).isChecked()) + + for item in self.parameters_line_edit: + setattr(self.settings.data, item, getattr(self.ui, self.prefix_line_edit + item).text()) + + for item in self.parameters_combo: + setattr(self.settings.data, item[0], getattr(self.ui, self.prefix_combo + item[0]).currentData()) + + for item in self.parameters_spin_box: + setattr(self.settings.data, item, getattr(self.ui, self.prefix_spin_box + item).value()) + + self.s_settings_save.emit() diff --git a/tidal_dl_ng/download.py b/tidal_dl_ng/download.py new file mode 100644 index 0000000..0807a1a --- /dev/null +++ b/tidal_dl_ng/download.py @@ -0,0 +1,805 @@ +import os +import pathlib +import random +import shutil +import tempfile +import time +from collections.abc import Callable +from concurrent import futures +from uuid import uuid4 + +import ffmpeg +import m3u8 +import requests +from requests.adapters import HTTPAdapter, Retry +from requests.exceptions import HTTPError +from rich.progress import Progress, TaskID +from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video +from tidalapi.exceptions import TooManyRequests +from tidalapi.media import AudioExtensions, Codec, Quality, Stream, StreamManifest, VideoExtensions + +from tidal_dl_ng.config import Settings +from tidal_dl_ng.constants import ( + CHUNK_SIZE, + COVER_NAME, + EXTENSION_LYRICS, + PLAYLIST_EXTENSION, + PLAYLIST_PREFIX, + REQUESTS_TIMEOUT_SEC, + MediaType, + QualityVideo, +) +from tidal_dl_ng.helper.decryption import decrypt_file, decrypt_security_token +from tidal_dl_ng.helper.exceptions import MediaMissing +from tidal_dl_ng.helper.path import check_file_exists, format_path_media, path_file_sanitize, url_to_filename +from tidal_dl_ng.helper.tidal import ( + instantiate_media, + items_results_all, + name_builder_album_artist, + name_builder_artist, + name_builder_item, + name_builder_title, +) +from tidal_dl_ng.metadata import Metadata +from tidal_dl_ng.model.downloader import DownloadSegmentResult +from tidal_dl_ng.model.gui_data import ProgressBars + + +# TODO: Set appropriate client string and use it for video download. +# https://github.com/globocom/m3u8#using-different-http-clients +class RequestsClient: + def download( + self, uri: str, timeout: int = REQUESTS_TIMEOUT_SEC, headers: dict | None = None, verify_ssl: bool = True + ): + if not headers: + headers = {} + + o = requests.get(uri, timeout=timeout, headers=headers) + + return o.text, o.url + + +# TODO: Use pathlib.Path everywhere +class Download: + settings: Settings + session: Session + skip_existing: bool = False + fn_logger: Callable + progress_gui: ProgressBars + progress: Progress + + def __init__( + self, + session: Session, + path_base: str, + fn_logger: Callable, + skip_existing: bool = False, + progress_gui: ProgressBars = None, + progress: Progress = None, + ): + self.settings = Settings() + self.session = session + self.skip_existing = skip_existing + self.fn_logger = fn_logger + self.progress_gui = progress_gui + self.progress = progress + self.path_base = path_base + + if not self.settings.data.path_binary_ffmpeg and ( + self.settings.data.video_convert_mp4 or self.settings.data.extract_flac + ): + self.settings.data.video_convert_mp4 = False + self.settings.data.extract_flac = False + + self.fn_logger.error( + "FFmpeg path is not set. Videos can be downloaded but will not be processed. FLAC cannot be " + "extracted from MP4 containers. Make sure FFmpeg is installed. The path to the FFmpeg binary must " + "be set in (`path_binary_ffmpeg`)." + ) + + def _download( + self, + media: Track | Video, + path_file: pathlib.Path, + stream_manifest: StreamManifest | None = None, + ) -> (bool, pathlib.Path): + media_name: str = name_builder_item(media) + urls: [str] + path_base: pathlib.Path = path_file.parent + result_segments: bool = True + dl_segment_results: [DownloadSegmentResult] = [] + result_merge: bool = False + + # Get urls for media. + try: + if isinstance(media, Track): + urls = stream_manifest.get_urls() + elif isinstance(media, Video): + m3u8_variant: m3u8.M3U8 = m3u8.load(media.get_url()) + # Find the desired video resolution or the next best one. + m3u8_playlist, codecs = self._extract_video_stream(m3u8_variant, int(self.settings.data.quality_video)) + # Populate urls. + urls = m3u8_playlist.files + except Exception: + return False, path_file + + # Set the correct progress output channel. + if self.progress_gui is None: + progress_to_stdout: bool = True + else: + progress_to_stdout: bool = False + # Send signal to GUI with media name + self.progress_gui.item_name.emit(media_name[:30]) + + # Compute total iterations for progress + urls_count: int = len(urls) + + if urls_count > 1: + progress_total: int = urls_count + block_size: int | None = None + elif urls_count == 1: + # Get file size and compute progress steps + r = requests.head(urls[0], timeout=REQUESTS_TIMEOUT_SEC) + total_size_in_bytes: int = int(r.headers.get("content-length", 0)) + block_size: int | None = 1048576 + progress_total: float = total_size_in_bytes / block_size + else: + raise ValueError + + # Create progress Task + p_task: TaskID = self.progress.add_task( + f"[blue]Item '{media_name[:30]}'", + total=progress_total, + visible=progress_to_stdout, + ) + + # Download segments until progress is finished. + # TODO: Compute download speed (https://github.com/Textualize/rich/blob/master/examples/downloader.py) + while not self.progress.tasks[p_task].finished: + with futures.ThreadPoolExecutor( + max_workers=self.settings.data.downloads_simultaneous_per_track_max + ) as executor: + # Dispatch all download tasks to worker threads + l_futures: [any] = [ + executor.submit(self._download_segment, url, path_base, block_size, p_task, progress_to_stdout) + for url in urls + ] + # Report results as they become available + for future in futures.as_completed(l_futures): + # Retrieve result + result_dl_segment: DownloadSegmentResult = future.result() + + dl_segment_results.append(result_dl_segment) + + # check for a link that was skipped + if not result_dl_segment.result and (result_dl_segment.url is not urls[-1]): + # Sometimes it happens, if a track is very short (< 8 seconds or so), that the last URL in `urls` is + # invalid (HTTP Error 500) and not necessary. File won't be corrupt. + # If this is NOT the case, but any other URL has resulted in an error, + # mark the whole thing as corrupt. + result_segments = False + self.fn_logger.error(f"Something went wrong while downloading {media_name}. File is corrupt!") + + tmp_path_file_decrypted: pathlib.Path = path_file + + # Only if no error happened while downloading. + if result_segments: + # Bring list into right order, so segments can be easily merged. + dl_segment_results.sort(key=lambda x: x.id_segment) + result_merge: bool = self._segments_merge(path_file, dl_segment_results) + + if not result_merge: + self.fn_logger.error(f"Something went wrong while writing to {media_name}. File is corrupt!") + elif result_merge and isinstance(media, Track) and stream_manifest.is_encrypted: + key, nonce = decrypt_security_token(stream_manifest.encryption_key) + tmp_path_file_decrypted = path_file.with_suffix(".decrypted") + decrypt_file(path_file, tmp_path_file_decrypted, key, nonce) + + return result_merge, tmp_path_file_decrypted + + def _segments_merge(self, path_file, dl_segment_results) -> bool: + result: bool + + # Copy the content of all segments into one file. + try: + with path_file.open("wb") as f_target: + for dl_segment_result in dl_segment_results: + with dl_segment_result.path_segment.open("rb") as f_segment: + # Read and write junks, which gives better HDD write performance + while segment := f_segment.read(CHUNK_SIZE): + f_target.write(segment) + + # Delete segment from HDD + dl_segment_result.path_segment.unlink() + + result = True + except Exception: + result = False + + return result + + def _download_segment( + self, url: str, path_base: pathlib.Path, block_size: int | None, p_task: TaskID, progress_to_stdout: bool + ) -> DownloadSegmentResult: + result: bool = False + path_segment: pathlib.Path = path_base / url_to_filename(url) + # Calculate the segment ID based on the file name within the URL. + filename_stem: str = str(path_segment.stem).split("_")[-1] + # CAUTION: This is a workaround, so BTS (LOW quality) track will work. They usually have only ONE link. + id_segment: int = int(filename_stem) if filename_stem.isdecimal() else 0 + error: HTTPError | None = None + + # Retry download on failed segments, with an exponential delay between retries + s = requests.Session() + retries = Retry(total=5, backoff_factor=1) # , status_forcelist=[ 502, 503, 504 ]) + + s.mount("https://", HTTPAdapter(max_retries=retries)) + + try: + # Create the request object with stream=True, so the content won't be loaded into memory at once. + r = s.get(url, stream=True, timeout=REQUESTS_TIMEOUT_SEC) + + r.raise_for_status() + + # Write the content to disk. If `chunk_size` is set to `None` the whole file will be written at once. + with path_segment.open("wb") as f: + for data in r.iter_content(chunk_size=block_size): + f.write(data) + # Advance progress bar. + self.progress.advance(p_task) + + result = True + except Exception: + self.progress.advance(p_task) + + # To send the progress to the GUI, we need to emit the percentage. + if not progress_to_stdout: + self.progress_gui.item.emit(self.progress.tasks[p_task].percentage) + + return DownloadSegmentResult( + result=result, url=url, path_segment=path_segment, id_segment=id_segment, error=error + ) + + def extension_guess(self, quality_audio: Quality, is_video: bool) -> AudioExtensions | VideoExtensions: + result: AudioExtensions | VideoExtensions + + if is_video: + result = AudioExtensions.MP4 if self.settings.data.video_convert_mp4 else VideoExtensions.TS + else: + result = ( + AudioExtensions.FLAC + if self.settings.data.extract_flac and quality_audio in (Quality.hi_res_lossless, Quality.high_lossless) + else AudioExtensions.M4A + ) + + return result + + def item( + self, + file_template: str, + media: Track | Video = None, + media_id: str = None, + media_type: MediaType = None, + video_download: bool = True, + download_delay: bool = False, + quality_audio: Quality | None = None, + quality_video: QualityVideo | None = None, + is_parent_album: bool = False, + ) -> (bool, pathlib.Path): + try: + if media_id and media_type: + # If no media instance is provided, we need to create the media instance. + media = instantiate_media(self.session, media_type, media_id) + elif isinstance(media, Track): # Check if media is available not deactivated / removed from TIDAL. + if not media.available: + self.fn_logger.info( + f"This track is not available for listening anymore on TIDAL. Skipping: {name_builder_item(media)}" + ) + + return False, "" + else: + # Re-create media instance with full album information + media = self.session.track(media.id, with_album=True) + elif not media: + raise MediaMissing + except: + return False, "" + + # If video download is not allowed end here + if not video_download and isinstance(media, Video): + self.fn_logger.info( + f"Video downloads are deactivated (see settings). Skipping video: {name_builder_item(media)}" + ) + + return False, "" + + # Create file name and path + file_extension_dummy: str = self.extension_guess(quality_audio, isinstance(media, Video)) + file_name_relative: str = format_path_media(file_template, media, self.settings.data.album_track_num_pad_min) + path_media_dst: pathlib.Path = ( + pathlib.Path(self.path_base).expanduser() / (file_name_relative + file_extension_dummy) + ).absolute() + + # Sanitize final path_file to fit into OS boundaries. + path_media_dst = pathlib.Path(path_file_sanitize(str(path_media_dst), adapt=True)) + + # Compute if and how downloads need to be skipped. + skip_download: bool = False + + if self.skip_existing: + skip_file: bool = check_file_exists(path_media_dst, extension_ignore=False) + + if self.settings.data.symlink_to_track and not isinstance(media, Video): + # Compute symlink tracks path, sanitize and check if file exists + file_name_track_dir_relative: str = format_path_media(self.settings.data.format_track, media) + path_media_track_dir: pathlib.Path = ( + pathlib.Path(self.path_base).expanduser() / (file_name_track_dir_relative + file_extension_dummy) + ).absolute() + path_media_track_dir = pathlib.Path(path_file_sanitize(str(path_media_track_dir), adapt=True)) + file_exists_track_dir: bool = check_file_exists(path_media_track_dir, extension_ignore=False) + file_exists_playlist_dir: bool = ( + not file_exists_track_dir and skip_file and not path_media_dst.is_symlink() + ) + skip_download = file_exists_playlist_dir or file_exists_track_dir + + # If + if skip_file and file_exists_playlist_dir: + skip_file = False + else: + skip_file: bool = False + + if not skip_file: + # If a quality is explicitly set, change it and remember the previously set quality. + quality_audio_old: Quality = self.adjust_quality_audio(quality_audio) if quality_audio else quality_audio + quality_video_old: QualityVideo = ( + self.adjust_quality_video(quality_video) if quality_video else quality_video + ) + do_flac_extract = False + # Get extension. + file_extension: str + stream_manifest: StreamManifest | None = None + + if isinstance(media, Track): + try: + media_stream: Stream = media.get_stream() + stream_manifest = media_stream.get_stream_manifest() + except TooManyRequests: + self.fn_logger.exception( + f"Too many requests against TIDAL backend. Skipping '{name_builder_item(media)}'. " + f"Consider to activate delay between downloads." + ) + + return False, "" + except Exception: + self.fn_logger.exception(f"Something went wrong. Skipping '{name_builder_item(media)}'.") + + return False, "" + + file_extension = stream_manifest.file_extension + + if self.settings.data.extract_flac and ( + stream_manifest.codecs.upper() == Codec.FLAC and file_extension != AudioExtensions.FLAC + ): + file_extension = AudioExtensions.FLAC + do_flac_extract = True + elif isinstance(media, Video): + file_extension = AudioExtensions.MP4 if self.settings.data.video_convert_mp4 else VideoExtensions.TS + + # Compute file name, sanitize once again and create destination directory + path_media_dst = path_media_dst.with_suffix(file_extension) + path_media_dst = pathlib.Path(path_file_sanitize(str(path_media_dst), adapt=True)) + os.makedirs(path_media_dst.parent, exist_ok=True) + + if not skip_download: + # Create a temp directory and file. + with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp_path_dir: + tmp_path_file: pathlib.Path = pathlib.Path(tmp_path_dir) / str(uuid4()) + + # Create empty file + tmp_path_file.touch() + + # Download media. + result_download, tmp_path_file = self._download( + media=media, stream_manifest=stream_manifest, path_file=tmp_path_file + ) + + if result_download: + # Convert video from TS to MP4 + if isinstance(media, Video) and self.settings.data.video_convert_mp4: + # Convert `*.ts` file to `*.mp4` using ffmpeg + tmp_path_file = self._video_convert(tmp_path_file) + + # Extract FLAC from MP4 container using ffmpeg + if isinstance(media, Track) and self.settings.data.extract_flac and do_flac_extract: + tmp_path_file = self._extract_flac(tmp_path_file) + + tmp_path_lyrics: pathlib.Path | None = None + tmp_path_cover: pathlib.Path | None = None + + # Write metadata to file. + if not isinstance(media, Video): + result_metadata, tmp_path_lyrics, tmp_path_cover = self.metadata_write( + media, tmp_path_file, is_parent_album, media_stream + ) + + # Move lyrics file + if self.settings.data.lyrics_file and not isinstance(media, Video) and tmp_path_lyrics: + self._move_lyrics(tmp_path_lyrics, path_media_dst) + + # Move cover file + # TODO: Cover is downloaded with every track of the album. Needs refactoring, so cover is only + # downloaded for an album once. + if self.settings.data.cover_album_file and tmp_path_cover: + self._move_cover(tmp_path_cover, path_media_dst) + + self.fn_logger.info(f"Downloaded item '{name_builder_item(media)}'.") + + # Move final file to the configured destination directory. + shutil.move(tmp_path_file, path_media_dst) + + # If files needs to be symlinked, do postprocessing here. + if self.settings.data.symlink_to_track and not isinstance(media, Video): + path_media_track_dir: pathlib.Path = self.media_move_and_symlink(media, path_media_dst, file_extension) + + if quality_audio: + # Set quality back to the global user value + self.adjust_quality_audio(quality_audio_old) + + if quality_video: + # Set quality back to the global user value + self.adjust_quality_video(quality_video_old) + else: + self.fn_logger.debug(f"Download skipped, since file exists: '{path_media_dst}'") + + status_download: bool = not skip_file + + # Whether a file was downloaded or skipped and the download delay is enabled, wait until the next download. + # Only use this, if you have a list of several Track items. + if download_delay and not skip_file: + time_sleep: float = round( + random.SystemRandom().uniform( + self.settings.data.download_delay_sec_min, self.settings.data.download_delay_sec_max + ), + 1, + ) + + self.fn_logger.debug(f"Next download will start in {time_sleep} seconds.") + time.sleep(time_sleep) + + return status_download, path_media_dst + + def media_move_and_symlink( + self, media: Track | Video, path_media_src: pathlib.Path, file_extension: str + ) -> pathlib.Path: + # Compute tracks path, sanitize and ensure path exists + file_name_relative: str = format_path_media(self.settings.data.format_track, media) + path_media_dst: pathlib.Path = ( + pathlib.Path(self.path_base).expanduser() / (file_name_relative + file_extension) + ).absolute() + path_media_dst = pathlib.Path(path_file_sanitize(str(path_media_dst), adapt=True)) + + os.makedirs(path_media_dst.parent, exist_ok=True) + + # Move item and symlink it + if path_media_dst != path_media_src: + if self.skip_existing: + skip_file: bool = check_file_exists(path_media_dst, extension_ignore=False) + skip_symlink: bool = path_media_src.is_symlink() + else: + skip_file: bool = False + skip_symlink: bool = False + + if not skip_file: + self.fn_logger.debug(f"Move: {path_media_src} -> {path_media_dst}") + shutil.move(path_media_src, path_media_dst) + + if not skip_symlink: + self.fn_logger.debug(f"Symlink: {path_media_src} -> {path_media_dst}") + path_media_src.unlink(missing_ok=True) + path_media_src.symlink_to(path_media_dst) + + return path_media_dst + + def adjust_quality_audio(self, quality) -> Quality: + # Save original quality settings + quality_old: Quality = self.session.audio_quality + self.session.audio_quality = quality + + return quality_old + + def adjust_quality_video(self, quality) -> QualityVideo: + quality_old: QualityVideo = self.settings.data.quality_video + + self.settings.data.quality_video = quality + + return quality_old + + def _move_file(self, path_file_source: pathlib.Path, path_file_destination: str | pathlib.Path) -> bool: + result: bool + + # Check if the file was downloaded + if path_file_source and path_file_source.is_file(): + # Move it. + shutil.move(path_file_source, path_file_destination) + + result = True + else: + result = False + + return result + + def _move_lyrics(self, path_lyrics: pathlib.Path, file_media_dst: pathlib.Path) -> bool: + # Build tmp lyrics filename + path_file_lyrics: pathlib.Path = file_media_dst.with_suffix(EXTENSION_LYRICS) + result: bool = self._move_file(path_lyrics, path_file_lyrics) + + return result + + def _move_cover(self, path_cover: pathlib.Path, file_media_dst: pathlib.Path) -> bool: + # Build tmp lyrics filename + path_file_cover: pathlib.Path = file_media_dst.parent / COVER_NAME + result: bool = self._move_file(path_cover, path_file_cover) + + return result + + def lyrics_to_file(self, dir_destination: pathlib.Path, lyrics: str) -> str: + return self.write_to_tmp_file(dir_destination, mode="x", content=lyrics) + + def cover_to_file(self, dir_destination: pathlib.Path, image: bytes) -> str: + return self.write_to_tmp_file(dir_destination, mode="xb", content=image) + + def write_to_tmp_file(self, dir_destination: pathlib.Path, mode: str, content: str | bytes) -> str: + result: str = dir_destination / str(uuid4()) + encoding: str | None = "utf-8" if isinstance(content, str) else None + + try: + with open(result, mode=mode, encoding=encoding) as f: + f.write(content) + except: + result = "" + + return result + + @staticmethod + def cover_data(url: str = None, path_file: str = None) -> str | bytes: + result: str | bytes = "" + + if url: + try: + result = requests.get(url, timeout=REQUESTS_TIMEOUT_SEC).content + except Exception as e: + # TODO: Implement propper logging. + print(e) + elif path_file: + try: + with open(path_file, "rb") as f: + result = f.read() + except OSError as e: + # TODO: Implement propper logging. + print(e) + + return result + + def metadata_write( + self, track: Track, path_media: pathlib.Path, is_parent_album: bool, media_stream: Stream + ) -> (bool, pathlib.Path | None, pathlib.Path | None): + result: bool = False + path_lyrics: pathlib.Path | None = None + path_cover: pathlib.Path | None = None + release_date: str = ( + track.album.available_release_date.strftime("%Y-%m-%d") + if track.album.available_release_date + else track.album.release_date.strftime("%Y-%m-%d") if track.album.release_date else "" + ) + copy_right: str = track.copyright if hasattr(track, "copyright") and track.copyright else "" + isrc: str = track.isrc if hasattr(track, "isrc") and track.isrc else "" + lyrics: str = "" + cover_data: bytes = None + + if self.settings.data.lyrics_embed or self.settings.data.lyrics_file: + # Try to retrieve lyrics. + try: + lyrics_obj = track.lyrics() + + if lyrics_obj.subtitles: + lyrics = lyrics_obj.subtitles + elif lyrics_obj.text: + lyrics = lyrics_obj.text + except: + lyrics = "" + # TODO: Implement proper logging. + print(f"Could not retrieve lyrics for `{name_builder_item(track)}`.") + + if lyrics and self.settings.data.lyrics_file: + path_lyrics = self.lyrics_to_file(path_media.parent, lyrics) + + if self.settings.data.metadata_cover_embed or (self.settings.data.cover_album_file and is_parent_album): + url_cover = track.album.image(int(self.settings.data.metadata_cover_dimension)) + cover_data = self.cover_data(url=url_cover) + + if cover_data and self.settings.data.cover_album_file and is_parent_album: + path_cover = self.cover_to_file(path_media.parent, cover_data) + + # `None` values are not allowed. + m: Metadata = Metadata( + path_file=path_media, + lyrics=lyrics, + copy_right=copy_right, + title=name_builder_title(track), + artists=name_builder_artist(track), + album=track.album.name if track.album else "", + tracknumber=track.track_num, + date=release_date, + isrc=isrc, + albumartist=name_builder_album_artist(track), + 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, + discnumber=track.volume_num if track.volume_num else 1, + cover_data=cover_data if self.settings.data.metadata_cover_embed else None, + album_replay_gain=media_stream.album_replay_gain, + album_peak_amplitude=media_stream.album_peak_amplitude, + track_replay_gain=media_stream.track_replay_gain, + track_peak_amplitude=media_stream.track_peak_amplitude, + ) + + m.save() + + result = True + + return result, path_lyrics, path_cover + + def items( + self, + file_template: str, + media: Album | Playlist | UserPlaylist | Mix = None, + media_id: str = None, + media_type: MediaType = None, + video_download: bool = False, + download_delay: bool = True, + quality_audio: Quality | None = None, + quality_video: QualityVideo | None = None, + ): + # If no media instance is provided, we need to create the media instance. + if media_id and media_type: + media = instantiate_media(self.session, media_type, media_id) + elif not media: + raise MediaMissing + + # Create file name and path + file_name_relative: str = format_path_media(file_template, media, self.settings.data.album_track_num_pad_min) + + # Get the name of the list and check, if videos should be included. + list_media_name: str = name_builder_title(media) + list_media_name_short: str = list_media_name[:30] + + # Get all items of the list. + items = items_results_all(media, videos_include=video_download) + + # Determine where to redirect the progress information. + if self.progress_gui is None: + progress_stdout: bool = True + else: + progress_stdout: bool = False + self.progress_gui.list_name.emit(list_media_name_short[:30]) + + # Create the list progress task. + p_task1: TaskID = self.progress.add_task( + f"[green]List '{list_media_name_short}'", total=len(items), visible=progress_stdout + ) + + is_album: bool = isinstance(media, Album) + result_dirs: [pathlib.Path] = [] + + # Iterate through list items + while not self.progress.finished: + with futures.ThreadPoolExecutor(max_workers=self.settings.data.downloads_concurrent_max) as executor: + # Dispatch all download tasks to worker threads + l_futures: [any] = [ + executor.submit( + self.item, + media=item_media, + file_template=file_name_relative, + quality_audio=quality_audio, + quality_video=quality_video, + download_delay=download_delay, + is_parent_album=is_album, + ) + for item_media in items + ] + # Report results as they become available + for future in futures.as_completed(l_futures): + # Retrieve result + status, result_path_file = future.result() + + if status: + result_dirs.append(result_path_file.parent) + + # Advance progress bar. + self.progress.advance(p_task1) + + if not progress_stdout: + self.progress_gui.list_item.emit(self.progress.tasks[p_task1].percentage) + + # Create playlist file + if self.settings.data.playlist_create: + self.playlist_populate(set(result_dirs), list_media_name, is_album) + + self.fn_logger.info(f"Finished list '{list_media_name}'.") + + def playlist_populate(self, dirs_scoped: [pathlib.Path], name_list: str, is_album: bool) -> [pathlib.Path]: + result: [pathlib.Path] = [] + + # For each dir, which contains tracks + for dir_scoped in dirs_scoped: + # Sanitize final playlist name to fit into OS boundaries. + path_playlist = dir_scoped / (PLAYLIST_PREFIX + name_list + PLAYLIST_EXTENSION) + path_playlist = pathlib.Path(path_file_sanitize(path_playlist, adapt=True)) + + self.fn_logger.debug(f"Playlist: Creating {path_playlist}") + + # Get all tracks in the directory + path_tracks: [pathlib.Path] = [] + + for extension_audio in AudioExtensions: + path_tracks = path_tracks + list(dir_scoped.glob(f"*{extension_audio!s}")) + + # If it is not an album sort by modification time + if not is_album: + path_tracks.sort(key=lambda x: os.path.getmtime(x)) + + # Write data to m3u file + with path_playlist.open(mode="w", encoding="utf-8") as f: + for path_track in path_tracks: + # If it's a symlink write the relative file path to the actual track into the playlist file + if path_track.is_symlink(): + media_file_target = path_track.resolve().relative_to(path_track.parent, walk_up=True) + + f.write(str(media_file_target) + os.linesep) + + result.append(path_playlist) + + return result + + def _video_convert(self, path_file: pathlib.Path) -> pathlib.Path: + path_file_out: pathlib.Path = path_file.with_suffix(AudioExtensions.MP4) + result, _ = ( + ffmpeg.input(path_file) + .output(str(path_file_out), map=0, c="copy", loglevel="quiet") + .run(cmd=self.settings.data.path_binary_ffmpeg) + ) + + return path_file_out + + def _extract_flac(self, path_media_src: pathlib.Path) -> pathlib.Path: + path_media_out = path_media_src.with_suffix(AudioExtensions.FLAC) + result, _ = ( + ffmpeg.input(path_media_src) + .output( + str(path_media_out), + map=0, + movflags="use_metadata_tags", + acodec="copy", + map_metadata="0:g", + loglevel="quiet", + ) + .run(cmd=self.settings.data.path_binary_ffmpeg) + ) + + return path_media_out + + def _extract_video_stream(self, m3u8_variant: m3u8.M3U8, quality: int) -> (m3u8.M3U8 | bool, str): + m3u8_playlist: m3u8.M3U8 | bool = False + resolution_best: int = 0 + mime_type: str = "" + + if m3u8_variant.is_variant: + for playlist in m3u8_variant.playlists: + if resolution_best < playlist.stream_info.resolution[1]: + resolution_best = playlist.stream_info.resolution[1] + m3u8_playlist = m3u8.load(playlist.uri) + mime_type = playlist.stream_info.codecs + + if quality == playlist.stream_info.resolution[1]: + break + + return m3u8_playlist, mime_type diff --git a/tidal_dl_ng/gui.py b/tidal_dl_ng/gui.py new file mode 100644 index 0000000..fffb7d1 --- /dev/null +++ b/tidal_dl_ng/gui.py @@ -0,0 +1,1103 @@ +import math +import sys +import time +from collections.abc import Callable, Sequence + +from requests.exceptions import HTTPError +from tidalapi.session import LinkLogin + +from tidal_dl_ng import __version__, update_available +from tidal_dl_ng.dialog import DialogLogin, DialogPreferences, DialogVersion +from tidal_dl_ng.helper.gui import ( + FilterHeader, + HumanProxyModel, + get_queue_download_media, + get_queue_download_quality, + get_results_media_item, + get_user_list_media_item, + set_queue_download_media, + set_user_list_media, +) +from tidal_dl_ng.helper.path import get_format_template, resource_path +from tidal_dl_ng.helper.tidal import ( + favorite_function_factory, + get_tidal_media_id, + get_tidal_media_type, + instantiate_media, + items_results_all, + name_builder_artist, + name_builder_title, + quality_audio_highest, + search_results_all, + user_media_lists, +) + +try: + import qdarktheme + from PySide6 import QtCore, QtGui, QtWidgets +except ImportError as e: + print(e) + print("Qt dependencies missing. Cannot start GUI. Please execute: 'pip install pyside6 pyqtdarktheme'") + sys.exit(1) + +import coloredlogs.converter +from rich.progress import Progress +from tidalapi import Album, Mix, Playlist, Quality, Track, UserPlaylist, Video +from tidalapi.artist import Artist +from tidalapi.session import SearchTypes + +from tidal_dl_ng.config import Settings, Tidal +from tidal_dl_ng.constants import FAVORITES, QualityVideo, QueueDownloadStatus, TidalLists +from tidal_dl_ng.download import Download +from tidal_dl_ng.logger import XStream, logger_gui +from tidal_dl_ng.model.gui_data import ProgressBars, QueueDownloadItem, ResultItem, StatusbarMessage +from tidal_dl_ng.model.meta import ReleaseLatest +from tidal_dl_ng.ui.main import Ui_MainWindow +from tidal_dl_ng.ui.spinner import QtWaitingSpinner +from tidal_dl_ng.worker import Worker + + +# TODO: Make more use of Exceptions +class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): + settings: Settings + tidal: Tidal + dl: Download + threadpool: QtCore.QThreadPool + tray: QtWidgets.QSystemTrayIcon + spinner: QtWaitingSpinner + cover_url_current: str = "" + model_tr_results: QtGui.QStandardItemModel = QtGui.QStandardItemModel() + proxy_tr_results: HumanProxyModel + s_spinner_start: QtCore.Signal = QtCore.Signal(QtWidgets.QWidget) + s_spinner_stop: QtCore.Signal = QtCore.Signal() + pb_item: QtWidgets.QProgressBar + s_item_advance: QtCore.Signal = QtCore.Signal(float) + s_item_name: QtCore.Signal = QtCore.Signal(str) + s_list_name: QtCore.Signal = QtCore.Signal(str) + pb_list: QtWidgets.QProgressBar + s_list_advance: QtCore.Signal = QtCore.Signal(float) + s_pb_reset: QtCore.Signal = QtCore.Signal() + s_populate_tree_lists: QtCore.Signal = QtCore.Signal(list) + s_statusbar_message: QtCore.Signal = QtCore.Signal(object) + s_tr_results_add_top_level_item: QtCore.Signal = QtCore.Signal(object) + s_settings_save: QtCore.Signal = QtCore.Signal() + s_pb_reload_status: QtCore.Signal = QtCore.Signal(bool) + s_update_check: QtCore.Signal = QtCore.Signal(bool) + s_update_show: QtCore.Signal = QtCore.Signal(bool, bool, object) + s_queue_download_item_downloading: QtCore.Signal = QtCore.Signal(object) + s_queue_download_item_finished: QtCore.Signal = QtCore.Signal(object) + s_queue_download_item_failed: QtCore.Signal = QtCore.Signal(object) + s_queue_download_item_skipped: QtCore.Signal = QtCore.Signal(object) + + def __init__(self, tidal: Tidal | None = None): + super().__init__() + self.setupUi(self) + # self.setGeometry(50, 50, 500, 300) + self.setWindowTitle("TIDAL Downloader Next Generation!") + + # Logging redirect. + XStream.stdout().messageWritten.connect(self._log_output) + # XStream.stderr().messageWritten.connect(self._log_output) + + self.settings = Settings() + + self._init_threads() + self._init_tree_results_model(self.model_tr_results) + self._init_tree_results(self.tr_results, self.model_tr_results) + self._init_tree_lists(self.tr_lists_user) + self._init_tree_queue(self.tr_queue_download) + self._init_info() + self._init_progressbar() + self._populate_quality(self.cb_quality_audio, Quality) + self._populate_quality(self.cb_quality_video, QualityVideo) + self._populate_search_types(self.cb_search_type, SearchTypes) + self.apply_settings(self.settings) + self._init_signals() + self.init_tidal(tidal) + + logger_gui.debug("All setup.") + + def init_tidal(self, tidal: Tidal = None): + result: bool = False + + if tidal: + self.tidal = tidal + result = True + else: + self.tidal = Tidal(self.settings) + result = self.tidal.login_token() + + if not result: + hint: str = "After you have finished the TIDAL login via web browser click the 'OK' button." + + while not result: + link_login: LinkLogin = self.tidal.session.get_link_login() + d_login: DialogLogin = DialogLogin( + url_login=link_login.verification_uri_complete, + hint=hint, + expires_in=link_login.expires_in, + parent=self, + ) + + if d_login.return_code == 1: + try: + self.tidal.session.process_link_login(link_login, until_expiry=False) + self.tidal.login_finalize() + + result = True + logger_gui.info("Login successful. Have fun!") + except (HTTPError, Exception): + hint = "Something was wrong with your redirect url. Please try again!" + logger_gui.warning("Login not successful. Try again...") + else: + # If user has pressed cancel. + sys.exit(1) + + if result: + self._init_dl() + self.thread_it(self.tidal_user_lists) + + def _init_threads(self): + self.threadpool = QtCore.QThreadPool() + self.thread_it(self.watcher_queue_download) + + def _init_dl(self): + # Init `Download` object. + data_pb: ProgressBars = ProgressBars( + item=self.s_item_advance, + list_item=self.s_list_advance, + item_name=self.s_item_name, + list_name=self.s_list_name, + ) + progress: Progress = Progress() + self.dl = Download( + session=self.tidal.session, + skip_existing=self.tidal.settings.data.skip_existing, + path_base=self.settings.data.download_base_path, + fn_logger=logger_gui, + progress_gui=data_pb, + progress=progress, + ) + + def _init_progressbar(self): + self.pb_list = QtWidgets.QProgressBar() + self.pb_item = QtWidgets.QProgressBar() + pbs = [self.pb_list, self.pb_item] + + for pb in pbs: + pb.setRange(0, 100) + # self.pb_progress.setVisible() + self.statusbar.addPermanentWidget(pb) + + def _init_info(self): + path_image: str = resource_path("tidal_dl_ng/ui/default_album_image.png") + + self.l_pm_cover.setPixmap(QtGui.QPixmap(path_image)) + + def on_progress_reset(self): + self.pb_list.setValue(0) + self.pb_item.setValue(0) + + def on_statusbar_message(self, data: StatusbarMessage): + self.statusbar.showMessage(data.message, data.timout) + + def _log_output(self, text): + display_msg = coloredlogs.converter.convert(text) + + cursor: QtGui.QTextCursor = self.te_debug.textCursor() + cursor.movePosition(QtGui.QTextCursor.End) + cursor.insertHtml(display_msg) + + self.te_debug.setTextCursor(cursor) + self.te_debug.ensureCursorVisible() + + def _populate_quality(self, ui_target: QtWidgets.QComboBox, options: type[Quality | QualityVideo]): + for item in options: + ui_target.addItem(item.name, item) + + def _populate_search_types(self, ui_target: QtWidgets.QComboBox, options: SearchTypes): + for item in options: + if item: + ui_target.addItem(item.__name__, item) + + self.cb_search_type.setCurrentIndex(2) + + def handle_filter_activated(self): + header: FilterHeader = self.tr_results.header() + filters = [] + + for i in range(header.count()): + text: str = header.filter_text(i) + + if text: + filters.append((i, text)) + + proxy_model: HumanProxyModel = self.tr_results.model() + proxy_model.filters = filters + + def _init_tree_results(self, tree: QtWidgets.QTreeView, model: QtGui.QStandardItemModel) -> None: + header: FilterHeader = FilterHeader(tree) + self.proxy_tr_results: HumanProxyModel = HumanProxyModel(self) + + tree.setHeader(header) + tree.setModel(model) + self.proxy_tr_results.setSourceModel(model) + tree.setModel(self.proxy_tr_results) + header.set_filter_boxes(model.columnCount()) + header.filter_activated.connect(self.handle_filter_activated) + ## Styling + tree.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder) + tree.setColumnHidden(1, True) + tree.setColumnWidth(2, 150) + tree.setColumnWidth(3, 150) + tree.setColumnWidth(4, 150) + header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents) + # Connect the contextmenu + tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + tree.customContextMenuRequested.connect(self.menu_context_tree_results) + + def _init_tree_results_model(self, model: QtGui.QStandardItemModel) -> None: + labels_column: [str] = ["#", "obj", "Artist", "Title", "Album", "Duration", "Quality", "Date Added"] + + model.setColumnCount(len(labels_column)) + model.setRowCount(0) + model.setHorizontalHeaderLabels(labels_column) + + def _init_tree_queue(self, tree: QtWidgets.QTableWidget): + tree.setColumnHidden(1, True) + tree.setColumnWidth(2, 200) + + header = tree.header() + + header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents) + header.setStretchLastSection(False) + + def tidal_user_lists(self): + # Start loading spinner + self.s_spinner_start.emit(self.tr_lists_user) + self.s_pb_reload_status.emit(False) + + user_all: [Playlist | UserPlaylist | Mix] = user_media_lists(self.tidal.session) + + self.s_populate_tree_lists.emit(user_all) + + def on_populate_tree_lists(self, user_lists: [Playlist | UserPlaylist | Mix]): + twi_playlists: QtWidgets.QTreeWidgetItem = self.tr_lists_user.findItems( + TidalLists.Playlists, QtCore.Qt.MatchExactly, 0 + )[0] + twi_mixes: QtWidgets.QTreeWidgetItem = self.tr_lists_user.findItems( + TidalLists.Mixes, QtCore.Qt.MatchExactly, 0 + )[0] + twi_favorites: QtWidgets.QTreeWidgetItem = self.tr_lists_user.findItems( + TidalLists.Favorites, QtCore.Qt.MatchExactly, 0 + )[0] + + # Remove all children if present + for twi in [twi_playlists, twi_mixes]: + for i in reversed(range(twi.childCount())): + twi.removeChild(twi.child(i)) + + # Populate dynamic user lists + for item in user_lists: + if isinstance(item, UserPlaylist | Playlist): + twi_child = QtWidgets.QTreeWidgetItem(twi_playlists) + name: str = item.name + description: str = f" {item.description}" if item.description else "" + info: str = f"({item.num_tracks + item.num_videos} Tracks){description}" + elif isinstance(item, Mix): + twi_child = QtWidgets.QTreeWidgetItem(twi_mixes) + name: str = item.title + info: str = item.sub_title + + twi_child.setText(0, name) + set_user_list_media(twi_child, item) + twi_child.setText(2, info) + + # Populate static favorites + for key, favorite in FAVORITES.items(): + twi_child = QtWidgets.QTreeWidgetItem(twi_favorites) + name: str = favorite["name"] + info: str = "" + + twi_child.setText(0, name) + set_user_list_media(twi_child, key) + twi_child.setText(2, info) + + # Stop load spinner + self.s_spinner_stop.emit() + self.s_pb_reload_status.emit(True) + + def _init_tree_lists(self, tree: QtWidgets.QTreeWidget): + # Adjust Tree. + tree.setColumnWidth(0, 200) + tree.setColumnHidden(1, True) + tree.setColumnWidth(2, 300) + tree.expandAll() + + # Connect the contextmenu + tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + tree.customContextMenuRequested.connect(self.menu_context_tree_lists) + + def on_update_check(self, on_startup: bool = True): + is_available, info = update_available() + + if (on_startup and is_available) or not on_startup: + self.s_update_show.emit(True, is_available, info) + + def apply_settings(self, settings: Settings): + l_cb = [ + {"element": self.cb_quality_audio, "setting": settings.data.quality_audio, "default_id": 1}, + {"element": self.cb_quality_video, "setting": settings.data.quality_video, "default_id": 0}, + ] + + for item in l_cb: + idx = item["element"].findData(item["setting"]) + + if idx > -1: + item["element"].setCurrentIndex(idx) + else: + item["element"].setCurrentIndex(item["default_id"]) + + def on_spinner_start(self, parent: QtWidgets.QWidget): + self.spinner = QtWaitingSpinner(parent, True, True) + self.spinner.setColor(QtGui.QColor(255, 255, 255)) + self.spinner.start() + + def on_spinner_stop(self): + self.spinner.stop() + self.spinner = None + + def menu_context_tree_lists(self, point: QtCore.QPoint): + # Infos about the node selected. + index = self.tr_lists_user.indexAt(point) + + # Do not open menu if something went wrong or a parent node is clicked. + if not index.isValid() or not index.parent().data(): + return + + # We build the menu. + menu = QtWidgets.QMenu() + menu.addAction("Download Playlist", lambda: self.thread_download_list_media(point)) + menu.addAction("Copy Share URL", lambda: self.thread_copy_url_share(self.tr_lists_user, point)) + + menu.exec(self.tr_lists_user.mapToGlobal(point)) + + def menu_context_tree_results(self, point: QtCore.QPoint): + # Infos about the node selected. + index = self.tr_results.indexAt(point) + + # Do not open menu if something went wrong or a parent node is clicked. + if not index.isValid(): + return + + # We build the menu. + menu = QtWidgets.QMenu() + menu.addAction("Copy Share URL", lambda: self.thread_copy_url_share(self.tr_results, point)) + + menu.exec(self.tr_results.mapToGlobal(point)) + + def thread_download_list_media(self, point: QtCore.QPoint): + self.thread_it(self.on_download_list_media, point) + + def thread_copy_url_share(self, tree_target: QtWidgets.QTreeWidget, point: QtCore.QPoint): + self.thread_it(self.on_copy_url_share, tree_target, point) + + def on_copy_url_share(self, tree_target: QtWidgets.QTreeWidget | QtWidgets.QTreeView, point: QtCore.QPoint = None): + if isinstance(tree_target, QtWidgets.QTreeWidget): + + item: QtWidgets.QTreeWidgetItem = tree_target.itemAt(point) + media: Album | Artist | Mix | Playlist = get_user_list_media_item(item) + else: + index: QtCore.QModelIndex = tree_target.indexAt(point) + media: Track | Video | Album | Artist | Mix | Playlist = get_results_media_item( + index, self.proxy_tr_results, self.model_tr_results + ) + + clipboard = QtWidgets.QApplication.clipboard() + url_share = media.share_url if hasattr(media, "share_url") else "No share URL available." + + clipboard.clear() + clipboard.setText(url_share) + + def on_download_list_media(self, point: QtCore.QPoint = None): + items: [QtWidgets.QTreeWidgetItem] + + if point: + items = [self.tr_lists_user.itemAt(point)] + else: + items = self.tr_lists_user.selectedItems() + + if len(items) == 0: + logger_gui.error("Please select a mix or playlist first.") + + for item in items: + media = get_user_list_media_item(item) + queue_dl_item: QueueDownloadItem | False = self.media_to_queue_download_model(media) + + if queue_dl_item: + self.queue_download_media(queue_dl_item) + + def search_populate_results(self, query: str, type_media: SearchTypes): + self.model_tr_results.removeRows(0, self.model_tr_results.rowCount()) + + results: [ResultItem] = self.search(query, [type_media]) + + self.populate_tree_results(results) + + def populate_tree_results(self, results: [ResultItem], parent: QtGui.QStandardItem = None): + if not parent: + self.model_tr_results.removeRows(0, self.model_tr_results.rowCount()) + + # Count how many digits the list length has, + count_digits: int = int(math.log10(len(results) if results else 1)) + 1 + + for item in results: + child: tuple = self.populate_tree_result_child(item=item, index_count_digits=count_digits) + + if parent: + parent.appendRow(child) + else: + self.s_tr_results_add_top_level_item.emit(child) + + def populate_tree_result_child( + self, item: [Track | Video | Mix | Album | Playlist], index_count_digits: int + ) -> Sequence[QtGui.QStandardItem]: + duration: str = "" + + # TODO: Duration needs to be calculated later to properly fill with zeros. + if item.duration_sec > -1: + # Format seconds to mm:ss. + m, s = divmod(item.duration_sec, 60) + duration: str = f"{m:02d}:{s:02d}" + + # Since sorting happens only by string, we need to pad the index and add 1 (to avoid start at 0) + index: str = str(item.position + 1).zfill(index_count_digits) + + # Populate child + child_index: QtGui.QStandardItem = QtGui.QStandardItem(index) + # TODO: Move to own method + child_obj: QtGui.QStandardItem = QtGui.QStandardItem() + + child_obj.setData(item.obj, QtCore.Qt.ItemDataRole.UserRole) + # set_results_media(child, item.obj) + + child_artist: QtGui.QStandardItem = QtGui.QStandardItem(item.artist) + child_title: QtGui.QStandardItem = QtGui.QStandardItem(item.title) + child_album: QtGui.QStandardItem = QtGui.QStandardItem(item.album) + child_duration: QtGui.QStandardItem = QtGui.QStandardItem(duration) + child_quality: QtGui.QStandardItem = QtGui.QStandardItem(item.quality) + child_date_added: QtGui.QStandardItem = QtGui.QStandardItem(item.date_user_added) + + if isinstance(item.obj, Mix | Playlist | Album | Artist): + # Add a disabled dummy child, so expansion arrow will appear. This Child will be replaced on expansion. + child_dummy: QtGui.QStandardItem = QtGui.QStandardItem() + + child_dummy.setEnabled(False) + child_index.appendRow(child_dummy) + + return ( + child_index, + child_obj, + child_artist, + child_title, + child_album, + child_duration, + child_quality, + child_date_added, + ) + + def on_tr_results_add_top_level_item(self, item_child: Sequence[QtGui.QStandardItem]): + self.model_tr_results.appendRow(item_child) + + def on_settings_save(self): + self.settings.save() + self.apply_settings(self.settings) + self._init_dl() + + def search(self, query: str, types_media: SearchTypes) -> [ResultItem]: + query = query.strip() + + # If a direct link was searched for, skip search and create the object from the link directly. + if "http" in query: + media_type = get_tidal_media_type(query) + item_id = get_tidal_media_id(query) + + try: + media = instantiate_media(self.tidal.session, media_type, item_id) + except: + logger_gui.error(f"Media not found (ID: {item_id}). Maybe it is not available anymore.") + + media = None + + result_search = {"direct": [media]} + else: + result_search: dict[str, [SearchTypes]] = search_results_all( + session=self.tidal.session, needle=query, types_media=types_media + ) + + result: [ResultItem] = [] + + for _media_type, l_media in result_search.items(): + if isinstance(l_media, list): + result = result + self.search_result_to_model(l_media) + + return result + + def search_result_to_model(self, items: [*SearchTypes]) -> [ResultItem]: + result = [] + + for idx, item in enumerate(items): + explicit: str = "" + # Check if item is available on TIDAL. + if hasattr(item, "available") and not item.available: + continue + + if isinstance(item, Track | Video | Album): + explicit = " 🅴" if item.explicit else "" + + date_user_added: str = item.user_date_added.strftime("%Y-%m-%d_%H:%M") if item.user_date_added else "" + + if isinstance(item, Track): + result_item: ResultItem = ResultItem( + position=idx, + artist=name_builder_artist(item), + title=f"{name_builder_title(item)}{explicit}", + album=item.album.name, + duration_sec=item.duration, + obj=item, + quality=quality_audio_highest(item), + explicit=bool(item.explicit), + date_user_added=date_user_added, + ) + + result.append(result_item) + elif isinstance(item, Video): + result_item: ResultItem = ResultItem( + position=idx, + artist=name_builder_artist(item), + title=f"{name_builder_title(item)}{explicit}", + album=item.album.name if item.album else "", + duration_sec=item.duration, + obj=item, + quality=item.video_quality, + explicit=bool(item.explicit), + date_user_added=date_user_added, + ) + + result.append(result_item) + elif isinstance(item, Playlist): + result_item: ResultItem = ResultItem( + position=idx, + artist=", ".join(artist.name for artist in item.promoted_artists) if item.promoted_artists else "", + title=item.name, + album="", + duration_sec=item.duration, + obj=item, + quality="", + explicit=False, + date_user_added=date_user_added, + ) + + result.append(result_item) + elif isinstance(item, Album): + result_item: ResultItem = ResultItem( + position=idx, + artist=name_builder_artist(item), + title="", + album=f"{item.name}{explicit}", + duration_sec=item.duration, + obj=item, + quality=quality_audio_highest(item), + explicit=bool(item.explicit), + date_user_added=date_user_added, + ) + + result.append(result_item) + elif isinstance(item, Mix): + result_item: ResultItem = ResultItem( + position=idx, + artist=item.sub_title, + title=item.title, + album="", + # TODO: Calculate total duration. + duration_sec=-1, + obj=item, + quality="", + explicit=False, + date_user_added=date_user_added, + ) + + result.append(result_item) + elif isinstance(item, Artist): + result_item: ResultItem = ResultItem( + position=idx, + artist=item.name, + title="", + album="", + duration_sec=-1, + obj=item, + quality="", + explicit=False, + date_user_added=date_user_added, + ) + + result.append(result_item) + + return result + + def media_to_queue_download_model( + self, media: Artist | Track | Video | Album | Playlist | Mix + ) -> QueueDownloadItem | bool: + result: QueueDownloadItem | False + name: str = "" + quality: Quality | QualityVideo | str = "" + explicit: str = "" + + # Check if item is available on TIDAL. + if hasattr(media, "available") and not media.available: + return False + + # Set "Explicit" tag + if isinstance(media, Track | Video | Album): + explicit = " 🅴" if media.explicit else "" + + # Build name and set quality + if isinstance(media, Track | Video): + name = f"{name_builder_artist(media)} - {name_builder_title(media)}{explicit}" + elif isinstance(media, Playlist | Artist): + name = media.name + quality = self.settings.data.quality_audio + elif isinstance(media, Album): + name = f"{name_builder_artist(media)} - {media.name}{explicit}" + elif isinstance(media, Mix): + name = media.title + quality = self.settings.data.quality_audio + + # Determine actual quality. + if isinstance(media, Track | Album): + quality_highest: str = quality_audio_highest(media) + + if ( + self.settings.data.quality_audio == quality_highest + or self.settings.data.quality_audio == Quality.hi_res_lossless + ): + quality = quality_highest + else: + quality = self.settings.data.quality_audio + elif isinstance(media, Video): + quality = self.settings.data.quality_video + + if name: + result = QueueDownloadItem( + name=name, + quality=quality, + type_media=type(media).__name__, + status=QueueDownloadStatus.Waiting, + obj=media, + ) + else: + result = False + + return result + + def _init_signals(self): + self.pb_download.clicked.connect(lambda: self.thread_it(self.on_download_results)) + self.pb_download_list.clicked.connect(lambda: self.thread_it(self.on_download_list_media)) + self.pb_reload_user_lists.clicked.connect(lambda: self.thread_it(self.tidal_user_lists)) + self.pb_queue_download_clear_all.clicked.connect(self.on_queue_download_clear_all) + self.pb_queue_download_clear_finished.clicked.connect(self.on_queue_download_clear_finished) + self.pb_queue_download_remove.clicked.connect(self.on_queue_download_remove) + self.l_search.returnPressed.connect( + lambda: self.search_populate_results(self.l_search.text(), self.cb_search_type.currentData()) + ) + self.pb_search.clicked.connect( + lambda: self.search_populate_results(self.l_search.text(), self.cb_search_type.currentData()) + ) + self.cb_quality_audio.currentIndexChanged.connect(self.on_quality_set_audio) + self.cb_quality_video.currentIndexChanged.connect(self.on_quality_set_video) + self.tr_lists_user.itemClicked.connect(self.on_list_items_show) + self.s_spinner_start[QtWidgets.QWidget].connect(self.on_spinner_start) + self.s_spinner_stop.connect(self.on_spinner_stop) + self.s_item_advance.connect(self.on_progress_item) + self.s_item_name.connect(self.on_progress_item_name) + self.s_list_name.connect(self.on_progress_list_name) + self.s_list_advance.connect(self.on_progress_list) + self.s_pb_reset.connect(self.on_progress_reset) + self.s_populate_tree_lists.connect(self.on_populate_tree_lists) + self.s_statusbar_message.connect(self.on_statusbar_message) + self.s_tr_results_add_top_level_item.connect(self.on_tr_results_add_top_level_item) + self.s_settings_save.connect(self.on_settings_save) + self.s_pb_reload_status.connect(self.button_reload_status) + self.s_update_check.connect(lambda: self.thread_it(self.on_update_check)) + self.s_update_show.connect(self.on_version) + + # Menubar + self.a_exit.triggered.connect(sys.exit) + self.a_version.triggered.connect(self.on_version) + self.a_preferences.triggered.connect(self.on_preferences) + self.a_logout.triggered.connect(self.on_logout) + self.a_updates_check.triggered.connect(lambda: self.on_update_check(False)) + + # Results + self.tr_results.expanded.connect(self.on_tr_results_expanded) + self.tr_results.clicked.connect(self.on_result_item_clicked) + + # Download Queue + self.tr_queue_download.itemClicked.connect(self.on_queue_download_item_clicked) + self.s_queue_download_item_downloading.connect(self.on_queue_download_item_downloading) + self.s_queue_download_item_finished.connect(self.on_queue_download_item_finished) + self.s_queue_download_item_failed.connect(self.on_queue_download_item_failed) + self.s_queue_download_item_skipped.connect(self.on_queue_download_item_skipped) + + def on_logout(self): + result: bool = self.tidal.logout() + + if result: + sys.exit(0) + + def on_progress_list(self, value: float): + self.pb_list.setValue(int(math.ceil(value))) + + def on_progress_item(self, value: float): + self.pb_item.setValue(int(math.ceil(value))) + + def on_progress_item_name(self, value: str): + self.pb_item.setFormat(f"%p% {value}") + + def on_progress_list_name(self, value: str): + self.pb_list.setFormat(f"%p% {value}") + + def on_quality_set_audio(self, index): + self.settings.data.quality_audio = Quality(self.cb_quality_audio.itemData(index)) + self.settings.save() + + if self.tidal: + self.tidal.settings_apply() + + def on_quality_set_video(self, index): + self.settings.data.quality_video = QualityVideo(self.cb_quality_video.itemData(index)) + self.settings.save() + + if self.tidal: + self.tidal.settings_apply() + + def on_list_items_show(self, item: QtWidgets.QTreeWidgetItem): + media_list: Album | Playlist | str = get_user_list_media_item(item) + + # Only if clicked item is not a top level item. + if media_list: + if isinstance(media_list, str) and media_list.startswith("fav_"): + function_list = favorite_function_factory(self.tidal, media_list) + + self.list_items_show_result(favorite_function=function_list) + else: + self.list_items_show_result(media_list) + self.cover_show(media_list) + + def on_result_item_clicked(self, index: QtCore.QModelIndex) -> None: + media: Track | Video | Album | Artist = get_results_media_item( + index, self.proxy_tr_results, self.model_tr_results + ) + + self.cover_show(media) + + def on_queue_download_item_clicked(self, item: QtWidgets.QTreeWidgetItem, column: int) -> None: + media: Track | Video | Album | Artist | Mix | Playlist = get_queue_download_media(item) + + self.cover_show(media) + + def cover_show(self, media: Album | Playlist | Track | Video | Album | Artist) -> None: + cover_url: str + + try: + cover_url = media.album.image() + except: + try: + cover_url = media.image() + except: + logger_gui.info(f"No cover available (media ID: {media.id}).") + + if cover_url and self.cover_url_current != cover_url: + self.cover_url_current = cover_url + data_cover: bytes = Download.cover_data(cover_url) + pixmap: QtGui.QPixmap = QtGui.QPixmap() + pixmap.loadFromData(data_cover) + self.l_pm_cover.setPixmap(pixmap) + + def list_items_show_result( + self, + media_list: Album | Playlist | Mix | Artist | None = None, + point: QtCore.QPoint | None = None, + parent: QtGui.QStandardItem = None, + favorite_function: Callable = None, + ) -> None: + if point: + item = self.tr_lists_user.itemAt(point) + media_list = get_user_list_media_item(item) + + # Get all results + if favorite_function or isinstance(media_list, str): + if isinstance(media_list, str): + favorite_function = favorite_function_factory(self.tidal, media_list) + + media_items: [Track | Video | Album] = favorite_function() + else: + media_items: [Track | Video | Album] = items_results_all(media_list) + + result: [ResultItem] = self.search_result_to_model(media_items) + + self.populate_tree_results(result, parent=parent) + + def thread_it(self, fn: Callable, *args, **kwargs): + # Any other args, kwargs are passed to the run function + worker = Worker(fn, *args, **kwargs) + + # Execute + self.threadpool.start(worker) + + def on_queue_download_clear_all(self): + self.on_clear_queue_download( + f"({QueueDownloadStatus.Waiting}|{QueueDownloadStatus.Finished}|{QueueDownloadStatus.Failed})" + ) + + def on_queue_download_clear_finished(self): + self.on_clear_queue_download(f"[{QueueDownloadStatus.Finished}]") + + def on_clear_queue_download(self, regex: str): + items: [QtWidgets.QTreeWidgetItem | None] = self.tr_queue_download.findItems( + regex, QtCore.Qt.MatchFlag.MatchRegularExpression, column=0 + ) + + for item in items: + self.tr_queue_download.takeTopLevelItem(self.tr_queue_download.indexOfTopLevelItem(item)) + + def on_queue_download_remove(self): + items: [QtWidgets.QTreeWidgetItem | None] = self.tr_queue_download.selectedItems() + + if len(items) == 0: + logger_gui.error("Please select an item from the queue first.") + else: + for item in items: + status: str = item.text(0) + + if status != QueueDownloadStatus.Downloading: + self.tr_queue_download.takeTopLevelItem(self.tr_queue_download.indexOfTopLevelItem(item)) + else: + logger_gui.info("Cannot remove a currently downloading item from queue.") + + # TODO: Must happen in main thread. Do not thread this. + def on_download_results(self) -> None: + items: [HumanProxyModel | None] = self.tr_results.selectionModel().selectedRows() + + if len(items) == 0: + logger_gui.error("Please select a row first.") + else: + for item in items: + media: Track | Album | Playlist | Video | Artist = get_results_media_item( + item, self.proxy_tr_results, self.model_tr_results + ) + queue_dl_item: QueueDownloadItem = self.media_to_queue_download_model(media) + + if queue_dl_item: + self.queue_download_media(queue_dl_item) + + def queue_download_media(self, queue_dl_item: QueueDownloadItem) -> None: + # Populate child + child: QtWidgets.QTreeWidgetItem = QtWidgets.QTreeWidgetItem() + + child.setText(0, queue_dl_item.status) + set_queue_download_media(child, queue_dl_item.obj) + child.setText(2, queue_dl_item.name) + child.setText(3, queue_dl_item.type_media) + child.setText(4, queue_dl_item.quality) + self.tr_queue_download.addTopLevelItem(child) + + def watcher_queue_download(self) -> None: + while True: + items: [QtWidgets.QTreeWidgetItem | None] = self.tr_queue_download.findItems( + QueueDownloadStatus.Waiting, QtCore.Qt.MatchFlag.MatchExactly, column=0 + ) + + if len(items) > 0: + result: QueueDownloadStatus + item: QtWidgets.QTreeWidgetItem = items[0] + media: Track | Album | Playlist | Video | Mix | Artist = get_queue_download_media(item) + tmp_quality: str = get_queue_download_quality(item) + quality: Quality | QualityVideo | None = tmp_quality if tmp_quality else None + + try: + self.s_queue_download_item_downloading.emit(item) + result = self.on_queue_download(media, quality=quality) + + if result == QueueDownloadStatus.Finished: + self.s_queue_download_item_finished.emit(item) + elif result == QueueDownloadStatus.Skipped: + self.s_queue_download_item_skipped.emit(item) + except Exception as e: + logger_gui.error(e) + self.s_queue_download_item_failed.emit(item) + else: + time.sleep(2) + + def on_queue_download_item_downloading(self, item: QtWidgets.QTreeWidgetItem) -> None: + self.queue_download_item_status(item, QueueDownloadStatus.Downloading) + + def on_queue_download_item_finished(self, item: QtWidgets.QTreeWidgetItem) -> None: + self.queue_download_item_status(item, QueueDownloadStatus.Finished) + + def on_queue_download_item_failed(self, item: QtWidgets.QTreeWidgetItem) -> None: + self.queue_download_item_status(item, QueueDownloadStatus.Failed) + + def on_queue_download_item_skipped(self, item: QtWidgets.QTreeWidgetItem) -> None: + self.queue_download_item_status(item, QueueDownloadStatus.Skipped) + + def queue_download_item_status(self, item: QtWidgets.QTreeWidgetItem, status: str) -> None: + item.setText(0, status) + + def on_queue_download( + self, media: Track | Album | Playlist | Video | Mix | Artist, quality: Quality | QualityVideo | None = None + ) -> QueueDownloadStatus: + result: QueueDownloadStatus + items_media: [Track | Album | Playlist | Video | Mix | Artist] + + if isinstance(media, Artist): + items_media: [Album] = items_results_all(media) + else: + items_media = [media] + + download_delay: bool = bool(isinstance(media, Track | Video) and self.settings.data.download_delay) + + for item_media in items_media: + result = self.download(item_media, self.dl, delay_track=download_delay, quality=quality) + + return result + + def download( + self, + media: Track | Album | Playlist | Video | Mix | Artist, + dl: Download, + delay_track: bool = False, + quality: Quality | QualityVideo | None = None, + ) -> QueueDownloadStatus: + result_dl: bool + path_file: str + result: QueueDownloadStatus + quality_audio: Quality | None + quality_video: QualityVideo | None + self.s_pb_reset.emit() + self.s_statusbar_message.emit(StatusbarMessage(message="Download started...")) + + file_template = get_format_template(media, self.settings) + + if isinstance(media, Track | Video): + if isinstance(media, Track): + quality_audio = quality + quality_video = None + else: + quality_audio = None + quality_video = quality + + result_dl, path_file = dl.item( + media=media, + file_template=file_template, + download_delay=delay_track, + quality_audio=quality_audio, + quality_video=quality_video, + ) + elif isinstance(media, Album | Playlist | Mix): + if isinstance(media, Album): + quality_audio = quality + quality_video = None + else: + quality_audio = None + quality_video = None + + dl.items( + media=media, + file_template=file_template, + video_download=self.settings.data.video_download, + download_delay=self.settings.data.download_delay, + quality_audio=quality_audio, + quality_video=quality_video, + ) + + # Dummy values + result_dl = True + path_file = "dummy" + + self.s_statusbar_message.emit(StatusbarMessage(message="Download finished.", timout=2000)) + + if result_dl and path_file: + result = QueueDownloadStatus.Finished + elif not result_dl and path_file: + result = QueueDownloadStatus.Skipped + else: + result = QueueDownloadStatus.Failed + + return result + + def on_version( + self, update_check: bool = False, update_available: bool = False, update_info: ReleaseLatest = None + ) -> None: + DialogVersion(self, update_check, update_available, update_info) + + def on_preferences(self) -> None: + DialogPreferences(settings=self.settings, settings_save=self.s_settings_save, parent=self) + + def on_tr_results_expanded(self, index: QtCore.QModelIndex) -> None: + # If the child is a dummy the list_item has not been expanded before + item: QtGui.QStandardItem = self.model_tr_results.itemFromIndex(self.proxy_tr_results.mapToSource(index)) + load_children: bool = not item.child(0, 0).isEnabled() + + if load_children: + item.removeRow(0) + media_list: [Mix | Album | Playlist | Artist] = get_results_media_item( + index, self.proxy_tr_results, self.model_tr_results + ) + + self.list_items_show_result(media_list=media_list, parent=item) + + def button_reload_status(self, status: bool): + button_text: str = "Reloading..." + if status: + button_text = "Reload" + + self.pb_reload_user_lists.setEnabled(status) + self.pb_reload_user_lists.setText(button_text) + + +# TODO: Comment with Google Docstrings. +def gui_activate(tidal: Tidal | None = None): + # Set dark theme and create QT app. + qdarktheme.enable_hi_dpi() + app = QtWidgets.QApplication(sys.argv) + # Fix for Windows: Tooltips have bright font color + # https://github.com/5yutan5/PyQtDarkTheme/issues/239 + # qdarktheme.setup_theme() + qdarktheme.setup_theme(additional_qss="QToolTip { border: 0px; }") + + # Create icon object and apply it to app window. + pixmap: QtGui.QPixmap = QtGui.QPixmap("tidal_dl_ng/ui/icon.png") + icon: QtGui.QIcon = QtGui.QIcon(pixmap) + app.setWindowIcon(icon) + + # This bit gets the taskbar icon working properly in Windows + if sys.platform.startswith("win"): + import ctypes + + # Make sure Pyinstaller icons are still grouped + if not sys.argv[0].endswith(".exe"): + # Arbitrary string + my_app_id: str = "exislow.tidal.dl-ng." + __version__ + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(my_app_id) + + window = MainWindow(tidal=tidal) + window.show() + # Check for updates + window.s_update_check.emit(True) + + sys.exit(app.exec()) + + +if __name__ == "__main__": + gui_activate() diff --git a/tidal_dl_ng/helper/__init__.py b/tidal_dl_ng/helper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tidal_dl_ng/helper/__pycache__/__init__.cpython-311.pyc b/tidal_dl_ng/helper/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..beadca6a344a4a82d141f53c5dae6ffe3caa5200 GIT binary patch literal 174 zcmZ3^%ge<81RrPTrGx0lAOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4TnFGu~*;?$yI z{nYH@lA^>6eV5eY?2`Nf{gTX-#2np}9NoNh2s=I{CmzVoNX;ooEz*yV&&i3nGix2$-!bb5Jr-jLoO0R2qvf~hQJzyzEo()fgF~`gW$>Ag!Ldf`Ko)SV=#wQSG}(H(eLB;UjN)| zHV~}mKdAnX9zuUQX0dAX%W(%Tj}S*3OEkn0#>hh-BJSNs+!x+sAE763$`$!MSV!&J zUyg0#JiH;9F`qLX&kc&#!u_D~OV z|9%*Sd*~}1EkURtZ>}e*FNICjr^2$G-x~zhX9GbhIf-2sBxZ_i2$Bp(QVfN%f|EoM zp7eS`153ti+c?SXZ97O+hpeYVQ8*i=#v~h3kf8t#cSf#o)nE_8QKlij=oFdc4Voxp znTmypafJ^;u3IfQ$6G)i;zwu#&%MfE3+pux*#+<*e+=hIrN0N=FV_SoXyW~Zzr$bq z6aT+HCdjqmRmsa1K`ZdixJc6Be|rI0JYcFX46&&zpKLLi36k~-<}QV55=-e_!gN1# zJs~_LDz$_`Q({}WbDwHqGp&lIl?arOxM*!(*CDsOX0nmcc}Qu@q@=W6%l%lIJOKA5 zuWycvcesEsUjjon8k8CbpmVkR;{EO&Bedy?O=C41bZ-i?Y12{HCY;GjTwYTB@=m#= za32T>LAzD^JELvA1m1SeRs-k;v$II52|Duukbid3tbvwW`|*LAzI7h<(aME~+4tjv z@w1f+znqz_y!YE#Ah7p7I$C{q@5X~$hnJr&zw<}y^n-5><7chaX}Ed}o>05tidJkW zD1{lJ!<1*z-Ipo-GGlVCIYB8;V@eCOw3~)J;7M%r7Ntx9?#w2s0yv5S^5z_#SQKDl zeGK&oCa&Kp9X8QV5zn? zLvKd(sYS3`@YC1r_6UCac@4mbB9C8DY38zf5ne4}xTenm>(Vx#0Gat1<0Eu#`s$w5 UPh&{t|78E>b0_`#HL4=mzlWb^8UO$Q literal 0 HcmV?d00001 diff --git a/tidal_dl_ng/helper/__pycache__/decryption.cpython-311.pyc b/tidal_dl_ng/helper/__pycache__/decryption.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90ed0b3def332ecd19d89d28985420cd6cda08d3 GIT binary patch literal 2646 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 literal 0 HcmV?d00001 diff --git a/tidal_dl_ng/helper/__pycache__/wrapper.cpython-311.pyc b/tidal_dl_ng/helper/__pycache__/wrapper.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5319e5bba1dab23a88ed948aec6f2851b7d8bb09 GIT binary patch literal 1792 zcmc&#F>ljA6uxup#7ScnS|}}Qq*ky*q76TwLP#J163S9lNM25E?vh$uZF6=gL>)SG zs1Oot9lCTXiug0F#87!&xeYU@Q@4MXH_k4brFOE|oU}pzGr)m)L z9fPr%BWL&koPELxr;=<^mr`PoXM`KOgcpur)K_>ia}92;lDhQ+!#YjvC!Unvx)iO^ z4|G!)1;Zvd`$Q0za^f0muz*_-rm(oN0zKRiX4~L}UDGXcbCeNu8Hrrz6xW>8FW4)Q2;^i@4f|X(iV27N}F6@~fojtgQK#*K2n1)W}Avll< zIfVAAZPOzPdj(Hs6n1o#C>tVG2Iw0KzlGF#>&dPd<*6n##l?EJ)0@ug)FciW-UR@W zcJ-6A3vUt}LQR0U|?@oT#E;{CNvg!`j za^P(W#?lI7n<4K?^s9`$>3TBj(Wi&PI!z}T^MW8uJX|DZOjU8dn+SLsDV_B!dRSO$ z9-yCnvbr3Vg)9P|(D4Dl&TrT0Ty5w1-o~+6gZsp+4NPh+9?lL3G?$@Btr~J8_>1Ai zq`K6!+Gf3wPPRe`{{}koWpBNuUr3hFB4(v46wjPKE00S2_Ry literal 0 HcmV?d00001 diff --git a/tidal_dl_ng/helper/decorator.py b/tidal_dl_ng/helper/decorator.py new file mode 100644 index 0000000..ba0f417 --- /dev/null +++ b/tidal_dl_ng/helper/decorator.py @@ -0,0 +1,22 @@ +from typing import ClassVar + + +class SingletonMeta(type): + """ + The Singleton class can be implemented in different ways in Python. Some + possible methods include: base class, decorator, metaclass. We will use the + metaclass because it is best suited for this purpose. + """ + + _instances: ClassVar[dict] = {} + + def __call__(cls, *args, **kwargs): + """ + Possible changes to the value of the `__init__` argument do not affect + the returned instance. + """ + if cls not in cls._instances: + instance = super().__call__(*args, **kwargs) + cls._instances[cls] = instance + + return cls._instances[cls] diff --git a/tidal_dl_ng/helper/decryption.py b/tidal_dl_ng/helper/decryption.py new file mode 100644 index 0000000..17ea19b --- /dev/null +++ b/tidal_dl_ng/helper/decryption.py @@ -0,0 +1,55 @@ +import base64 +import pathlib + +from Crypto.Cipher import AES +from Crypto.Util import Counter + + +def decrypt_security_token(security_token: str) -> (str, str): + """ + Decrypts security token into key and nonce pair + + security_token should match the securityToken value from the web response + """ + + # Do not change this + master_key = "UIlTTEMmmLfGowo/UC60x2H45W6MdGgTRfo/umg4754=" + + # Decode the base64 strings to ascii strings + master_key = base64.b64decode(master_key) + security_token = base64.b64decode(security_token) + + # Get the IV from the first 16 bytes of the securityToken + iv = security_token[:16] + encrypted_st = security_token[16:] + + # Initialize decryptor + decryptor = AES.new(master_key, AES.MODE_CBC, iv) + + # Decrypt the security token + decrypted_st = decryptor.decrypt(encrypted_st) + + # Get the audio stream decryption key and nonce from the decrypted security token + key = decrypted_st[:16] + nonce = decrypted_st[16:24] + + return key, nonce + + +def decrypt_file(path_file_encrypted: pathlib.Path, path_file_destination: pathlib.Path, key: str, nonce: str) -> None: + """ + Decrypts an encrypted MQA file given the file, key and nonce. + TODO: Is it really only necessary for MQA of for all other formats, too? + """ + + # Initialize counter and file decryptor + counter = Counter.new(64, prefix=nonce, initial_value=0) + decryptor = AES.new(key, AES.MODE_CTR, counter=counter) + + # Open and decrypt + with path_file_encrypted.open("rb") as f_src: + audio_decrypted = decryptor.decrypt(f_src.read()) + + # Replace with decrypted file + with path_file_destination.open("wb") as f_dst: + f_dst.write(audio_decrypted) diff --git a/tidal_dl_ng/helper/exceptions.py b/tidal_dl_ng/helper/exceptions.py new file mode 100644 index 0000000..be1bdee --- /dev/null +++ b/tidal_dl_ng/helper/exceptions.py @@ -0,0 +1,14 @@ +class LoginError(Exception): + pass + + +class MediaUnknown(Exception): + pass + + +class UnknownManifestFormat(Exception): + pass + + +class MediaMissing(Exception): + pass diff --git a/tidal_dl_ng/helper/gui.py b/tidal_dl_ng/helper/gui.py new file mode 100644 index 0000000..187f6a8 --- /dev/null +++ b/tidal_dl_ng/helper/gui.py @@ -0,0 +1,201 @@ +import re + +from PySide6 import QtCore, QtGui, QtWidgets +from tidalapi import Album, Mix, Playlist, Track, UserPlaylist, Video +from tidalapi.artist import Artist +from tidalapi.media import Quality + + +def get_table_data( + item: QtWidgets.QTreeWidgetItem, column: int +) -> Track | Video | Album | Artist | Mix | Playlist | UserPlaylist: + result: Track | Video | Album | Artist = item.data(column, QtCore.Qt.ItemDataRole.UserRole) + + return result + + +def get_table_text(item: QtWidgets.QTreeWidgetItem, column: int) -> str: + result: str = item.text(column) + + return result + + +def get_results_media_item( + index: QtCore.QModelIndex, proxy: QtCore.QSortFilterProxyModel, model: QtGui.QStandardItemModel +) -> Track | Video | Album | Artist | Playlist | Mix: + # Switch column to "obj" column and map proxy data to our model. + item: QtGui.QStandardItem = model.itemFromIndex(proxy.mapToSource(index.siblingAtColumn(1))) + result: Track | Video | Album | Artist = item.data(QtCore.Qt.ItemDataRole.UserRole) + + return result + + +def get_user_list_media_item(item: QtWidgets.QTreeWidgetItem) -> Mix | Playlist | UserPlaylist: + result: Mix | Playlist | UserPlaylist = get_table_data(item, 1) + + return result + + +def get_queue_download_media( + item: QtWidgets.QTreeWidgetItem, +) -> Mix | Playlist | UserPlaylist | Track | Video | Album | Artist: + result: Mix | Playlist | UserPlaylist | Track | Video | Album | Artist = get_table_data(item, 1) + + return result + + +def get_queue_download_quality( + item: QtWidgets.QTreeWidgetItem, +) -> Quality: + result: Quality = get_table_text(item, 4) + + return result + + +def set_table_data( + item: QtWidgets.QTreeWidgetItem, data: Track | Video | Album | Artist | Mix | Playlist | UserPlaylist, column: int +): + item.setData(column, QtCore.Qt.ItemDataRole.UserRole, data) + + +def set_results_media(item: QtWidgets.QTreeWidgetItem, media: Track | Video | Album | Artist): + set_table_data(item, media, 1) + + +def set_user_list_media( + item: QtWidgets.QTreeWidgetItem, media: Track | Video | Album | Artist | Mix | Playlist | UserPlaylist +): + set_table_data(item, media, 1) + + +def set_queue_download_media( + item: QtWidgets.QTreeWidgetItem, media: Mix | Playlist | UserPlaylist | Track | Video | Album | Artist +): + set_table_data(item, media, 1) + + +class FilterHeader(QtWidgets.QHeaderView): + filter_activated = QtCore.Signal() + + def __init__(self, parent): + super().__init__(QtCore.Qt.Horizontal, parent) + self._editors = [] + self._padding = 4 + self.setCascadingSectionResizes(True) + self.setSectionResizeMode(QtWidgets.QHeaderView.Interactive) + self.setStretchLastSection(True) + self.setDefaultAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + self.setSortIndicatorShown(False) + self.setSectionsMovable(True) + self.sectionResized.connect(self.adjust_positions) + parent.horizontalScrollBar().valueChanged.connect(self.adjust_positions) + + def set_filter_boxes(self, count): + while self._editors: + editor = self._editors.pop() + editor.deleteLater() + + for _ in range(count): + editor = QtWidgets.QLineEdit(self.parent()) + editor.setPlaceholderText("Filter") + editor.setClearButtonEnabled(True) + editor.returnPressed.connect(self.filter_activated.emit) + self._editors.append(editor) + + self.adjust_positions() + + def sizeHint(self): + size = super().sizeHint() + + if self._editors: + height = self._editors[0].sizeHint().height() + + size.setHeight(size.height() + height + self._padding) + + return size + + def updateGeometries(self): + if self._editors: + height = self._editors[0].sizeHint().height() + + self.setViewportMargins(0, 0, 0, height + self._padding) + else: + self.setViewportMargins(0, 0, 0, 0) + + super().updateGeometries() + self.adjust_positions() + + def adjust_positions(self): + for index, editor in enumerate(self._editors): + height = editor.sizeHint().height() + + editor.move(self.sectionPosition(index) - self.offset() + 2, height + (self._padding // 2)) + editor.resize(self.sectionSize(index), height) + + def filter_text(self, index) -> str: + if 0 <= index < len(self._editors): + return self._editors[index].text() + + return "" + + def set_filter_text(self, index, text): + if 0 <= index < len(self._editors): + self._editors[index].setText(text) + + def clear_filters(self): + for editor in self._editors: + editor.clear() + + +class HumanProxyModel(QtCore.QSortFilterProxyModel): + def _human_key(self, key): + parts = re.split(r"(\d*\.\d+|\d+)", key) + + return tuple((e.swapcase() if i % 2 == 0 else float(e)) for i, e in enumerate(parts)) + + def lessThan(self, source_left, source_right): + data_left = source_left.data() + data_right = source_right.data() + + if isinstance(data_left, str) and isinstance(data_right, str): + return self._human_key(data_left) < self._human_key(data_right) + + return super().lessThan(source_left, source_right) + + @property + def filters(self): + if not hasattr(self, "_filters"): + self._filters = [] + + return self._filters + + @filters.setter + def filters(self, filters): + self._filters = filters + + self.invalidateFilter() + + def filterAcceptsRow(self, source_row: int, source_parent: QtCore.QModelIndex) -> bool: + model = self.sourceModel() + source_index = model.index(source_row, 0, source_parent) + result: [bool] = [] + + # Show top level children + for child_row in range(model.rowCount(source_index)): + if self.filterAcceptsRow(child_row, source_index): + return True + + # Filter for actual needle + for i, text in self.filters: + if 0 <= i < self.columnCount(): + ix = self.sourceModel().index(source_row, i, source_parent) + data = ix.data() + + # Append results to list to enable an AND operator for filtering. + result.append(bool(re.search(rf"{text}", data, re.MULTILINE | re.IGNORECASE)) if data else False) + + # If no filter set, just set the result to True. + if not result: + result.append(True) + + return all(result) diff --git a/tidal_dl_ng/helper/path.py b/tidal_dl_ng/helper/path.py new file mode 100644 index 0000000..a275cca --- /dev/null +++ b/tidal_dl_ng/helper/path.py @@ -0,0 +1,324 @@ +import math +import os +import pathlib +import posixpath +import re +import sys +from urllib.parse import unquote, urlsplit + +from pathvalidate import sanitize_filename, sanitize_filepath +from pathvalidate.error import ValidationError +from tidalapi import Album, Mix, Playlist, Track, UserPlaylist, Video +from tidalapi.media import AudioExtensions + +from tidal_dl_ng import __name_display__ +from tidal_dl_ng.constants import FILENAME_SANITIZE_PLACEHOLDER, UNIQUIFY_THRESHOLD, MediaType +from tidal_dl_ng.helper.tidal import name_builder_album_artist, name_builder_artist, name_builder_title + + +def path_home() -> str: + if "XDG_CONFIG_HOME" in os.environ: + return os.environ["XDG_CONFIG_HOME"] + elif "HOME" in os.environ: + return os.environ["HOME"] + elif "HOMEDRIVE" in os.environ and "HOMEPATH" in os.environ: + return os.path.join(os.environ["HOMEDRIVE"], os.environ["HOMEPATH"]) + else: + return os.path.abspath("./") + + +def path_config_base() -> str: + # https://wiki.archlinux.org/title/XDG_Base_Directory + # X11 workaround: If user specified config path is set, do not point to "~/.config" + path_user_custom: str = os.environ.get("XDG_CONFIG_HOME", "") + path_config: str = ".config" if not path_user_custom else "" + path_base: str = os.path.join(path_home(), path_config, __name_display__) + + return path_base + + +def path_file_log() -> str: + return os.path.join(path_config_base(), "app.log") + + +def path_file_token() -> str: + return os.path.join(path_config_base(), "token.json") + + +def path_file_settings() -> str: + return os.path.join(path_config_base(), "settings.json") + + +def format_path_media( + fmt_template: str, media: Track | Album | Playlist | UserPlaylist | Video | Mix, album_track_num_pad_min: int = 0 +) -> str: + result = fmt_template + + # Search track format template for placeholder. + regex = r"\{(.+?)\}" + matches = re.finditer(regex, fmt_template, re.MULTILINE) + + for _matchNum, match in enumerate(matches, start=1): + template_str = match.group() + result_fmt = format_str_media(match.group(1), media, album_track_num_pad_min) + + if result_fmt != match.group(1): + value = sanitize_filename(result_fmt) + result = result.replace(template_str, value) + + return result + + +def format_str_media( + name: str, media: Track | Album | Playlist | UserPlaylist | Video | Mix, album_track_num_pad_min: int = 0 +) -> str: + result: str = name + + try: + match name: + case "artist_name": + if isinstance(media, Track | Video): + if hasattr(media, "artists"): + result = name_builder_artist(media) + elif hasattr(media, "artist"): + result = media.artist.name + case "album_artist": + result = name_builder_album_artist(media) + case "track_title": + if isinstance(media, Track | Video): + result = name_builder_title(media) + case "mix_name": + if isinstance(media, Mix): + result = media.title + case "playlist_name": + if isinstance(media, Playlist | UserPlaylist): + result = media.name + case "album_title": + if isinstance(media, Album): + result = media.name + elif isinstance(media, Track): + result = media.album.name + case "album_track_num": + if isinstance(media, Track | Video): + num_tracks: int = media.album.num_tracks if hasattr(media, "album") else 1 + count_digits: int = int(math.log10(num_tracks)) + 1 + count_digits_computed: int = ( + count_digits if count_digits > album_track_num_pad_min else album_track_num_pad_min + ) + result = str(media.track_num).zfill(count_digits_computed) + case "album_num_tracks": + if isinstance(media, Track | Video): + result = str(media.album.num_tracks if hasattr(media, "album") else 1) + case "track_id": + if isinstance(media, Track | Video): + result = str(media.id) + case "playlist_id": + if isinstance(media, Playlist): + result = str(media.id) + case "album_id": + if isinstance(media, Album): + result = str(media.id) + elif isinstance(media, Track): + result = str(media.album.id) + case "track_duration_seconds": + if isinstance(media, Track | Video): + result = str(media.duration) + case "track_duration_minutes": + if isinstance(media, Track | Video): + m, s = divmod(media.duration, 60) + result = f"{m:01d}:{s:02d}" + case "album_duration_seconds": + if isinstance(media, Album): + result = str(media.duration) + case "album_duration_minutes": + if isinstance(media, Album): + m, s = divmod(media.duration, 60) + result = f"{m:01d}:{s:02d}" + case "playlist_duration_seconds": + if isinstance(media, Album): + result = str(media.duration) + case "playlist_duration_minutes": + if isinstance(media, Album): + m, s = divmod(media.duration, 60) + result = f"{m:01d}:{s:02d}" + case "album_year": + if isinstance(media, Album): + result = str(media.year) + elif isinstance(media, Track): + result = str(media.album.year) + case "video_quality": + if isinstance(media, Video): + result = media.video_quality + case "track_quality": + if isinstance(media, Track): + result = ", ".join(tag for tag in media.media_metadata_tags) + case "track_explicit": + if isinstance(media, Track | Video): + result = " (Explicit)" if media.explicit else "" + case "album_explicit": + if isinstance(media, Album): + result = " (Explicit)" if media.explicit else "" + case "album_num_volumes": + if isinstance(media, Album): + result = str(media.num_volumes) + case "track_volume_num": + if isinstance(media, Track | Video): + result = str(media.volume_num) + case "track_volume_num_optional": + if isinstance(media, Track | Video): + num_volumes: int = media.album.num_volumes if hasattr(media, "album") else 1 + result = "" if num_volumes == 1 else str(media.volume_num) + case "track_volume_num_optional_CD": + if isinstance(media, Track | Video): + num_volumes: int = media.album.num_volumes if hasattr(media, "album") else 1 + result = "" if num_volumes == 1 else f"CD{media.volume_num!s}" + except Exception as e: + # TODO: Implement better exception logging. + print(e) + + pass + + 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 or media == MediaType.ARTIST: + 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, uniquify: bool = False) -> (bool, str): + # Split into path and filename + pathname, filename = os.path.split(path_file) + file_extension: str = pathlib.Path(path_file).suffix + + # Sanitize path + try: + pathname_sanitized: str = sanitize_filepath( + pathname, replacement_text=" ", validate_after_sanitize=True, platform="auto" + ) + except ValidationError: + # If adaption of path is allowed in case of an error set path to HOME. + if adapt: + pathname_sanitized: str = str(pathlib.Path.home()) + else: + raise + + # Sanitize filename + try: + filename_sanitized: str = sanitize_filename( + filename, replacement_text=" ", validate_after_sanitize=True, platform="auto" + ) + + # Check if the file extension was removed by shortening the filename length + if not filename_sanitized.endswith(file_extension): + # Add the original file extension + file_suffix: str = FILENAME_SANITIZE_PLACEHOLDER + file_extension + filename_sanitized = filename_sanitized[: -len(file_suffix)] + file_suffix + except ValidationError as e: + # TODO: Implement proper exception handling and logging. + # Hacky stuff, since the sanitizing function does not shorten the filename somehow (bug?) + # TODO: Remove after pathvalidate update. + # If filename too long + if e.description.startswith("[PV1101]"): + byte_ct: int = len(filename.encode("utf-8")) - 255 + filename_sanitized = ( + filename[: -byte_ct - len(FILENAME_SANITIZE_PLACEHOLDER) - len(file_extension)] + + FILENAME_SANITIZE_PLACEHOLDER + + file_extension + ) + else: + print(e) + + # Join path and filename + result: str = os.path.join(pathname_sanitized, filename_sanitized) + + # Uniquify + if uniquify: + unique_suffix: str = file_unique_suffix(result) + + if unique_suffix: + file_suffix = unique_suffix + file_extension + # For most OS filename has a character limit of 255. + filename_sanitized = ( + filename_sanitized[: -len(file_suffix)] + file_suffix + if len(filename_sanitized + unique_suffix) > 255 + else filename_sanitized[: -len(file_extension)] + file_suffix + ) + + # Join path and filename + result = os.path.join(pathname_sanitized, filename_sanitized) + + return result + + +def file_unique_suffix(path_file: str, seperator: str = "_") -> str: + threshold_zfill: int = len(str(UNIQUIFY_THRESHOLD)) + count: int = 0 + path_file_tmp: str = path_file + unique_suffix: str = "" + + while check_file_exists(path_file_tmp) and count < UNIQUIFY_THRESHOLD: + count += 1 + unique_suffix = seperator + str(count).zfill(threshold_zfill) + filename, file_extension = os.path.splitext(path_file_tmp) + path_file_tmp = filename + unique_suffix + file_extension + + return unique_suffix + + +def check_file_exists(path_file: pathlib.Path, extension_ignore: bool = False) -> bool: + if extension_ignore: + path_file_stem: str = pathlib.Path(path_file).stem + path_parent: pathlib.Path = pathlib.Path(path_file).parent + path_files: [str] = [] + + for extension in AudioExtensions: + path_files.append(str(path_parent.joinpath(path_file_stem + extension))) + else: + path_files: [str] = [path_file] + + result = bool(sum([[True] if os.path.isfile(_file) else [] for _file in path_files], [])) + + return result + + +def resource_path(relative_path): + try: + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + + return os.path.join(base_path, relative_path) + + +def url_to_filename(url: str) -> str: + """Return basename corresponding to url. + >>> print(url_to_filename('http://example.com/path/to/file%C3%80?opt=1')) + fileÀ + >>> print(url_to_filename('http://example.com/slash%2fname')) # '/' in name + Taken from https://gist.github.com/zed/c2168b9c52b032b5fb7d + Traceback (most recent call last): + ... + ValueError + """ + urlpath: str = urlsplit(url).path + basename: str = posixpath.basename(unquote(urlpath)) + + if os.path.basename(basename) != basename or unquote(posixpath.basename(urlpath)) != basename: + raise ValueError # reject '%2f' or 'dir%5Cbasename.ext' on Windows + + return basename diff --git a/tidal_dl_ng/helper/tidal.py b/tidal_dl_ng/helper/tidal.py new file mode 100644 index 0000000..e568cc6 --- /dev/null +++ b/tidal_dl_ng/helper/tidal.py @@ -0,0 +1,205 @@ +from collections.abc import Callable + +from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video +from tidalapi.artist import Artist, Role +from tidalapi.media import MediaMetadataTags, Quality +from tidalapi.session import SearchTypes +from tidalapi.user import LoggedInUser + +from tidal_dl_ng.constants import FAVORITES, MediaType +from tidal_dl_ng.helper.exceptions import MediaUnknown + + +def name_builder_artist(media: Track | Video | Album) -> str: + return ", ".join(artist.name for artist in media.artists) + + +def name_builder_album_artist(media: Track | Album) -> str: + artists_tmp: [str] = [] + artists: [Artist] = media.album.artists if isinstance(media, Track) else media.artists + + for artist in artists: + if Role.main in artist.roles: + artists_tmp.append(artist.name) + + return ", ".join(artists_tmp) + + +def name_builder_title(media: Track | Video | Mix | Playlist | Album | Video) -> str: + result: str = ( + media.title if isinstance(media, Mix) else media.full_name if hasattr(media, "full_name") else media.name + ) + + return result + + +def name_builder_item(media: Track) -> str: + 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 + elif media_name == "artist": + result = MediaType.ARTIST + + return result + + +def search_results_all(session: Session, needle: str, types_media: SearchTypes = None) -> dict[str, [SearchTypes]]: + limit: int = 300 + offset: int = 0 + done: bool = False + result: dict[str, [SearchTypes]] = {} + + while not done: + tmp_result: dict[str, [SearchTypes]] = session.search( + query=needle, models=types_media, limit=limit, offset=offset + ) + tmp_done: bool = True + + for key, value in tmp_result.items(): + # Append pagination results, if there are any + if offset == 0: + result = tmp_result + tmp_done = False + elif bool(value): + result[key] += value + tmp_done = False + + # Next page + offset += limit + done = tmp_done + + return result + + +def items_results_all( + media_list: [Mix | Playlist | Album | Artist], videos_include: bool = True +) -> [Track | Video | Album]: + result: [Track | Video | Album] = [] + + if isinstance(media_list, Mix): + result = media_list.items() + else: + func_get_items_media: [Callable] = [] + + if isinstance(media_list, Playlist | Album): + if videos_include: + func_get_items_media.append(media_list.items) + else: + func_get_items_media.append(media_list.tracks) + else: + func_get_items_media.append(media_list.get_albums) + func_get_items_media.append(media_list.get_ep_singles) + + result = paginate_results(func_get_items_media) + + return result + + +def all_artist_album_ids(media_artist: Artist) -> [int | None]: + result: [int] = [] + func_get_items_media: [Callable] = [media_artist.get_albums, media_artist.get_ep_singles] + albums: [Album] = paginate_results(func_get_items_media) + + for album in albums: + result.append(album.id) + + return result + + +def paginate_results(func_get_items_media: [Callable]) -> [Track | Video | Album | Playlist | UserPlaylist]: + result: [Track | Video | Album] = [] + + for func_media in func_get_items_media: + limit: int = 100 + offset: int = 0 + done: bool = False + + if func_media.__func__ == LoggedInUser.playlist_and_favorite_playlists: + limit: int = 50 + + while not done: + tmp_result: [Track | Video | Album | Playlist | UserPlaylist] = func_media(limit=limit, offset=offset) + + if bool(tmp_result): + result += tmp_result + # Get the next page in the next iteration. + offset += limit + else: + done = True + + return result + + +def user_media_lists(session: Session) -> [Playlist | UserPlaylist | Mix]: + user_playlists: [Playlist | UserPlaylist] = paginate_results([session.user.playlist_and_favorite_playlists]) + user_mixes: [Mix] = session.mixes().categories[0].items + result: [Playlist | UserPlaylist | Mix] = user_playlists + user_mixes + + return result + + +def instantiate_media( + session: Session, + media_type: type[MediaType.TRACK, MediaType.VIDEO, MediaType.ALBUM, MediaType.PLAYLIST, MediaType.MIX], + id_media: str, +) -> Track | Video | Album | Playlist | Mix | Artist: + if media_type == MediaType.TRACK: + media = session.track(id_media, with_album=True) + elif media_type == MediaType.VIDEO: + media = session.video(id_media) + elif media_type == MediaType.ALBUM: + media = session.album(id_media) + elif media_type == MediaType.PLAYLIST: + media = session.playlist(id_media) + elif media_type == MediaType.MIX: + media = session.mix(id_media) + elif media_type == MediaType.ARTIST: + media = session.artist(id_media) + else: + raise MediaUnknown + + return media + + +def quality_audio_highest(media: Track | Album) -> Quality: + quality: Quality + + if MediaMetadataTags.hi_res_lossless in media.media_metadata_tags: + quality = Quality.hi_res_lossless + elif MediaMetadataTags.lossless in media.media_metadata_tags: + quality = Quality.high_lossless + else: + quality = media.audio_quality + + return quality + + +def favorite_function_factory(tidal, favorite_item: str): + function_name: str = FAVORITES[favorite_item]["function_name"] + function_list: Callable = getattr(tidal.session.user.favorites, function_name) + + return function_list diff --git a/tidal_dl_ng/helper/wrapper.py b/tidal_dl_ng/helper/wrapper.py new file mode 100644 index 0000000..cd8e6ba --- /dev/null +++ b/tidal_dl_ng/helper/wrapper.py @@ -0,0 +1,26 @@ +from collections.abc import Callable + + +class LoggerWrapped: + fn_print: Callable = None + + def __init__(self, fn_print: Callable): + self.fn_print = fn_print + + def debug(self, value): + self.fn_print(value) + + def warning(self, value): + self.fn_print(value) + + def info(self, value): + self.fn_print(value) + + def error(self, value): + self.fn_print(value) + + def critical(self, value): + self.fn_print(value) + + def exception(self, value): + self.fn_print(value) diff --git a/tidal_dl_ng/logger.py b/tidal_dl_ng/logger.py new file mode 100644 index 0000000..83477a7 --- /dev/null +++ b/tidal_dl_ng/logger.py @@ -0,0 +1,65 @@ +import logging +import sys + +import coloredlogs +from PySide6 import QtCore + + +class XStream(QtCore.QObject): + _stdout = None + _stderr = None + messageWritten = QtCore.Signal(str) + + def flush(self): + pass + + def fileno(self): + return -1 + + def write(self, msg): + if not self.signalsBlocked(): + self.messageWritten.emit(msg) + + @staticmethod + def stdout(): + if not XStream._stdout: + XStream._stdout = XStream() + sys.stdout = XStream._stdout + return XStream._stdout + + @staticmethod + def stderr(): + if not XStream._stderr: + XStream._stderr = XStream() + sys.stderr = XStream._stderr + return XStream._stderr + + +class QtHandler(logging.Handler): + def __init__(self): + logging.Handler.__init__(self) + + def emit(self, record): + record = self.format(record) + + if record: + # originally: XStream.stdout().write("{}\n".format(record)) + XStream.stdout().write("%s\n" % record) + + +logger_gui = logging.getLogger(__name__) +handler_qt: QtHandler = QtHandler() +# log_fmt: str = "[%(asctime)s] %(levelname)s: %(message)s" +log_fmt: str = "> %(message)s" +# formatter = logging.Formatter(log_fmt) +formatter = coloredlogs.ColoredFormatter(fmt=log_fmt) +handler_qt.setFormatter(formatter) +logger_gui.addHandler(handler_qt) +logger_gui.setLevel(logging.DEBUG) + +logger_cli = logging.getLogger(__name__) +handler_stream: logging.StreamHandler = logging.StreamHandler() +formatter = coloredlogs.ColoredFormatter(fmt=log_fmt) +handler_stream.setFormatter(formatter) +logger_cli.addHandler(handler_stream) +logger_cli.setLevel(logging.DEBUG) diff --git a/tidal_dl_ng/metadata.py b/tidal_dl_ng/metadata.py new file mode 100644 index 0000000..62d96cd --- /dev/null +++ b/tidal_dl_ng/metadata.py @@ -0,0 +1,164 @@ +import mutagen +from mutagen import flac, id3, mp4 +from mutagen.id3 import APIC, TALB, TCOM, TCOP, TDRC, TIT2, TOPE, TPE1, TRCK, TSRC, TXXX, USLT + + +class Metadata: + path_file: str + title: str + album: str + albumartist: str + artists: [str] + copy_right: str + tracknumber: int + discnumber: int + totaldisc: int + totaltrack: int + date: str + composer: [str] + isrc: str + lyrics: str + path_cover: str + cover_data: bytes + album_replay_gain: float + album_peak_amplitude: float + track_replay_gain: float + track_peak_amplitude: float + m: mutagen.mp4.MP4 | mutagen.mp4.MP4 | mutagen.flac.FLAC + + def __init__( + self, + path_file: str, + album: str = "", + title: str = "", + artists: str = "", + copy_right: str = "", + tracknumber: int = 0, + discnumber: int = 0, + totaltrack: int = 0, + totaldisc: int = 0, + composer: list[str] | None = None, + isrc: str = "", + albumartist: str = "", + date: str = "", + lyrics: str = "", + cover_data: bytes = None, + album_replay_gain: float = 1.0, + album_peak_amplitude: float = 1.0, + track_replay_gain: float = 1.0, + track_peak_amplitude: float = 1.0, + ): + self.path_file = path_file + self.title = title + self.album = album + self.albumartist = albumartist + self.artists = artists + self.copy_right = copy_right + self.tracknumber = tracknumber + self.discnumber = discnumber + self.totaldisc = totaldisc + self.totaltrack = totaltrack + self.date = date + self.composer = composer + self.isrc = isrc + self.lyrics = lyrics + self.cover_data = cover_data + self.album_replay_gain = album_replay_gain + self.album_peak_amplitude = album_peak_amplitude + self.track_replay_gain = track_replay_gain + self.track_peak_amplitude = track_peak_amplitude + self.m: mutagen.mp4.MP4 | mutagen.flac.FLAC | mutagen.mp3.MP3 = mutagen.File(self.path_file) + + def _cover(self) -> bool: + result: bool = False + + if self.cover_data: + if isinstance(self.m, mutagen.flac.FLAC): + flac_cover = flac.Picture() + flac_cover.type = id3.PictureType.COVER_FRONT + flac_cover.data = self.cover_data + flac_cover.mime = "image/jpeg" + + self.m.clear_pictures() + self.m.add_picture(flac_cover) + elif isinstance(self.m, mutagen.mp3.MP3): + self.m.tags.add(APIC(encoding=3, data=self.cover_data)) + elif isinstance(self.m, mutagen.mp4.MP4): + cover_mp4 = mp4.MP4Cover(self.cover_data) + self.m.tags["covr"] = [cover_mp4] + + result = True + + return result + + def save(self): + if not self.m.tags: + self.m.add_tags() + + if isinstance(self.m, mutagen.flac.FLAC): + self.set_flac() + elif isinstance(self.m, mutagen.mp3.MP3): + self.set_mp3() + elif isinstance(self.m, mutagen.mp4.MP4): + self.set_mp4() + + self._cover() + self.m.save() + + return True + + def set_flac(self): + self.m.tags["TITLE"] = self.title + self.m.tags["ALBUM"] = self.album + self.m.tags["ALBUMARTIST"] = self.albumartist + self.m.tags["ARTIST"] = self.artists + self.m.tags["COPYRIGHT"] = self.copy_right + self.m.tags["TRACKNUMBER"] = str(self.tracknumber) + self.m.tags["TRACKTOTAL"] = str(self.totaltrack) + self.m.tags["DISCNUMBER"] = str(self.discnumber) + self.m.tags["DISCTOTAL"] = str(self.totaldisc) + self.m.tags["DATE"] = self.date + self.m.tags["COMPOSER"] = ", ".join(self.composer) if self.composer else "" + self.m.tags["ISRC"] = self.isrc + self.m.tags["LYRICS"] = self.lyrics + self.m.tags["REPLAYGAIN_ALBUM_GAIN"] = str(self.album_replay_gain) + self.m.tags["REPLAYGAIN_ALBUM_PEAK"] = str(self.album_peak_amplitude) + self.m.tags["REPLAYGAIN_TRACK_GAIN"] = str(self.track_replay_gain) + self.m.tags["REPLAYGAIN_TRACK_PEAK"] = str(self.track_peak_amplitude) + + def set_mp3(self): + # ID3 Frame (tags) overview: https://exiftool.org/TagNames/ID3.html / https://id3.org/id3v2.3.0 + # Mapping overview: https://docs.mp3tag.de/mapping/ + self.m.tags.add(TIT2(encoding=3, text=self.title)) + self.m.tags.add(TALB(encoding=3, text=self.album)) + self.m.tags.add(TOPE(encoding=3, text=self.albumartist)) + self.m.tags.add(TPE1(encoding=3, text=self.artists)) + self.m.tags.add(TCOP(encoding=3, text=self.copy_right)) + self.m.tags.add(TRCK(encoding=3, text=str(self.tracknumber))) + self.m.tags.add(TRCK(encoding=3, text=self.discnumber)) + self.m.tags.add(TDRC(encoding=3, text=self.date)) + self.m.tags.add(TCOM(encoding=3, text=", ".join(self.composer) if self.composer else "")) + self.m.tags.add(TSRC(encoding=3, text=self.isrc)) + self.m.tags.add(USLT(encoding=3, lang="eng", desc="desc", text=self.lyrics)) + self.m.tags.add(TXXX(encoding=3, desc="REPLAYGAIN_ALBUM_GAIN", text=str(self.album_replay_gain))) + self.m.tags.add(TXXX(encoding=3, desc="REPLAYGAIN_ALBUM_PEAK", text=str(self.album_peak_amplitude))) + self.m.tags.add(TXXX(encoding=3, desc="REPLAYGAIN_TRACK_GAIN", text=str(self.track_replay_gain))) + self.m.tags.add(TXXX(encoding=3, desc="REPLAYGAIN_TRACK_PEAK", text=str(self.track_peak_amplitude))) + + def set_mp4(self): + self.m.tags["\xa9nam"] = self.title + self.m.tags["\xa9alb"] = self.album + self.m.tags["aART"] = self.albumartist + self.m.tags["\xa9ART"] = self.artists + self.m.tags["cprt"] = self.copy_right + self.m.tags["trkn"] = [[self.tracknumber, self.totaltrack]] + self.m.tags["disk"] = [[self.discnumber, self.totaldisc]] + # self.m.tags['\xa9gen'] = self.genre + self.m.tags["\xa9day"] = self.date + self.m.tags["\xa9wrt"] = ", ".join(self.composer) if self.composer else "" + self.m.tags["\xa9lyr"] = self.lyrics + self.m.tags["isrc"] = self.isrc + self.m.tags["----:com.apple.iTunes:REPLAYGAIN_ALBUM_GAIN"] = str(self.album_replay_gain).encode("utf-8") + self.m.tags["----:com.apple.iTunes:REPLAYGAIN_ALBUM_PEAK"] = str(self.album_peak_amplitude).encode("utf-8") + self.m.tags["----:com.apple.iTunes:REPLAYGAIN_TRACK_GAIN"] = str(self.track_replay_gain).encode("utf-8") + self.m.tags["----:com.apple.iTunes:REPLAYGAIN_TRACK_PEAK"] = str(self.track_peak_amplitude).encode("utf-8") diff --git a/tidal_dl_ng/model/__init__.py b/tidal_dl_ng/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tidal_dl_ng/model/__pycache__/__init__.cpython-311.pyc b/tidal_dl_ng/model/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be06b6168ce2d2fcf004c844dd1d15f3550e9d6a GIT binary patch literal 173 zcmZ3^%ge<81RrPTrGx0lAOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4TnF9-e5;?$yI z{nYH@lA^>6eV5eY?2`Nf{gTX-#2np}9NoNh2s=I{CmzVo%}+_q(T|VM%*!l^kJl@x k{Ka7d6f4b1wJTx;8VItom>)=dU}j`w{J;PsikN|70FLo0WB>pF literal 0 HcmV?d00001 diff --git a/tidal_dl_ng/model/__pycache__/cfg.cpython-311.pyc b/tidal_dl_ng/model/__pycache__/cfg.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..425803ba652b3fc3f5f4a8a64d546569c4fa6fae GIT binary patch literal 6823 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 literal 0 HcmV?d00001 diff --git a/tidal_dl_ng/model/__pycache__/downloader.cpython-311.pyc b/tidal_dl_ng/model/__pycache__/downloader.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c7af78763a52b5c096f41395abbda915c2cd22d2 GIT binary patch literal 821 zcmZXRy>AmS6u|8}-{rGV6%|yFB2lJDw3(4A2GoiSQ4`UY;TG#!TWzRwm)edHOc$vG zBLh+g_9E1OCfQD&DlsvUC`_GrzDp_!_Vatc_wK#(v;A#psf=JuzedNu!2Yn#Vt8|y zbpS39LktTPSnCMfa{>ojc7@v`0l|nn4tk9kIY-R9bP&3Nm5)m>R|^LP9>aU6?*Fur z-8wcU7V7Xwgi2Ybxxe4pkusJ?w-o^H+?l-vxIi2Qn4!QqK-}eo;Vp=A56H>Le^;32 zBZ#?KtO!IbF)t|bGV?w{4#fOC1|1d5>UR7-5^>1(_;HU%`VChDp^Yzd>kgza)qbc? zsLB&exyu&3YdmiIu2)Qv(kSe4N==#4Ud#r5$YC4{bDz>Mieeq=ZX78{ za+Q{*VD}K+Lqj?M#_dL0R-koBKz&Cud$^ldjRVD{YVcDi2~Qf^T%GE;-_TtaiVY?< zqT`$?6BHQw;G)6iC**Ro|3Uipr&ihfQudvbzvgvyIcX)WVJj_FlV;K!Hq({0FV9D$ z1~MkKbooKjPTIqET3b(cKE3#?$CW1|vJNuNG=O}#*rM*s6<|}jeQvI#eOFVI{A|FL zR@wRTrdDlPR{IF(rnr?~<4gG%TntO~K}}qYaf#(rBhbs_J3?Vrr7qd)GW)*S!< literal 0 HcmV?d00001 diff --git a/tidal_dl_ng/model/__pycache__/gui_data.cpython-311.pyc b/tidal_dl_ng/model/__pycache__/gui_data.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa9ed39e8f5fda31d5855e79cf15c1d1f62dd879 GIT binary patch literal 2355 zcmaJ>NpIUm6dtakR?Ayq!);-ws|qAWFFmDAkfbTRDRGP(tP4wf1}YtjREH!S-zdO` z9EzR_aHT?9=n>TOfEqs3zi#Y;M?{~lc zk4nhDIJtg|KDjUzLS7R}sN#`^Qd1PE}&NNSN7&Sv7Oi{@XgzBe+8gIbj zTX@sw4g9^mo{*a9FMUE-=2ODU3Xyxv$vri5pZNMQd8zG_W2J991A5ktwynNq7Bq7= zYG(VIdC)A}sF|b1JyI$ChaOZEk)=-L9C}U|!X&dBJDwYz05x}`hXH4Y14n^q{mI3T zKwc9@Y6>Ma^*LeMk=|b;%%I9$n2gEPDKc$}Oh*|t;EJXwJmo>1voL(%@KES(#2O-t zX!}lsiLB>_k)28MnGea0WaA0&)^;^**3?b5aym+_Z?5n*tX;Q zLF7bk;D@&TC%Hg(-rZk+9x@)Tvln5+ouAepvG7F{G}j}SI^J#S-S+D#x2b0XhqIWs zUXNWHqk5-#!ZTn-f(0bp1W4ZdotGAVX|$~s7-!Z>XMXv&Z`;-?j5BNXrdg`Q=dO*9 zWC@B~J{KkU8)k_r5K~p&rCQN=0Ty`?0oC{{g0we}Y;bLqhcf`fYj>=|D_!c296KnE zBhVfmi^ak8en}X>(n3I%rXQJ1y3{jdNthrrCBxN>WEo)AK$b;DbKp{5ZY82qeuZo;^KTU&6}tWKp$dFieC@6V)NqhA8jBHxh>j4&Q;#(y23%X{3Z>e-viq zHi9^+Rx)Gp;%l%hH>=FU)q2^DQQ_Us0M^zTOWDt7E)fQMMwCKq S_`j}vMgE$*r~R$m)BXnnkP!a> literal 0 HcmV?d00001 diff --git a/tidal_dl_ng/model/__pycache__/meta.cpython-311.pyc b/tidal_dl_ng/model/__pycache__/meta.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91cc19ea2e0e1fbd4a4dbc90425a6e6a4f7836e7 GIT binary patch literal 894 zcmah{F>ezw6n;Km(xyphDNtZULP8zd;|D}cl?YN4AyAhrmKD1t)VT|`gVgORb?GnY zz+P1Kk1(cN;}|MJGXm1bmm zfaDwm2%?}*I)s2E0lWnfoPwYi0l*cGqFvy0H8-FW%#|jo7Prz^xAIkL z(@SDUhg!LsE;d;&K1kY^CAZ30%=?ltx5ijM5kuv9jj=C7u4X&bSnbvs<8ho=ZZVi) z>=LH_n755+-(ZGF9-)eViuR;AvdJK_J;BwEP&@Hq!I@ADIp&Zm>PyRa2gll%KZg*B zd5w_%_5G}Gq^-1d(i&H4X)|q}G{>7yMxpQ5IU>;;^APvf@Fi~luZDtRnn!8q>Y<4-1&EiJc_NLtay z;u#Uqo*rHX1kioF_k@NrcKhtPQ1@h=h#L^I%iU{rrMYf)CkC@7$=QzP9gm&v!((wY42FID z8!L{ueXven%}RCOWxCB$31#b`*!-O@IZydn$r;#svNZL9N6-QY0}BQ{)@v&t5rj%a&wQJ zGuUyBzqs=8>e-dO7w+nhZyr3jK5O@%Oa}k}ngZJob^8|F_+HTmZUe!D0By@tc{lL- zPo1fpR3|HE@q_ZsC`u=&J=0t7uYjUWjL|E) z+~RC^6y+RkBXVib1T#0fOjX6lj&~G{+3yDicZx%22BuiqdQf9HQAUbMfv_hUM$~u= zJXXc&OGQvw*bD9ELiONimU-y0i>knpu7C#D!tfsr$<=RX{@8V!yh%+l9aRySBr0uH zQ1Os^H@#N>Kz&*&+!NHiZA01N$65YLhj4fj@2z*+50md+Ze;@=waiqQA16rWB;Kmb zul8ZE5l~}W{GiV5OAeLE8wo_4>m=T(cG+GdYw}}-jl|FF`m;)KlVfsx<9y&0R`#(Z zU|72_v{uoL`G z3u!Dw9`duC8E?xU`!1DwHx+GhXEl3LH2^ahII=m|J7w+;VdloEboBU(XzS(y?Y4z) zoDB4@FDIX71f9&zJ(Y#f=__Uic;_K71OXF?o0nfzxSqt3*!NOcWV-B}m<`ovH8wld z!EC=Ogk|BoteG~HRFt$3c36^lwfhqlTF`&GK=Onf;Ha6Eq;;uG*psn5`fmPNbFU5e zBtI$jr*G+rm)psZh4?KQR-cA5nzdOsCITXRd>MObv};t=mquOCJY`| zHe|5tm+n!zG*;(i$xn}A?pxS1cXn>2lTeMY6^je!MJ~}mjVFk!v5p1ybgiq0AdAal zV@|UgW)~blKzYqn^W2@Uy=SOgm(bO-+fG~F>7-G=r7snZhluHE!JILLABHY%yv&RRq+AU;-IGVs0xLPf5U;ltTwk(scD$_q0`22l=Q8D zvk{D(-{B~*Y*U(=OYG_2U}%Dp_UMFfNgBFRJrn@%IG_j$D($33+hpfm!8_JC>fH#toMr$D*jIv`YaZqg^7YKu@2Y>@A^{4x zZwzr$#e;k`^P9ibU$&2fqn?s>IJYwTWt!b?I+bcx{D_1(JeJVC+^-E6Rbsa%$NW($ zI(cQwmZ^B8PV(@|MMWUjr(3miEMh@&uYGiBel`_~zw!olm=(IXSmNi`DKX)m?hvT{ zM53Adn-KUfH9#y7m=Ufozo^KR0H5YFly%tViR&sa*~?ELyvT_?MQHIT0sdzHv*MsE zRQ;8LUz#YjHQ^sBdanj3n=Fu0K5$e%`jbyA#F1c1aS(KO+p>Z&Wp>gAVIP$%=&o7m z>etRKx7alJMPL81#x%laXsLZ_zPiENEu~!Y_%L_!GdczCL}g%@8yN4Fbc)t&_5R$VXX_J-pKwpJg!yqYCXO`$ zJh*797QVE&87l2u$#%7BZ-~hsMpU~(<;1Z^sShOXE)*G=p24xo4-V0x`0X{*n%71Z zp2>e?u=68%{!qM`i-6!Sx0)c+z+6rs@mx<4=8Rl8!0=aW(^e_HNDcJ_}58#qD@` zS*GQx9f(bOXOhFfEb*GM4`_$^i572dh)%%|-OUF3oQJZ^TdfntnJ`6Rht?NJSkLp- zrYTWiPNCj4(`GF~u%2zz7k>+JY|^5I&g!*b;N5fuvr^j_Ebgjx~`Ja?@R+kYvJ0->%K|sNsvY!PRAefHsEUi%#85jl)Dd$ayP-e)}0x_kg$4lMa>`cbyzGs5QT5r~WG2y*0W`0T^AuC`Q$Q+uYj z1j0UTe(8vv)RnGubcA&$nW!foP|}rVp){T~M`lIoD*%~^b^dXqOOrlJCRcgmTWzTM zM&yJlhfeE4u_7wM9_##5>AXCk*V&b&Xs9dMnA87Jl|%_{AvY9stu(UxSVhZ({ayFS zX_rQAxiRj(g4p7>u0eCY0(FR-()GQ;_gu;sUsKvAPEMHm6~2i~?X1^5b3Py@_uCGa zwtHDiOA#GX+q~1N9&5?1wx-;YIJB2J16;y^8S&Ka{TrQi)^f*>x=*d_ps+5`SOokd&0^N3fc=;}Sm>$t6 zr?d2WfCyw?b^nXUZ7TY$_}BN%msGdg;KZ@#KL_z+`4zDmC>lV3V41sC4$%YCT_2fT#zv?e7;jr_fYq;lhAQe)r|r!kyghh9tV5G z0xS7bM^j1cC@pzPRNVE+n+e$}5P9iUrlayt?z+|NAVUCa*5y&ZBwfV67iQpT%iZ}H zw$*CLJoP)fbSne74a#-88{QY( z8*|-$!9K>yhB|n*4@{7+=pYDPM-cj!s@QJX-JIIv>cs0XKUA{z@C#d7@c4<^k71C7 zzkF#P<<{84L7nXM4UT$P5^|zmFYNH;neO*tyUnsjUu>1dV^dy7?{u~gjobK%_S^Bj zTzwK-T(4|OV%5K<+`>7>xlr4G`}WsAv~dBD#R120XzvB)qYTulbjnKCA~%7E0jz>9 zq_zB2ZL11Z=cO$h^W}kG`HO;<4z3K1YW=gL#mZ}94UkZtNF1Vy?S^|U=mq9r4-2HF zvGWC&x8^ZP?9bQE`=(|GT|?N`!k$iL&IJ<+C?M7(-(a%wSqM+`-To!%4<8}1d85VQ zG-=VH$#@;+Mt?kRNn+0&*$@e12&w2ON0X9;egE+g1Ywodfq^fEZ{wNhK@O6r7WYsY z1a3`rMSxT(?8}O_X9KQ{TOS}WUybBxjIp66g+O+gF=aq9lL~SR5MF76zA7M(AtA;3 zje1g87}%)1SC)oJ7olkwKeaKG0Nj-U+6h~cXN5$mTm@cj01(i7SuQ^$t7#nWMsmfliyP^oINqW+@hgk$-b^YbPssbg2cq-)d_QE0@gM5MjcNO633-yk- zre@%kI$kbE%y?h07`Bri@w*eZKQcWN%?>IgVLje4snmr1F|9|nc_NKNhW}tgy=e5wr@`V@PLH}Mp^c2YuRy@s za39#J`MuJiLr!?3Ch5e`7EoGLvaX65VATSHif*m@?oUY2`)#Nr+S7tm0{j5PX1q%u z?6FzMX}?T?stH!4KnAXF0FFBR`&t%kFkpQ|0_eUbeeg+#lEsQ097PO4ZsTR?3sN+y zM9d4(EivI;;)lJ47|}|a=Iy)B!LmPqFi>#dB<{v?us!T~&Ssc;X!V&qs5oXnOx?5k zOvIwSgFv4AB%X@aO*jlwEmtoo11|kP3D7FS=3UZwV%DT*^;7sTLR1*jzBH`cHzYI9 zzl!km{f=p}CO^_2oevU)(sA?hO;47M02~#9aP#FWG^!lV%dMp%hm!*Lv~l?_H;L|{ zazKJ=b?ecT_T@B`D3-YoQr4f<1O=v?0Y&mJ%{fu06x=?gWTEbi$V=fY?!fM1*!(32 z^kpw@xXtfBikZ@c^UAi&=rp{M2zc2vtw`hYW5e+8WM&_+zB<7aZ(Q4?y;bt|@gP?? z>Su$R_*(G9^Dv4TjSI)`7a|h@%lO0<2=d7sR_NDB*PlDb%iNpfmGM{NNt@_%1sF%)1f8oSg-V{Pcp3F`78){cH8uIcV;c0 zz!#_i1%{T=_?l5}zY#e7GPX}LVL~POR|Yx|s(+V#I+@|0itazod#E(fS998FAq@qZ zoUHHO(f*+zn*w`ULBDl&+Q>E(WqEPn~7xFP)yrd$rL#6 zeSy))s~_4wfA}V+)?16%o)e6g|7iNwCDd||r*ZLPBY}JG*nJzldShM!)K21&8s~QQ zcTD)yjbNu`MbltkoVr}_L3;f%n3vg#e=bjt_f;p zl$^WUHf2MAO$*U{z2N{(=Ico9nKzjiuZdR1lW%s>xx9EwM0v8G_1$vqH^KxLdk}k# z=Q;O5wQzlN@Q#x4M3X1Bt{_&$F@jJgCLM^-t$*ixvju7B^2Nsen__EAlVNy&kD{48 z2-r3>Mx{G*{?z+ZmIySEgC;*+~aqo+Tz+Z=$c?+;qSwG@JxY^n=Oh$>61qe z3i}SYwJ}k8WY;-Ik_Vy*KK=U-`0HfZEeR^sNQ(7&%ItN2{Qb;RaFi)=V57^92TyqW zO4j$fY===at~^lhzQ4Wbyjj)FEf2q+uuG0V6ZEZ*DUHEkxJb&>sl@iY|6P~(*J8y# s#2K%DuDJXMLdib|{6Dmy|3t#UZt_9B$sro1qPf60+q>J{wV|i}7b49PLI3~& literal 0 HcmV?d00001 diff --git a/tidal_dl_ng/ui/dialog_login.py b/tidal_dl_ng/ui/dialog_login.py new file mode 100644 index 0000000..59a78da --- /dev/null +++ b/tidal_dl_ng/ui/dialog_login.py @@ -0,0 +1,119 @@ +################################################################################ +## Form generated from reading UI file 'dialog_login.ui' +## +## Created by: Qt User Interface Compiler version 6.8.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import QCoreApplication, QMetaObject, QRect, Qt +from PySide6.QtGui import QFont +from PySide6.QtWidgets import QDialogButtonBox, QHBoxLayout, QLabel, QSizePolicy, QTextBrowser, QVBoxLayout, QWidget + + +class Ui_DialogLogin: + def setupUi(self, DialogLogin): + if not DialogLogin.objectName(): + DialogLogin.setObjectName("DialogLogin") + DialogLogin.resize(451, 400) + sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(DialogLogin.sizePolicy().hasHeightForWidth()) + DialogLogin.setSizePolicy(sizePolicy) + self.bb_dialog = QDialogButtonBox(DialogLogin) + self.bb_dialog.setObjectName("bb_dialog") + self.bb_dialog.setGeometry(QRect(20, 350, 411, 32)) + sizePolicy.setHeightForWidth(self.bb_dialog.sizePolicy().hasHeightForWidth()) + self.bb_dialog.setSizePolicy(sizePolicy) + self.bb_dialog.setLayoutDirection(Qt.LayoutDirection.LeftToRight) + self.bb_dialog.setStyleSheet("") + self.bb_dialog.setOrientation(Qt.Orientation.Horizontal) + self.bb_dialog.setStandardButtons(QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Ok) + self.verticalLayoutWidget = QWidget(DialogLogin) + self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") + self.verticalLayoutWidget.setGeometry(QRect(20, 20, 411, 325)) + self.lv_main = QVBoxLayout(self.verticalLayoutWidget) + self.lv_main.setObjectName("lv_main") + self.lv_main.setContentsMargins(0, 0, 0, 0) + self.l_header = QLabel(self.verticalLayoutWidget) + self.l_header.setObjectName("l_header") + sizePolicy.setHeightForWidth(self.l_header.sizePolicy().hasHeightForWidth()) + self.l_header.setSizePolicy(sizePolicy) + font = QFont() + font.setPointSize(23) + font.setBold(True) + self.l_header.setFont(font) + + self.lv_main.addWidget(self.l_header) + + self.l_description = QLabel(self.verticalLayoutWidget) + self.l_description.setObjectName("l_description") + sizePolicy.setHeightForWidth(self.l_description.sizePolicy().hasHeightForWidth()) + self.l_description.setSizePolicy(sizePolicy) + font1 = QFont() + font1.setItalic(True) + self.l_description.setFont(font1) + self.l_description.setWordWrap(True) + + self.lv_main.addWidget(self.l_description) + + self.tb_url_login = QTextBrowser(self.verticalLayoutWidget) + self.tb_url_login.setObjectName("tb_url_login") + self.tb_url_login.setOpenExternalLinks(True) + + self.lv_main.addWidget(self.tb_url_login) + + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.l_expires_description = QLabel(self.verticalLayoutWidget) + self.l_expires_description.setObjectName("l_expires_description") + + self.horizontalLayout.addWidget(self.l_expires_description) + + self.l_expires_date_time = QLabel(self.verticalLayoutWidget) + self.l_expires_date_time.setObjectName("l_expires_date_time") + font2 = QFont() + font2.setBold(True) + self.l_expires_date_time.setFont(font2) + self.l_expires_date_time.setAlignment( + Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTrailing | Qt.AlignmentFlag.AlignVCenter + ) + + self.horizontalLayout.addWidget(self.l_expires_date_time) + + self.lv_main.addLayout(self.horizontalLayout) + + self.l_hint = QLabel(self.verticalLayoutWidget) + self.l_hint.setObjectName("l_hint") + sizePolicy.setHeightForWidth(self.l_hint.sizePolicy().hasHeightForWidth()) + self.l_hint.setSizePolicy(sizePolicy) + self.l_hint.setFont(font1) + self.l_hint.setWordWrap(True) + + self.lv_main.addWidget(self.l_hint) + + self.retranslateUi(DialogLogin) + self.bb_dialog.accepted.connect(DialogLogin.accept) + self.bb_dialog.rejected.connect(DialogLogin.reject) + + QMetaObject.connectSlotsByName(DialogLogin) + + # setupUi + + def retranslateUi(self, DialogLogin): + DialogLogin.setWindowTitle(QCoreApplication.translate("DialogLogin", "Dialog", None)) + self.l_header.setText(QCoreApplication.translate("DialogLogin", "TIDAL Login (as Device)", None)) + self.l_description.setText( + QCoreApplication.translate( + "DialogLogin", + "Click the link below and login with your TIDAL credentials. TIDAL will ask you, if you like to add this app as a new device. You need to confirm this.", + None, + ) + ) + self.tb_url_login.setPlaceholderText(QCoreApplication.translate("DialogLogin", "Copy this login URL...", None)) + self.l_expires_description.setText(QCoreApplication.translate("DialogLogin", "This link expires at:", None)) + self.l_expires_date_time.setText(QCoreApplication.translate("DialogLogin", "COMPUTING", None)) + self.l_hint.setText(QCoreApplication.translate("DialogLogin", "Waiting...", None)) + + # retranslateUi diff --git a/tidal_dl_ng/ui/dialog_login.ui b/tidal_dl_ng/ui/dialog_login.ui new file mode 100644 index 0000000..2ff401f --- /dev/null +++ b/tidal_dl_ng/ui/dialog_login.ui @@ -0,0 +1,195 @@ + + + DialogLogin + + + + 0 + 0 + 451 + 400 + + + + + 0 + 0 + + + + Dialog + + + + + 20 + 350 + 411 + 32 + + + + + 0 + 0 + + + + Qt::LayoutDirection::LeftToRight + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + 20 + 20 + 411 + 325 + + + + + + + + 0 + 0 + + + + + 23 + true + + + + TIDAL Login (as Device) + + + + + + + + 0 + 0 + + + + + true + + + + Click the link below and login with your TIDAL credentials. TIDAL will ask you, if you like to add this app as a new device. You need to confirm this. + + + true + + + + + + + Copy this login URL... + + + true + + + + + + + + + This link expires at: + + + + + + + + true + + + + COMPUTING + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + + + 0 + 0 + + + + + true + + + + Waiting... + + + true + + + + + + + + + + bb_dialog + accepted() + DialogLogin + accept() + + + 248 + 254 + + + 157 + 274 + + + + + bb_dialog + rejected() + DialogLogin + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/tidal_dl_ng/ui/dialog_settings.py b/tidal_dl_ng/ui/dialog_settings.py new file mode 100644 index 0000000..3098817 --- /dev/null +++ b/tidal_dl_ng/ui/dialog_settings.py @@ -0,0 +1,631 @@ +################################################################################ +## Form generated from reading UI file 'dialog_settings.ui' +## +## Created by: Qt User Interface Compiler version 6.8.1 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import QCoreApplication, QMetaObject, Qt +from PySide6.QtWidgets import ( + QCheckBox, + QComboBox, + QDialogButtonBox, + QGroupBox, + QHBoxLayout, + QLabel, + QLayout, + QLineEdit, + QPushButton, + QSizePolicy, + QSpinBox, + QVBoxLayout, +) + + +class Ui_DialogSettings: + def setupUi(self, DialogSettings): + if not DialogSettings.objectName(): + DialogSettings.setObjectName("DialogSettings") + DialogSettings.resize(640, 800) + sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + sizePolicy.setHorizontalStretch(100) + sizePolicy.setVerticalStretch(100) + sizePolicy.setHeightForWidth(DialogSettings.sizePolicy().hasHeightForWidth()) + DialogSettings.setSizePolicy(sizePolicy) + DialogSettings.setSizeGripEnabled(True) + self.lv_dialog_settings = QVBoxLayout(DialogSettings) + self.lv_dialog_settings.setObjectName("lv_dialog_settings") + self.lv_dialog_settings.setContentsMargins(0, 0, 0, 0) + self.lv_main = QVBoxLayout() + self.lv_main.setObjectName("lv_main") + self.lv_main.setContentsMargins(12, 12, 12, 12) + self.gb_flags = QGroupBox(DialogSettings) + self.gb_flags.setObjectName("gb_flags") + sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + sizePolicy1.setHorizontalStretch(100) + sizePolicy1.setVerticalStretch(100) + sizePolicy1.setHeightForWidth(self.gb_flags.sizePolicy().hasHeightForWidth()) + self.gb_flags.setSizePolicy(sizePolicy1) + self.gb_flags.setFlat(False) + self.gb_flags.setCheckable(False) + self.lv_flags = QVBoxLayout(self.gb_flags) + self.lv_flags.setObjectName("lv_flags") + self.lh_flags_1 = QHBoxLayout() + self.lh_flags_1.setObjectName("lh_flags_1") + self.lv_flag_video_download = QVBoxLayout() + self.lv_flag_video_download.setObjectName("lv_flag_video_download") + self.cb_video_download = QCheckBox(self.gb_flags) + self.cb_video_download.setObjectName("cb_video_download") + sizePolicy1.setHeightForWidth(self.cb_video_download.sizePolicy().hasHeightForWidth()) + self.cb_video_download.setSizePolicy(sizePolicy1) + + self.lv_flag_video_download.addWidget(self.cb_video_download) + + self.lh_flags_1.addLayout(self.lv_flag_video_download) + + self.lv_flag_video_convert = QVBoxLayout() + self.lv_flag_video_convert.setObjectName("lv_flag_video_convert") + self.cb_video_convert_mp4 = QCheckBox(self.gb_flags) + self.cb_video_convert_mp4.setObjectName("cb_video_convert_mp4") + sizePolicy1.setHeightForWidth(self.cb_video_convert_mp4.sizePolicy().hasHeightForWidth()) + self.cb_video_convert_mp4.setSizePolicy(sizePolicy1) + + self.lv_flag_video_convert.addWidget(self.cb_video_convert_mp4) + + self.lh_flags_1.addLayout(self.lv_flag_video_convert) + + self.lv_flags.addLayout(self.lh_flags_1) + + self.lh_flags_2 = QHBoxLayout() + self.lh_flags_2.setObjectName("lh_flags_2") + self.lv_flag_lyrics_embed = QVBoxLayout() + self.lv_flag_lyrics_embed.setObjectName("lv_flag_lyrics_embed") + self.cb_lyrics_embed = QCheckBox(self.gb_flags) + self.cb_lyrics_embed.setObjectName("cb_lyrics_embed") + sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + sizePolicy2.setHorizontalStretch(0) + sizePolicy2.setVerticalStretch(0) + sizePolicy2.setHeightForWidth(self.cb_lyrics_embed.sizePolicy().hasHeightForWidth()) + self.cb_lyrics_embed.setSizePolicy(sizePolicy2) + + self.lv_flag_lyrics_embed.addWidget(self.cb_lyrics_embed) + + self.lh_flags_2.addLayout(self.lv_flag_lyrics_embed) + + self.lv_flag_lyrics_file = QVBoxLayout() + self.lv_flag_lyrics_file.setObjectName("lv_flag_lyrics_file") + self.cb_lyrics_file = QCheckBox(self.gb_flags) + self.cb_lyrics_file.setObjectName("cb_lyrics_file") + sizePolicy1.setHeightForWidth(self.cb_lyrics_file.sizePolicy().hasHeightForWidth()) + self.cb_lyrics_file.setSizePolicy(sizePolicy1) + + self.lv_flag_lyrics_file.addWidget(self.cb_lyrics_file) + + self.lh_flags_2.addLayout(self.lv_flag_lyrics_file) + + self.lv_flags.addLayout(self.lh_flags_2) + + self.lh_flag_3 = QHBoxLayout() + self.lh_flag_3.setObjectName("lh_flag_3") + self.lv_flag_download_delay = QVBoxLayout() + self.lv_flag_download_delay.setObjectName("lv_flag_download_delay") + self.cb_download_delay = QCheckBox(self.gb_flags) + self.cb_download_delay.setObjectName("cb_download_delay") + sizePolicy1.setHeightForWidth(self.cb_download_delay.sizePolicy().hasHeightForWidth()) + self.cb_download_delay.setSizePolicy(sizePolicy1) + + self.lv_flag_download_delay.addWidget(self.cb_download_delay) + + self.lh_flag_3.addLayout(self.lv_flag_download_delay) + + self.lv_flag_extract_flac = QVBoxLayout() + self.lv_flag_extract_flac.setObjectName("lv_flag_extract_flac") + self.cb_extract_flac = QCheckBox(self.gb_flags) + self.cb_extract_flac.setObjectName("cb_extract_flac") + sizePolicy2.setHeightForWidth(self.cb_extract_flac.sizePolicy().hasHeightForWidth()) + self.cb_extract_flac.setSizePolicy(sizePolicy2) + + self.lv_flag_extract_flac.addWidget(self.cb_extract_flac) + + self.lh_flag_3.addLayout(self.lv_flag_extract_flac) + + self.lv_flags.addLayout(self.lh_flag_3) + + self.lh_flags_4 = QHBoxLayout() + self.lh_flags_4.setObjectName("lh_flags_4") + self.lv_flag_metadata_cover_embed = QVBoxLayout() + self.lv_flag_metadata_cover_embed.setObjectName("lv_flag_metadata_cover_embed") + self.cb_metadata_cover_embed = QCheckBox(self.gb_flags) + self.cb_metadata_cover_embed.setObjectName("cb_metadata_cover_embed") + + self.lv_flag_metadata_cover_embed.addWidget(self.cb_metadata_cover_embed) + + self.lh_flags_4.addLayout(self.lv_flag_metadata_cover_embed) + + self.lv_flag_cover_album_file = QVBoxLayout() + self.lv_flag_cover_album_file.setObjectName("lv_flag_cover_album_file") + self.cb_cover_album_file = QCheckBox(self.gb_flags) + self.cb_cover_album_file.setObjectName("cb_cover_album_file") + + self.lv_flag_cover_album_file.addWidget(self.cb_cover_album_file) + + self.lh_flags_4.addLayout(self.lv_flag_cover_album_file) + + self.lv_flags.addLayout(self.lh_flags_4) + + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.lv_flag_skip_existing = QVBoxLayout() + self.lv_flag_skip_existing.setObjectName("lv_flag_skip_existing") + self.cb_skip_existing = QCheckBox(self.gb_flags) + self.cb_skip_existing.setObjectName("cb_skip_existing") + + self.lv_flag_skip_existing.addWidget(self.cb_skip_existing) + + self.horizontalLayout.addLayout(self.lv_flag_skip_existing) + + self.lv_symlink_to_track = QVBoxLayout() + self.lv_symlink_to_track.setObjectName("lv_symlink_to_track") + self.cb_symlink_to_track = QCheckBox(self.gb_flags) + self.cb_symlink_to_track.setObjectName("cb_symlink_to_track") + + self.lv_symlink_to_track.addWidget(self.cb_symlink_to_track) + + self.horizontalLayout.addLayout(self.lv_symlink_to_track) + + self.lv_flags.addLayout(self.horizontalLayout) + + self.horizontalLayout_12 = QHBoxLayout() + self.horizontalLayout_12.setObjectName("horizontalLayout_12") + self.lv_playlist_create = QVBoxLayout() + self.lv_playlist_create.setObjectName("lv_playlist_create") + self.cb_playlist_create = QCheckBox(self.gb_flags) + self.cb_playlist_create.setObjectName("cb_playlist_create") + + self.lv_playlist_create.addWidget(self.cb_playlist_create) + + self.horizontalLayout_12.addLayout(self.lv_playlist_create) + + self.verticalLayout_4 = QVBoxLayout() + self.verticalLayout_4.setObjectName("verticalLayout_4") + + self.horizontalLayout_12.addLayout(self.verticalLayout_4) + + self.lv_flags.addLayout(self.horizontalLayout_12) + + self.lv_main.addWidget(self.gb_flags) + + self.gb_choices = QGroupBox(DialogSettings) + self.gb_choices.setObjectName("gb_choices") + sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + sizePolicy3.setHorizontalStretch(0) + sizePolicy3.setVerticalStretch(0) + sizePolicy3.setHeightForWidth(self.gb_choices.sizePolicy().hasHeightForWidth()) + self.gb_choices.setSizePolicy(sizePolicy3) + self.lv_choices = QVBoxLayout(self.gb_choices) + self.lv_choices.setObjectName("lv_choices") + self.lh_choices_quality_audio = QHBoxLayout() + self.lh_choices_quality_audio.setObjectName("lh_choices_quality_audio") + self.l_icon_quality_audio = QLabel(self.gb_choices) + self.l_icon_quality_audio.setObjectName("l_icon_quality_audio") + sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + sizePolicy4.setHorizontalStretch(0) + sizePolicy4.setVerticalStretch(0) + sizePolicy4.setHeightForWidth(self.l_icon_quality_audio.sizePolicy().hasHeightForWidth()) + self.l_icon_quality_audio.setSizePolicy(sizePolicy4) + + self.lh_choices_quality_audio.addWidget(self.l_icon_quality_audio) + + self.l_quality_audio = QLabel(self.gb_choices) + self.l_quality_audio.setObjectName("l_quality_audio") + sizePolicy4.setHeightForWidth(self.l_quality_audio.sizePolicy().hasHeightForWidth()) + self.l_quality_audio.setSizePolicy(sizePolicy4) + + self.lh_choices_quality_audio.addWidget(self.l_quality_audio) + + self.c_quality_audio = QComboBox(self.gb_choices) + self.c_quality_audio.setObjectName("c_quality_audio") + + self.lh_choices_quality_audio.addWidget(self.c_quality_audio) + + self.lh_choices_quality_audio.setStretch(2, 50) + + self.lv_choices.addLayout(self.lh_choices_quality_audio) + + self.lh_choices_quality_video = QHBoxLayout() + self.lh_choices_quality_video.setObjectName("lh_choices_quality_video") + self.l_icon_quality_video = QLabel(self.gb_choices) + self.l_icon_quality_video.setObjectName("l_icon_quality_video") + sizePolicy4.setHeightForWidth(self.l_icon_quality_video.sizePolicy().hasHeightForWidth()) + self.l_icon_quality_video.setSizePolicy(sizePolicy4) + + self.lh_choices_quality_video.addWidget(self.l_icon_quality_video) + + self.l_quality_video = QLabel(self.gb_choices) + self.l_quality_video.setObjectName("l_quality_video") + sizePolicy4.setHeightForWidth(self.l_quality_video.sizePolicy().hasHeightForWidth()) + self.l_quality_video.setSizePolicy(sizePolicy4) + + self.lh_choices_quality_video.addWidget(self.l_quality_video) + + self.c_quality_video = QComboBox(self.gb_choices) + self.c_quality_video.setObjectName("c_quality_video") + + self.lh_choices_quality_video.addWidget(self.c_quality_video) + + self.lh_choices_quality_video.setStretch(2, 50) + + self.lv_choices.addLayout(self.lh_choices_quality_video) + + self.lh_choices_cover_dimension = QHBoxLayout() + self.lh_choices_cover_dimension.setObjectName("lh_choices_cover_dimension") + self.lh_choices_cover_dimension.setSizeConstraint(QLayout.SizeConstraint.SetDefaultConstraint) + self.l_icon_metadata_cover_dimension = QLabel(self.gb_choices) + self.l_icon_metadata_cover_dimension.setObjectName("l_icon_metadata_cover_dimension") + sizePolicy4.setHeightForWidth(self.l_icon_metadata_cover_dimension.sizePolicy().hasHeightForWidth()) + self.l_icon_metadata_cover_dimension.setSizePolicy(sizePolicy4) + + self.lh_choices_cover_dimension.addWidget(self.l_icon_metadata_cover_dimension) + + self.l_metadata_cover_dimension = QLabel(self.gb_choices) + self.l_metadata_cover_dimension.setObjectName("l_metadata_cover_dimension") + sizePolicy4.setHeightForWidth(self.l_metadata_cover_dimension.sizePolicy().hasHeightForWidth()) + self.l_metadata_cover_dimension.setSizePolicy(sizePolicy4) + + self.lh_choices_cover_dimension.addWidget(self.l_metadata_cover_dimension) + + self.c_metadata_cover_dimension = QComboBox(self.gb_choices) + self.c_metadata_cover_dimension.setObjectName("c_metadata_cover_dimension") + sizePolicy5 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + sizePolicy5.setHorizontalStretch(10) + sizePolicy5.setVerticalStretch(0) + sizePolicy5.setHeightForWidth(self.c_metadata_cover_dimension.sizePolicy().hasHeightForWidth()) + self.c_metadata_cover_dimension.setSizePolicy(sizePolicy5) + + self.lh_choices_cover_dimension.addWidget(self.c_metadata_cover_dimension) + + self.lh_choices_cover_dimension.setStretch(2, 50) + + self.lv_choices.addLayout(self.lh_choices_cover_dimension) + + self.lv_main.addWidget(self.gb_choices) + + self.gb_numbers = QGroupBox(DialogSettings) + self.gb_numbers.setObjectName("gb_numbers") + self.verticalLayout_8 = QVBoxLayout(self.gb_numbers) + self.verticalLayout_8.setObjectName("verticalLayout_8") + self.horizontalLayout_9 = QHBoxLayout() + self.horizontalLayout_9.setObjectName("horizontalLayout_9") + self.l_album_track_num_pad_min = QLabel(self.gb_numbers) + self.l_album_track_num_pad_min.setObjectName("l_album_track_num_pad_min") + + self.horizontalLayout_9.addWidget(self.l_album_track_num_pad_min) + + self.l_icon_album_track_num_pad_min = QLabel(self.gb_numbers) + self.l_icon_album_track_num_pad_min.setObjectName("l_icon_album_track_num_pad_min") + + self.horizontalLayout_9.addWidget(self.l_icon_album_track_num_pad_min) + + self.sb_album_track_num_pad_min = QSpinBox(self.gb_numbers) + self.sb_album_track_num_pad_min.setObjectName("sb_album_track_num_pad_min") + self.sb_album_track_num_pad_min.setMaximum(4) + + self.horizontalLayout_9.addWidget(self.sb_album_track_num_pad_min) + + self.verticalLayout_8.addLayout(self.horizontalLayout_9) + + self.horizontalLayout_11 = QHBoxLayout() + self.horizontalLayout_11.setObjectName("horizontalLayout_11") + self.l_downloads_concurrent_max = QLabel(self.gb_numbers) + self.l_downloads_concurrent_max.setObjectName("l_downloads_concurrent_max") + + self.horizontalLayout_11.addWidget(self.l_downloads_concurrent_max) + + self.l_icon_downloads_concurrent_max = QLabel(self.gb_numbers) + self.l_icon_downloads_concurrent_max.setObjectName("l_icon_downloads_concurrent_max") + + self.horizontalLayout_11.addWidget(self.l_icon_downloads_concurrent_max) + + self.sb_downloads_concurrent_max = QSpinBox(self.gb_numbers) + self.sb_downloads_concurrent_max.setObjectName("sb_downloads_concurrent_max") + self.sb_downloads_concurrent_max.setMinimum(1) + self.sb_downloads_concurrent_max.setMaximum(5) + + self.horizontalLayout_11.addWidget(self.sb_downloads_concurrent_max) + + self.verticalLayout_8.addLayout(self.horizontalLayout_11) + + self.lv_main.addWidget(self.gb_numbers) + + self.gb_path = QGroupBox(DialogSettings) + self.gb_path.setObjectName("gb_path") + self.horizontalLayout_2 = QHBoxLayout(self.gb_path) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.verticalLayout_2 = QVBoxLayout() + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.lh_path_base = QHBoxLayout() + self.lh_path_base.setObjectName("lh_path_base") + self.l_icon_download_base_path = QLabel(self.gb_path) + self.l_icon_download_base_path.setObjectName("l_icon_download_base_path") + sizePolicy4.setHeightForWidth(self.l_icon_download_base_path.sizePolicy().hasHeightForWidth()) + self.l_icon_download_base_path.setSizePolicy(sizePolicy4) + + self.lh_path_base.addWidget(self.l_icon_download_base_path) + + self.l_download_base_path = QLabel(self.gb_path) + self.l_download_base_path.setObjectName("l_download_base_path") + sizePolicy3.setHeightForWidth(self.l_download_base_path.sizePolicy().hasHeightForWidth()) + self.l_download_base_path.setSizePolicy(sizePolicy3) + + self.lh_path_base.addWidget(self.l_download_base_path) + + self.verticalLayout_2.addLayout(self.lh_path_base) + + self.lh_path_fmt_track = QHBoxLayout() + self.lh_path_fmt_track.setObjectName("lh_path_fmt_track") + self.l_icon_format_track = QLabel(self.gb_path) + self.l_icon_format_track.setObjectName("l_icon_format_track") + sizePolicy4.setHeightForWidth(self.l_icon_format_track.sizePolicy().hasHeightForWidth()) + self.l_icon_format_track.setSizePolicy(sizePolicy4) + + self.lh_path_fmt_track.addWidget(self.l_icon_format_track) + + self.l_format_track = QLabel(self.gb_path) + self.l_format_track.setObjectName("l_format_track") + sizePolicy3.setHeightForWidth(self.l_format_track.sizePolicy().hasHeightForWidth()) + self.l_format_track.setSizePolicy(sizePolicy3) + + self.lh_path_fmt_track.addWidget(self.l_format_track) + + self.verticalLayout_2.addLayout(self.lh_path_fmt_track) + + self.lh_path_fmt_video = QHBoxLayout() + self.lh_path_fmt_video.setObjectName("lh_path_fmt_video") + self.l_icon_format_video = QLabel(self.gb_path) + self.l_icon_format_video.setObjectName("l_icon_format_video") + sizePolicy4.setHeightForWidth(self.l_icon_format_video.sizePolicy().hasHeightForWidth()) + self.l_icon_format_video.setSizePolicy(sizePolicy4) + + self.lh_path_fmt_video.addWidget(self.l_icon_format_video) + + self.l_format_video = QLabel(self.gb_path) + self.l_format_video.setObjectName("l_format_video") + sizePolicy3.setHeightForWidth(self.l_format_video.sizePolicy().hasHeightForWidth()) + self.l_format_video.setSizePolicy(sizePolicy3) + + self.lh_path_fmt_video.addWidget(self.l_format_video) + + self.verticalLayout_2.addLayout(self.lh_path_fmt_video) + + self.lh_path_fmt_album = QHBoxLayout() + self.lh_path_fmt_album.setObjectName("lh_path_fmt_album") + self.l_icon_format_album = QLabel(self.gb_path) + self.l_icon_format_album.setObjectName("l_icon_format_album") + sizePolicy6 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) + sizePolicy6.setHorizontalStretch(0) + sizePolicy6.setVerticalStretch(0) + sizePolicy6.setHeightForWidth(self.l_icon_format_album.sizePolicy().hasHeightForWidth()) + self.l_icon_format_album.setSizePolicy(sizePolicy6) + + self.lh_path_fmt_album.addWidget(self.l_icon_format_album) + + self.l_format_album = QLabel(self.gb_path) + self.l_format_album.setObjectName("l_format_album") + sizePolicy3.setHeightForWidth(self.l_format_album.sizePolicy().hasHeightForWidth()) + self.l_format_album.setSizePolicy(sizePolicy3) + + self.lh_path_fmt_album.addWidget(self.l_format_album) + + self.verticalLayout_2.addLayout(self.lh_path_fmt_album) + + self.lh_fpath_mt_playlist = QHBoxLayout() + self.lh_fpath_mt_playlist.setObjectName("lh_fpath_mt_playlist") + self.l_icon_format_playlist = QLabel(self.gb_path) + self.l_icon_format_playlist.setObjectName("l_icon_format_playlist") + sizePolicy4.setHeightForWidth(self.l_icon_format_playlist.sizePolicy().hasHeightForWidth()) + self.l_icon_format_playlist.setSizePolicy(sizePolicy4) + + self.lh_fpath_mt_playlist.addWidget(self.l_icon_format_playlist) + + self.l_format_playlist = QLabel(self.gb_path) + self.l_format_playlist.setObjectName("l_format_playlist") + sizePolicy3.setHeightForWidth(self.l_format_playlist.sizePolicy().hasHeightForWidth()) + self.l_format_playlist.setSizePolicy(sizePolicy3) + + self.lh_fpath_mt_playlist.addWidget(self.l_format_playlist) + + self.verticalLayout_2.addLayout(self.lh_fpath_mt_playlist) + + self.lh_path_fmt_mix = QHBoxLayout() + self.lh_path_fmt_mix.setObjectName("lh_path_fmt_mix") + self.l_icon_format_mix = QLabel(self.gb_path) + self.l_icon_format_mix.setObjectName("l_icon_format_mix") + sizePolicy6.setHeightForWidth(self.l_icon_format_mix.sizePolicy().hasHeightForWidth()) + self.l_icon_format_mix.setSizePolicy(sizePolicy6) + + self.lh_path_fmt_mix.addWidget(self.l_icon_format_mix) + + self.l_format_mix = QLabel(self.gb_path) + self.l_format_mix.setObjectName("l_format_mix") + sizePolicy3.setHeightForWidth(self.l_format_mix.sizePolicy().hasHeightForWidth()) + self.l_format_mix.setSizePolicy(sizePolicy3) + + self.lh_path_fmt_mix.addWidget(self.l_format_mix) + + self.verticalLayout_2.addLayout(self.lh_path_fmt_mix) + + self.lh_path_binary_ffmpeg = QHBoxLayout() + self.lh_path_binary_ffmpeg.setObjectName("lh_path_binary_ffmpeg") + self.l_icon_path_binary_ffmpeg = QLabel(self.gb_path) + self.l_icon_path_binary_ffmpeg.setObjectName("l_icon_path_binary_ffmpeg") + sizePolicy4.setHeightForWidth(self.l_icon_path_binary_ffmpeg.sizePolicy().hasHeightForWidth()) + self.l_icon_path_binary_ffmpeg.setSizePolicy(sizePolicy4) + + self.lh_path_binary_ffmpeg.addWidget(self.l_icon_path_binary_ffmpeg) + + self.l_path_binary_ffmpeg = QLabel(self.gb_path) + self.l_path_binary_ffmpeg.setObjectName("l_path_binary_ffmpeg") + sizePolicy3.setHeightForWidth(self.l_path_binary_ffmpeg.sizePolicy().hasHeightForWidth()) + self.l_path_binary_ffmpeg.setSizePolicy(sizePolicy3) + + self.lh_path_binary_ffmpeg.addWidget(self.l_path_binary_ffmpeg) + + self.verticalLayout_2.addLayout(self.lh_path_binary_ffmpeg) + + self.horizontalLayout_2.addLayout(self.verticalLayout_2) + + self.verticalLayout = QVBoxLayout() + self.verticalLayout.setObjectName("verticalLayout") + self.horizontalLayout_10 = QHBoxLayout() + self.horizontalLayout_10.setObjectName("horizontalLayout_10") + self.le_download_base_path = QLineEdit(self.gb_path) + self.le_download_base_path.setObjectName("le_download_base_path") + sizePolicy2.setHeightForWidth(self.le_download_base_path.sizePolicy().hasHeightForWidth()) + self.le_download_base_path.setSizePolicy(sizePolicy2) + self.le_download_base_path.setDragEnabled(True) + + self.horizontalLayout_10.addWidget(self.le_download_base_path) + + self.pb_download_base_path = QPushButton(self.gb_path) + self.pb_download_base_path.setObjectName("pb_download_base_path") + + self.horizontalLayout_10.addWidget(self.pb_download_base_path) + + self.verticalLayout.addLayout(self.horizontalLayout_10) + + self.horizontalLayout_7 = QHBoxLayout() + self.horizontalLayout_7.setObjectName("horizontalLayout_7") + self.le_format_track = QLineEdit(self.gb_path) + self.le_format_track.setObjectName("le_format_track") + + self.horizontalLayout_7.addWidget(self.le_format_track) + + self.verticalLayout.addLayout(self.horizontalLayout_7) + + self.horizontalLayout_5 = QHBoxLayout() + self.horizontalLayout_5.setObjectName("horizontalLayout_5") + self.le_format_video = QLineEdit(self.gb_path) + self.le_format_video.setObjectName("le_format_video") + + self.horizontalLayout_5.addWidget(self.le_format_video) + + self.verticalLayout.addLayout(self.horizontalLayout_5) + + self.horizontalLayout_6 = QHBoxLayout() + self.horizontalLayout_6.setObjectName("horizontalLayout_6") + self.le_format_album = QLineEdit(self.gb_path) + self.le_format_album.setObjectName("le_format_album") + + self.horizontalLayout_6.addWidget(self.le_format_album) + + self.verticalLayout.addLayout(self.horizontalLayout_6) + + self.horizontalLayout_4 = QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.le_format_playlist = QLineEdit(self.gb_path) + self.le_format_playlist.setObjectName("le_format_playlist") + sizePolicy2.setHeightForWidth(self.le_format_playlist.sizePolicy().hasHeightForWidth()) + self.le_format_playlist.setSizePolicy(sizePolicy2) + + self.horizontalLayout_4.addWidget(self.le_format_playlist) + + self.verticalLayout.addLayout(self.horizontalLayout_4) + + self.horizontalLayout_8 = QHBoxLayout() + self.horizontalLayout_8.setObjectName("horizontalLayout_8") + self.le_format_mix = QLineEdit(self.gb_path) + self.le_format_mix.setObjectName("le_format_mix") + + self.horizontalLayout_8.addWidget(self.le_format_mix) + + self.verticalLayout.addLayout(self.horizontalLayout_8) + + self.horizontalLayout_3 = QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.le_path_binary_ffmpeg = QLineEdit(self.gb_path) + self.le_path_binary_ffmpeg.setObjectName("le_path_binary_ffmpeg") + sizePolicy2.setHeightForWidth(self.le_path_binary_ffmpeg.sizePolicy().hasHeightForWidth()) + self.le_path_binary_ffmpeg.setSizePolicy(sizePolicy2) + self.le_path_binary_ffmpeg.setDragEnabled(True) + + self.horizontalLayout_3.addWidget(self.le_path_binary_ffmpeg) + + self.pb_path_binary_ffmpeg = QPushButton(self.gb_path) + self.pb_path_binary_ffmpeg.setObjectName("pb_path_binary_ffmpeg") + + self.horizontalLayout_3.addWidget(self.pb_path_binary_ffmpeg) + + self.verticalLayout.addLayout(self.horizontalLayout_3) + + self.horizontalLayout_2.addLayout(self.verticalLayout) + + self.horizontalLayout_2.setStretch(1, 50) + + self.lv_main.addWidget(self.gb_path) + + self.bb_dialog = QDialogButtonBox(DialogSettings) + self.bb_dialog.setObjectName("bb_dialog") + self.bb_dialog.setOrientation(Qt.Orientation.Horizontal) + self.bb_dialog.setStandardButtons(QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Ok) + + self.lv_main.addWidget(self.bb_dialog) + + self.lv_dialog_settings.addLayout(self.lv_main) + + self.retranslateUi(DialogSettings) + self.bb_dialog.accepted.connect(DialogSettings.accept) + self.bb_dialog.rejected.connect(DialogSettings.reject) + + QMetaObject.connectSlotsByName(DialogSettings) + + # setupUi + + def retranslateUi(self, DialogSettings): + DialogSettings.setWindowTitle(QCoreApplication.translate("DialogSettings", "Preferences", None)) + self.gb_flags.setTitle(QCoreApplication.translate("DialogSettings", "Flags", None)) + self.cb_video_download.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) + self.cb_video_convert_mp4.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) + # if QT_CONFIG(whatsthis) + self.cb_lyrics_embed.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + self.cb_lyrics_embed.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) + self.cb_lyrics_file.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) + self.cb_download_delay.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) + self.cb_extract_flac.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) + self.cb_metadata_cover_embed.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) + self.cb_cover_album_file.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) + self.cb_skip_existing.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) + self.cb_symlink_to_track.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) + self.cb_playlist_create.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None)) + self.gb_choices.setTitle(QCoreApplication.translate("DialogSettings", "Choices", None)) + self.l_icon_quality_audio.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_quality_audio.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_icon_quality_video.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_quality_video.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_icon_metadata_cover_dimension.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_metadata_cover_dimension.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.gb_numbers.setTitle(QCoreApplication.translate("DialogSettings", "Numbers", None)) + self.l_album_track_num_pad_min.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_icon_album_track_num_pad_min.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_downloads_concurrent_max.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_icon_downloads_concurrent_max.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.gb_path.setTitle(QCoreApplication.translate("DialogSettings", "Path", None)) + self.l_icon_download_base_path.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_download_base_path.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_icon_format_track.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_format_track.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_icon_format_video.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_format_video.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_icon_format_album.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_format_album.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_icon_format_playlist.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_format_playlist.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_icon_format_mix.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_format_mix.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_icon_path_binary_ffmpeg.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.l_path_binary_ffmpeg.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None)) + self.pb_download_base_path.setText(QCoreApplication.translate("DialogSettings", "...", None)) + self.pb_path_binary_ffmpeg.setText(QCoreApplication.translate("DialogSettings", "...", None)) + + # retranslateUi diff --git a/tidal_dl_ng/ui/dialog_settings.ui b/tidal_dl_ng/ui/dialog_settings.ui new file mode 100644 index 0000000..5287e38 --- /dev/null +++ b/tidal_dl_ng/ui/dialog_settings.ui @@ -0,0 +1,812 @@ + + + DialogSettings + + + + 0 + 0 + 640 + 800 + + + + + 100 + 100 + + + + Preferences + + + true + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 12 + + + 12 + + + 12 + + + 12 + + + + + + 100 + 100 + + + + Flags + + + false + + + false + + + + + + + + + + + 100 + 100 + + + + CheckBox + + + + + + + + + + + + 100 + 100 + + + + CheckBox + + + + + + + + + + + + + + + + 0 + 0 + + + + + + + CheckBox + + + + + + + + + + + + 100 + 100 + + + + CheckBox + + + + + + + + + + + + + + + + 100 + 100 + + + + CheckBox + + + + + + + + + + + + 0 + 0 + + + + CheckBox + + + + + + + + + + + + + + + CheckBox + + + + + + + + + + + CheckBox + + + + + + + + + + + + + + + CheckBox + + + + + + + + + + + CheckBox + + + + + + + + + + + + + + + CheckBox + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Choices + + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + + + + + QLayout::SizeConstraint::SetDefaultConstraint + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + 10 + 0 + + + + + + + + + + + + + Numbers + + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + 4 + + + + + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + 1 + + + 5 + + + + + + + + + + + + Path + + + + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + + + + + + + + + 0 + 0 + + + + true + + + + + + + ... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + true + + + + + + + ... + + + + + + + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + + + + bb_dialog + accepted() + DialogSettings + accept() + + + 319 + 661 + + + 319 + 340 + + + + + bb_dialog + rejected() + DialogSettings + reject() + + + 319 + 661 + + + 319 + 340 + + + + + diff --git a/tidal_dl_ng/ui/dialog_version.py b/tidal_dl_ng/ui/dialog_version.py new file mode 100644 index 0000000..b8cf294 --- /dev/null +++ b/tidal_dl_ng/ui/dialog_version.py @@ -0,0 +1,161 @@ +################################################################################ +## Form generated from reading UI file 'dialog_version.ui' +## +## Created by: Qt User Interface Compiler version 6.6.1 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import QCoreApplication, QMetaObject, QSize, Qt +from PySide6.QtGui import QFont +from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QSizePolicy, QSpacerItem, QVBoxLayout + + +class Ui_DialogVersion: + def setupUi(self, DialogVersion): + if not DialogVersion.objectName(): + DialogVersion.setObjectName("DialogVersion") + DialogVersion.resize(436, 235) + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(DialogVersion.sizePolicy().hasHeightForWidth()) + DialogVersion.setSizePolicy(sizePolicy) + DialogVersion.setMaximumSize(QSize(436, 235)) + self.verticalLayout = QVBoxLayout(DialogVersion) + self.verticalLayout.setObjectName("verticalLayout") + self.l_name_app = QLabel(DialogVersion) + self.l_name_app.setObjectName("l_name_app") + sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.l_name_app.sizePolicy().hasHeightForWidth()) + self.l_name_app.setSizePolicy(sizePolicy1) + font = QFont() + font.setBold(True) + self.l_name_app.setFont(font) + self.l_name_app.setAlignment(Qt.AlignCenter) + self.l_name_app.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.TextSelectableByMouse) + + self.verticalLayout.addWidget(self.l_name_app) + + self.lv_version = QVBoxLayout() + self.lv_version.setObjectName("lv_version") + self.lh_version = QHBoxLayout() + self.lh_version.setObjectName("lh_version") + self.l_h_version = QLabel(DialogVersion) + self.l_h_version.setObjectName("l_h_version") + self.l_h_version.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.TextSelectableByMouse) + + self.lh_version.addWidget(self.l_h_version) + + self.l_version = QLabel(DialogVersion) + self.l_version.setObjectName("l_version") + self.l_version.setFont(font) + self.l_version.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.TextSelectableByMouse) + + self.lh_version.addWidget(self.l_version) + + self.lv_version.addLayout(self.lh_version) + + self.verticalLayout.addLayout(self.lv_version) + + self.lv_update = QVBoxLayout() + self.lv_update.setObjectName("lv_update") + self.l_error = QLabel(DialogVersion) + self.l_error.setObjectName("l_error") + self.l_error.setFont(font) + self.l_error.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.TextSelectableByMouse) + + self.lv_update.addWidget(self.l_error) + + self.l_error_details = QLabel(DialogVersion) + self.l_error_details.setObjectName("l_error_details") + self.l_error_details.setFont(font) + self.l_error_details.setAlignment(Qt.AlignCenter) + self.l_error_details.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.TextSelectableByMouse) + + self.lv_update.addWidget(self.l_error_details) + + self.lh_update_version = QHBoxLayout() + self.lh_update_version.setObjectName("lh_update_version") + self.l_h_version_new = QLabel(DialogVersion) + self.l_h_version_new.setObjectName("l_h_version_new") + self.l_h_version_new.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.TextSelectableByMouse) + + self.lh_update_version.addWidget(self.l_h_version_new) + + self.l_version_new = QLabel(DialogVersion) + self.l_version_new.setObjectName("l_version_new") + self.l_version_new.setFont(font) + self.l_version_new.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.TextSelectableByMouse) + + self.lh_update_version.addWidget(self.l_version_new) + + self.lv_update.addLayout(self.lh_update_version) + + self.l_changelog = QLabel(DialogVersion) + self.l_changelog.setObjectName("l_changelog") + self.l_changelog.setFont(font) + self.l_changelog.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.TextSelectableByMouse) + + self.lv_update.addWidget(self.l_changelog) + + self.l_changelog_details = QLabel(DialogVersion) + self.l_changelog_details.setObjectName("l_changelog_details") + self.l_changelog_details.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.TextSelectableByMouse) + + self.lv_update.addWidget(self.l_changelog_details) + + self.lv_download = QHBoxLayout() + self.lv_download.setObjectName("lv_download") + self.lv_download.setContentsMargins(-1, 20, -1, -1) + self.pb_download = QPushButton(DialogVersion) + self.pb_download.setObjectName("pb_download") + self.pb_download.setFlat(False) + + self.lv_download.addWidget(self.pb_download) + + self.sh_download = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) + + self.lv_download.addItem(self.sh_download) + + self.lv_update.addLayout(self.lv_download) + + self.verticalLayout.addLayout(self.lv_update) + + self.l_url_github = QLabel(DialogVersion) + self.l_url_github.setObjectName("l_url_github") + self.l_url_github.setAlignment(Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter) + self.l_url_github.setOpenExternalLinks(True) + self.l_url_github.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.TextSelectableByMouse) + + self.verticalLayout.addWidget(self.l_url_github) + + self.retranslateUi(DialogVersion) + + QMetaObject.connectSlotsByName(DialogVersion) + + # setupUi + + def retranslateUi(self, DialogVersion): + DialogVersion.setWindowTitle(QCoreApplication.translate("DialogVersion", "Version", None)) + self.l_name_app.setText(QCoreApplication.translate("DialogVersion", "TIDAL Downloader Next Generation!", None)) + self.l_h_version.setText(QCoreApplication.translate("DialogVersion", "Installed Version:", None)) + self.l_version.setText(QCoreApplication.translate("DialogVersion", "v1.2.3", None)) + self.l_error.setText(QCoreApplication.translate("DialogVersion", "ERROR", None)) + self.l_error_details.setText(QCoreApplication.translate("DialogVersion", "", None)) + self.l_h_version_new.setText(QCoreApplication.translate("DialogVersion", "New Version Available:", None)) + self.l_version_new.setText(QCoreApplication.translate("DialogVersion", "v0.0.0", None)) + self.l_changelog.setText(QCoreApplication.translate("DialogVersion", "Changelog", None)) + self.l_changelog_details.setText(QCoreApplication.translate("DialogVersion", "", None)) + self.pb_download.setText(QCoreApplication.translate("DialogVersion", "Download", None)) + self.l_url_github.setText( + QCoreApplication.translate( + "DialogVersion", + 'https://github.com/exislow/tidal-dl-ng/', + None, + ) + ) + + # retranslateUi diff --git a/tidal_dl_ng/ui/dialog_version.ui b/tidal_dl_ng/ui/dialog_version.ui new file mode 100644 index 0000000..0cea9bd --- /dev/null +++ b/tidal_dl_ng/ui/dialog_version.ui @@ -0,0 +1,227 @@ + + + DialogVersion + + + + 0 + 0 + 436 + 235 + + + + + 0 + 0 + + + + + 436 + 235 + + + + Version + + + + + + + 0 + 0 + + + + + true + + + + TIDAL Downloader Next Generation! + + + Qt::AlignCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + Installed Version: + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + true + + + + v1.2.3 + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + + + + + true + + + + ERROR + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + true + + + + <ERROR> + + + Qt::AlignCenter + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + New Version Available: + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + true + + + + v0.0.0 + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + true + + + + Changelog + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + <CHANGELOG> + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + 20 + + + + + Download + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + <a href="https://github.com/exislow/tidal-dl-ng/">https://github.com/exislow/tidal-dl-ng/</a> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + diff --git a/tidal_dl_ng/ui/dummy_register.py b/tidal_dl_ng/ui/dummy_register.py new file mode 100644 index 0000000..dfffdc2 --- /dev/null +++ b/tidal_dl_ng/ui/dummy_register.py @@ -0,0 +1,33 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection + +from .dummy_wiggly import WigglyWidget + +# Set PYSIDE_DESIGNER_PLUGINS to point to this directory and load the plugin + + +TOOLTIP = "A cool wiggly widget (Python)" +DOM_XML = """ + + + + + 0 + 0 + 400 + 200 + + + + Hello, world + + + +""" + +if __name__ == "__main__": + QPyDesignerCustomWidgetCollection.registerCustomWidget( + WigglyWidget, module="wigglywidget", tool_tip=TOOLTIP, xml=DOM_XML + ) diff --git a/tidal_dl_ng/ui/dummy_wiggly.py b/tidal_dl_ng/ui/dummy_wiggly.py new file mode 100644 index 0000000..e6eb3d3 --- /dev/null +++ b/tidal_dl_ng/ui/dummy_wiggly.py @@ -0,0 +1,68 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Property, QBasicTimer +from PySide6.QtGui import QColor, QFontMetrics, QPainter, QPalette +from PySide6.QtWidgets import QWidget + + +class WigglyWidget(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._step = 0 + self._text = "" + self.setBackgroundRole(QPalette.Midlight) + self.setAutoFillBackground(True) + + new_font = self.font() + new_font.setPointSize(new_font.pointSize() + 20) + self.setFont(new_font) + self._timer = QBasicTimer() + + def isRunning(self): + return self._timer.isActive() + + def setRunning(self, r): + if r == self.isRunning(): + return + if r: + self._timer.start(60, self) + else: + self._timer.stop() + + def paintEvent(self, event): + if not self._text: + return + + sineTable = [0, 38, 71, 92, 100, 92, 71, 38, 0, -38, -71, -92, -100, -92, -71, -38] + + metrics = QFontMetrics(self.font()) + x = (self.width() - metrics.horizontalAdvance(self.text)) / 2 + y = (self.height() + metrics.ascent() - metrics.descent()) / 2 + color = QColor() + + with QPainter(self) as painter: + for i in range(len(self.text)): + index = (self._step + i) % 16 + color.setHsv((15 - index) * 16, 255, 191) + painter.setPen(color) + dy = (sineTable[index] * metrics.height()) / 400 + c = self._text[i] + painter.drawText(x, y - dy, str(c)) + x += metrics.horizontalAdvance(c) + + def timerEvent(self, event): + if event.timerId() == self._timer.timerId(): + self._step += 1 + self.update() + else: + QWidget.timerEvent(event) + + def text(self): + return self._text + + def setText(self, text): + self._text = text + + running = Property(bool, isRunning, setRunning) + text = Property(str, text, setText) diff --git a/tidal_dl_ng/ui/icon.icns b/tidal_dl_ng/ui/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..e41a4f3a34f5fa482bc486d2b611d2427d41e570 GIT binary patch literal 91254 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 literal 0 HcmV?d00001 diff --git a/tidal_dl_ng/ui/icon.ico b/tidal_dl_ng/ui/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..177f3dc3c1e64d5a143f844d778c1e2926bc378d GIT binary patch literal 220222 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 N1`(@W_UDT~_J3N@lBxgz literal 0 HcmV?d00001 diff --git a/tidal_dl_ng/ui/icon.png b/tidal_dl_ng/ui/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8bc9e0d648215c1927738310cbc857f1fd90e140 GIT binary patch literal 54166 zcmV)>K!d-DP)_Rn$lcq6~^aF$y9|DKm(a%_u0?+4}nRYxT)cPzVAd zDhk;Bp6#;%1#LeRkg4V=t8%EEGcq#fdF=mRYp*?=dm=I-t17DS#f7MhxcA(1&)sML zd#&}af302PHXa)zurUG;`v}xL>}T9K;l>DTjDXt!*2W~*7=cG%1U7*62n_1RDK|!7 z16Uh#U}FRxff3jM)*~>e8>ieDfem17%z=#&cmzgZ16Ys1pl+OUV+1yUwJ`@aM&J<` zfem0i0)x77%8e1&0M^DF*cgFFU<4jEU@a{z)jIc~#<8)6mf_)n^XJ_{b9-|`e{XN! z5$75fojY&7rsk*SAA#?%amtMmSk(x8Y3UVpCvVzXKh-+XFg3TdYhm8C^NmZ3iv#oZ zbAwI2a}#@C`Q_PFo$M?JJuJYw|G@9O@{Mo(-WSgd47kaONmo~2FOrTLx9Dn?mRx;9 z#dUOcy34M3^3;=ltn2XWU(@=UhK6^4@8S3Y8|T;7k56e`wOG@Ph4`x zckXEG??36Lrl#HO%&c3$a*yeWiMr@oTiaY`_a=A4TYln`lM4eM*!$*B4Lp>QeaOIi zuX$V zdxzVyt!whP-n08RcJ#gTV-Mv^Y`k}41kw?B)A!%hQupcasbvMfVJf2fiRc=IYsxTZuhuLE}nbdGb*ibe)Fg1&UIuT60rJ* z{_|x&|Eh1@a`3+b4un>yQFTU)EEtE+SMuv~Kl7C<#QIq8OmhULqRjEuNS zW20+nZFSFj?sa1?_}SjCJoQP_zfn{3jzj1AMK<2IF#-fskoJR`47MGgU$T_ z)zFzix=yYZ3w@=&BLCLZ;0gRJE-Yc8U&5ll;AZhNH#ZM!HsLO~@IrUP+pc-{yKnft z|NC4=^&tW4g)e#O((&Ub1UMI6{5aRq-UgG}>ROwd1y~IY4fsa-5j8Nm2D0(-aW{fh zWMpL2ojiF8E5-tjuej&_?9)e|cI~FyuYc}4pNTJzHD%+mF#=~Ffwz3)4bN&l*z;e$ z_2;)-JvuVvW~QfQQ74e@7r!3Kl>F_A92pWT3Y&K%TurTYhKj?8h2@t)}M9ljriq zHs15^e*|9lFRyEF|KaBEp1fu7lCj}ockIX!arHn(09X}Q2kT^@QeeSC;dLTw0U^l( zT|oZ6i1&D4@%=aeMsD7{>HR->_4{9Y^Eq(==M1bPCw})Que<7#|Fmn*c|uHV-@YB7 zYH`i5R?W>#0w@BDmW!sht_GxG^5E*gFF_Y^baYhQ!v`Nc;wAyI(Xnyw(=ZQZeb z@elsX*5@`?-tmwB{-?>tH(c!q?7iz14X3}m^-F`_oO;oB|MlB71V}CBSbKZBYlN3n zUtcE{NPr~&s%4TN@BWdqQ1Zfn#naG~q$>_UvSIRL8>@e_g70CQYdr#`LtwO9w_AI}-`Ia)CS zjPB$F2$1RN88RYc*)+keeUF1NAH|~b=O@VeBI~1u=JV#{+EB@&Ihn)u{Lks z?3!V*n!pMYSQ0E&2rRW&3M_l*-v&1O;)f=- zz33%RzUyb6IlH&L<;K5W>lfWP{zrQRe)Su#y>9k@bbWK+WPj~Fcitryig8qPYm2M* zgv1h1MS#d+O=|>T@xl2e{~E9eE3sw@CUSy&k)W*LT#IlGXFy^+{U@$-7hJLQ(!Ia> zng4gKqjyfg`jbC<-@pFj*H2vcz3+V6op=5PuA`&F_4f9lfZQ{zqp7=<*F5?s~(qznc90+rNEF3nTRgEY=;J9j*!UY$@@0#HE9k5`sbiW1v$1 zC;$tw6(2^m3%~?&O0bdaSb|wwf;+f#=QelgH5Yv6_xFD6X=@#|a{|_HeBd1;xBlRs zj^js;x?OvAi*MW0(<7EjK*a>HSfn~If$9R*)z-PjCa^TfZQ

(C|AM@MzuWEAnpw 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 OHOx$`jO%Yf68;ZN+=34P literal 0 HcmV?d00001 diff --git a/tidal_dl_ng/ui/main.py b/tidal_dl_ng/ui/main.py new file mode 100644 index 0000000..9c1525d --- /dev/null +++ b/tidal_dl_ng/ui/main.py @@ -0,0 +1,588 @@ +################################################################################ +## Form generated from reading UI file 'main.ui' +## +## Created by: Qt User Interface Compiler version 6.7.0 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import QCoreApplication, QLocale, QMetaObject, QRect, QSize, Qt +from PySide6.QtGui import QAction, QFont, QPixmap +from PySide6.QtWidgets import ( + QAbstractItemView, + QComboBox, + QFrame, + QHBoxLayout, + QLabel, + QLineEdit, + QMenu, + QMenuBar, + QPlainTextEdit, + QPushButton, + QSizePolicy, + QStatusBar, + QTreeView, + QTreeWidget, + QTreeWidgetItem, + QVBoxLayout, + QWidget, +) + + +class Ui_MainWindow: + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(1200, 800) + self.a_preferences = QAction(MainWindow) + self.a_preferences.setObjectName("a_preferences") + self.a_preferences.setEnabled(True) + self.a_preferences.setText("Preferences...") + self.a_preferences.setIconText("Preferences...") + # if QT_CONFIG(tooltip) + self.a_preferences.setToolTip("Preferences...") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.a_preferences.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.a_preferences.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + self.a_version = QAction(MainWindow) + self.a_version.setObjectName("a_version") + self.a_exit = QAction(MainWindow) + self.a_exit.setObjectName("a_exit") + self.a_logout = QAction(MainWindow) + self.a_logout.setObjectName("a_logout") + self.a_updates_check = QAction(MainWindow) + self.a_updates_check.setObjectName("a_updates_check") + self.w_central = QWidget(MainWindow) + self.w_central.setObjectName("w_central") + self.w_central.setEnabled(True) + sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding) + sizePolicy.setHorizontalStretch(100) + sizePolicy.setVerticalStretch(100) + sizePolicy.setHeightForWidth(self.w_central.sizePolicy().hasHeightForWidth()) + self.w_central.setSizePolicy(sizePolicy) + # if QT_CONFIG(tooltip) + self.w_central.setToolTip("") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.w_central.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.w_central.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.w_central.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.w_central.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.horizontalLayout = QHBoxLayout(self.w_central) + self.horizontalLayout.setObjectName("horizontalLayout") + self.lv_list_user = QVBoxLayout() + self.lv_list_user.setObjectName("lv_list_user") + self.tr_lists_user = QTreeWidget(self.w_central) + __qtreewidgetitem = QTreeWidgetItem() + __qtreewidgetitem.setText(2, "Info") + __qtreewidgetitem.setText(0, "Name") + self.tr_lists_user.setHeaderItem(__qtreewidgetitem) + __qtreewidgetitem1 = QTreeWidgetItem(self.tr_lists_user) + __qtreewidgetitem1.setFlags(Qt.ItemIsEnabled) + __qtreewidgetitem2 = QTreeWidgetItem(self.tr_lists_user) + __qtreewidgetitem2.setFlags(Qt.ItemIsEnabled) + __qtreewidgetitem3 = QTreeWidgetItem(self.tr_lists_user) + __qtreewidgetitem3.setFlags(Qt.ItemIsEnabled) + self.tr_lists_user.setObjectName("tr_lists_user") + # if QT_CONFIG(tooltip) + self.tr_lists_user.setToolTip("") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.tr_lists_user.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.tr_lists_user.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.tr_lists_user.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.tr_lists_user.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.tr_lists_user.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.tr_lists_user.setProperty("showDropIndicator", False) + self.tr_lists_user.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.tr_lists_user.setIndentation(10) + self.tr_lists_user.setUniformRowHeights(True) + self.tr_lists_user.setSortingEnabled(True) + self.tr_lists_user.header().setCascadingSectionResizes(True) + self.tr_lists_user.header().setHighlightSections(True) + self.tr_lists_user.header().setProperty("showSortIndicator", True) + + self.lv_list_user.addWidget(self.tr_lists_user) + + self.lv_list_control = QHBoxLayout() + self.lv_list_control.setObjectName("lv_list_control") + self.pb_reload_user_lists = QPushButton(self.w_central) + self.pb_reload_user_lists.setObjectName("pb_reload_user_lists") + sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + sizePolicy1.setHorizontalStretch(0) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.pb_reload_user_lists.sizePolicy().hasHeightForWidth()) + self.pb_reload_user_lists.setSizePolicy(sizePolicy1) + + self.lv_list_control.addWidget(self.pb_reload_user_lists) + + self.pb_download_list = QPushButton(self.w_central) + self.pb_download_list.setObjectName("pb_download_list") + sizePolicy1.setHeightForWidth(self.pb_download_list.sizePolicy().hasHeightForWidth()) + self.pb_download_list.setSizePolicy(sizePolicy1) + + self.lv_list_control.addWidget(self.pb_download_list) + + self.lv_list_user.addLayout(self.lv_list_control) + + self.horizontalLayout.addLayout(self.lv_list_user) + + self.lv_search_result = QVBoxLayout() + # ifndef Q_OS_MAC + self.lv_search_result.setSpacing(-1) + # endif + self.lv_search_result.setObjectName("lv_search_result") + self.lh_search = QHBoxLayout() + self.lh_search.setObjectName("lh_search") + self.l_search = QLineEdit(self.w_central) + self.l_search.setObjectName("l_search") + self.l_search.setAcceptDrops(False) + # if QT_CONFIG(tooltip) + self.l_search.setToolTip("") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.l_search.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.l_search.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.l_search.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.l_search.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.l_search.setLocale(QLocale(QLocale.English, QLocale.UnitedStates)) + self.l_search.setText("") + self.l_search.setPlaceholderText("Type and press ENTER to search...") + self.l_search.setClearButtonEnabled(True) + + self.lh_search.addWidget(self.l_search) + + self.cb_search_type = QComboBox(self.w_central) + self.cb_search_type.setObjectName("cb_search_type") + self.cb_search_type.setMinimumSize(QSize(100, 0)) + # if QT_CONFIG(tooltip) + self.cb_search_type.setToolTip("") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.cb_search_type.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.cb_search_type.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.cb_search_type.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.cb_search_type.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.cb_search_type.setCurrentText("") + self.cb_search_type.setSizeAdjustPolicy(QComboBox.AdjustToContentsOnFirstShow) + self.cb_search_type.setPlaceholderText("") + + self.lh_search.addWidget(self.cb_search_type) + + self.pb_search = QPushButton(self.w_central) + self.pb_search.setObjectName("pb_search") + # if QT_CONFIG(statustip) + self.pb_search.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.pb_search.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.pb_search.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.pb_search.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.pb_search.setText("Search") + # if QT_CONFIG(shortcut) + self.pb_search.setShortcut("") + # endif // QT_CONFIG(shortcut) + + self.lh_search.addWidget(self.pb_search) + + self.lv_search_result.addLayout(self.lh_search) + + self.tr_results = QTreeView(self.w_central) + self.tr_results.setObjectName("tr_results") + self.tr_results.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.tr_results.setProperty("showDropIndicator", False) + self.tr_results.setDragDropOverwriteMode(False) + self.tr_results.setAlternatingRowColors(False) + self.tr_results.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.tr_results.setIndentation(10) + self.tr_results.setSortingEnabled(True) + + self.lv_search_result.addWidget(self.tr_results) + + self.lh_download = QHBoxLayout() + self.lh_download.setObjectName("lh_download") + self.l_quality_audio = QLabel(self.w_central) + self.l_quality_audio.setObjectName("l_quality_audio") + # if QT_CONFIG(tooltip) + self.l_quality_audio.setToolTip("") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.l_quality_audio.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.l_quality_audio.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.l_quality_audio.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.l_quality_audio.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.l_quality_audio.setText("Audio") + self.l_quality_audio.setAlignment(Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter) + + self.lh_download.addWidget(self.l_quality_audio) + + self.cb_quality_audio = QComboBox(self.w_central) + self.cb_quality_audio.setObjectName("cb_quality_audio") + self.cb_quality_audio.setMinimumSize(QSize(140, 0)) + # if QT_CONFIG(tooltip) + self.cb_quality_audio.setToolTip("") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.cb_quality_audio.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.cb_quality_audio.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.cb_quality_audio.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.cb_quality_audio.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.cb_quality_audio.setCurrentText("") + self.cb_quality_audio.setSizeAdjustPolicy(QComboBox.AdjustToContentsOnFirstShow) + self.cb_quality_audio.setPlaceholderText("") + self.cb_quality_audio.setFrame(True) + + self.lh_download.addWidget(self.cb_quality_audio) + + self.l_quality_video = QLabel(self.w_central) + self.l_quality_video.setObjectName("l_quality_video") + # if QT_CONFIG(tooltip) + self.l_quality_video.setToolTip("") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.l_quality_video.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.l_quality_video.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.l_quality_video.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.l_quality_video.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.l_quality_video.setText("Video") + self.l_quality_video.setAlignment(Qt.AlignRight | Qt.AlignTrailing | Qt.AlignVCenter) + + self.lh_download.addWidget(self.l_quality_video) + + self.cb_quality_video = QComboBox(self.w_central) + self.cb_quality_video.setObjectName("cb_quality_video") + self.cb_quality_video.setMinimumSize(QSize(100, 0)) + # if QT_CONFIG(tooltip) + self.cb_quality_video.setToolTip("") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.cb_quality_video.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.cb_quality_video.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.cb_quality_video.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.cb_quality_video.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.cb_quality_video.setCurrentText("") + self.cb_quality_video.setSizeAdjustPolicy(QComboBox.AdjustToContentsOnFirstShow) + self.cb_quality_video.setPlaceholderText("") + + self.lh_download.addWidget(self.cb_quality_video) + + self.pb_download = QPushButton(self.w_central) + self.pb_download.setObjectName("pb_download") + # if QT_CONFIG(tooltip) + self.pb_download.setToolTip("") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.pb_download.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.pb_download.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.pb_download.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.pb_download.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.pb_download.setText("Download") + # if QT_CONFIG(shortcut) + self.pb_download.setShortcut("") + # endif // QT_CONFIG(shortcut) + + self.lh_download.addWidget(self.pb_download) + + self.lh_download.setStretch(0, 5) + self.lh_download.setStretch(2, 5) + self.lh_download.setStretch(4, 15) + + self.lv_search_result.addLayout(self.lh_download) + + self.te_debug = QPlainTextEdit(self.w_central) + self.te_debug.setObjectName("te_debug") + self.te_debug.setEnabled(True) + sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Maximum) + sizePolicy2.setHorizontalStretch(0) + sizePolicy2.setVerticalStretch(0) + sizePolicy2.setHeightForWidth(self.te_debug.sizePolicy().hasHeightForWidth()) + self.te_debug.setSizePolicy(sizePolicy2) + self.te_debug.setMaximumSize(QSize(16777215, 16777215)) + self.te_debug.setAcceptDrops(False) + # if QT_CONFIG(tooltip) + self.te_debug.setToolTip("") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.te_debug.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.te_debug.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.te_debug.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.te_debug.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.te_debug.setUndoRedoEnabled(False) + self.te_debug.setReadOnly(True) + + self.lv_search_result.addWidget(self.te_debug) + + self.horizontalLayout.addLayout(self.lv_search_result) + + self.lv_info = QVBoxLayout() + self.lv_info.setObjectName("lv_info") + self.lv_info_item = QVBoxLayout() + self.lv_info_item.setObjectName("lv_info_item") + self.horizontalLayout_2 = QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.l_pm_cover = QLabel(self.w_central) + self.l_pm_cover.setObjectName("l_pm_cover") + sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + sizePolicy3.setHorizontalStretch(0) + sizePolicy3.setVerticalStretch(0) + sizePolicy3.setHeightForWidth(self.l_pm_cover.sizePolicy().hasHeightForWidth()) + self.l_pm_cover.setSizePolicy(sizePolicy3) + self.l_pm_cover.setMaximumSize(QSize(280, 280)) + self.l_pm_cover.setBaseSize(QSize(0, 0)) + self.l_pm_cover.setFrameShape(QFrame.NoFrame) + self.l_pm_cover.setPixmap(QPixmap("default_album_image.png")) + self.l_pm_cover.setScaledContents(True) + self.l_pm_cover.setAlignment(Qt.AlignHCenter | Qt.AlignTop) + + self.horizontalLayout_2.addWidget(self.l_pm_cover) + + self.lv_info_item.addLayout(self.horizontalLayout_2) + + self.lv_info.addLayout(self.lv_info_item) + + self.lv_queue_download = QVBoxLayout() + self.lv_queue_download.setObjectName("lv_queue_download") + self.l_h_queue_download = QLabel(self.w_central) + self.l_h_queue_download.setObjectName("l_h_queue_download") + font = QFont() + font.setBold(True) + font.setItalic(False) + font.setKerning(True) + self.l_h_queue_download.setFont(font) + + self.lv_queue_download.addWidget(self.l_h_queue_download) + + self.tr_queue_download = QTreeWidget(self.w_central) + __qtreewidgetitem4 = QTreeWidgetItem() + __qtreewidgetitem4.setText(0, "\ud83e\uddd1\u200d\ud83d\udcbb\ufe0f") + self.tr_queue_download.setHeaderItem(__qtreewidgetitem4) + self.tr_queue_download.setObjectName("tr_queue_download") + self.tr_queue_download.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.tr_queue_download.setTabKeyNavigation(False) + self.tr_queue_download.setProperty("showDropIndicator", False) + self.tr_queue_download.setDragDropOverwriteMode(False) + self.tr_queue_download.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.tr_queue_download.setSelectionBehavior(QAbstractItemView.SelectRows) + self.tr_queue_download.setRootIsDecorated(False) + self.tr_queue_download.setItemsExpandable(False) + self.tr_queue_download.setSortingEnabled(False) + self.tr_queue_download.setExpandsOnDoubleClick(False) + self.tr_queue_download.header().setProperty("showSortIndicator", False) + self.tr_queue_download.header().setStretchLastSection(False) + + self.lv_queue_download.addWidget(self.tr_queue_download) + + self.pb_queue_download_remove = QPushButton(self.w_central) + self.pb_queue_download_remove.setObjectName("pb_queue_download_remove") + self.pb_queue_download_remove.setEnabled(True) + + self.lv_queue_download.addWidget(self.pb_queue_download_remove) + + self.horizontalLayout_3 = QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.pb_queue_download_clear_finished = QPushButton(self.w_central) + self.pb_queue_download_clear_finished.setObjectName("pb_queue_download_clear_finished") + + self.horizontalLayout_3.addWidget(self.pb_queue_download_clear_finished) + + self.pb_queue_download_clear_all = QPushButton(self.w_central) + self.pb_queue_download_clear_all.setObjectName("pb_queue_download_clear_all") + + self.horizontalLayout_3.addWidget(self.pb_queue_download_clear_all) + + self.lv_queue_download.addLayout(self.horizontalLayout_3) + + self.lv_info.addLayout(self.lv_queue_download) + + self.horizontalLayout.addLayout(self.lv_info) + + self.horizontalLayout.setStretch(1, 50) + self.horizontalLayout.setStretch(2, 25) + MainWindow.setCentralWidget(self.w_central) + self.menubar = QMenuBar(MainWindow) + self.menubar.setObjectName("menubar") + self.menubar.setGeometry(QRect(0, 0, 1200, 24)) + # if QT_CONFIG(tooltip) + self.menubar.setToolTip("") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.menubar.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.menubar.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.menubar.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.menubar.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.m_file = QMenu(self.menubar) + self.m_file.setObjectName("m_file") + # if QT_CONFIG(tooltip) + self.m_file.setToolTip("") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.m_file.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.m_file.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.m_file.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.m_file.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.m_help = QMenu(self.menubar) + self.m_help.setObjectName("m_help") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + # if QT_CONFIG(tooltip) + self.statusbar.setToolTip("") + # endif // QT_CONFIG(tooltip) + # if QT_CONFIG(statustip) + self.statusbar.setStatusTip("") + # endif // QT_CONFIG(statustip) + # if QT_CONFIG(whatsthis) + self.statusbar.setWhatsThis("") + # endif // QT_CONFIG(whatsthis) + # if QT_CONFIG(accessibility) + self.statusbar.setAccessibleName("") + # endif // QT_CONFIG(accessibility) + # if QT_CONFIG(accessibility) + self.statusbar.setAccessibleDescription("") + # endif // QT_CONFIG(accessibility) + self.statusbar.setLayoutDirection(Qt.LeftToRight) + MainWindow.setStatusBar(self.statusbar) + + self.menubar.addAction(self.m_file.menuAction()) + self.menubar.addAction(self.m_help.menuAction()) + self.m_file.addAction(self.a_preferences) + self.m_file.addAction(self.a_logout) + self.m_file.addAction(self.a_exit) + self.m_help.addAction(self.a_version) + self.m_help.addAction(self.a_updates_check) + + self.retranslateUi(MainWindow) + + QMetaObject.connectSlotsByName(MainWindow) + + # setupUi + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", "MainWindow", None)) + self.a_version.setText(QCoreApplication.translate("MainWindow", "Version", None)) + self.a_exit.setText(QCoreApplication.translate("MainWindow", "Exit", None)) + self.a_logout.setText(QCoreApplication.translate("MainWindow", "Logout", None)) + self.a_updates_check.setText(QCoreApplication.translate("MainWindow", "Check for Updates", None)) + ___qtreewidgetitem = self.tr_lists_user.headerItem() + ___qtreewidgetitem.setText(1, QCoreApplication.translate("MainWindow", "obj", None)) + + __sortingEnabled = self.tr_lists_user.isSortingEnabled() + self.tr_lists_user.setSortingEnabled(False) + ___qtreewidgetitem1 = self.tr_lists_user.topLevelItem(0) + ___qtreewidgetitem1.setText(0, QCoreApplication.translate("MainWindow", "Playlists", None)) + ___qtreewidgetitem2 = self.tr_lists_user.topLevelItem(1) + ___qtreewidgetitem2.setText(0, QCoreApplication.translate("MainWindow", "Mixes", None)) + ___qtreewidgetitem3 = self.tr_lists_user.topLevelItem(2) + ___qtreewidgetitem3.setText(0, QCoreApplication.translate("MainWindow", "Favorites", None)) + self.tr_lists_user.setSortingEnabled(__sortingEnabled) + + self.pb_reload_user_lists.setText(QCoreApplication.translate("MainWindow", "Reload", None)) + self.pb_download_list.setText(QCoreApplication.translate("MainWindow", "Download List", None)) + self.te_debug.setPlaceholderText(QCoreApplication.translate("MainWindow", "Logs...", None)) + self.l_pm_cover.setText("") + self.l_h_queue_download.setText(QCoreApplication.translate("MainWindow", "Download Queue", None)) + ___qtreewidgetitem4 = self.tr_queue_download.headerItem() + ___qtreewidgetitem4.setText(4, QCoreApplication.translate("MainWindow", "Quality", None)) + ___qtreewidgetitem4.setText(3, QCoreApplication.translate("MainWindow", "Type", None)) + ___qtreewidgetitem4.setText(2, QCoreApplication.translate("MainWindow", "Name", None)) + ___qtreewidgetitem4.setText(1, QCoreApplication.translate("MainWindow", "obj", None)) + self.pb_queue_download_remove.setText(QCoreApplication.translate("MainWindow", "Remove", None)) + self.pb_queue_download_clear_finished.setText(QCoreApplication.translate("MainWindow", "Clear Finished", None)) + self.pb_queue_download_clear_all.setText(QCoreApplication.translate("MainWindow", "Clear All", None)) + self.m_file.setTitle(QCoreApplication.translate("MainWindow", "File", None)) + self.m_help.setTitle(QCoreApplication.translate("MainWindow", "Help", None)) + + # retranslateUi diff --git a/tidal_dl_ng/ui/main.ui b/tidal_dl_ng/ui/main.ui new file mode 100644 index 0000000..0ad3693 --- /dev/null +++ b/tidal_dl_ng/ui/main.ui @@ -0,0 +1,801 @@ + + + MainWindow + + + + 0 + 0 + 1200 + 800 + + + + MainWindow + + + + true + + + + 100 + 100 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + QAbstractItemView::ExtendedSelection + + + 10 + + + true + + + true + + + true + + + true + + + true + + + + Name + + + + + + + + + + + + + + obj + + + + + Info + + + + + + + + + + + + + + Playlists + + + + + + + + + ItemIsEnabled + + + + + Mixes + + + + + + + + + ItemIsEnabled + + + + + Favorites + + + + + + + + + ItemIsEnabled + + + + + + + + + + + 0 + 0 + + + + Reload + + + + + + + + 0 + 0 + + + + Download List + + + + + + + + + + + -1 + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + Type and press ENTER to search... + + + true + + + + + + + + 100 + 0 + + + + + + + + + + + + + + + + + + + + + + QComboBox::AdjustToContentsOnFirstShow + + + + + + + + + + + + + + + + + + + + + + Search + + + + + + + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + false + + + false + + + QAbstractItemView::ExtendedSelection + + + 10 + + + true + + + + + + + + + + + + + + + + + + + + + + + + Audio + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 140 + 0 + + + + + + + + + + + + + + + + + + + + + + QComboBox::AdjustToContentsOnFirstShow + + + + + + true + + + + + + + + + + + + + + + + + + + + + + Video + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 100 + 0 + + + + + + + + + + + + + + + + + + + + + + QComboBox::AdjustToContentsOnFirstShow + + + + + + + + + + + + + + + + + + + + + + + + + Download + + + + + + + + + + + + true + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + false + + + + + + + + + + + + + + + + + + false + + + true + + + Logs... + + + + + + + + + + + + + + + + 0 + 0 + + + + + 280 + 280 + + + + + 0 + 0 + + + + QFrame::NoFrame + + + + + + default_album_image.png + + + true + + + Qt::AlignHCenter|Qt::AlignTop + + + + + + + + + + + + + + false + true + true + + + + Download Queue + + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + false + + + false + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + false + + + false + + + false + + + false + + + false + + + false + + + + 🧑‍💻️ + + + + + obj + + + + + Name + + + + + Type + + + + + Quality + + + + + + + + true + + + Remove + + + + + + + + + Clear Finished + + + + + + + Clear All + + + + + + + + + + + + + + + 0 + 0 + 1200 + 24 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + File + + + + + + + + Help + + + + + + + + + + + + + + + + + + + + + + + + + Qt::LeftToRight + + + + + true + + + Preferences... + + + Preferences... + + + Preferences... + + + + + + + + + + + Version + + + + + Exit + + + + + Logout + + + + + Check for Updates + + + + + + diff --git a/tidal_dl_ng/ui/spinner.py b/tidal_dl_ng/ui/spinner.py new file mode 100644 index 0000000..124b7f4 --- /dev/null +++ b/tidal_dl_ng/ui/spinner.py @@ -0,0 +1,221 @@ +""" +The MIT License (MIT) + +Copyright (c) 2012-2014 Alexander Turkin +Copyright (c) 2014 William Hallatt +Copyright (c) 2015 Jacob Dawid +Copyright (c) 2016 Luca Weiss + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import math + +from PySide6.QtCore import QRect, Qt, QTimer +from PySide6.QtGui import QColor, QPainter +from PySide6.QtWidgets import QWidget + + +# Taken from https://github.com/COOLMSF/QtWaitingSpinnerForPyQt6 and adapted for PySide6. +class QtWaitingSpinner(QWidget): + def __init__( + self, parent, centerOnParent=True, disableParentWhenSpinning=False, modality=Qt.WindowModality.NonModal + ): + super().__init__(parent) + + self._centerOnParent = centerOnParent + self._disableParentWhenSpinning = disableParentWhenSpinning + + # WAS IN initialize() + self._color = QColor(Qt.GlobalColor.black) + self._roundness = 100.0 + self._minimumTrailOpacity = 3.14159265358979323846 + self._trailFadePercentage = 80.0 + self._revolutionsPerSecond = 1.57079632679489661923 + self._numberOfLines = 20 + self._lineLength = 10 + self._lineWidth = 2 + self._innerRadius = 10 + self._currentCounter = 0 + self._isSpinning = False + + self._timer = QTimer(self) + self._timer.timeout.connect(self.rotate) + self.updateSize() + self.updateTimer() + self.hide() + # END initialize() + + self.setWindowModality(modality) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + def paintEvent(self, QPaintEvent): + self.updatePosition() + painter = QPainter(self) + painter.fillRect(self.rect(), Qt.GlobalColor.transparent) + # Can't found in Qt6 + # painter.setRenderHint(QPainter.Antialiasing, True) + + if self._currentCounter >= self._numberOfLines: + self._currentCounter = 0 + + painter.setPen(Qt.PenStyle.NoPen) + for i in range(0, self._numberOfLines): + painter.save() + painter.translate(self._innerRadius + self._lineLength, self._innerRadius + self._lineLength) + rotateAngle = float(360 * i) / float(self._numberOfLines) + painter.rotate(rotateAngle) + painter.translate(self._innerRadius, 0) + distance = self.lineCountDistanceFromPrimary(i, self._currentCounter, self._numberOfLines) + color = self.currentLineColor( + distance, self._numberOfLines, self._trailFadePercentage, self._minimumTrailOpacity, self._color + ) + painter.setBrush(color) + rect = QRect(0, int(-self._lineWidth / 2), int(self._lineLength), int(self._lineWidth)) + painter.drawRoundedRect(rect, self._roundness, self._roundness, Qt.SizeMode.RelativeSize) + painter.restore() + + def start(self): + self.updatePosition() + self._isSpinning = True + self.show() + + if self.parentWidget and self._disableParentWhenSpinning: + self.parentWidget().setEnabled(False) + + if not self._timer.isActive(): + self._timer.start() + self._currentCounter = 0 + + def stop(self): + self._isSpinning = False + self.hide() + + if self.parentWidget() and self._disableParentWhenSpinning: + self.parentWidget().setEnabled(True) + + if self._timer.isActive(): + self._timer.stop() + self._currentCounter = 0 + + def setNumberOfLines(self, lines): + self._numberOfLines = lines + self._currentCounter = 0 + self.updateTimer() + + def setLineLength(self, length): + self._lineLength = length + self.updateSize() + + def setLineWidth(self, width): + self._lineWidth = width + self.updateSize() + + def setInnerRadius(self, radius): + self._innerRadius = radius + self.updateSize() + + def color(self): + return self._color + + def roundness(self): + return self._roundness + + def minimumTrailOpacity(self): + return self._minimumTrailOpacity + + def trailFadePercentage(self): + return self._trailFadePercentage + + def revolutionsPersSecond(self): + return self._revolutionsPerSecond + + def numberOfLines(self): + return self._numberOfLines + + def lineLength(self): + return self._lineLength + + def lineWidth(self): + return self._lineWidth + + def innerRadius(self): + return self._innerRadius + + def isSpinning(self): + return self._isSpinning + + def setRoundness(self, roundness): + self._roundness = max(0.0, min(100.0, roundness)) + + def setColor(self, color=Qt.GlobalColor.black): + self._color = QColor(color) + + def setRevolutionsPerSecond(self, revolutionsPerSecond): + self._revolutionsPerSecond = revolutionsPerSecond + self.updateTimer() + + def setTrailFadePercentage(self, trail): + self._trailFadePercentage = trail + + def setMinimumTrailOpacity(self, minimumTrailOpacity): + self._minimumTrailOpacity = minimumTrailOpacity + + def rotate(self): + self._currentCounter += 1 + if self._currentCounter >= self._numberOfLines: + self._currentCounter = 0 + self.update() + + def updateSize(self): + size = int((self._innerRadius + self._lineLength) * 2) + self.setFixedSize(size, size) + + def updateTimer(self): + self._timer.setInterval(int(1000 / (self._numberOfLines * self._revolutionsPerSecond))) + + def updatePosition(self): + if self.parentWidget() and self._centerOnParent: + self.move( + int(self.parentWidget().width() / 2 - self.width() / 2), + int(self.parentWidget().height() / 2 - self.height() / 2), + ) + + def lineCountDistanceFromPrimary(self, current, primary, totalNrOfLines): + distance = primary - current + if distance < 0: + distance += totalNrOfLines + return distance + + def currentLineColor(self, countDistance, totalNrOfLines, trailFadePerc, minOpacity, colorinput): + color = QColor(colorinput) + if countDistance == 0: + return color + minAlphaF = minOpacity / 100.0 + distanceThreshold = int(math.ceil((totalNrOfLines - 1) * trailFadePerc / 100.0)) + if countDistance > distanceThreshold: + color.setAlphaF(minAlphaF) + else: + alphaDiff = color.alphaF() - minAlphaF + gradient = alphaDiff / float(distanceThreshold + 1) + resultAlpha = color.alphaF() - gradient * countDistance + # If alpha is out of bounds, clip it. + resultAlpha = min(1.0, max(0.0, resultAlpha)) + color.setAlphaF(resultAlpha) + return color diff --git a/tidal_dl_ng/worker.py b/tidal_dl_ng/worker.py new file mode 100644 index 0000000..a02b0f1 --- /dev/null +++ b/tidal_dl_ng/worker.py @@ -0,0 +1,34 @@ +from PySide6 import QtCore + + +# Taken from https://www.pythonguis.com/tutorials/multithreading-pyside6-applications-qthreadpool/ +class Worker(QtCore.QRunnable): + """ + Worker thread + + Inherits from QRunnable to handler worker thread setup, signals and wrap-up. + + :param callback: The function callback to run on this worker thread. Supplied args and + kwargs will be passed through to the runner. + :type callback: function + :param args: Arguments to pass to the callback function + :param kwargs: Keywords to pass to the callback function + + """ + + def __init__(self, fn, *args, **kwargs): + super().__init__() + # Store constructor arguments (re-used for processing) + self.fn = fn + self.args = args + self.kwargs = kwargs + + @QtCore.Slot() # QtCore.Slot + def run(self): + """ + Initialise the runner function with passed args, kwargs. + """ + self.fn(*self.args, **self.kwargs) + + def thread(self) -> QtCore.QThread: + return QtCore.QThread.currentThread()