Merge branch 'master' into nuitka
This commit is contained in:
@@ -16,7 +16,6 @@ on:
|
|||||||
- "v*.*.*"
|
- "v*.*.*"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
RELEASE_UPLOAD_URL: "release_upload_url"
|
|
||||||
ASSET_EXTENSION: ".zip"
|
ASSET_EXTENSION: ".zip"
|
||||||
OUT_NAME_FILE: "TIDAL-Downloader-NG"
|
OUT_NAME_FILE: "TIDAL-Downloader-NG"
|
||||||
ARCH_MACOS_X64: "macos-x64"
|
ARCH_MACOS_X64: "macos-x64"
|
||||||
@@ -46,18 +45,11 @@ jobs:
|
|||||||
POETRY_REQUESTS_TIMEOUT: 120
|
POETRY_REQUESTS_TIMEOUT: 120
|
||||||
- name: GitHub Release -- Create
|
- name: GitHub Release -- Create
|
||||||
id: release_create
|
id: release_create
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
draft: false
|
draft: false
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: GitHub Release -- Output Release URL File
|
|
||||||
run: echo "${{steps.release_create.outputs.upload_url}}" > ${{env.RELEASE_UPLOAD_URL}}.txt
|
|
||||||
- name: GitHub Release -- Save Upload URL File for publish
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{env.RELEASE_UPLOAD_URL}}
|
|
||||||
path: ${{env.RELEASE_UPLOAD_URL}}.txt
|
|
||||||
deploy-docs:
|
deploy-docs:
|
||||||
needs: publish
|
needs: publish
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
@@ -90,8 +82,6 @@ jobs:
|
|||||||
echo "ARCH_WINDOWS_X64=${{ env.ARCH_WINDOWS_X64 }}" >> $GITHUB_OUTPUT
|
echo "ARCH_WINDOWS_X64=${{ env.ARCH_WINDOWS_X64 }}" >> $GITHUB_OUTPUT
|
||||||
build-gui-and-upload-assets:
|
build-gui-and-upload-assets:
|
||||||
needs: ["publish", "compute"]
|
needs: ["publish", "compute"]
|
||||||
env:
|
|
||||||
ASSET_MIME: "application/zip"
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -144,23 +134,11 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-poetry-env
|
uses: ./.github/actions/setup-poetry-env
|
||||||
- name: Build with pyinstaller for ${{matrix.TARGET}}
|
- name: Build with pyinstaller for ${{matrix.TARGET}}
|
||||||
run: ${{matrix.CMD_BUILD}}
|
run: ${{matrix.CMD_BUILD}}
|
||||||
- name: Load Release URL File from release job
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{env.RELEASE_UPLOAD_URL}}
|
|
||||||
- name: Get Release File Name & Upload URL
|
|
||||||
id: get_release_info
|
|
||||||
run: |
|
|
||||||
VALUE=`cat ${{env.RELEASE_UPLOAD_URL}}.txt`
|
|
||||||
echo "upload_url=$VALUE" >> $GITHUB_OUTPUT
|
|
||||||
- name: Upload Release Asset
|
- name: Upload Release Asset
|
||||||
# TODO: Switch to https://github.com/softprops/action-gh-release
|
|
||||||
id: upload-release-asset
|
id: upload-release-asset
|
||||||
uses: actions/upload-release-asset@v1
|
uses: softprops/action-gh-release@v2
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||||
with:
|
with:
|
||||||
upload_url: ${{steps.get_release_info.outputs.upload_url}}
|
files: |
|
||||||
asset_path: ./dist/${{needs.compute.outputs.OUT_NAME_FILE}}_${{matrix.OS_ARCH}}${{needs.compute.outputs.ASSET_EXTENSION}}
|
./dist/${{needs.compute.outputs.OUT_NAME_FILE}}_${{matrix.OS_ARCH}}${{needs.compute.outputs.ASSET_EXTENSION}}
|
||||||
asset_name: ${{needs.compute.outputs.OUT_NAME_FILE}}_${{matrix.OS_ARCH}}${{needs.compute.outputs.ASSET_EXTENSION}}
|
|
||||||
asset_content_type: ${{env.ASSET_MIME}}
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ This tool allows to download songs and videos from TIDAL. Multithreaded and mult
|
|||||||
|
|
||||||
⚠️ **Windows** Defender / **Anti Virus** software / web browser alerts, while you try to download the app binary: This is a **false positive**. Please read [this issue](https://github.com/exislow/tidal-dl-ng/issues/231), [PyInstaller (used by this project) statement ](https://github.com/pyinstaller/pyinstaller/blob/develop/.github/ISSUE_TEMPLATE/antivirus.md) and [the alternative installation solution](https://github.com/exislow/tidal-dl-ng/?tab=readme-ov-file#-installation--upgrade). ⚠️
|
⚠️ **Windows** Defender / **Anti Virus** software / web browser alerts, while you try to download the app binary: This is a **false positive**. Please read [this issue](https://github.com/exislow/tidal-dl-ng/issues/231), [PyInstaller (used by this project) statement ](https://github.com/pyinstaller/pyinstaller/blob/develop/.github/ISSUE_TEMPLATE/antivirus.md) and [the alternative installation solution](https://github.com/exislow/tidal-dl-ng/?tab=readme-ov-file#-installation--upgrade). ⚠️
|
||||||
|
|
||||||
**A paid plan is required!** Audio quality varies up to HiRes Lossless / TIDAL MAX 24-bit, 192 kHz depending on the song available. You can use the command line or GUI version of this tool.
|
**A paid TIDAL plan is required!** Audio quality varies up to HiRes Lossless / TIDAL MAX 24-bit, 192 kHz depending on the song available. You can use the command line or GUI version of this tool.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -28,27 +28,31 @@ $ tidal-dl-ng --help
|
|||||||
│ for this option. To set a value for an option simply pass the value │
|
│ for this option. To set a value for an option simply pass the value │
|
||||||
│ as the second argument │
|
│ as the second argument │
|
||||||
│ dl │
|
│ dl │
|
||||||
|
│ dl_fav Download from a favorites collection. │
|
||||||
│ gui │
|
│ gui │
|
||||||
│ login │
|
│ login │
|
||||||
|
│ logout │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||||
```
|
```
|
||||||
|
|
||||||
If you like this projects and want to support it, you can buy me a coffee :-)
|
If you like this projects and want to support it, feel free to buy me a coffee 🙃✌️
|
||||||
|
|
||||||
<a href="https://www.buymeacoffee.com/exislow" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/arial-orange.png" alt="Buy Me A Coffee" style="height: 51px !important;width: 217px !important;" ></a>
|
<a href="https://www.buymeacoffee.com/exislow" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/arial-orange.png" alt="Buy Me A Coffee" style="height: 51px !important;width: 217px !important;" ></a>
|
||||||
<a href="https://ko-fi.com/exislow" target="_blank" rel="noopener noreferrer"><img src="https://help.ko-fi.com/hc/article_attachments/11833788361117" alt="61e11d430afb112ea33c3aa5_Button-1-p-500"></a>
|
<a href="https://ko-fi.com/exislow" target="_blank" rel="noopener noreferrer"><img src="https://help.ko-fi.com/hc/article_attachments/11833788361117" alt="61e11d430afb112ea33c3aa5_Button-1-p-500"></a>
|
||||||
|
|
||||||
## 💻 Installation / Upgrade
|
## 💻 Installation / Upgrade
|
||||||
|
|
||||||
**Requirements**: Python >= 3.12 (other versions might work but are not tested!)
|
**Requirements**: Python == 3.12 (other versions might work but are not tested!)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install --upgrade tidal-dl-ng
|
pip install --upgrade tidal-dl-ng
|
||||||
# AND if you like to have the GUI as well
|
# If you like to have the GUI as well use this command instead
|
||||||
pip install --upgrade tidal-dl-ng[gui]
|
pip install --upgrade tidal-dl-ng[gui]
|
||||||
```
|
```
|
||||||
|
|
||||||
You can use the command line (CLI) version to download media:
|
## ⌨️ Usage
|
||||||
|
|
||||||
|
You can use the command line (CLI) version to download media by URL:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tidal-dl-ng dl https://tidal.com/browse/track/46755209
|
tidal-dl-ng dl https://tidal.com/browse/track/46755209
|
||||||
@@ -56,7 +60,16 @@ tidal-dl-ng dl https://tidal.com/browse/track/46755209
|
|||||||
tdn dl https://tidal.com/browse/track/46755209
|
tdn dl https://tidal.com/browse/track/46755209
|
||||||
```
|
```
|
||||||
|
|
||||||
But also the GUI:
|
Or by your favorites collections:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tidal-dl-ng dl_fav tracks
|
||||||
|
tidal-dl-ng dl_fav artists
|
||||||
|
tidal-dl-ng dl_fav albums
|
||||||
|
tidal-dl-ng dl_fav videos
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use the GUI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tidal-dl-ng-gui
|
tidal-dl-ng-gui
|
||||||
@@ -71,7 +84,7 @@ If you like to have the GUI version only as a binary, have a look at the
|
|||||||
|
|
||||||
## 🧁 Features
|
## 🧁 Features
|
||||||
|
|
||||||
- Download Tracks, Videos, Albums, Playlists etc.
|
- Download tracks, videos, albums, playlists, your favorites etc.
|
||||||
- Multithreaded and multi-chunked downloads
|
- Multithreaded and multi-chunked downloads
|
||||||
- Metadata for songs
|
- Metadata for songs
|
||||||
- Adjustable audio and video download quality.
|
- Adjustable audio and video download quality.
|
||||||
@@ -105,20 +118,26 @@ The GUI is build with `PySide6` using the [Qt Designer](https://doc.qt.io/qt-6/q
|
|||||||
PYSIDE_DESIGNER_PLUGINS=tidal_dl_ng/ui pyside6-designer
|
PYSIDE_DESIGNER_PLUGINS=tidal_dl_ng/ui pyside6-designer
|
||||||
```
|
```
|
||||||
|
|
||||||
After all changes are saved you need to translate the Qt Designer `*.ui` file into Python code:
|
After all changes are saved you need to translate the Qt Designer `*.ui` file into Python code, for instance:
|
||||||
|
|
||||||
```
|
```
|
||||||
pyside6-uic tidal_dl_ng/ui/main.ui -o tidal_dl_ng/ui/main.py
|
pyside6-uic tidal_dl_ng/ui/main.ui -o tidal_dl_ng/ui/main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This needs to be done for each created / modified `*.ui` file accordingly.
|
||||||
|
|
||||||
### 🏗 Build the project
|
### 🏗 Build the project
|
||||||
|
|
||||||
To build the project use this command:
|
To build the project use this command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make install
|
make install
|
||||||
|
# OR
|
||||||
|
make gui-macos
|
||||||
```
|
```
|
||||||
|
|
||||||
|
See the `Makefile` for all available build commands.
|
||||||
|
|
||||||
The CI/CD pipeline will be triggered when you open a pull request, merge to main, or when you create a new release.
|
The CI/CD pipeline will be triggered when you open a pull request, merge to main, or when you create a new release.
|
||||||
|
|
||||||
To finalize the set-up for publishing to PyPi or Artifactory, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/publishing/#set-up-for-pypi).
|
To finalize the set-up for publishing to PyPi or Artifactory, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/publishing/#set-up-for-pypi).
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "tidal-dl-ng"
|
name = "tidal-dl-ng"
|
||||||
authors = ["Robert Honz <cen.sored@gmail.com>"]
|
authors = ["Robert Honz <cen.sored@gmail.com>"]
|
||||||
version = "0.23.5"
|
version = "0.23.8"
|
||||||
description = "TIDAL Medial Downloader Next Generation!"
|
description = "TIDAL Medial Downloader Next Generation!"
|
||||||
repository = "https://github.com/exislow/tidal-dl-ng"
|
repository = "https://github.com/exislow/tidal-dl-ng"
|
||||||
documentation = "https://exislow.github.io/tidal-dl-ng/"
|
documentation = "https://exislow.github.io/tidal-dl-ng/"
|
||||||
|
|||||||
+187
-71
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated, Optional
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
@@ -23,6 +24,13 @@ from tidal_dl_ng.helper.wrapper import LoggerWrapped
|
|||||||
from tidal_dl_ng.model.cfg import HelpSettings
|
from tidal_dl_ng.model.cfg import HelpSettings
|
||||||
|
|
||||||
app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]}, add_completion=False)
|
app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]}, add_completion=False)
|
||||||
|
dl_fav_group = typer.Typer(
|
||||||
|
context_settings={"help_option_names": ["-h", "--help"]},
|
||||||
|
add_completion=True,
|
||||||
|
help="Download from a favorites collection.",
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_typer(dl_fav_group, name="dl_fav")
|
||||||
|
|
||||||
|
|
||||||
def version_callback(value: bool):
|
def version_callback(value: bool):
|
||||||
@@ -31,6 +39,92 @@ def version_callback(value: bool):
|
|||||||
raise typer.Exit()
|
raise typer.Exit()
|
||||||
|
|
||||||
|
|
||||||
|
def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bool:
|
||||||
|
"""Invokes download function and tracks progress.
|
||||||
|
|
||||||
|
:param ctx: The typer context object.
|
||||||
|
:type ctx: typer.Context
|
||||||
|
:param urls: The list of URLs to download.
|
||||||
|
:type urls: list[str]
|
||||||
|
:param try_login: If true, attempts to login to TIDAL.
|
||||||
|
:type try_login: bool
|
||||||
|
:return: True if ran successfully.
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
if try_login:
|
||||||
|
# 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)
|
||||||
|
else:
|
||||||
|
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.callback()
|
@app.callback()
|
||||||
def callback_app(
|
def callback_app(
|
||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
@@ -54,6 +148,7 @@ def settings_management(
|
|||||||
If only one argument is given, the value will be printed for this option.
|
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
|
To set a value for an option simply pass the value as the second argument
|
||||||
|
|
||||||
|
:param editor: If set, your favorite system editor will be opened.
|
||||||
:param names: (Optional) None (list all options), one (list the value only for this option) or two arguments
|
:param names: (Optional) None (list all options), one (list the value only for this option) or two arguments
|
||||||
(set the value for the option).
|
(set the value for the option).
|
||||||
"""
|
"""
|
||||||
@@ -136,90 +231,111 @@ def download(
|
|||||||
help="List with URLs to download. One per line",
|
help="List with URLs to download. One per line",
|
||||||
),
|
),
|
||||||
] = None,
|
] = None,
|
||||||
):
|
) -> bool:
|
||||||
if not urls:
|
if not urls:
|
||||||
# Read the text file provided.
|
# Read the text file provided.
|
||||||
if file_urls:
|
if file_urls:
|
||||||
text = file_urls.read_text()
|
text: str = file_urls.read_text()
|
||||||
urls = text.splitlines()
|
urls = text.splitlines()
|
||||||
else:
|
else:
|
||||||
print("Provide either URLs, IDs or a file containing URLs (one per line).")
|
print("Provide either URLs, IDs or a file containing URLs (one per line).")
|
||||||
|
|
||||||
raise typer.Abort()
|
raise typer.Abort()
|
||||||
|
|
||||||
|
return _download(ctx, urls)
|
||||||
|
|
||||||
|
|
||||||
|
@dl_fav_group.command(
|
||||||
|
name="tracks",
|
||||||
|
help="Download your favorite track collection.",
|
||||||
|
)
|
||||||
|
def download_fav_tracks(ctx: typer.Context) -> bool:
|
||||||
|
"""Download your favorite track collection.
|
||||||
|
|
||||||
|
:param ctx: Typer context object.
|
||||||
|
:type ctx: typer.Context
|
||||||
|
:return: Download result.
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
# Method name
|
||||||
|
func_name_favorites: str = "tracks"
|
||||||
|
|
||||||
|
return _download_fav_factory(ctx, func_name_favorites)
|
||||||
|
|
||||||
|
|
||||||
|
@dl_fav_group.command(
|
||||||
|
name="artists",
|
||||||
|
help="Download your favorite artist collection.",
|
||||||
|
)
|
||||||
|
def download_fav_artists(ctx: typer.Context) -> bool:
|
||||||
|
"""Download your favorite artist collection.
|
||||||
|
|
||||||
|
:param ctx: Typer context object.
|
||||||
|
:type ctx: typer.Context
|
||||||
|
:return: Download result.
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
# Method name
|
||||||
|
func_name_favorites: str = "artists"
|
||||||
|
|
||||||
|
return _download_fav_factory(ctx, func_name_favorites)
|
||||||
|
|
||||||
|
|
||||||
|
@dl_fav_group.command(
|
||||||
|
name="albums",
|
||||||
|
help="Download your favorite album collection.",
|
||||||
|
)
|
||||||
|
def download_fav_albums(ctx: typer.Context) -> bool:
|
||||||
|
"""Download your favorite album collection.
|
||||||
|
|
||||||
|
:param ctx: Typer context object.
|
||||||
|
:type ctx: typer.Context
|
||||||
|
:return: Download result.
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
# Method name
|
||||||
|
func_name_favorites: str = "albums"
|
||||||
|
|
||||||
|
return _download_fav_factory(ctx, func_name_favorites)
|
||||||
|
|
||||||
|
|
||||||
|
@dl_fav_group.command(
|
||||||
|
name="videos",
|
||||||
|
help="Download your favorite video collection.",
|
||||||
|
)
|
||||||
|
def download_fav_videos(ctx: typer.Context) -> bool:
|
||||||
|
"""Download your favorite video collection.
|
||||||
|
|
||||||
|
:param ctx: Typer context object.
|
||||||
|
:type ctx: typer.Context
|
||||||
|
:return: Download result.
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
# Method name
|
||||||
|
func_name_favorites: str = "videos"
|
||||||
|
|
||||||
|
return _download_fav_factory(ctx, func_name_favorites)
|
||||||
|
|
||||||
|
|
||||||
|
def _download_fav_factory(ctx: typer.Context, func_name_favorites: str) -> bool:
|
||||||
|
"""Factory which helps to download items from the favorites collections.
|
||||||
|
|
||||||
|
:param ctx: Typer context object.
|
||||||
|
:type ctx: typer.Context
|
||||||
|
:param func_name_favorites: Method name to call from `tidalapi` favorites object.
|
||||||
|
:type func_name_favorites: str
|
||||||
|
:return: Download result.
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
# Call login method to validate the token.
|
# Call login method to validate the token.
|
||||||
ctx.invoke(login, ctx)
|
ctx.invoke(login, ctx)
|
||||||
|
|
||||||
# Create initial objects.
|
# Get the method from the module
|
||||||
settings: Settings = Settings()
|
func_favorites: Callable = getattr(ctx.obj[CTX_TIDAL].session.user.favorites, func_name_favorites)
|
||||||
progress: Progress = Progress(
|
# Get favorite videos
|
||||||
"{task.description}",
|
media_urls: [str] = [media.share_url for media in func_favorites()]
|
||||||
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.
|
return _download(ctx, media_urls, try_login=False)
|
||||||
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()
|
@app.command()
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ from collections.abc import Callable
|
|||||||
from concurrent import futures
|
from concurrent import futures
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from ffmpeg import FFmpeg
|
|
||||||
import m3u8
|
import m3u8
|
||||||
import requests
|
import requests
|
||||||
|
from ffmpeg import FFmpeg
|
||||||
from requests.adapters import HTTPAdapter, Retry
|
from requests.adapters import HTTPAdapter, Retry
|
||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
from rich.progress import Progress, TaskID
|
from rich.progress import Progress, TaskID
|
||||||
@@ -495,8 +495,10 @@ class Download:
|
|||||||
|
|
||||||
if not skip_symlink:
|
if not skip_symlink:
|
||||||
self.fn_logger.debug(f"Symlink: {path_media_src} -> {path_media_dst}")
|
self.fn_logger.debug(f"Symlink: {path_media_src} -> {path_media_dst}")
|
||||||
|
path_media_dst_relative: pathlib.Path = path_media_dst.relative_to(path_media_src.parent, walk_up=True)
|
||||||
|
|
||||||
path_media_src.unlink(missing_ok=True)
|
path_media_src.unlink(missing_ok=True)
|
||||||
path_media_src.symlink_to(path_media_dst)
|
path_media_src.symlink_to(path_media_dst_relative)
|
||||||
|
|
||||||
return path_media_dst
|
return path_media_dst
|
||||||
|
|
||||||
@@ -779,11 +781,12 @@ class Download:
|
|||||||
FFmpeg(executable=self.settings.data.path_binary_ffmpeg)
|
FFmpeg(executable=self.settings.data.path_binary_ffmpeg)
|
||||||
.input(url=path_media_src)
|
.input(url=path_media_src)
|
||||||
.output(
|
.output(
|
||||||
url=path_media_out, map=0,
|
url=path_media_out,
|
||||||
|
map=0,
|
||||||
movflags="use_metadata_tags",
|
movflags="use_metadata_tags",
|
||||||
acodec="copy",
|
acodec="copy",
|
||||||
map_metadata="0:g",
|
map_metadata="0:g",
|
||||||
loglevel="quiet"
|
loglevel="quiet",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -83,7 +83,7 @@ try:
|
|||||||
from PySide6 import QtCore, QtGui, QtWidgets
|
from PySide6 import QtCore, QtGui, QtWidgets
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(e)
|
print(e)
|
||||||
print("Qt dependencies missing. Cannot start GUI. Please execute: 'pip install pyside6 pyqtdarktheme'")
|
print("Qt dependencies missing. Cannot start GUI. Please read the 'README.md' carefully.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
import coloredlogs.converter
|
import coloredlogs.converter
|
||||||
|
|||||||
Reference in New Issue
Block a user