+1
-1
@@ -171,7 +171,7 @@ profile = "black"
|
|||||||
multi_line_output = 3
|
multi_line_output = 3
|
||||||
indent = 4
|
indent = 4
|
||||||
color_output = true
|
color_output = true
|
||||||
known_third_party = ["Crypto", "PySide6", "coloredlogs", "dataclasses_json", "ffmpeg", "m3u8", "mutagen", "pathvalidate", "requests", "rich", "tidalapi", "toml", "typer"]
|
known_third_party = ["Crypto", "PySide6", "coloredlogs", "constants", "dataclasses_json", "ffmpeg", "m3u8", "mutagen", "pathvalidate", "requests", "rich", "tidalapi", "toml", "typer"]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
# https://mypy.readthedocs.io/en/latest/config_file.html#using-a-pyproject-toml-file
|
# https://mypy.readthedocs.io/en/latest/config_file.html#using-a-pyproject-toml-file
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
@@ -31,7 +32,9 @@ class BaseConfig:
|
|||||||
os.makedirs(self.path_base, exist_ok=True)
|
os.makedirs(self.path_base, exist_ok=True)
|
||||||
|
|
||||||
with open(self.file_path, encoding="utf-8", mode="w") as f:
|
with open(self.file_path, encoding="utf-8", mode="w") as f:
|
||||||
f.write(data_json)
|
# 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:
|
def set_option(self, key: str, value: Any) -> None:
|
||||||
value_old: Any = getattr(self.data, key)
|
value_old: Any = getattr(self.data, key)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ REQUESTS_TIMEOUT_SEC: int = 45
|
|||||||
EXTENSION_LYRICS: str = ".lrc"
|
EXTENSION_LYRICS: str = ".lrc"
|
||||||
UNIQUIFY_THRESHOLD: int = 99
|
UNIQUIFY_THRESHOLD: int = 99
|
||||||
FILENAME_SANITIZE_PLACEHOLDER: str = "_"
|
FILENAME_SANITIZE_PLACEHOLDER: str = "_"
|
||||||
|
COVER_NAME: str = "cover.jpg"
|
||||||
|
|
||||||
|
|
||||||
class QualityVideo(StrEnum):
|
class QualityVideo(StrEnum):
|
||||||
|
|||||||
@@ -171,6 +171,8 @@ class DialogPreferences(QtWidgets.QDialog):
|
|||||||
"download_delay",
|
"download_delay",
|
||||||
"video_convert_mp4",
|
"video_convert_mp4",
|
||||||
"extract_flac",
|
"extract_flac",
|
||||||
|
"metadata_cover_embed",
|
||||||
|
"cover_album_file",
|
||||||
]
|
]
|
||||||
|
|
||||||
def gui_populate(self):
|
def gui_populate(self):
|
||||||
|
|||||||
+79
-15
@@ -10,6 +10,7 @@ from uuid import uuid4
|
|||||||
import ffmpeg
|
import ffmpeg
|
||||||
import m3u8
|
import m3u8
|
||||||
import requests
|
import requests
|
||||||
|
from constants import COVER_NAME
|
||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
from rich.progress import Progress, TaskID
|
from rich.progress import Progress, TaskID
|
||||||
from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video
|
from tidalapi import Album, Mix, Playlist, Session, Track, UserPlaylist, Video
|
||||||
@@ -189,6 +190,7 @@ class Download:
|
|||||||
download_delay: bool = False,
|
download_delay: bool = False,
|
||||||
quality_audio: Quality | None = None,
|
quality_audio: Quality | None = None,
|
||||||
quality_video: QualityVideo | None = None,
|
quality_video: QualityVideo | None = None,
|
||||||
|
is_parent_album: bool = False,
|
||||||
) -> (bool, str):
|
) -> (bool, str):
|
||||||
try:
|
try:
|
||||||
if media_id and media_type:
|
if media_id and media_type:
|
||||||
@@ -278,10 +280,13 @@ class Download:
|
|||||||
tmp_path_file = self._extract_flac(tmp_path_file)
|
tmp_path_file = self._extract_flac(tmp_path_file)
|
||||||
|
|
||||||
tmp_path_lyrics: pathlib.Path | None = None
|
tmp_path_lyrics: pathlib.Path | None = None
|
||||||
|
tmp_path_cover: pathlib.Path | None = None
|
||||||
|
|
||||||
# Write metadata to file.
|
# Write metadata to file.
|
||||||
if not isinstance(media, Video):
|
if not isinstance(media, Video):
|
||||||
result_metadata, tmp_path_lyrics = self.metadata_write(media, tmp_path_file)
|
result_metadata, tmp_path_lyrics, tmp_path_cover = self.metadata_write(
|
||||||
|
media, tmp_path_file, is_parent_album
|
||||||
|
)
|
||||||
|
|
||||||
# Move final file to the configured destination directory.
|
# Move final file to the configured destination directory.
|
||||||
os.makedirs(os.path.dirname(path_media_dst), exist_ok=True)
|
os.makedirs(os.path.dirname(path_media_dst), exist_ok=True)
|
||||||
@@ -289,7 +294,11 @@ class Download:
|
|||||||
|
|
||||||
# Move lyrics file
|
# Move lyrics file
|
||||||
if self.settings.data.lyrics_file and not isinstance(media, Video):
|
if self.settings.data.lyrics_file and not isinstance(media, Video):
|
||||||
self._move_lyrics(path_media_dst, tmp_path_lyrics)
|
self._move_lyrics(tmp_path_lyrics, path_media_dst)
|
||||||
|
|
||||||
|
# Move cover file
|
||||||
|
if self.settings.data.cover_album_file:
|
||||||
|
self._move_cover(tmp_path_cover, path_media_dst)
|
||||||
else:
|
else:
|
||||||
self.fn_logger.debug(f"Download skipped, since file exists: '{path_media_dst}'")
|
self.fn_logger.debug(f"Download skipped, since file exists: '{path_media_dst}'")
|
||||||
|
|
||||||
@@ -327,13 +336,13 @@ class Download:
|
|||||||
|
|
||||||
return quality_old
|
return quality_old
|
||||||
|
|
||||||
def _move_lyrics(self, file_media_dst: str, path_lyrics: pathlib.Path) -> bool:
|
def _move_file(self, path_file_source: pathlib.Path, path_file_destination: str | pathlib.Path) -> bool:
|
||||||
result: bool
|
result: bool
|
||||||
# Build tmp lyrics filename
|
|
||||||
# Check if the file was downloaded
|
# Check if the file was downloaded
|
||||||
if path_lyrics and os.path.isfile(path_lyrics):
|
if path_file_source and os.path.isfile(path_file_source):
|
||||||
# Move it.
|
# Move it.
|
||||||
shutil.move(path_lyrics, os.path.splitext(file_media_dst)[0] + EXTENSION_LYRICS)
|
shutil.move(path_file_source, path_file_destination)
|
||||||
|
|
||||||
result = True
|
result = True
|
||||||
else:
|
else:
|
||||||
@@ -341,20 +350,64 @@ class Download:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def lyrics_write_file(self, dir_destination: pathlib.Path, lyrics: str) -> str:
|
def _move_lyrics(self, path_lyrics: pathlib.Path, file_media_dst: str) -> bool:
|
||||||
|
# Build tmp lyrics filename
|
||||||
|
path_file_lyrics: str = os.path.splitext(file_media_dst)[0] + 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: str) -> bool:
|
||||||
|
# Build tmp lyrics filename
|
||||||
|
path_file_cover: pathlib.Path = pathlib.Path(file_media_dst).parent.absolute() / 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())
|
result: str = dir_destination / str(uuid4())
|
||||||
|
encoding: str | None = "utf-8" if isinstance(content, str) else None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(result, "x", encoding="utf-8") as f:
|
with open(result, mode=mode, encoding=encoding) as f:
|
||||||
f.write(lyrics)
|
f.write(content)
|
||||||
except:
|
except:
|
||||||
result = ""
|
result = ""
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def metadata_write(self, track: Track, path_media: str) -> (bool, pathlib.Path | None):
|
@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: str, is_parent_album: str
|
||||||
|
) -> (bool, pathlib.Path | None, pathlib.Path | None):
|
||||||
result: bool = False
|
result: bool = False
|
||||||
path_lyrics: pathlib.Path | None = None
|
path_lyrics: pathlib.Path | None = None
|
||||||
|
path_cover: pathlib.Path | None = None
|
||||||
release_date: str = (
|
release_date: str = (
|
||||||
track.album.available_release_date.strftime("%Y-%m-%d")
|
track.album.available_release_date.strftime("%Y-%m-%d")
|
||||||
if track.album.available_release_date
|
if track.album.available_release_date
|
||||||
@@ -363,6 +416,7 @@ class Download:
|
|||||||
copy_right: str = track.copyright if hasattr(track, "copyright") and track.copyright 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 ""
|
isrc: str = track.isrc if hasattr(track, "isrc") and track.isrc else ""
|
||||||
lyrics: str = ""
|
lyrics: str = ""
|
||||||
|
cover_data: bytes = None
|
||||||
|
|
||||||
if self.settings.data.lyrics_embed or self.settings.data.lyrics_file:
|
if self.settings.data.lyrics_embed or self.settings.data.lyrics_file:
|
||||||
# Try to retrieve lyrics.
|
# Try to retrieve lyrics.
|
||||||
@@ -379,7 +433,14 @@ class Download:
|
|||||||
print(f"Could not retrieve lyrics for `{name_builder_item(track)}`.")
|
print(f"Could not retrieve lyrics for `{name_builder_item(track)}`.")
|
||||||
|
|
||||||
if lyrics and self.settings.data.lyrics_file:
|
if lyrics and self.settings.data.lyrics_file:
|
||||||
path_lyrics = self.lyrics_write_file(pathlib.Path(path_media).parent, lyrics)
|
path_lyrics = self.lyrics_to_file(pathlib.Path(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(pathlib.Path(path_media).parent, cover_data)
|
||||||
|
|
||||||
# `None` values are not allowed.
|
# `None` values are not allowed.
|
||||||
m: Metadata = Metadata(
|
m: Metadata = Metadata(
|
||||||
@@ -396,14 +457,14 @@ class Download:
|
|||||||
totaltrack=track.album.num_tracks if track.album and track.album.num_tracks else 1,
|
totaltrack=track.album.num_tracks if track.album and track.album.num_tracks else 1,
|
||||||
totaldisc=track.album.num_volumes if track.album and track.album.num_volumes else 1,
|
totaldisc=track.album.num_volumes if track.album and track.album.num_volumes else 1,
|
||||||
discnumber=track.volume_num if track.volume_num else 1,
|
discnumber=track.volume_num if track.volume_num else 1,
|
||||||
url_cover=track.album.image(int(self.settings.data.metadata_cover_dimension)),
|
cover_data=cover_data if self.settings.data.metadata_cover_embed else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
m.save()
|
m.save()
|
||||||
|
|
||||||
result = True
|
result = True
|
||||||
|
|
||||||
return result, path_lyrics
|
return result, path_lyrics, path_cover
|
||||||
|
|
||||||
def items(
|
def items(
|
||||||
self,
|
self,
|
||||||
@@ -451,16 +512,19 @@ class Download:
|
|||||||
f"[green]List '{list_media_name}'", total=len(items), visible=progress_stdout
|
f"[green]List '{list_media_name}'", total=len(items), visible=progress_stdout
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is_album: bool = isinstance(media, Album)
|
||||||
|
|
||||||
# Iterate through list items
|
# Iterate through list items
|
||||||
while not self.progress.finished:
|
while not self.progress.finished:
|
||||||
for media in items:
|
for item_media in items:
|
||||||
# Download the item.
|
# Download the item.
|
||||||
status_download, result_path_file = self.item(
|
status_download, result_path_file = self.item(
|
||||||
media=media,
|
media=item_media,
|
||||||
file_template=file_name_relative,
|
file_template=file_name_relative,
|
||||||
quality_audio=quality_audio,
|
quality_audio=quality_audio,
|
||||||
quality_video=quality_video,
|
quality_video=quality_video,
|
||||||
download_delay=download_delay,
|
download_delay=download_delay,
|
||||||
|
is_parent_album=is_album,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Advance progress bar.
|
# Advance progress bar.
|
||||||
|
|||||||
+64
-8
@@ -31,7 +31,6 @@ from tidal_dl_ng.helper.tidal import (
|
|||||||
search_results_all,
|
search_results_all,
|
||||||
user_media_lists,
|
user_media_lists,
|
||||||
)
|
)
|
||||||
from tidal_dl_ng.metadata import Metadata
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import qdarktheme
|
import qdarktheme
|
||||||
@@ -249,11 +248,16 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
tree.setColumnWidth(3, 150)
|
tree.setColumnWidth(3, 150)
|
||||||
tree.setColumnWidth(4, 150)
|
tree.setColumnWidth(4, 150)
|
||||||
header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
|
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:
|
def _init_tree_results_model(self, model: QtGui.QStandardItemModel) -> None:
|
||||||
model.setColumnCount(7)
|
labels_column: [str] = ["#", "obj", "Artist", "Title", "Album", "Duration", "Quality", "Date Added"]
|
||||||
|
|
||||||
|
model.setColumnCount(len(labels_column))
|
||||||
model.setRowCount(0)
|
model.setRowCount(0)
|
||||||
model.setHorizontalHeaderLabels(["#", "obj", "Artist", "Title", "Album", "Duration", "Quality"])
|
model.setHorizontalHeaderLabels(labels_column)
|
||||||
|
|
||||||
def _init_tree_queue(self, tree: QtWidgets.QTableWidget):
|
def _init_tree_queue(self, tree: QtWidgets.QTableWidget):
|
||||||
tree.setColumnHidden(1, True)
|
tree.setColumnHidden(1, True)
|
||||||
@@ -359,7 +363,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
self.spinner = None
|
self.spinner = None
|
||||||
|
|
||||||
def menu_context_tree_lists(self, point):
|
def menu_context_tree_lists(self, point: QtCore.QPoint):
|
||||||
# Infos about the node selected.
|
# Infos about the node selected.
|
||||||
index = self.tr_lists_user.indexAt(point)
|
index = self.tr_lists_user.indexAt(point)
|
||||||
|
|
||||||
@@ -370,12 +374,46 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
# We build the menu.
|
# We build the menu.
|
||||||
menu = QtWidgets.QMenu()
|
menu = QtWidgets.QMenu()
|
||||||
menu.addAction("Download Playlist", lambda: self.thread_download_list_media(point))
|
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))
|
menu.exec(self.tr_lists_user.mapToGlobal(point))
|
||||||
|
|
||||||
def thread_download_list_media(self, 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)
|
self.thread_it(self.on_download_list_media, point)
|
||||||
self.thread_it(self.list_items_show_result, point=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):
|
def on_download_list_media(self, point: QtCore.QPoint = None):
|
||||||
items: [QtWidgets.QTreeWidgetItem]
|
items: [QtWidgets.QTreeWidgetItem]
|
||||||
@@ -444,6 +482,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
child_album: QtGui.QStandardItem = QtGui.QStandardItem(item.album)
|
child_album: QtGui.QStandardItem = QtGui.QStandardItem(item.album)
|
||||||
child_duration: QtGui.QStandardItem = QtGui.QStandardItem(duration)
|
child_duration: QtGui.QStandardItem = QtGui.QStandardItem(duration)
|
||||||
child_quality: QtGui.QStandardItem = QtGui.QStandardItem(item.quality)
|
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):
|
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.
|
# Add a disabled dummy child, so expansion arrow will appear. This Child will be replaced on expansion.
|
||||||
@@ -452,7 +491,16 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
child_dummy.setEnabled(False)
|
child_dummy.setEnabled(False)
|
||||||
child_index.appendRow(child_dummy)
|
child_index.appendRow(child_dummy)
|
||||||
|
|
||||||
return child_index, child_obj, child_artist, child_title, child_album, child_duration, child_quality
|
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]):
|
def on_tr_results_add_top_level_item(self, item_child: Sequence[QtGui.QStandardItem]):
|
||||||
self.model_tr_results.appendRow(item_child)
|
self.model_tr_results.appendRow(item_child)
|
||||||
@@ -501,6 +549,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
if isinstance(item, Track | Video | Album):
|
if isinstance(item, Track | Video | Album):
|
||||||
explicit = " 🅴" if item.explicit else ""
|
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):
|
if isinstance(item, Track):
|
||||||
result_item: ResultItem = ResultItem(
|
result_item: ResultItem = ResultItem(
|
||||||
position=idx,
|
position=idx,
|
||||||
@@ -511,6 +561,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
obj=item,
|
obj=item,
|
||||||
quality=quality_audio_highest(item),
|
quality=quality_audio_highest(item),
|
||||||
explicit=bool(item.explicit),
|
explicit=bool(item.explicit),
|
||||||
|
date_user_added=date_user_added,
|
||||||
)
|
)
|
||||||
|
|
||||||
result.append(result_item)
|
result.append(result_item)
|
||||||
@@ -524,6 +575,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
obj=item,
|
obj=item,
|
||||||
quality=item.video_quality,
|
quality=item.video_quality,
|
||||||
explicit=bool(item.explicit),
|
explicit=bool(item.explicit),
|
||||||
|
date_user_added=date_user_added,
|
||||||
)
|
)
|
||||||
|
|
||||||
result.append(result_item)
|
result.append(result_item)
|
||||||
@@ -537,6 +589,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
obj=item,
|
obj=item,
|
||||||
quality="",
|
quality="",
|
||||||
explicit=False,
|
explicit=False,
|
||||||
|
date_user_added=date_user_added,
|
||||||
)
|
)
|
||||||
|
|
||||||
result.append(result_item)
|
result.append(result_item)
|
||||||
@@ -550,6 +603,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
obj=item,
|
obj=item,
|
||||||
quality=quality_audio_highest(item),
|
quality=quality_audio_highest(item),
|
||||||
explicit=bool(item.explicit),
|
explicit=bool(item.explicit),
|
||||||
|
date_user_added=date_user_added,
|
||||||
)
|
)
|
||||||
|
|
||||||
result.append(result_item)
|
result.append(result_item)
|
||||||
@@ -564,6 +618,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
obj=item,
|
obj=item,
|
||||||
quality="",
|
quality="",
|
||||||
explicit=False,
|
explicit=False,
|
||||||
|
date_user_added=date_user_added,
|
||||||
)
|
)
|
||||||
|
|
||||||
result.append(result_item)
|
result.append(result_item)
|
||||||
@@ -577,6 +632,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
obj=item,
|
obj=item,
|
||||||
quality="",
|
quality="",
|
||||||
explicit=False,
|
explicit=False,
|
||||||
|
date_user_added=date_user_added,
|
||||||
)
|
)
|
||||||
|
|
||||||
result.append(result_item)
|
result.append(result_item)
|
||||||
@@ -757,7 +813,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
|
|
||||||
if cover_url and self.cover_url_current != cover_url:
|
if cover_url and self.cover_url_current != cover_url:
|
||||||
self.cover_url_current = cover_url
|
self.cover_url_current = cover_url
|
||||||
data_cover: bytes = Metadata.cover_data(cover_url)
|
data_cover: bytes = Download.cover_data(cover_url)
|
||||||
pixmap: QtGui.QPixmap = QtGui.QPixmap()
|
pixmap: QtGui.QPixmap = QtGui.QPixmap()
|
||||||
pixmap.loadFromData(data_cover)
|
pixmap.loadFromData(data_cover)
|
||||||
self.l_pm_cover.setPixmap(pixmap)
|
self.l_pm_cover.setPixmap(pixmap)
|
||||||
|
|||||||
+7
-33
@@ -1,10 +1,7 @@
|
|||||||
import mutagen
|
import mutagen
|
||||||
import requests
|
|
||||||
from mutagen import flac, id3, mp4
|
from mutagen import flac, id3, mp4
|
||||||
from mutagen.id3 import APIC, TALB, TCOM, TCOP, TDRC, TIT2, TOPE, TPE1, TRCK, TSRC, USLT
|
from mutagen.id3 import APIC, TALB, TCOM, TCOP, TDRC, TIT2, TOPE, TPE1, TRCK, TSRC, USLT
|
||||||
|
|
||||||
from tidal_dl_ng.constants import REQUESTS_TIMEOUT_SEC
|
|
||||||
|
|
||||||
|
|
||||||
class Metadata:
|
class Metadata:
|
||||||
path_file: str
|
path_file: str
|
||||||
@@ -22,7 +19,7 @@ class Metadata:
|
|||||||
isrc: str
|
isrc: str
|
||||||
lyrics: str
|
lyrics: str
|
||||||
path_cover: str
|
path_cover: str
|
||||||
url_cover: str
|
cover_data: bytes
|
||||||
m: mutagen.mp4.MP4 | mutagen.mp4.MP4 | mutagen.flac.FLAC
|
m: mutagen.mp4.MP4 | mutagen.mp4.MP4 | mutagen.flac.FLAC
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -41,8 +38,7 @@ class Metadata:
|
|||||||
albumartist: str = "",
|
albumartist: str = "",
|
||||||
date: str = "",
|
date: str = "",
|
||||||
lyrics: str = "",
|
lyrics: str = "",
|
||||||
path_cover: str = "",
|
cover_data: bytes = None,
|
||||||
url_cover: str = "",
|
|
||||||
):
|
):
|
||||||
self.path_file = path_file
|
self.path_file = path_file
|
||||||
self.title = title
|
self.title = title
|
||||||
@@ -58,27 +54,25 @@ class Metadata:
|
|||||||
self.composer = composer
|
self.composer = composer
|
||||||
self.isrc = isrc
|
self.isrc = isrc
|
||||||
self.lyrics = lyrics
|
self.lyrics = lyrics
|
||||||
self.path_cover = path_cover
|
self.cover_data = cover_data
|
||||||
self.url_cover = url_cover
|
|
||||||
self.m: mutagen.mp4.MP4 | mutagen.flac.FLAC | mutagen.mp3.MP3 = mutagen.File(self.path_file)
|
self.m: mutagen.mp4.MP4 | mutagen.flac.FLAC | mutagen.mp3.MP3 = mutagen.File(self.path_file)
|
||||||
|
|
||||||
def _cover(self) -> bool:
|
def _cover(self) -> bool:
|
||||||
result: bool = False
|
result: bool = False
|
||||||
data_cover: str | bytes = self.cover_data(url=self.url_cover, path_file=self.path_cover)
|
|
||||||
|
|
||||||
if data_cover:
|
if self.cover_data:
|
||||||
if isinstance(self.m, mutagen.flac.FLAC):
|
if isinstance(self.m, mutagen.flac.FLAC):
|
||||||
flac_cover = flac.Picture()
|
flac_cover = flac.Picture()
|
||||||
flac_cover.type = id3.PictureType.COVER_FRONT
|
flac_cover.type = id3.PictureType.COVER_FRONT
|
||||||
flac_cover.data = data_cover
|
flac_cover.data = self.cover_data
|
||||||
flac_cover.mime = "image/jpeg"
|
flac_cover.mime = "image/jpeg"
|
||||||
|
|
||||||
self.m.clear_pictures()
|
self.m.clear_pictures()
|
||||||
self.m.add_picture(flac_cover)
|
self.m.add_picture(flac_cover)
|
||||||
elif isinstance(self.m, mutagen.mp3.MP3):
|
elif isinstance(self.m, mutagen.mp3.MP3):
|
||||||
self.m.tags.add(APIC(encoding=3, data=data_cover))
|
self.m.tags.add(APIC(encoding=3, data=self.cover_data))
|
||||||
elif isinstance(self.m, mutagen.mp4.MP4):
|
elif isinstance(self.m, mutagen.mp4.MP4):
|
||||||
cover_mp4 = mp4.MP4Cover(data_cover)
|
cover_mp4 = mp4.MP4Cover(self.cover_data)
|
||||||
self.m.tags["covr"] = [cover_mp4]
|
self.m.tags["covr"] = [cover_mp4]
|
||||||
|
|
||||||
result = True
|
result = True
|
||||||
@@ -142,23 +136,3 @@ class Metadata:
|
|||||||
self.m.tags["\xa9wrt"] = ", ".join(self.composer) if self.composer else ""
|
self.m.tags["\xa9wrt"] = ", ".join(self.composer) if self.composer else ""
|
||||||
self.m.tags["\xa9lyr"] = self.lyrics
|
self.m.tags["\xa9lyr"] = self.lyrics
|
||||||
self.m.tags["isrc"] = self.isrc
|
self.m.tags["isrc"] = self.isrc
|
||||||
|
|
||||||
@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
|
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ from tidal_dl_ng.constants import CoverDimensions, QualityVideo, SkipExisting
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Settings:
|
class Settings:
|
||||||
skip_existing: SkipExisting = SkipExisting.Disabled
|
skip_existing: SkipExisting = SkipExisting.Disabled
|
||||||
# TODO: Implement cover download to a separate file.
|
|
||||||
# album_cover_save: bool = True
|
|
||||||
lyrics_embed: bool = False
|
lyrics_embed: bool = False
|
||||||
lyrics_file: bool = False
|
lyrics_file: bool = False
|
||||||
# TODO: Implement API KEY selection.
|
# TODO: Implement API KEY selection.
|
||||||
@@ -36,6 +34,8 @@ class Settings:
|
|||||||
video_convert_mp4: bool = True
|
video_convert_mp4: bool = True
|
||||||
path_binary_ffmpeg: str = ""
|
path_binary_ffmpeg: str = ""
|
||||||
metadata_cover_dimension: CoverDimensions = CoverDimensions.Px320
|
metadata_cover_dimension: CoverDimensions = CoverDimensions.Px320
|
||||||
|
metadata_cover_embed: bool = True
|
||||||
|
cover_album_file: bool = True
|
||||||
extract_flac: bool = True
|
extract_flac: bool = True
|
||||||
|
|
||||||
|
|
||||||
@@ -79,6 +79,8 @@ class HelpSettings:
|
|||||||
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, 640x640x 1280x1280."
|
||||||
)
|
)
|
||||||
|
metadata_cover_embed: str = "Embed album cover into file."
|
||||||
|
cover_album_file: str = "Save cover to 'cover.jpg', if an album is downloaded."
|
||||||
extract_flac: str = "Extract FLAC audio tracks from MP4 containers and save them as `*.flac` (uses FFmpeg)."
|
extract_flac: str = "Extract FLAC audio tracks from MP4 containers and save them as `*.flac` (uses FFmpeg)."
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class ResultItem:
|
|||||||
obj: object
|
obj: object
|
||||||
quality: str
|
quality: str
|
||||||
explicit: bool
|
explicit: bool
|
||||||
|
date_user_added: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ class Ui_DialogSettings:
|
|||||||
|
|
||||||
self.lh_flags_2 = QHBoxLayout()
|
self.lh_flags_2 = QHBoxLayout()
|
||||||
self.lh_flags_2.setObjectName("lh_flags_2")
|
self.lh_flags_2.setObjectName("lh_flags_2")
|
||||||
self.lh_flag_lyrics_embed = QHBoxLayout()
|
self.lv_flag_lyrics_embed = QVBoxLayout()
|
||||||
self.lh_flag_lyrics_embed.setObjectName("lh_flag_lyrics_embed")
|
self.lv_flag_lyrics_embed.setObjectName("lv_flag_lyrics_embed")
|
||||||
self.cb_lyrics_embed = QCheckBox(self.gb_flags)
|
self.cb_lyrics_embed = QCheckBox(self.gb_flags)
|
||||||
self.cb_lyrics_embed.setObjectName("cb_lyrics_embed")
|
self.cb_lyrics_embed.setObjectName("cb_lyrics_embed")
|
||||||
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||||
@@ -88,20 +88,20 @@ class Ui_DialogSettings:
|
|||||||
sizePolicy2.setHeightForWidth(self.cb_lyrics_embed.sizePolicy().hasHeightForWidth())
|
sizePolicy2.setHeightForWidth(self.cb_lyrics_embed.sizePolicy().hasHeightForWidth())
|
||||||
self.cb_lyrics_embed.setSizePolicy(sizePolicy2)
|
self.cb_lyrics_embed.setSizePolicy(sizePolicy2)
|
||||||
|
|
||||||
self.lh_flag_lyrics_embed.addWidget(self.cb_lyrics_embed)
|
self.lv_flag_lyrics_embed.addWidget(self.cb_lyrics_embed)
|
||||||
|
|
||||||
self.lh_flags_2.addLayout(self.lh_flag_lyrics_embed)
|
self.lh_flags_2.addLayout(self.lv_flag_lyrics_embed)
|
||||||
|
|
||||||
self.lh_flag_lyrics_file = QHBoxLayout()
|
self.lv_flag_lyrics_file = QVBoxLayout()
|
||||||
self.lh_flag_lyrics_file.setObjectName("lh_flag_lyrics_file")
|
self.lv_flag_lyrics_file.setObjectName("lv_flag_lyrics_file")
|
||||||
self.cb_lyrics_file = QCheckBox(self.gb_flags)
|
self.cb_lyrics_file = QCheckBox(self.gb_flags)
|
||||||
self.cb_lyrics_file.setObjectName("cb_lyrics_file")
|
self.cb_lyrics_file.setObjectName("cb_lyrics_file")
|
||||||
sizePolicy1.setHeightForWidth(self.cb_lyrics_file.sizePolicy().hasHeightForWidth())
|
sizePolicy1.setHeightForWidth(self.cb_lyrics_file.sizePolicy().hasHeightForWidth())
|
||||||
self.cb_lyrics_file.setSizePolicy(sizePolicy1)
|
self.cb_lyrics_file.setSizePolicy(sizePolicy1)
|
||||||
|
|
||||||
self.lh_flag_lyrics_file.addWidget(self.cb_lyrics_file)
|
self.lv_flag_lyrics_file.addWidget(self.cb_lyrics_file)
|
||||||
|
|
||||||
self.lh_flags_2.addLayout(self.lh_flag_lyrics_file)
|
self.lh_flags_2.addLayout(self.lv_flag_lyrics_file)
|
||||||
|
|
||||||
self.lv_flags.addLayout(self.lh_flags_2)
|
self.lv_flags.addLayout(self.lh_flags_2)
|
||||||
|
|
||||||
@@ -131,6 +131,28 @@ class Ui_DialogSettings:
|
|||||||
|
|
||||||
self.lv_flags.addLayout(self.lh_flag_3)
|
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.lv_main.addWidget(self.gb_flags)
|
self.lv_main.addWidget(self.gb_flags)
|
||||||
|
|
||||||
self.gb_choices = QGroupBox(DialogSettings)
|
self.gb_choices = QGroupBox(DialogSettings)
|
||||||
@@ -508,6 +530,8 @@ class Ui_DialogSettings:
|
|||||||
self.cb_lyrics_file.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_download_delay.setText(QCoreApplication.translate("DialogSettings", "CheckBox", None))
|
||||||
self.cb_extract_flac.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.gb_choices.setTitle(QCoreApplication.translate("DialogSettings", "Choices", None))
|
self.gb_choices.setTitle(QCoreApplication.translate("DialogSettings", "Choices", None))
|
||||||
self.l_icon_skip_existing.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
|
self.l_icon_skip_existing.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
|
||||||
self.l_skip_existing.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
|
self.l_skip_existing.setText(QCoreApplication.translate("DialogSettings", "TextLabel", None))
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="lh_flags_2">
|
<layout class="QHBoxLayout" name="lh_flags_2">
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="lh_flag_lyrics_embed">
|
<layout class="QVBoxLayout" name="lv_flag_lyrics_embed">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="cb_lyrics_embed">
|
<widget class="QCheckBox" name="cb_lyrics_embed">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="lh_flag_lyrics_file">
|
<layout class="QVBoxLayout" name="lv_flag_lyrics_file">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="cb_lyrics_file">
|
<widget class="QCheckBox" name="cb_lyrics_file">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
@@ -184,6 +184,32 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="lh_flags_4">
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="lv_flag_metadata_cover_embed">
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="cb_metadata_cover_embed">
|
||||||
|
<property name="text">
|
||||||
|
<string>CheckBox</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="lv_flag_cover_album_file">
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="cb_cover_album_file">
|
||||||
|
<property name="text">
|
||||||
|
<string>CheckBox</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
Reference in New Issue
Block a user