summaryrefslogtreecommitdiff
path: root/tidal_dl_ng/gui.py
diff options
context:
space:
mode:
authorexislow <exislow@users.noreply.github.com>2025-07-03 09:57:26 +0200
committerGitHub <noreply@github.com>2025-07-03 09:57:26 +0200
commit7da0d4c41e4abb6ec03ee1b0f64474ee970a230e (patch)
treea5e625565e2439ad92c590f7f9d2aeaf49824fe9 /tidal_dl_ng/gui.py
parent29ea5c51825628dbe96ce4404488b87d5fadb80d (diff)
parent4662d18fdfc25d42d496cf6abb1c8d44be0c76dd (diff)
downloadtidal-dl-ng-7da0d4c41e4abb6ec03ee1b0f64474ee970a230e.tar.gz
Merge pull request #478 from exislow/ai-refactoring
Diffstat (limited to 'tidal_dl_ng/gui.py')
-rw-r--r--tidal_dl_ng/gui.py810
1 files changed, 629 insertions, 181 deletions
diff --git a/tidal_dl_ng/gui.py b/tidal_dl_ng/gui.py
index 73da806..80dc07e 100644
--- a/tidal_dl_ng/gui.py
+++ b/tidal_dl_ng/gui.py
@@ -47,7 +47,8 @@
import math
import sys
import time
-from collections.abc import Callable, Sequence
+from collections.abc import Callable, Iterable, Sequence
+from typing import Any
from requests.exceptions import HTTPError
from tidalapi.session import LinkLogin
@@ -106,6 +107,11 @@ from tidal_dl_ng.worker import Worker
# TODO: Make more use of Exceptions
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
+ """Main application window for TIDAL Downloader Next Generation.
+
+ Handles GUI setup, user interactions, and download logic.
+ """
+
settings: Settings
tidal: Tidal
dl: Download
@@ -137,7 +143,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
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):
+ def __init__(self, tidal: Tidal | None = None) -> None:
+ """Initialize the main window and all components.
+
+ Args:
+ tidal (Tidal | None): Optional Tidal session object.
+ """
super().__init__()
self.setupUi(self)
# self.setGeometry(50, 50, 500, 300)
@@ -167,10 +178,16 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
logger_gui.debug("All setup.")
- def _init_gui(self):
- self.spinners = {}
+ def _init_gui(self) -> None:
+ """Initialize GUI-specific variables and state."""
+ self.spinners: dict[QtWidgets.QWidget, QtWaitingSpinner] = {}
+
+ def init_tidal(self, tidal: Tidal | None = None):
+ """Initialize Tidal session and handle login flow.
- def init_tidal(self, tidal: Tidal = None):
+ Args:
+ tidal (Tidal, optional): Existing Tidal session. Defaults to None.
+ """
result: bool = False
if tidal:
@@ -185,10 +202,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
while not result:
link_login: LinkLogin = self.tidal.session.get_link_login()
+ expires_in = int(link_login.expires_in) if hasattr(link_login, "expires_in") else 0
d_login: DialogLogin = DialogLogin(
url_login=link_login.verification_uri_complete,
hint=hint,
- expires_in=link_login.expires_in,
+ expires_in=expires_in,
parent=self,
)
@@ -211,10 +229,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.thread_it(self.tidal_user_lists)
def _init_threads(self):
+ """Initialize thread pool and start background workers."""
self.threadpool = QtCore.QThreadPool()
self.thread_it(self.watcher_queue_download)
def _init_dl(self):
+ """Initialize Download object and related progress bars."""
# Init `Download` object.
data_pb: ProgressBars = ProgressBars(
item=self.s_item_advance,
@@ -236,6 +256,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
)
def _init_progressbar(self):
+ """Initialize and add progress bars to the status bar."""
self.pb_list = QtWidgets.QProgressBar()
self.pb_item = QtWidgets.QProgressBar()
pbs = [self.pb_list, self.pb_item]
@@ -246,18 +267,30 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.statusbar.addPermanentWidget(pb)
def _init_info(self):
+ """Set default album cover image in the GUI."""
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):
+ """Reset progress bars to zero."""
self.pb_list.setValue(0)
self.pb_item.setValue(0)
def on_statusbar_message(self, data: StatusbarMessage):
+ """Show a message in the status bar.
+
+ Args:
+ data (StatusbarMessage): Message and timeout.
+ """
self.statusbar.showMessage(data.message, data.timeout)
- def _log_output(self, text):
+ def _log_output(self, text: str) -> None:
+ """Redirect log output to the debug text area.
+
+ Args:
+ text (str): Log message.
+ """
display_msg = coloredlogs.converter.convert(text)
cursor: QtGui.QTextCursor = self.te_debug.textCursor()
@@ -267,21 +300,33 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.te_debug.setTextCursor(cursor)
self.te_debug.ensureCursorVisible()
- def _populate_quality(self, ui_target: QtWidgets.QComboBox, options: type[Quality | QualityVideo]):
+ def _populate_quality(self, ui_target: QtWidgets.QComboBox, options: Iterable[Any]) -> None:
+ """Populate a combo box with quality options.
+
+ Args:
+ ui_target (QComboBox): Target combo box.
+ options (Iterable): Enum of quality options.
+ """
for item in options:
ui_target.addItem(item.name, item)
- def _populate_search_types(self, ui_target: QtWidgets.QComboBox, options: SearchTypes):
+ def _populate_search_types(self, ui_target: QtWidgets.QComboBox, options: Iterable[Any]) -> None:
+ """Populate a combo box with search type options.
+
+ Args:
+ ui_target (QComboBox): Target combo box.
+ options (Iterable): Enum of search types.
+ """
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 = []
-
+ def handle_filter_activated(self) -> None:
+ """Handle activation of filter headers in the results tree."""
+ header = self.tr_results.header()
+ filters: list[tuple[int, str]] = []
for i in range(header.count()):
text: str = header.filter_text(i)
@@ -292,6 +337,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
proxy_model.filters = filters
def _init_tree_results(self, tree: QtWidgets.QTreeView, model: QtGui.QStandardItemModel) -> None:
+ """Initialize the results tree view and its model.
+
+ Args:
+ tree (QTreeView): The tree view widget.
+ model (QStandardItemModel): The model for the tree.
+ """
header: FilterHeader = FilterHeader(tree)
self.proxy_tr_results: HumanProxyModel = HumanProxyModel(self)
@@ -313,31 +364,48 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
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"]
+ """Initialize the model for the results tree view.
+
+ Args:
+ model (QStandardItemModel): The model to initialize.
+ """
+ labels_column: list[str] = ["#", "obj", "Artist", "Title", "Album", "Duration", "Quality", "Date"]
model.setColumnCount(len(labels_column))
model.setRowCount(0)
model.setHorizontalHeaderLabels(labels_column)
- def _init_tree_queue(self, tree: QtWidgets.QTableWidget):
+ def _init_tree_queue(self, tree: QtWidgets.QTableWidget) -> None:
+ """Initialize the download queue table widget.
+
+ Args:
+ tree (QTableWidget): The table widget.
+ """
tree.setColumnHidden(1, True)
tree.setColumnWidth(2, 200)
header = tree.header()
- header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
- header.setStretchLastSection(False)
+ if hasattr(header, "setSectionResizeMode"):
+ header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
+ tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
- def tidal_user_lists(self):
+ def tidal_user_lists(self) -> None:
+ """Fetch and emit user playlists, mixes, and favorites from Tidal."""
# 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)
+ user_all: list[Any] = 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]):
+ def on_populate_tree_lists(self, user_lists: list[Playlist | UserPlaylist | Mix]) -> None:
+ """Populate the user lists tree with playlists, mixes, and favorites.
+
+ Args:
+ user_lists (list[Playlist | UserPlaylist | Mix]): List of user playlists, mixes, and favorites.
+ """
twi_playlists: QtWidgets.QTreeWidgetItem = self.tr_lists_user.findItems(
TidalLists.Playlists, QtCore.Qt.MatchExactly, 0
)[0]
@@ -357,7 +425,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
for item in user_lists:
if isinstance(item, UserPlaylist | Playlist):
twi_child = QtWidgets.QTreeWidgetItem(twi_playlists)
- name: str = item.name
+ name: str = item.name if getattr(item, "name", None) is not None else ""
description: str = f" {item.description}" if item.description else ""
info: str = f"({item.num_tracks + item.num_videos} Tracks){description}"
elif isinstance(item, Mix):
@@ -389,7 +457,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.s_spinner_stop.emit()
self.s_pb_reload_status.emit(True)
- def _init_tree_lists(self, tree: QtWidgets.QTreeWidget):
+ def _init_tree_lists(self, tree: QtWidgets.QTreeWidget) -> None:
+ """Initialize the user lists tree widget.
+
+ Args:
+ tree (QTreeWidget): The tree widget.
+ """
# Adjust Tree.
tree.setColumnWidth(0, 200)
tree.setColumnHidden(1, True)
@@ -400,19 +473,31 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
tree.customContextMenuRequested.connect(self.menu_context_tree_lists)
- def on_update_check(self, on_startup: bool = True):
+ def on_update_check(self, on_startup: bool = True) -> None:
+ """Check for application updates and emit update signals.
+
+ Args:
+ on_startup (bool, optional): Whether this is called on startup. Defaults to 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},
+ def apply_settings(self, settings: Settings) -> None:
+ """Apply user settings to the GUI.
+
+ Args:
+ settings (Settings): The settings object.
+ """
+ quality_audio = getattr(getattr(settings, "data", None), "quality_audio", 1)
+ quality_video = getattr(getattr(settings, "data", None), "quality_video", 0)
+ elements = [
+ {"element": self.cb_quality_audio, "setting": quality_audio, "default_id": 1},
+ {"element": self.cb_quality_video, "setting": quality_video, "default_id": 0},
]
- for item in l_cb:
+ for item in elements:
idx = item["element"].findData(item["setting"])
if idx > -1:
@@ -420,7 +505,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
else:
item["element"].setCurrentIndex(item["default_id"])
- def on_spinner_start(self, parent: QtWidgets.QWidget):
+ def on_spinner_start(self, parent: QtWidgets.QWidget) -> None:
+ """Start a loading spinner on the given parent widget.
+
+ Args:
+ parent (QWidget): The parent widget.
+ """
# Stop any existing spinner for this parent
if parent in self.spinners:
spinner = self.spinners[parent]
@@ -437,7 +527,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.spinners[parent] = spinner
- def on_spinner_stop(self):
+ def on_spinner_stop(self) -> None:
+ """Stop all active loading spinners."""
# Stop all spinners
for spinner in list(self.spinners.values()):
spinner.stop()
@@ -445,7 +536,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.spinners.clear()
- def menu_context_tree_lists(self, point: QtCore.QPoint):
+ def menu_context_tree_lists(self, point: QtCore.QPoint) -> None:
+ """Show context menu for user lists tree.
+
+ Args:
+ point (QPoint): The point where the menu is requested.
+ """
# Infos about the node selected.
index = self.tr_lists_user.indexAt(point)
@@ -460,7 +556,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
menu.exec(self.tr_lists_user.mapToGlobal(point))
- def menu_context_tree_results(self, point: QtCore.QPoint):
+ def menu_context_tree_results(self, point: QtCore.QPoint) -> None:
+ """Show context menu for results tree.
+
+ Args:
+ point (QPoint): The point where the menu is requested.
+ """
# Infos about the node selected.
index = self.tr_results.indexAt(point)
@@ -474,10 +575,23 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
menu.exec(self.tr_results.mapToGlobal(point))
- def thread_download_list_media(self, point: QtCore.QPoint):
+ def thread_download_list_media(self, point: QtCore.QPoint) -> None:
+ """Start download of a list media item in a thread.
+
+ Args:
+ point (QPoint): The point in the tree.
+ """
self.thread_it(self.on_download_list_media, point)
- def on_copy_url_share(self, tree_target: QtWidgets.QTreeWidget | QtWidgets.QTreeView, point: QtCore.QPoint = None):
+ def on_copy_url_share(
+ self, tree_target: QtWidgets.QTreeWidget | QtWidgets.QTreeView, point: QtCore.QPoint = None
+ ) -> None:
+ """Copy the share URL of a media item to the clipboard.
+
+ Args:
+ tree_target (QTreeWidget | QTreeView): The tree widget.
+ point (QPoint, optional): The point in the tree. Defaults to None.
+ """
if isinstance(tree_target, QtWidgets.QTreeWidget):
item: QtWidgets.QTreeWidgetItem = tree_target.itemAt(point)
@@ -494,8 +608,13 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
clipboard.clear()
clipboard.setText(url_share)
- def on_download_list_media(self, point: QtCore.QPoint = None):
- items: [QtWidgets.QTreeWidgetItem]
+ def on_download_list_media(self, point: QtCore.QPoint | None = None) -> None:
+ """Download all media items in a selected list.
+
+ Args:
+ point (QPoint | None, optional): The point in the tree. Defaults to None.
+ """
+ items: list[QtWidgets.QTreeWidgetItem] = []
if point:
items = [self.tr_lists_user.itemAt(point)]
@@ -507,19 +626,29 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
for item in items:
media = get_user_list_media_item(item)
- queue_dl_item: QueueDownloadItem | False = self.media_to_queue_download_model(media)
+ queue_dl_item: QueueDownloadItem | None = 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())
+ def search_populate_results(self, query: str, type_media: Any) -> None:
+ """Populate the results tree with search results.
- results: [ResultItem] = self.search(query, [type_media])
+ Args:
+ query (str): The search query.
+ type_media (SearchTypes): The type of media to search for.
+ """
+ results: list[ResultItem] = self.search(query, [type_media])
self.populate_tree_results(results)
- def populate_tree_results(self, results: [ResultItem], parent: QtGui.QStandardItem = None):
+ def populate_tree_results(self, results: list[ResultItem], parent: QtGui.QStandardItem | None = None) -> None:
+ """Populate the results tree with ResultItem objects.
+
+ Args:
+ results (list[ResultItem]): The results to display.
+ parent (QStandardItem, optional): Parent item for nested results. Defaults to None.
+ """
if not parent:
self.model_tr_results.removeRows(0, self.model_tr_results.rowCount())
@@ -535,6 +664,15 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.s_tr_results_add_top_level_item.emit(child)
def populate_tree_result_child(self, item: ResultItem, index_count_digits: int) -> Sequence[QtGui.QStandardItem]:
+ """Create a row of QStandardItems for a ResultItem.
+
+ Args:
+ item (ResultItem): The result item.
+ index_count_digits (int): Number of digits for index formatting.
+
+ Returns:
+ Sequence[QStandardItem]: The row of items.
+ """
duration: str = ""
# TODO: Duration needs to be calculated later to properly fill with zeros.
@@ -582,14 +720,29 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
)
def on_tr_results_add_top_level_item(self, item_child: Sequence[QtGui.QStandardItem]):
+ """Add a top-level item to the results tree model.
+
+ Args:
+ item_child (Sequence[QStandardItem]): The row to add.
+ """
self.model_tr_results.appendRow(item_child)
- def on_settings_save(self):
+ def on_settings_save(self) -> None:
+ """Save settings and re-apply them to the GUI."""
self.settings.save()
self.apply_settings(self.settings)
self._init_dl()
- def search(self, query: str, types_media: SearchTypes) -> [ResultItem]:
+ def search(self, query: str, types_media: list[Any]) -> list[ResultItem]:
+ """Perform a search and return a list of ResultItems.
+
+ Args:
+ query (str): The search query.
+ types_media (list[Any]): The types of media to search for.
+
+ Returns:
+ list[ResultItem]: The search results.
+ """
query = query.strip()
# If a direct link was searched for, skip search and create the object from the link directly.
@@ -606,11 +759,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
result_search = {"direct": [media]}
else:
- result_search: dict[str, [SearchTypes]] = search_results_all(
+ result_search: dict[str, list[SearchTypes]] = search_results_all(
session=self.tidal.session, needle=query, types_media=types_media
)
- result: [ResultItem] = []
+ result: list[ResultItem] = []
for _media_type, l_media in result_search.items():
if isinstance(l_media, list):
@@ -618,129 +771,257 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
return result
- def search_result_to_model(self, items: [*SearchTypes]) -> [ResultItem]:
- result = []
+ def search_result_to_model(self, items: list[SearchTypes]) -> list[ResultItem]:
+ """Convert search results to ResultItem models.
+
+ Args:
+ items (list[SearchTypes]): List of search result items.
+
+ Returns:
+ list[ResultItem]: List of ResultItem models.
+ """
+ result: list[ResultItem] = []
for idx, item in enumerate(items):
- if not item:
- continue
+ result_item = self._to_result_item(idx, item)
- explicit: str = ""
- # Check if item is available on TIDAL.
- if hasattr(item, "available") and not item.available:
- continue
+ if result_item is not None:
+ result.append(result_item)
- if isinstance(item, Track | Video | Album):
- explicit = " 🅴" if item.explicit else ""
+ return result
- date_user_added: str = item.user_date_added.strftime("%Y-%m-%d_%H:%M") if item.user_date_added else ""
- date_release: str = (
- item.album.release_date.strftime("%Y-%m-%d_%H:%M")
- if hasattr(item, "album") and item.album and item.album.release_date
- else (
- item.release_date.strftime("%Y-%m-%d_%H:%M")
- if hasattr(item, "release_date") and item.release_date
- else ""
- )
- )
+ def _to_result_item(self, idx: int, item) -> ResultItem | None:
+ """Helper to convert a single item to ResultItem, or None if not valid.
- 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,
- date_release=date_release,
- )
+ Args:
+ idx (int): Index of the item.
+ item: The item to convert.
- 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,
- date_release=date_release,
- )
+ Returns:
+ ResultItem | None: The converted ResultItem or None if not valid.
+ """
+ if not item:
+ return None
- 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,
- date_release=date_release,
- )
+ if hasattr(item, "available") and not item.available:
+ return None
- 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,
- date_release=date_release,
- )
+ explicit: str = ""
+ if isinstance(item, Track | Video | Album):
+ explicit = " 🅴" if item.explicit else ""
- 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,
- date_release=date_release,
- )
+ date_user_added: str = (
+ item.user_date_added.strftime("%Y-%m-%d_%H:%M") if getattr(item, "user_date_added", None) else ""
+ )
+ date_release: str = self._get_date_release(item)
- 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,
- date_release=date_release,
- )
+ if isinstance(item, Track):
+ return self._result_item_from_track(idx, item, explicit, date_user_added, date_release)
- result.append(result_item)
+ if isinstance(item, Video):
+ return self._result_item_from_video(idx, item, explicit, date_user_added, date_release)
- return result
+ if isinstance(item, Playlist):
+ return self._result_item_from_playlist(idx, item, date_user_added, date_release)
+
+ if isinstance(item, Album):
+ return self._result_item_from_album(idx, item, explicit, date_user_added, date_release)
+
+ if isinstance(item, Mix):
+ return self._result_item_from_mix(idx, item, date_user_added, date_release)
+
+ if isinstance(item, Artist):
+ return self._result_item_from_artist(idx, item, date_user_added, date_release)
+
+ return None
+
+ def _get_date_release(self, item) -> str:
+ """Get the release date string for an item.
+
+ Args:
+ item: The item to extract the release date from.
+
+ Returns:
+ str: The formatted release date or empty string.
+ """
+ if hasattr(item, "album") and item.album and getattr(item.album, "release_date", None):
+ return item.album.release_date.strftime("%Y-%m-%d_%H:%M")
+
+ if hasattr(item, "release_date") and item.release_date:
+ return item.release_date.strftime("%Y-%m-%d_%H:%M")
+
+ return ""
+
+ def _result_item_from_track(
+ self, idx: int, item, explicit: str, date_user_added: str, date_release: str
+ ) -> ResultItem:
+ """Create a ResultItem from a Track.
+
+ Args:
+ idx (int): Index of the item.
+ item: The Track item.
+ explicit (str): Explicit tag.
+ date_user_added (str): Date user added.
+ date_release (str): Release date.
+
+ Returns:
+ ResultItem: The constructed ResultItem.
+ """
+ return 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,
+ date_release=date_release,
+ )
+
+ def _result_item_from_video(
+ self, idx: int, item, explicit: str, date_user_added: str, date_release: str
+ ) -> ResultItem:
+ """Create a ResultItem from a Video.
+
+ Args:
+ idx (int): Index of the item.
+ item: The Video item.
+ explicit (str): Explicit tag.
+ date_user_added (str): Date user added.
+ date_release (str): Release date.
+
+ Returns:
+ ResultItem: The constructed ResultItem.
+ """
+ return 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,
+ date_release=date_release,
+ )
+
+ def _result_item_from_playlist(self, idx: int, item, date_user_added: str, date_release: str) -> ResultItem:
+ """Create a ResultItem from a Playlist.
+
+ Args:
+ idx (int): Index of the item.
+ item: The Playlist item.
+ date_user_added (str): Date user added.
+ date_release (str): Release date.
+
+ Returns:
+ ResultItem: The constructed ResultItem.
+ """
+ return 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,
+ date_release=date_release,
+ )
+
+ def _result_item_from_album(
+ self, idx: int, item, explicit: str, date_user_added: str, date_release: str
+ ) -> ResultItem:
+ """Create a ResultItem from an Album.
+
+ Args:
+ idx (int): Index of the item.
+ item: The Album item.
+ explicit (str): Explicit tag.
+ date_user_added (str): Date user added.
+ date_release (str): Release date.
+
+ Returns:
+ ResultItem: The constructed ResultItem.
+ """
+ return 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,
+ date_release=date_release,
+ )
+
+ def _result_item_from_mix(self, idx: int, item, date_user_added: str, date_release: str) -> ResultItem:
+ """Create a ResultItem from a Mix.
+
+ Args:
+ idx (int): Index of the item.
+ item: The Mix item.
+ date_user_added (str): Date user added.
+ date_release (str): Release date.
+
+ Returns:
+ ResultItem: The constructed ResultItem.
+ """
+ return ResultItem(
+ position=idx,
+ artist=item.sub_title,
+ title=item.title,
+ album="",
+ duration_sec=-1, # TODO: Calculate total duration.
+ obj=item,
+ quality="",
+ explicit=False,
+ date_user_added=date_user_added,
+ date_release=date_release,
+ )
+
+ def _result_item_from_artist(self, idx: int, item, date_user_added: str, date_release: str) -> ResultItem:
+ """Create a ResultItem from an Artist.
+
+ Args:
+ idx (int): Index of the item.
+ item: The Artist item.
+ date_user_added (str): Date user added.
+ date_release (str): Release date.
+
+ Returns:
+ ResultItem: The constructed ResultItem.
+ """
+ return ResultItem(
+ position=idx,
+ artist=item.name,
+ title="",
+ album="",
+ duration_sec=-1,
+ obj=item,
+ quality="",
+ explicit=False,
+ date_user_added=date_user_added,
+ date_release=date_release,
+ )
def media_to_queue_download_model(
self, media: Artist | Track | Video | Album | Playlist | Mix
) -> QueueDownloadItem | bool:
+ """Convert a media object to a QueueDownloadItem for the download queue.
+
+ Args:
+ media (Artist | Track | Video | Album | Playlist | Mix): The media object.
+
+ Returns:
+ QueueDownloadItem | bool: The queue item or False if not available.
+ """
result: QueueDownloadItem | False
name: str = ""
quality_audio: Quality = self.settings.data.quality_audio
@@ -789,7 +1070,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
return result
- def _init_signals(self):
+ def _init_signals(self) -> None:
+ """Connect signals to their respective slots."""
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))
@@ -839,45 +1121,87 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
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 _init_buttons(self):
+ def _init_buttons(self) -> None:
+ """Initialize the state of the download buttons."""
self.pb_queue_download_run()
- def on_logout(self):
+ def on_logout(self) -> None:
+ """Log out from TIDAL and close the application."""
result: bool = self.tidal.logout()
if result:
sys.exit(0)
- def on_progress_list(self, value: float):
+ def on_progress_list(self, value: float) -> None:
+ """Update the progress of the list progress bar.
+
+ Args:
+ value (float): The progress value as a percentage.
+ """
self.pb_list.setValue(int(math.ceil(value)))
- def on_progress_item(self, value: float):
+ def on_progress_item(self, value: float) -> None:
+ """Update the progress of the item progress bar.
+
+ Args:
+ value (float): The progress value as a percentage.
+ """
self.pb_item.setValue(int(math.ceil(value)))
- def on_progress_item_name(self, value: str):
+ def on_progress_item_name(self, value: str) -> None:
+ """Set the format of the item progress bar.
+
+ Args:
+ value (str): The item name.
+ """
self.pb_item.setFormat(f"%p% {value}")
- def on_progress_list_name(self, value: str):
+ def on_progress_list_name(self, value: str) -> None:
+ """Set the format of the list progress bar.
+
+ Args:
+ value (str): The list name.
+ """
self.pb_list.setFormat(f"%p% {value}")
- def on_quality_set_audio(self, index):
+ def on_quality_set_audio(self, index: int) -> None:
+ """Set the audio quality for downloads.
+
+ Args:
+ index: The index of the selected quality in the combo box.
+ """
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):
+ def on_quality_set_video(self, index: int) -> None:
+ """Set the video quality for downloads.
+
+ Args:
+ index: The index of the selected quality in the combo box.
+ """
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):
+ def on_list_items_show(self, item: QtWidgets.QTreeWidgetItem) -> None:
+ """Show the items in the selected playlist or mix.
+
+ Args:
+ item (QtWidgets.QTreeWidgetItem): The selected tree widget item.
+ """
self.thread_it(self.list_items_show, item)
- def list_items_show(self, item: QtWidgets.QTreeWidgetItem):
+ def list_items_show(self, item: QtWidgets.QTreeWidgetItem) -> None:
+ """Fetch and display the items in a playlist or mix.
+
+ Args:
+ item (QtWidgets.QTreeWidgetItem): The tree widget item representing a playlist or mix.
+ """
media_list: Album | Playlist | str = get_user_list_media_item(item)
# Only if clicked item is not a top level item.
@@ -896,6 +1220,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.s_spinner_stop.emit()
def on_result_item_clicked(self, index: QtCore.QModelIndex) -> None:
+ """Handle the event when a result item is clicked.
+
+ Args:
+ index (QtCore.QModelIndex): The index of the clicked item.
+ """
media: Track | Video | Album | Artist = get_results_media_item(
index, self.proxy_tr_results, self.model_tr_results
)
@@ -904,12 +1233,23 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.thread_it(self.cover_show, media)
def on_queue_download_item_clicked(self, item: QtWidgets.QTreeWidgetItem, column: int) -> None:
+ """Handle the event when a queue download item is clicked.
+
+ Args:
+ item (QtWidgets.QTreeWidgetItem): The clicked tree widget item.
+ column (int): The column index of the clicked item.
+ """
media: Track | Video | Album | Artist | Mix | Playlist = get_queue_download_media(item)
# Load cover asynchronously to avoid blocking the GUI
self.thread_it(self.cover_show, media)
def cover_show(self, media: Album | Playlist | Track | Video | Album | Artist) -> None:
+ """Show the cover image of the selected media item.
+
+ Args:
+ media (Album | Playlist | Track | Video | Album | Artist): The media item.
+ """
cover_url: str = ""
# Show spinner in the cover label itself
parent_widget = self.l_pm_cover
@@ -948,9 +1288,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self,
media_list: Album | Playlist | Mix | Artist | None = None,
point: QtCore.QPoint | None = None,
- parent: QtGui.QStandardItem = None,
- favorite_function: Callable = None,
+ parent: QtGui.QStandardItem | None = None,
+ favorite_function: Callable | None = None,
) -> None:
+ """Populate the results tree with the items of a media list.
+
+ Args:
+ media_list (Album | Playlist | Mix | Artist | None, optional): The media list to show. Defaults to None.
+ point (QPoint | None, optional): The point in the tree. Defaults to None.
+ parent (QStandardItem | None, optional): Parent item for nested results. Defaults to None.
+ favorite_function (Callable | None, optional): Function to fetch favorite items. Defaults to None.
+ """
if point:
item = self.tr_lists_user.itemAt(point)
media_list = get_user_list_media_item(item)
@@ -960,39 +1308,54 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
if isinstance(media_list, str):
favorite_function = favorite_function_factory(self.tidal, media_list)
- media_items: [Track | Video | Album] = favorite_function()
+ media_items: list[Track | Video | Album] = favorite_function()
else:
- media_items: [Track | Video | Album] = items_results_all(media_list)
+ media_items: list[Track | Video | Album] = items_results_all(media_list)
- result: [ResultItem] = self.search_result_to_model(media_items)
+ result: list[ResultItem] = self.search_result_to_model(media_items)
self.populate_tree_results(result, parent=parent)
- def thread_it(self, fn: Callable, *args, **kwargs):
+ def thread_it(self, fn: Callable, *args, **kwargs) -> None:
+ """Run a function in a separate thread.
+
+ Args:
+ fn (Callable): The function to run.
+ *args: Positional arguments for the function.
+ **kwargs: Keyword arguments for the function.
+ """
# 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):
+ def on_queue_download_clear_all(self) -> None:
+ """Clear all items from the download queue."""
self.on_clear_queue_download(
f"({QueueDownloadStatus.Waiting}|{QueueDownloadStatus.Finished}|{QueueDownloadStatus.Failed})"
)
- def on_queue_download_clear_finished(self):
+ def on_queue_download_clear_finished(self) -> None:
+ """Clear finished items from the download queue."""
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(
+ def on_clear_queue_download(self, regex: str) -> None:
+ """Clear items from the download queue matching the given regex.
+
+ Args:
+ regex (str): Regular expression to match items.
+ """
+ items: list[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()
+ def on_queue_download_remove(self) -> None:
+ """Remove selected items from the download queue."""
+ items: list[QtWidgets.QTreeWidgetItem | None] = self.tr_queue_download.selectedItems()
if len(items) == 0:
logger_gui.error("Please select an item from the queue first.")
@@ -1017,7 +1380,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
else:
self.pb_queue_download_run()
- def pb_queue_download_run(self):
+ def pb_queue_download_run(self) -> None:
+ """Start the download queue and update the button state."""
handling_app: HandlingApp = HandlingApp()
handling_app.event_run.set()
@@ -1026,7 +1390,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.pb_queue_download_toggle.setIcon(icon)
self.pb_queue_download_toggle.setStyleSheet("background-color: #218838; color: #fff")
- def pb_queue_download_pause(self):
+ def pb_queue_download_pause(self) -> None:
+ """Pause the download queue and update the button state."""
handling_app: HandlingApp = HandlingApp()
handling_app.event_run.clear()
@@ -1037,6 +1402,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
# TODO: Must happen in main thread. Do not thread this.
def on_download_results(self) -> None:
+ """Download the selected results in the results tree."""
items: [HumanProxyModel | None] = self.tr_results.selectionModel().selectedRows()
if len(items) == 0:
@@ -1052,6 +1418,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.queue_download_media(queue_dl_item)
def queue_download_media(self, queue_dl_item: QueueDownloadItem) -> None:
+ """Add a media item to the download queue.
+
+ Args:
+ queue_dl_item (QueueDownloadItem): The item to add to the queue.
+ """
# Populate child
child: QtWidgets.QTreeWidgetItem = QtWidgets.QTreeWidgetItem()
@@ -1064,10 +1435,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.tr_queue_download.addTopLevelItem(child)
def watcher_queue_download(self) -> None:
+ """Monitor the download queue and process items as they become available."""
handling_app: HandlingApp = HandlingApp()
while not handling_app.event_abort.is_set():
- items: [QtWidgets.QTreeWidgetItem | None] = self.tr_queue_download.findItems(
+ items: list[QtWidgets.QTreeWidgetItem | None] = self.tr_queue_download.findItems(
QueueDownloadStatus.Waiting, QtCore.Qt.MatchFlag.MatchExactly, column=0
)
@@ -1093,18 +1465,44 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
time.sleep(2)
def on_queue_download_item_downloading(self, item: QtWidgets.QTreeWidgetItem) -> None:
+ """Update the status of a queue download item to 'Downloading'.
+
+ Args:
+ item (QtWidgets.QTreeWidgetItem): The item to update.
+ """
self.queue_download_item_status(item, QueueDownloadStatus.Downloading)
def on_queue_download_item_finished(self, item: QtWidgets.QTreeWidgetItem) -> None:
+ """Update the status of a queue download item to 'Finished'.
+
+ Args:
+ item (QtWidgets.QTreeWidgetItem): The item to update.
+ """
self.queue_download_item_status(item, QueueDownloadStatus.Finished)
def on_queue_download_item_failed(self, item: QtWidgets.QTreeWidgetItem) -> None:
+ """Update the status of a queue download item to 'Failed'.
+
+ Args:
+ item (QtWidgets.QTreeWidgetItem): The item to update.
+ """
self.queue_download_item_status(item, QueueDownloadStatus.Failed)
def on_queue_download_item_skipped(self, item: QtWidgets.QTreeWidgetItem) -> None:
+ """Update the status of a queue download item to 'Skipped'.
+
+ Args:
+ item (QtWidgets.QTreeWidgetItem): The item to update.
+ """
self.queue_download_item_status(item, QueueDownloadStatus.Skipped)
def queue_download_item_status(self, item: QtWidgets.QTreeWidgetItem, status: str) -> None:
+ """Set the status text of a queue download item.
+
+ Args:
+ item (QtWidgets.QTreeWidgetItem): The item to update.
+ status (str): The status text.
+ """
item.setText(0, status)
def on_queue_download(
@@ -1113,6 +1511,16 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
quality_audio: Quality | None = None,
quality_video: QualityVideo | None = None,
) -> QueueDownloadStatus:
+ """Download the specified media item(s) and return the result status.
+
+ Args:
+ media (Track | Album | Playlist | Video | Mix | Artist): The media item(s) to download.
+ quality_audio (Quality | None, optional): Desired audio quality. Defaults to None.
+ quality_video (QualityVideo | None, optional): Desired video quality. Defaults to None.
+
+ Returns:
+ QueueDownloadStatus: The status of the download operation.
+ """
result: QueueDownloadStatus
items_media: [Track | Album | Playlist | Video | Mix | Artist]
@@ -1142,6 +1550,18 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
quality_audio: Quality | None = None,
quality_video: QualityVideo | None = None,
) -> QueueDownloadStatus:
+ """Download a media item and return the result status.
+
+ Args:
+ media (Track | Album | Playlist | Video | Mix | Artist): The media item to download.
+ dl (Download): The Download object to use.
+ delay_track (bool, optional): Whether to apply download delay. Defaults to False.
+ quality_audio (Quality | None, optional): Desired audio quality. Defaults to None.
+ quality_video (QualityVideo | None, optional): Desired video quality. Defaults to None.
+
+ Returns:
+ QueueDownloadStatus: The status of the download operation.
+ """
result_dl: bool
path_file: str
result: QueueDownloadStatus
@@ -1184,24 +1604,42 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
return result
def on_version(
- self, update_check: bool = False, update_available: bool = False, update_info: ReleaseLatest = None
+ self, update_check: bool = False, update_available: bool = False, update_info: ReleaseLatest | None = None
) -> None:
+ """Show the version information dialog.
+
+ Args:
+ update_check (bool, optional): Whether to check for updates. Defaults to False.
+ update_available (bool, optional): Whether an update is available. Defaults to False.
+ update_info (ReleaseLatest | None, optional): Information about the latest release. Defaults to None.
+ """
DialogVersion(self, update_check, update_available, update_info)
def on_preferences(self) -> None:
+ """Open the preferences dialog."""
DialogPreferences(settings=self.settings, settings_save=self.s_settings_save, parent=self)
def on_tr_results_expanded(self, index: QtCore.QModelIndex) -> None:
+ """Handle the event when a result item group is expanded.
+
+ Args:
+ index (QtCore.QModelIndex): The index of the expanded item.
+ """
self.thread_it(self.tr_results_expanded, index)
def tr_results_expanded(self, index: QtCore.QModelIndex) -> None:
+ """Load and display the children of an expanded result item.
+
+ Args:
+ index (QtCore.QModelIndex): The index of the expanded item.
+ """
# 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(
+ media_list: list[Mix | Album | Playlist | Artist] = get_results_media_item(
index, self.proxy_tr_results, self.model_tr_results
)
@@ -1213,7 +1651,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
finally:
self.s_spinner_stop.emit()
- def button_reload_status(self, status: bool):
+ def button_reload_status(self, status: bool) -> None:
+ """Update the reload button's state and text.
+
+ Args:
+ status (bool): The new status.
+ """
button_text: str = "Reloading..."
if status:
@@ -1222,7 +1665,12 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
self.pb_reload_user_lists.setEnabled(status)
self.pb_reload_user_lists.setText(button_text)
- def closeEvent(self, event):
+ def closeEvent(self, event: QtGui.QCloseEvent) -> None:
+ """Handle the close event of the main window.
+
+ Args:
+ event (QtGui.QCloseEvent): The close event.
+ """
self.shutdown = True
handling_app: HandlingApp = HandlingApp()