Merge remote-tracking branch 'warry-github/master' into feature/informations-window
# Conflicts: # CONTRIBUTING.md # README.md
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
docs/source
|
docs/source
|
||||||
|
vs_code/
|
||||||
|
|
||||||
# From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
|
# From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ Anything tagged with "enhancement" and "help wanted" is open to whoever wants to
|
|||||||
|
|
||||||
## Write Documentation
|
## Write Documentation
|
||||||
|
|
||||||
Cookiecutter PyPackage could always use more documentation, whether as part of the official docs, in docstrings, or even on the web in blog posts, articles, and such.
|
The TIDAL Downloader project could always use more documentation, whether as part of the official docs, in docstrings, or even on the web in blog posts, articles, and such.
|
||||||
|
|
||||||
## Submit Feedback
|
## Submit Feedback
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
[](https://img.shields.io/github/commit-activity/m/exislow/tidal-dl-ng)
|
[](https://img.shields.io/github/commit-activity/m/exislow/tidal-dl-ng)
|
||||||
[](https://img.shields.io/github/license/exislow/tidal-dl-ng)
|
[](https://img.shields.io/github/license/exislow/tidal-dl-ng)
|
||||||
|
|
||||||
This tool allows to download songs and videos from TIDAL. Multithreaded and multi-chunked downloads are supported.
|
This tool allows you to download songs and videos from TIDAL. Multithreaded and multi-chunked downloads are supported.
|
||||||
|
|
||||||
⚠️ **Windows** Defender / **Anti Virus** software / web browser alerts, while you try to download the app binary: This
|
⚠️ **Windows** Defender / **Anti Virus** software / web browser alerts, while you try to download the app binary: This
|
||||||
is a **false positive**. Please
|
is a **false positive**. Please
|
||||||
@@ -39,7 +39,7 @@ $ tidal-dl-ng --help
|
|||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||||
```
|
```
|
||||||
|
|
||||||
If you like this projects and want to support it, feel free to buy me a coffee 🙃✌️
|
If you like this project 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>
|
||||||
@@ -83,8 +83,8 @@ tdng
|
|||||||
tidal-dl-ng gui
|
tidal-dl-ng gui
|
||||||
```
|
```
|
||||||
|
|
||||||
If you like to have the GUI version only as a binary, have a look at the
|
If you would like to use the GUI version as a binary, have a look at the
|
||||||
[release page](https://github.com/exislow/tidal-dl-ng/releases) and download the correct version for your platform.
|
[release page](https://github.com/exislow/tidal-dl-ng/releases) and download the correct version for your OS.
|
||||||
|
|
||||||
## 🧁 Features
|
## 🧁 Features
|
||||||
|
|
||||||
@@ -121,13 +121,13 @@ tidal_ng_dl/gui.py
|
|||||||
|
|
||||||
### 📺 GUI Builder
|
### 📺 GUI Builder
|
||||||
|
|
||||||
The GUI is build with `PySide6` using the [Qt Designer](https://doc.qt.io/qt-6/qtdesigner-manual.html):
|
The GUI is built with `PySide6` using the [Qt Designer](https://doc.qt.io/qt-6/qtdesigner-manual.html):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
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, for instance:
|
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
|
||||||
@@ -144,9 +144,9 @@ To build the project use this command:
|
|||||||
make install
|
make install
|
||||||
# Build macOS GUI
|
# Build macOS GUI
|
||||||
make gui-macos-dmg
|
make gui-macos-dmg
|
||||||
# OR Build macOS GUI
|
# OR Build Windows GUI
|
||||||
make gui-windows
|
make gui-windows
|
||||||
# OR Build macOS GUI
|
# OR Build Linux GUI
|
||||||
make gui-linux
|
make gui-linux
|
||||||
# Check build output
|
# Check build output
|
||||||
ls dist/
|
ls dist/
|
||||||
@@ -206,7 +206,7 @@ sudo apt install libxcb-cursor0
|
|||||||
|
|
||||||
### A terminal is flashing when I run this app on Windows
|
### A terminal is flashing when I run this app on Windows
|
||||||
|
|
||||||
Please see this isse [#103](https://github.com/exislow/tidal-dl-ng/issues/103).
|
Please see this issue [#103](https://github.com/exislow/tidal-dl-ng/issues/103).
|
||||||
|
|
||||||
This is due to the Python `ffmpeg` library which is used and only happens on windows if `extract_flac` is activated.
|
This is due to the Python `ffmpeg` library which is used and only happens on windows if `extract_flac` is activated.
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
name = "tidal-dl-ng"
|
name = "tidal-dl-ng"
|
||||||
authors = [{ name = "Robert Honz", email = "<cen.sored@gmail.com>" }]
|
authors = [{ name = "Robert Honz", email = "<cen.sored@gmail.com>" }]
|
||||||
version = "0.31.4"
|
version = "0.31.4"
|
||||||
description = "TIDAL Medial Downloader Next Generation!"
|
description = "TIDAL Media Downloader Next Generation!"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
@@ -132,7 +132,7 @@ ignore = [
|
|||||||
# Do not use bare `except`
|
# Do not use bare `except`
|
||||||
"E722",
|
"E722",
|
||||||
# PEP 484 prohibits implicit `Optional`
|
# PEP 484 prohibits implicit `Optional`
|
||||||
# Disabled only temporarely.
|
# Disabled only temporarily.
|
||||||
"RUF013",
|
"RUF013",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -171,7 +171,7 @@ def _process_url(
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if "http" not in url:
|
if "http" not in url:
|
||||||
print(f"It seems like that you have supplied an invalid URL: {url}")
|
print(f"It seems like you have supplied an invalid URL: {url}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
url_clean: str = url_ending_clean(url)
|
url_clean: str = url_ending_clean(url)
|
||||||
@@ -286,7 +286,7 @@ def settings_management(
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
names (list[str] | None, optional): None (list all options), one (list the value only for this option) or two arguments (set the value for the option). Defaults to None.
|
names (list[str] | None, optional): None (list all options), one (list the value only for this option) or two arguments (set the value for the option). Defaults to None.
|
||||||
editor (bool, optional): If set, your favorite system editor will be opened. Defaults to False.
|
editor (bool, optional): If set, your default system editor will be opened. Defaults to False.
|
||||||
"""
|
"""
|
||||||
if editor:
|
if editor:
|
||||||
config_path: Path = Path(path_file_settings())
|
config_path: Path = Path(path_file_settings())
|
||||||
@@ -333,7 +333,7 @@ def login(ctx: typer.Context) -> bool:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True if login was successful, False otherwise.
|
bool: True if login was successful, False otherwise.
|
||||||
"""
|
"""
|
||||||
print("Let us check, if you are already logged in... ", end="")
|
print("Let us check if you are already logged in... ", end="")
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
tidal = Tidal(settings)
|
tidal = Tidal(settings)
|
||||||
@@ -375,7 +375,7 @@ def download(
|
|||||||
writable=False,
|
writable=False,
|
||||||
readable=True,
|
readable=True,
|
||||||
resolve_path=True,
|
resolve_path=True,
|
||||||
help="List with URLs to download. One per line",
|
help="File with URLs to download. One URL per line.",
|
||||||
),
|
),
|
||||||
] = None,
|
] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -395,7 +395,7 @@ def download(
|
|||||||
text: str = 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 or a file containing URLs (one per line).")
|
||||||
|
|
||||||
raise typer.Abort()
|
raise typer.Abort()
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ class BaseConfig:
|
|||||||
|
|
||||||
# Move the invalid config file to the backup location.
|
# Move the invalid config file to the backup location.
|
||||||
shutil.move(path, path_bak)
|
shutil.move(path, path_bak)
|
||||||
# TODO: Implement better global logger.
|
|
||||||
print(
|
print(
|
||||||
"Something is wrong with your config. Maybe it is not compatible anymore due to a new app version."
|
"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."
|
f" You can find a backup of your old config here: '{path_bak}'. A new default config was created."
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ class DialogPreferences(QtWidgets.QDialog):
|
|||||||
):
|
):
|
||||||
# If a path is set, use it otherwise the users home directory.
|
# 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 ""
|
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
|
# Check if obj_line_edit is empty but path_default can be used instead
|
||||||
path_settings = (
|
path_settings = (
|
||||||
path_settings if path_settings else os.path.expanduser(path_default) if path_default else path_settings
|
path_settings if path_settings else os.path.expanduser(path_default) if path_default else path_settings
|
||||||
)
|
)
|
||||||
@@ -223,7 +223,7 @@ class DialogPreferences(QtWidgets.QDialog):
|
|||||||
|
|
||||||
dialog.setDirectory(dir_current)
|
dialog.setDirectory(dir_current)
|
||||||
|
|
||||||
# Execute dialog and set path is something is choosen.
|
# Execute dialog and set path if something is chosen.
|
||||||
if dialog.exec():
|
if dialog.exec():
|
||||||
dir_name: str = dialog.selectedFiles()[0]
|
dir_name: str = dialog.selectedFiles()[0]
|
||||||
path: Path = Path(dir_name)
|
path: Path = Path(dir_name)
|
||||||
|
|||||||
+17
-3
@@ -411,7 +411,7 @@ class Download:
|
|||||||
with path_file.open("wb") as f_target:
|
with path_file.open("wb") as f_target:
|
||||||
for dl_segment_result in dl_segment_results:
|
for dl_segment_result in dl_segment_results:
|
||||||
with dl_segment_result.path_segment.open("rb") as f_segment:
|
with dl_segment_result.path_segment.open("rb") as f_segment:
|
||||||
# Read and write junks, which gives better HDD write performance
|
# Read and write chunks, which gives better HDD write performance
|
||||||
while segment := f_segment.read(CHUNK_SIZE):
|
while segment := f_segment.read(CHUNK_SIZE):
|
||||||
f_target.write(segment)
|
f_target.write(segment)
|
||||||
|
|
||||||
@@ -1249,7 +1249,7 @@ class Download:
|
|||||||
response: requests.Response = requests.get(url, timeout=REQUESTS_TIMEOUT_SEC)
|
response: requests.Response = requests.get(url, timeout=REQUESTS_TIMEOUT_SEC)
|
||||||
result = response.content
|
result = response.content
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# TODO: Implement propper logging.
|
# TODO: Implement proper logging.
|
||||||
print(e)
|
print(e)
|
||||||
finally:
|
finally:
|
||||||
response.close()
|
response.close()
|
||||||
@@ -1258,7 +1258,7 @@ class Download:
|
|||||||
with open(path_file, "rb") as f:
|
with open(path_file, "rb") as f:
|
||||||
result = f.read()
|
result = f.read()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
# TODO: Implement propper logging.
|
# TODO: Implement proper logging.
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -1750,15 +1750,22 @@ class Download:
|
|||||||
pathlib.Path: Path to the converted MP4 file.
|
pathlib.Path: Path to the converted MP4 file.
|
||||||
"""
|
"""
|
||||||
path_file_out: pathlib.Path = path_file.with_suffix(AudioExtensions.MP4)
|
path_file_out: pathlib.Path = path_file.with_suffix(AudioExtensions.MP4)
|
||||||
|
|
||||||
|
self.fn_logger.debug(f"Converting video: {path_file.name} -> {path_file_out.name}")
|
||||||
|
|
||||||
ffmpeg = (
|
ffmpeg = (
|
||||||
FFmpeg(executable=self.settings.data.path_binary_ffmpeg)
|
FFmpeg(executable=self.settings.data.path_binary_ffmpeg)
|
||||||
.option("y")
|
.option("y")
|
||||||
|
.option("hide_banner")
|
||||||
|
.option("nostdin")
|
||||||
.input(url=path_file)
|
.input(url=path_file)
|
||||||
.output(url=path_file_out, codec="copy", map=0, loglevel="quiet")
|
.output(url=path_file_out, codec="copy", map=0, loglevel="quiet")
|
||||||
)
|
)
|
||||||
|
|
||||||
ffmpeg.execute()
|
ffmpeg.execute()
|
||||||
|
|
||||||
|
self.fn_logger.debug(f"Video conversion complete: {path_file_out.name}")
|
||||||
|
|
||||||
return path_file_out
|
return path_file_out
|
||||||
|
|
||||||
def _extract_flac(self, path_media_src: pathlib.Path) -> pathlib.Path:
|
def _extract_flac(self, path_media_src: pathlib.Path) -> pathlib.Path:
|
||||||
@@ -1771,8 +1778,13 @@ class Download:
|
|||||||
pathlib.Path: Path to the extracted FLAC file.
|
pathlib.Path: Path to the extracted FLAC file.
|
||||||
"""
|
"""
|
||||||
path_media_out = path_media_src.with_suffix(AudioExtensions.FLAC)
|
path_media_out = path_media_src.with_suffix(AudioExtensions.FLAC)
|
||||||
|
|
||||||
|
self.fn_logger.debug(f"Extracting FLAC: {path_media_src.name} -> {path_media_out.name}")
|
||||||
|
|
||||||
ffmpeg = (
|
ffmpeg = (
|
||||||
FFmpeg(executable=self.settings.data.path_binary_ffmpeg)
|
FFmpeg(executable=self.settings.data.path_binary_ffmpeg)
|
||||||
|
.option("hide_banner")
|
||||||
|
.option("nostdin")
|
||||||
.input(url=path_media_src)
|
.input(url=path_media_src)
|
||||||
.output(
|
.output(
|
||||||
url=path_media_out,
|
url=path_media_out,
|
||||||
@@ -1786,6 +1798,8 @@ class Download:
|
|||||||
|
|
||||||
ffmpeg.execute()
|
ffmpeg.execute()
|
||||||
|
|
||||||
|
self.fn_logger.debug(f"FLAC extraction complete: {path_media_out.name}")
|
||||||
|
|
||||||
return path_media_out
|
return path_media_out
|
||||||
|
|
||||||
def _extract_video_stream(self, m3u8_variant: m3u8.M3U8, quality: int) -> tuple[m3u8.M3U8 | bool, str]:
|
def _extract_video_stream(self, m3u8_variant: m3u8.M3U8, quality: int) -> tuple[m3u8.M3U8 | bool, str]:
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class Settings:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class HelpSettings:
|
class HelpSettings:
|
||||||
skip_existing: str = "Skip download if file already exists."
|
skip_existing: str = "Skip download if file already exists."
|
||||||
album_cover_save: str = "Safe cover to album folder."
|
album_cover_save: str = "Save cover to album folder."
|
||||||
lyrics_embed: str = "Embed lyrics in audio file, if lyrics are available."
|
lyrics_embed: str = "Embed lyrics in audio file, if lyrics are available."
|
||||||
use_primary_album_artist: str = "Use only the primary album artist for folder paths instead of track artists."
|
use_primary_album_artist: str = "Use only the primary album artist for folder paths instead of track artists."
|
||||||
lyrics_file: str = "Save lyrics to separate *.lrc file, if lyrics are available."
|
lyrics_file: str = "Save lyrics to separate *.lrc file, if lyrics are available."
|
||||||
@@ -97,11 +97,11 @@ class HelpSettings:
|
|||||||
"will be converted to MP4. FFmpeg must be installed."
|
"will be converted to MP4. FFmpeg must be installed."
|
||||||
)
|
)
|
||||||
path_binary_ffmpeg: str = (
|
path_binary_ffmpeg: str = (
|
||||||
"Path to FFmpeg binary file (executable). Only necessary if FFmpeg not set in $PATH. Mandatory for Windows: "
|
"Path to FFmpeg binary file (executable). Only necessary if FFmpeg is not set in $PATH. Mandatory for Windows: "
|
||||||
"The directory of `ffmpeg.exe`must be set in %PATH%."
|
"The directory of `ffmpeg.exe` must be set in %PATH%."
|
||||||
)
|
)
|
||||||
metadata_cover_dimension: str = (
|
metadata_cover_dimension: str = (
|
||||||
"The dimensions of the cover image embedded into the track. Possible values: 320x320, 640x640x 1280x1280."
|
"The dimensions of the cover image embedded into the track. Possible values: 320x320, 640x640, 1280x1280."
|
||||||
)
|
)
|
||||||
metadata_cover_embed: str = "Embed album cover into file."
|
metadata_cover_embed: str = "Embed album cover into file."
|
||||||
mark_explicit: str = "Mark explicit tracks with '🅴' in track title (only applies to metadata)."
|
mark_explicit: str = "Mark explicit tracks with '🅴' in track title (only applies to metadata)."
|
||||||
|
|||||||
Reference in New Issue
Block a user