Files
tidal-dl/tidal_dl_ng/gui/main_window.py
T

225 lines
8.3 KiB
Python

"""Main window for TIDAL Downloader Next Generation.
This module combines all GUI functionality through mixins to create
the main application window.
"""
from collections.abc import Callable
from PySide6 import QtCore, QtGui, QtWidgets
from tidalapi import Quality
from tidal_dl_ng.cache import TrackExtrasCache
from tidal_dl_ng.config import HandlingApp, Settings, Tidal
from tidal_dl_ng.constants import QualityVideo
from tidal_dl_ng.download import Download
from tidal_dl_ng.gui.context_menus import ContextMenusMixin
from tidal_dl_ng.gui.covers import CoverManager
from tidal_dl_ng.gui.downloads import DownloadsMixin
from tidal_dl_ng.gui.history import HistoryMixin
from tidal_dl_ng.gui.initialization import InitializationMixin
from tidal_dl_ng.gui.playlist import GuiPlaylistManager
from tidal_dl_ng.gui.progress import ProgressMixin
from tidal_dl_ng.gui.queue import GuiQueueManager
from tidal_dl_ng.gui.search import GuiSearchManager
from tidal_dl_ng.gui.signals import SignalsMixin
from tidal_dl_ng.gui.tidal_session import TidalSessionMixin
from tidal_dl_ng.gui.track_extras import TrackExtrasMixin
from tidal_dl_ng.gui.trees_results import TreesResultsMixin
from tidal_dl_ng.gui.ui_helpers import UIHelpersMixin
from tidal_dl_ng.gui.updates import UpdatesMixin
from tidal_dl_ng.helper.gui import HumanProxyModel
from tidal_dl_ng.helper.hover_manager import HoverManager
from tidal_dl_ng.history import HistoryService
from tidal_dl_ng.logger import XStream, logger_gui
from tidal_dl_ng.ui.info_tab_widget import InfoTabWidget
from tidal_dl_ng.ui.main import Ui_MainWindow
from tidal_dl_ng.worker import Worker
# TODO: Make more use of Exceptions
class MainWindow(
QtWidgets.QMainWindow,
Ui_MainWindow,
InitializationMixin,
TidalSessionMixin,
SignalsMixin,
ProgressMixin,
UIHelpersMixin,
TrackExtrasMixin,
UpdatesMixin,
DownloadsMixin,
TreesResultsMixin,
ContextMenusMixin,
HistoryMixin,
):
"""Main application window for TIDAL Downloader Next Generation.
Handles GUI setup, user interactions, and download logic through
a combination of mixins for better code organization.
"""
# Type hints for class attributes
settings: Settings
tidal: Tidal
dl: Download
history_service: HistoryService
threadpool: QtCore.QThreadPool
tray: QtWidgets.QSystemTrayIcon
spinners: dict
cover_url_current: str = ""
shutdown: bool = False
model_tr_results: QtGui.QStandardItemModel = QtGui.QStandardItemModel()
proxy_tr_results: HumanProxyModel
info_tab_widget: InfoTabWidget
hover_manager: HoverManager
queue_manager: GuiQueueManager
playlist_manager: GuiPlaylistManager
search_manager: GuiSearchManager
cover_manager: CoverManager
track_extras_cache: TrackExtrasCache
_pending_extras_workers: dict[str, Worker]
_track_extras_callbacks: dict[str, Callable]
# Qt Signals
s_spinner_start: QtCore.Signal = QtCore.Signal(QtWidgets.QWidget)
s_spinner_stop: QtCore.Signal = QtCore.Signal()
s_track_extras_ready: QtCore.Signal = QtCore.Signal(str, object)
s_invoke_callback: QtCore.Signal = QtCore.Signal(str, object)
pb_item: QtWidgets.QProgressBar
s_item_advance: QtCore.Signal = QtCore.Signal(float)
s_item_name: QtCore.Signal = QtCore.Signal(str)
s_list_name: QtCore.Signal = QtCore.Signal(str)
pb_list: QtWidgets.QProgressBar
s_list_advance: QtCore.Signal = QtCore.Signal(float)
s_pb_reset: QtCore.Signal = QtCore.Signal()
s_populate_tree_lists: QtCore.Signal = QtCore.Signal(dict)
s_populate_folder_children: QtCore.Signal = QtCore.Signal(object, list, list)
s_statusbar_message: QtCore.Signal = QtCore.Signal(object)
s_tr_results_add_top_level_item: QtCore.Signal = QtCore.Signal(object)
s_settings_save: QtCore.Signal = QtCore.Signal()
s_pb_reload_status: QtCore.Signal = QtCore.Signal(bool)
s_update_check: QtCore.Signal = QtCore.Signal(bool)
s_update_show: QtCore.Signal = QtCore.Signal(bool, bool, object)
s_queue_download_item_downloading: QtCore.Signal = QtCore.Signal(object)
s_queue_download_item_finished: QtCore.Signal = QtCore.Signal(object)
s_queue_download_item_failed: QtCore.Signal = QtCore.Signal(object)
s_queue_download_item_skipped: QtCore.Signal = QtCore.Signal(object)
def __init__(self, tidal: Tidal | None = None) -> None:
"""Initialize the main window and all components.
Args:
tidal (Tidal | None): Optional Tidal session object.
"""
super().__init__()
self.setupUi(self)
self.setWindowTitle("TIDAL Downloader Next Generation!")
# Initialize settings first
self.settings = Settings()
# Initialize managers that depend on settings
self.queue_manager = GuiQueueManager(self)
self.playlist_manager = GuiPlaylistManager(self)
self.search_manager = GuiSearchManager(self)
self.info_tab_widget = InfoTabWidget(self, self.tabWidget)
# Logging redirect
XStream.stdout().messageWritten.connect(self._log_output)
self.history_service = HistoryService()
# Core components
self._init_threads()
self._init_gui()
self.track_extras_cache = TrackExtrasCache()
self._pending_extras_workers: dict[str, Worker] = {}
self._track_extras_callbacks: dict[str, Callable] = {}
# Managers that have dependencies
self.cover_manager = CoverManager(self, self.threadpool, self.info_tab_widget)
# Initialize the rest of the UI
self.info_tab_widget.set_track_extras_provider(self.get_track_extras)
self._init_tree_results_model(self.model_tr_results)
self._init_tree_results(self.tr_results, self.model_tr_results)
self.playlist_manager.init_ui()
self.queue_manager.init_ui()
self._init_tree_lists(self.tr_lists_user)
self._init_tree_queue(self.tr_queue_download)
self._init_info()
self._init_progressbar()
self._populate_quality(self.cb_quality_audio, Quality)
self._populate_quality(self.cb_quality_video, QualityVideo)
from tidalapi.session import SearchTypes
self._populate_search_types(self.cb_search_type, SearchTypes)
self.apply_settings(self.settings)
self._init_menu_actions()
self._init_signals()
# Connect signal for invoking track extras callbacks
self.s_invoke_callback.connect(self._on_invoke_callback)
# Connect playlist manager signals
self.s_populate_tree_lists.connect(self.on_populate_tree_lists)
self.init_tidal(tidal)
logger_gui.debug("All setup.")
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
"""Handle the close event of the main window.
Args:
event (QtGui.QCloseEvent): The close event.
"""
# Save the main window size and position
self.settings.data.window_x = self.x()
self.settings.data.window_y = self.y()
self.settings.data.window_w = self.width()
self.settings.data.window_h = self.height()
self.settings.save()
self.shutdown = True
handling_app: HandlingApp = HandlingApp()
handling_app.event_abort.set()
event.accept()
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 elements:
idx = item["element"].findData(item["setting"])
if idx > -1:
item["element"].setCurrentIndex(idx)
else:
item["element"].setCurrentIndex(item["default_id"])
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.
"""
worker = Worker(fn, *args, **kwargs)
self.threadpool.start(worker)