Files
tidal-dl/tidal_dl_ng/gui.py
T

977 lines
37 KiB
Python
Raw Normal View History

2023-12-19 23:58:57 +01:00
import math
import sys
import time
2023-12-19 23:58:57 +01:00
from collections.abc import Callable
from requests.exceptions import HTTPError
from tidal_dl_ng import __version__, update_available
2024-02-04 12:19:40 +01:00
from tidal_dl_ng.dialog import DialogLogin, DialogPreferences, DialogVersion
2024-03-29 12:34:55 +01:00
from tidal_dl_ng.helper.gui import (
2024-04-01 12:26:10 +02:00
get_queue_download_media,
get_queue_download_quality,
2024-03-29 12:34:55 +01:00
get_results_media_item,
get_user_list_media_item,
set_queue_download_media,
2024-03-29 12:34:55 +01:00
set_results_media,
set_user_list_media,
)
2024-04-01 19:54:11 +02:00
from tidal_dl_ng.helper.path import get_format_template, resource_path
from tidal_dl_ng.helper.tidal import (
get_tidal_media_id,
get_tidal_media_type,
instantiate_media,
items_results_all,
2024-03-14 06:30:47 +01:00
name_builder_artist,
name_builder_title,
quality_audio_highest,
search_results_all,
user_media_lists,
)
2024-03-25 16:53:51 +01:00
from tidal_dl_ng.metadata import Metadata
2024-01-14 23:09:43 +01:00
2023-12-19 23:58:57 +01:00
try:
import qdarktheme
from PySide6 import QtCore, QtGui, QtWidgets
except ImportError as e:
print(e)
print("Qt dependencies missing. Cannot start GUI. Please execute: 'pip install pyside6 pyqtdarktheme'")
sys.exit(1)
import coloredlogs.converter
from rich.progress import Progress
from tidalapi import Album, Mix, Playlist, Quality, Track, UserPlaylist, Video
2024-03-22 17:31:33 +01:00
from tidalapi.artist import Artist
2023-12-19 23:58:57 +01:00
from tidalapi.session import SearchTypes
2024-01-12 10:21:34 +01:00
2023-12-19 23:58:57 +01:00
from tidal_dl_ng.config import Settings, Tidal
from tidal_dl_ng.constants import QualityVideo, QueueDownloadStatus, TidalLists
2023-12-19 23:58:57 +01:00
from tidal_dl_ng.download import Download
2024-01-12 10:21:34 +01:00
from tidal_dl_ng.logger import XStream, logger_gui
from tidal_dl_ng.model.gui_data import ProgressBars, QueueDownloadItem, ResultItem, StatusbarMessage
from tidal_dl_ng.model.meta import ReleaseLatest
2023-12-19 23:58:57 +01:00
from tidal_dl_ng.ui.main import Ui_MainWindow
from tidal_dl_ng.ui.spinner import QtWaitingSpinner
from tidal_dl_ng.worker import Worker
# TODO: Make more use of Exceptions
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
2024-01-23 17:22:10 +01:00
settings: Settings
tidal: Tidal
dl: Download
threadpool: QtCore.QThreadPool
tray: QtWidgets.QSystemTrayIcon
spinner: QtWaitingSpinner
2024-03-29 13:10:22 +01:00
cover_url_current: str = ""
s_spinner_start: QtCore.Signal = QtCore.Signal(QtWidgets.QWidget)
s_spinner_stop: QtCore.Signal = QtCore.Signal()
2024-01-23 17:22:10 +01:00
pb_item: QtWidgets.QProgressBar
2023-12-19 23:58:57 +01:00
s_item_advance: QtCore.Signal = QtCore.Signal(float)
s_item_name: QtCore.Signal = QtCore.Signal(str)
2024-01-19 07:25:41 +01:00
s_list_name: QtCore.Signal = QtCore.Signal(str)
2024-01-23 17:22:10 +01:00
pb_list: QtWidgets.QProgressBar
2023-12-19 23:58:57 +01:00
s_list_advance: QtCore.Signal = QtCore.Signal(float)
s_pb_reset: QtCore.Signal = QtCore.Signal()
2024-01-16 07:09:15 +01:00
s_populate_tree_lists: QtCore.Signal = QtCore.Signal(list)
2024-01-19 07:25:41 +01:00
s_statusbar_message: QtCore.Signal = QtCore.Signal(object)
s_tr_results_add_top_level_item: QtCore.Signal = QtCore.Signal(object)
2024-02-04 12:50:19 +01:00
s_settings_save: QtCore.Signal = QtCore.Signal()
s_pb_reload_status: QtCore.Signal = QtCore.Signal(bool)
s_update_check: QtCore.Signal = QtCore.Signal()
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)
2023-12-19 23:58:57 +01:00
def __init__(self, tidal: Tidal | None = None):
super().__init__()
self.setupUi(self)
# self.setGeometry(50, 50, 500, 300)
2024-04-01 19:02:00 +02:00
self.setWindowTitle("TIDAL Downloader Next Generation!")
2023-12-19 23:58:57 +01:00
# Logging redirect.
XStream.stdout().messageWritten.connect(self._log_output)
# XStream.stderr().messageWritten.connect(self._log_output)
self.settings = Settings()
self._init_threads()
2023-12-19 23:58:57 +01:00
self._init_tree_results(self.tr_results)
self._init_tree_lists(self.tr_lists_user)
2024-04-01 16:06:02 +02:00
self._init_tree_queue(self.tr_queue_download)
2024-03-23 14:11:11 +01:00
self._init_info()
2023-12-19 23:58:57 +01:00
self._init_progressbar()
self._populate_quality(self.cb_quality_audio, Quality)
self._populate_quality(self.cb_quality_video, QualityVideo)
self._populate_search_types(self.cb_search_type, SearchTypes)
self.apply_settings(self.settings)
self._init_signals()
2024-01-16 07:09:15 +01:00
self.init_tidal(tidal)
2023-12-19 23:58:57 +01:00
logger_gui.debug("All setup.")
def init_tidal(self, tidal: Tidal = None):
result: bool = False
if tidal:
self.tidal = tidal
result = True
else:
self.tidal = Tidal(self.settings)
result = self.tidal.login_token()
2023-12-19 23:58:57 +01:00
if not result:
hint: str = "Watiting for user input."
while not result:
url_login = self.tidal.session.pkce_login_url()
d_login: DialogLogin = DialogLogin(url_login=url_login, hint=hint, parent=self)
url_redirect: str = d_login.url_redirect
2023-12-19 23:58:57 +01:00
if d_login.return_code == 1:
try:
token: dict[str, str | int] = self.tidal.session.pkce_get_auth_token(url_redirect)
self.tidal.session.process_auth_token(token)
self.tidal.login_finalize()
result = True
logger_gui.info("Login not successful. Have fun!")
except (HTTPError, Exception):
hint = "Something was wrong with your redirect url. Please try again!"
logger_gui.warning("Login not successful. Try again...")
else:
# If user has pressed cancel.
sys.exit(1)
2023-12-19 23:58:57 +01:00
if result:
self._init_dl()
2024-01-16 07:09:15 +01:00
self.thread_it(self.tidal_user_lists)
2023-12-19 23:58:57 +01:00
def _init_threads(self):
self.threadpool = QtCore.QThreadPool()
self.thread_it(self.watcher_queue_download)
def _init_dl(self):
# Init `Download` object.
data_pb: ProgressBars = ProgressBars(
item=self.s_item_advance,
list_item=self.s_list_advance,
item_name=self.s_item_name,
list_name=self.s_list_name,
)
progress: Progress = Progress()
self.dl = Download(
session=self.tidal.session,
skip_existing=self.tidal.settings.data.skip_existing,
path_base=self.settings.data.download_base_path,
fn_logger=logger_gui,
progress_gui=data_pb,
progress=progress,
)
2023-12-19 23:58:57 +01:00
def _init_progressbar(self):
self.pb_list = QtWidgets.QProgressBar()
self.pb_item = QtWidgets.QProgressBar()
pbs = [self.pb_list, self.pb_item]
for pb in pbs:
pb.setRange(0, 100)
# self.pb_progress.setVisible()
self.statusbar.addPermanentWidget(pb)
2024-03-23 14:11:11 +01:00
def _init_info(self):
2024-04-01 19:54:11 +02:00
path_image: str = resource_path("tidal_dl_ng/ui/default_album_image.png")
self.l_pm_cover.setPixmap(QtGui.QPixmap(path_image))
2024-03-23 14:11:11 +01:00
2024-01-18 22:46:34 +01:00
def on_progress_reset(self):
2023-12-19 23:58:57 +01:00
self.pb_list.setValue(0)
self.pb_item.setValue(0)
2024-01-19 07:25:41 +01:00
def on_statusbar_message(self, data: StatusbarMessage):
self.statusbar.showMessage(data.message, data.timout)
2023-12-19 23:58:57 +01:00
def _log_output(self, text):
display_msg = coloredlogs.converter.convert(text)
cursor: QtGui.QTextCursor = self.te_debug.textCursor()
cursor.movePosition(QtGui.QTextCursor.End)
cursor.insertHtml(display_msg)
self.te_debug.setTextCursor(cursor)
self.te_debug.ensureCursorVisible()
def _populate_quality(self, ui_target: QtWidgets.QComboBox, options: type[Quality | QualityVideo]):
for item in options:
ui_target.addItem(item.name, item)
def _populate_search_types(self, ui_target: QtWidgets.QComboBox, options: SearchTypes):
for item in options:
2024-03-22 17:31:33 +01:00
if item:
2023-12-19 23:58:57 +01:00
ui_target.addItem(item.__name__, item)
2024-03-22 17:31:33 +01:00
self.cb_search_type.setCurrentIndex(2)
2023-12-19 23:58:57 +01:00
def _init_tree_results(self, tree: QtWidgets.QTableWidget):
2024-01-16 06:12:22 +01:00
tree.sortByColumn(0, QtCore.Qt.SortOrder.AscendingOrder)
2024-03-23 14:00:33 +01:00
tree.setColumnHidden(1, True)
2024-04-01 16:06:02 +02:00
tree.setColumnWidth(2, 150)
tree.setColumnWidth(3, 150)
tree.setColumnWidth(4, 150)
2023-12-19 23:58:57 +01:00
2024-04-01 16:06:02 +02:00
header = tree.header()
header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
def _init_tree_queue(self, tree: QtWidgets.QTableWidget):
2024-03-23 14:11:11 +01:00
tree.setColumnHidden(1, True)
2024-04-01 16:06:02 +02:00
tree.setColumnWidth(2, 200)
header = tree.header()
header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
header.setStretchLastSection(False)
2023-12-19 23:58:57 +01:00
2024-01-16 07:09:15 +01:00
def tidal_user_lists(self):
2023-12-19 23:58:57 +01:00
# Start loading spinner
self.s_spinner_start.emit(self.tr_lists_user)
self.s_pb_reload_status.emit(False)
2024-01-16 07:09:15 +01:00
user_all: [Playlist | UserPlaylist | Mix] = user_media_lists(self.tidal.session)
2024-01-16 07:09:15 +01:00
self.s_populate_tree_lists.emit(user_all)
def on_populate_tree_lists(self, user_lists: [Playlist | UserPlaylist | Mix]):
2024-01-16 06:35:59 +01:00
twi_playlists: QtWidgets.QTreeWidgetItem = self.tr_lists_user.findItems(
2024-04-02 16:26:57 +02:00
TidalLists.Playlists, QtCore.Qt.MatchExactly, 0
)[0]
2024-01-16 06:35:59 +01:00
twi_mixes: QtWidgets.QTreeWidgetItem = self.tr_lists_user.findItems(
2024-04-02 16:26:57 +02:00
TidalLists.Favorites, QtCore.Qt.MatchExactly, 0
)[0]
2024-01-16 06:35:59 +01:00
twi_favorites: QtWidgets.QTreeWidgetItem = self.tr_lists_user.findItems(
2024-04-02 16:26:57 +02:00
TidalLists.Mixes, QtCore.Qt.MatchExactly, 0
2023-12-19 23:58:57 +01:00
)[0]
# Remove all children if present
for twi in [twi_playlists, twi_mixes, twi_favorites]:
for i in reversed(range(twi.childCount())):
twi.removeChild(twi.child(i))
2024-01-16 07:09:15 +01:00
for item in user_lists:
2023-12-19 23:58:57 +01:00
if isinstance(item, UserPlaylist):
twi_child = QtWidgets.QTreeWidgetItem(twi_playlists)
name: str = item.name
info: str = f"({item.num_tracks + item.num_videos} Tracks)"
elif isinstance(item, Playlist):
twi_child = QtWidgets.QTreeWidgetItem(twi_mixes)
name: str = item.name
info: str = f"({item.num_tracks + item.num_videos} Tracks) {item.description}"
elif isinstance(item, Mix):
twi_child = QtWidgets.QTreeWidgetItem(twi_favorites)
name: str = item.title
info: str = item.sub_title
twi_child.setText(0, name)
2024-03-29 12:34:55 +01:00
set_user_list_media(twi_child, item)
2024-03-23 14:00:33 +01:00
twi_child.setText(2, info)
2023-12-19 23:58:57 +01:00
# Stop load spinner
self.s_spinner_stop.emit()
self.s_pb_reload_status.emit(True)
2023-12-19 23:58:57 +01:00
def _init_tree_lists(self, tree: QtWidgets.QTreeWidget):
2024-01-16 07:09:15 +01:00
# Adjust Tree.
2023-12-19 23:58:57 +01:00
tree.setColumnWidth(0, 200)
2024-03-23 14:00:33 +01:00
tree.setColumnHidden(1, True)
tree.setColumnWidth(2, 300)
2023-12-19 23:58:57 +01:00
tree.expandAll()
# Connect the contextmenu
tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
tree.customContextMenuRequested.connect(self.menu_context_tree_lists)
def on_update_check(self):
is_available, info = update_available()
if is_available:
self.s_update_show.emit(True, is_available, info)
2023-12-19 23:58:57 +01:00
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},
2023-12-19 23:58:57 +01:00
]
for item in l_cb:
2024-04-20 17:29:42 +02:00
idx = item["element"].findData(item["setting"])
2024-01-18 22:46:34 +01:00
2023-12-19 23:58:57 +01:00
if idx > -1:
item["element"].setCurrentIndex(idx)
else:
item["element"].setCurrentIndex(item["default_id"])
def on_spinner_start(self, parent: QtWidgets.QWidget):
self.spinner = QtWaitingSpinner(parent, True, True)
self.spinner.setColor(QtGui.QColor(255, 255, 255))
self.spinner.start()
def on_spinner_stop(self):
self.spinner.stop()
self.spinner = None
def menu_context_tree_lists(self, point):
# Infos about the node selected.
index = self.tr_lists_user.indexAt(point)
# Do not open menu if something went wrong or a parent node is clicked.
if not index.isValid() or not index.parent().data():
return
# We build the menu.
menu = QtWidgets.QMenu()
2024-01-16 06:12:22 +01:00
menu.addAction("Download Playlist", lambda: self.thread_download_list_media(point))
2023-12-19 23:58:57 +01:00
menu.exec(self.tr_lists_user.mapToGlobal(point))
def thread_download_list_media(self, point):
self.thread_it(self.on_download_list_media, point)
self.thread_it(self.list_items_show_result, point=point)
2023-12-19 23:58:57 +01:00
def on_download_list_media(self, point: QtCore.QPoint = None):
2024-04-01 19:02:00 +02:00
items: [QtWidgets.QTreeWidgetItem]
2023-12-19 23:58:57 +01:00
if point:
2024-04-01 19:02:00 +02:00
items = [self.tr_lists_user.itemAt(point)]
else:
2024-04-01 19:02:00 +02:00
items = self.tr_lists_user.selectedItems()
if len(items) == 0:
logger_gui.error("Please select a mix or playlist first.")
2024-04-01 19:02:00 +02:00
for item in items:
media = get_user_list_media_item(item)
queue_dl_item: QueueDownloadItem | False = self.media_to_queue_download_model(media)
if queue_dl_item:
self.queue_download_media(queue_dl_item)
2024-01-19 07:25:41 +01:00
2023-12-19 23:58:57 +01:00
def search_populate_results(self, query: str, type_media: SearchTypes):
self.tr_results.clear()
results: [ResultItem] = self.search(query, [type_media])
2023-12-19 23:58:57 +01:00
self.populate_tree_results(results)
def populate_tree_results(self, results: [ResultItem], parent: QtWidgets.QTreeWidgetItem = None):
if not parent:
self.tr_results.clear()
# Count how many digits the list length has,
2024-01-27 16:46:26 +01:00
count_digits: int = int(math.log10(len(results) if results else 1)) + 1
2023-12-19 23:58:57 +01:00
for item in results:
child = self.populate_tree_result_child(item=item, index_count_digits=count_digits)
2023-12-19 23:58:57 +01:00
if parent:
parent.addChild(child)
else:
self.s_tr_results_add_top_level_item.emit(child)
2023-12-19 23:58:57 +01:00
def populate_tree_result_child(self, item: [Track | Video | Mix | Album | Playlist], index_count_digits: int):
2024-03-22 17:31:33 +01:00
duration: str = ""
# TODO: Duration needs to be calculated later to properly fill with zeros.
2024-03-22 17:31:33 +01:00
if item.duration_sec > -1:
# Format seconds to mm:ss.
m, s = divmod(item.duration_sec, 60)
duration: str = f"{m:02d}:{s:02d}"
# Since sorting happens only by string, we need to pad the index and add 1 (to avoid start at 0)
index: str = str(item.position + 1).zfill(index_count_digits)
# Populate child
child: QtWidgets.QTreeWidgetItem = QtWidgets.QTreeWidgetItem()
child.setText(0, index)
2024-03-29 12:34:55 +01:00
set_results_media(child, item.obj)
2024-03-23 14:00:33 +01:00
child.setText(2, item.artist)
child.setText(3, item.title)
child.setText(4, item.album)
child.setText(5, duration)
child.setText(6, item.quality)
2024-03-22 17:31:33 +01:00
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.
child_dummy: QtWidgets.QTreeWidgetItem = QtWidgets.QTreeWidgetItem()
child_dummy.setDisabled(True)
child.addChild(child_dummy)
return child
2024-01-19 07:25:41 +01:00
def on_tr_results_add_top_level_item(self, widget_item: QtWidgets.QTreeWidgetItem):
self.tr_results.addTopLevelItem(widget_item)
2023-12-19 23:58:57 +01:00
2024-02-04 12:50:19 +01:00
def on_settings_save(self):
self.settings.save()
self.apply_settings(self.settings)
self._init_dl()
2024-02-04 12:19:40 +01:00
def search(self, query: str, types_media: SearchTypes) -> [ResultItem]:
# If a direct link was searched for, skip search and create the object from the link directly.
if "http" in query:
media_type = get_tidal_media_type(query)
item_id = get_tidal_media_id(query)
2024-02-26 07:34:07 +01:00
try:
media = instantiate_media(self.tidal.session, media_type, item_id)
except:
logger_gui.error(f"Media not found (ID: {item_id}). Maybe it is not available anymore.")
2024-02-26 07:34:07 +01:00
media = None
result_search = {"direct": [media]}
else:
result_search: dict[str, [SearchTypes]] = search_results_all(
session=self.tidal.session, needle=query, types_media=types_media
)
result: [ResultItem] = []
2023-12-19 23:58:57 +01:00
2024-01-12 10:21:34 +01:00
for _media_type, l_media in result_search.items():
2023-12-19 23:58:57 +01:00
if isinstance(l_media, list):
result = result + self.search_result_to_model(l_media)
return result
def search_result_to_model(self, items: [*SearchTypes]) -> [ResultItem]:
2023-12-19 23:58:57 +01:00
result = []
for idx, item in enumerate(items):
explicit: str = ""
2024-02-26 07:02:02 +01:00
# Check if item is available on TIDAL.
if hasattr(item, "available") and not item.available:
continue
if isinstance(item, Track | Video | Album):
explicit = " 🅴" if item.explicit else ""
2023-12-19 23:58:57 +01:00
if isinstance(item, Track):
result_item: ResultItem = ResultItem(
2023-12-19 23:58:57 +01:00
position=idx,
2024-03-14 06:30:47 +01:00
artist=name_builder_artist(item),
title=f"{name_builder_title(item)}{explicit}",
2023-12-19 23:58:57 +01:00
album=item.album.name,
duration_sec=item.duration,
obj=item,
quality=quality_audio_highest(item),
explicit=bool(item.explicit),
2023-12-19 23:58:57 +01:00
)
result.append(result_item)
elif isinstance(item, Video):
result_item: ResultItem = ResultItem(
2023-12-19 23:58:57 +01:00
position=idx,
2024-03-14 06:30:47 +01:00
artist=name_builder_artist(item),
title=f"{name_builder_title(item)}{explicit}",
2023-12-19 23:58:57 +01:00
album=item.album.name if item.album else "",
duration_sec=item.duration,
obj=item,
quality=item.video_quality,
explicit=bool(item.explicit),
2023-12-19 23:58:57 +01:00
)
result.append(result_item)
elif isinstance(item, Playlist):
result_item: ResultItem = ResultItem(
2023-12-19 23:58:57 +01:00
position=idx,
artist=", ".join(artist.name for artist in item.promoted_artists) if item.promoted_artists else "",
2023-12-19 23:58:57 +01:00
title=item.name,
album="",
duration_sec=item.duration,
obj=item,
quality="",
explicit=False,
2023-12-19 23:58:57 +01:00
)
result.append(result_item)
elif isinstance(item, Album):
result_item: ResultItem = ResultItem(
2023-12-19 23:58:57 +01:00
position=idx,
2024-03-14 06:30:47 +01:00
artist=name_builder_artist(item),
2023-12-19 23:58:57 +01:00
title="",
album=f"{item.name}{explicit}",
2023-12-19 23:58:57 +01:00
duration_sec=item.duration,
obj=item,
quality=quality_audio_highest(item),
explicit=bool(item.explicit),
2023-12-19 23:58:57 +01:00
)
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,
)
2024-03-22 17:31:33 +01:00
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,
2024-03-22 17:31:33 +01:00
)
2023-12-19 23:58:57 +01:00
result.append(result_item)
return result
def media_to_queue_download_model(
self, media: Artist | Track | Video | Album | Playlist | Mix
) -> QueueDownloadItem | bool:
result: QueueDownloadItem | False
name: str = ""
2024-04-21 22:42:19 +02:00
quality: Quality | QualityVideo | str = ""
explicit: str = ""
# Check if item is available on TIDAL.
if hasattr(media, "available") and not media.available:
return False
if isinstance(media, Track | Video | Album):
explicit = " 🅴" if media.explicit else ""
if isinstance(media, Track | Video):
name = f"{name_builder_artist(media)} - {name_builder_title(media)}{explicit}"
elif isinstance(media, Playlist | Artist):
name = media.name
elif isinstance(media, Album):
name = f"{name_builder_artist(media)} - {media.name}{explicit}"
elif isinstance(media, Mix):
name = media.title
if isinstance(media, Track | Album):
quality_highest: str = quality_audio_highest(media)
if (
self.settings.data.quality_audio == quality_highest
or self.settings.data.quality_audio == Quality.hi_res_lossless
or (self.settings.data.quality_audio == Quality.hi_res and quality_highest == Quality.hi_res)
):
quality = quality_highest
else:
quality = self.settings.data.quality_audio
elif isinstance(media, Video):
2024-04-21 22:42:19 +02:00
quality = self.settings.data.quality_video
if name:
result = QueueDownloadItem(
name=name,
quality=quality,
type_media=type(media).__name__,
2024-04-02 16:26:57 +02:00
status=QueueDownloadStatus.Waiting,
obj=media,
)
else:
result = False
return result
2023-12-19 23:58:57 +01:00
def _init_signals(self):
2024-03-22 15:02:26 +01:00
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))
self.pb_queue_download_clear_all.clicked.connect(self.on_queue_download_clear_all)
self.pb_queue_download_clear_finished.clicked.connect(self.on_queue_download_clear_finished)
self.pb_queue_download_remove.clicked.connect(self.on_queue_download_remove)
2023-12-19 23:58:57 +01:00
self.l_search.returnPressed.connect(
lambda: self.search_populate_results(self.l_search.text(), self.cb_search_type.currentData())
)
2024-03-22 15:02:26 +01:00
self.pb_search.clicked.connect(
2023-12-19 23:58:57 +01:00
lambda: self.search_populate_results(self.l_search.text(), self.cb_search_type.currentData())
)
2024-01-18 22:46:34 +01:00
self.cb_quality_audio.currentIndexChanged.connect(self.on_quality_set_audio)
self.cb_quality_video.currentIndexChanged.connect(self.on_quality_set_video)
2023-12-19 23:58:57 +01:00
self.tr_lists_user.itemClicked.connect(self.on_list_items_show)
self.s_spinner_start[QtWidgets.QWidget].connect(self.on_spinner_start)
self.s_spinner_stop.connect(self.on_spinner_stop)
2024-01-18 22:46:34 +01:00
self.s_item_advance.connect(self.on_progress_item)
self.s_item_name.connect(self.on_progress_item_name)
2024-01-19 07:25:41 +01:00
self.s_list_name.connect(self.on_progress_list_name)
2024-01-18 22:46:34 +01:00
self.s_list_advance.connect(self.on_progress_list)
self.s_pb_reset.connect(self.on_progress_reset)
2024-01-16 07:09:15 +01:00
self.s_populate_tree_lists.connect(self.on_populate_tree_lists)
2024-01-19 07:25:41 +01:00
self.s_statusbar_message.connect(self.on_statusbar_message)
self.s_tr_results_add_top_level_item.connect(self.on_tr_results_add_top_level_item)
2024-02-04 12:19:40 +01:00
self.s_settings_save.connect(self.on_settings_save)
self.s_pb_reload_status.connect(self.button_reload_status)
self.s_update_check.connect(lambda: self.thread_it(self.on_update_check))
self.s_update_show.connect(self.on_version)
2023-12-19 23:58:57 +01:00
2024-01-19 10:00:27 +01:00
# Menubar
self.a_exit.triggered.connect(sys.exit)
self.a_version.triggered.connect(self.on_version)
2024-02-04 12:19:40 +01:00
self.a_preferences.triggered.connect(self.on_preferences)
self.a_logout.triggered.connect(self.on_logout)
self.a_updates_check.triggered.connect(self.on_update_check)
2024-01-19 10:00:27 +01:00
# Results
self.tr_results.itemExpanded.connect(self.on_tr_results_expanded)
2024-03-25 16:53:51 +01:00
self.tr_results.itemClicked.connect(self.on_result_item_clicked)
2024-04-01 12:26:10 +02:00
# Download Queue
self.tr_queue_download.itemClicked.connect(self.on_queue_download_item_clicked)
self.s_queue_download_item_downloading.connect(self.on_queue_download_item_downloading)
self.s_queue_download_item_finished.connect(self.on_queue_download_item_finished)
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 on_logout(self):
result: bool = self.tidal.logout()
if result:
sys.exit(0)
2024-01-18 22:46:34 +01:00
def on_progress_list(self, value: float):
2023-12-19 23:58:57 +01:00
self.pb_list.setValue(int(math.ceil(value)))
2024-01-18 22:46:34 +01:00
def on_progress_item(self, value: float):
2023-12-19 23:58:57 +01:00
self.pb_item.setValue(int(math.ceil(value)))
2024-01-18 22:46:34 +01:00
def on_progress_item_name(self, value: str):
2023-12-19 23:58:57 +01:00
self.pb_item.setFormat(f"%p% {value}")
2024-01-19 07:25:41 +01:00
def on_progress_list_name(self, value: str):
2023-12-19 23:58:57 +01:00
self.pb_list.setFormat(f"%p% {value}")
2024-01-18 22:46:34 +01:00
def on_quality_set_audio(self, index):
2024-04-02 16:26:57 +02:00
self.settings.data.quality_audio = Quality(self.cb_quality_audio.itemData(index))
2023-12-19 23:58:57 +01:00
self.settings.save()
if self.tidal:
self.tidal.settings_apply()
2024-01-18 22:46:34 +01:00
def on_quality_set_video(self, index):
2024-04-02 16:26:57 +02:00
self.settings.data.quality_video = QualityVideo(self.cb_quality_video.itemData(index))
2023-12-19 23:58:57 +01:00
self.settings.save()
if self.tidal:
self.tidal.settings_apply()
def on_list_items_show(self, item: QtWidgets.QTreeWidgetItem):
2024-03-29 12:57:46 +01:00
media_list: Album | Playlist = get_user_list_media_item(item)
2023-12-19 23:58:57 +01:00
# Only if clicked item is not a top level item.
if media_list:
self.list_items_show_result(media_list)
2024-03-29 13:10:22 +01:00
self.cover_show(media_list)
2023-12-19 23:58:57 +01:00
2024-03-25 16:53:51 +01:00
def on_result_item_clicked(self, item: QtWidgets.QTreeWidgetItem, column: int) -> None:
media: Track | Video | Album | Artist = get_results_media_item(item)
2024-03-29 12:02:12 +01:00
2024-03-29 13:10:22 +01:00
self.cover_show(media)
2024-04-01 12:26:10 +02:00
def on_queue_download_item_clicked(self, item: QtWidgets.QTreeWidgetItem, column: int) -> None:
media: Track | Video | Album | Artist | Mix | Playlist = get_queue_download_media(item)
self.cover_show(media)
def on_download_item_clicked(self, item: QtWidgets.QTreeWidgetItem, column: int) -> None:
media: Track | Video | Album | Artist = get_results_media_item(item)
self.cover_show(media)
2024-04-01 12:26:10 +02:00
def cover_show(self, media: Album | Playlist | Track | Video | Album | Artist) -> None:
2024-03-29 13:10:22 +01:00
cover_url: str
try:
cover_url = media.album.image()
except:
try:
cover_url = media.image()
except:
logger_gui.info(f"No cover available (media ID: {media.id}).")
if cover_url and self.cover_url_current != cover_url:
2024-03-29 12:02:12 +01:00
self.cover_url_current = cover_url
data_cover: bytes = Metadata.cover_data(cover_url)
pixmap: QtGui.QPixmap = QtGui.QPixmap()
pixmap.loadFromData(data_cover)
self.l_pm_cover.setPixmap(pixmap)
2024-03-25 16:53:51 +01:00
def list_items_show_result(
self,
2024-03-22 17:31:33 +01:00
media_list: Album | Playlist | Mix | Artist | None = None,
point: QtCore.QPoint | None = None,
parent: QtWidgets.QTreeWidgetItem = None,
) -> None:
2023-12-19 23:58:57 +01:00
if point:
item = self.tr_lists_user.itemAt(point)
2024-03-29 12:57:46 +01:00
media_list = get_user_list_media_item(item)
2023-12-19 23:58:57 +01:00
2024-01-18 21:57:35 +01:00
# Get all results
2024-03-22 17:31:33 +01:00
media_items: [Track | Video | Album] = items_results_all(media_list)
result: [ResultItem] = self.search_result_to_model(media_items)
2023-12-19 23:58:57 +01:00
self.populate_tree_results(result, parent=parent)
2023-12-19 23:58:57 +01:00
def thread_it(self, fn: Callable, *args, **kwargs):
# 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):
self.on_clear_queue_download(
2024-04-02 16:26:57 +02:00
f"({QueueDownloadStatus.Waiting}|{QueueDownloadStatus.Finished}|{QueueDownloadStatus.Failed})"
)
def on_queue_download_clear_finished(self):
2024-04-02 16:26:57 +02:00
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(
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()
if len(items) == 0:
logger_gui.error("Please select an item from the queue first.")
else:
for item in items:
status: str = item.text(0)
2024-04-02 16:26:57 +02:00
if status != QueueDownloadStatus.Downloading:
self.tr_queue_download.takeTopLevelItem(self.tr_queue_download.indexOfTopLevelItem(item))
else:
logger_gui.info("Cannot remove a currently downloading item from queue.")
# TODO: Must happen in main thread. Do not thread this.
def on_download_results(self) -> None:
items: [QtWidgets.QTreeWidgetItem | None] = self.tr_results.selectedItems()
2024-03-29 12:57:46 +01:00
if len(items) == 0:
logger_gui.error("Please select a row first.")
else:
for item in items:
media: Track | Album | Playlist | Video | Artist = get_results_media_item(item)
queue_dl_item: QueueDownloadItem = self.media_to_queue_download_model(media)
2024-03-29 12:57:46 +01:00
if queue_dl_item:
self.queue_download_media(queue_dl_item)
def queue_download_media(self, queue_dl_item: QueueDownloadItem) -> None:
# Populate child
child: QtWidgets.QTreeWidgetItem = QtWidgets.QTreeWidgetItem()
child.setText(0, queue_dl_item.status)
set_queue_download_media(child, queue_dl_item.obj)
child.setText(2, queue_dl_item.name)
child.setText(3, queue_dl_item.type_media)
child.setText(4, queue_dl_item.quality)
self.tr_queue_download.addTopLevelItem(child)
2024-03-29 12:57:46 +01:00
def watcher_queue_download(self) -> None:
while True:
items: [QtWidgets.QTreeWidgetItem | None] = self.tr_queue_download.findItems(
2024-04-02 16:26:57 +02:00
QueueDownloadStatus.Waiting, QtCore.Qt.MatchFlag.MatchExactly, column=0
)
if len(items) > 0:
result: QueueDownloadStatus
item: QtWidgets.QTreeWidgetItem = items[0]
media: Track | Album | Playlist | Video | Mix | Artist = get_queue_download_media(item)
2024-04-21 22:42:19 +02:00
tmp_quality: str = get_queue_download_quality(item)
quality: Quality | QualityVideo | None = tmp_quality if tmp_quality else None
try:
self.s_queue_download_item_downloading.emit(item)
result = self.on_queue_download(media, quality=quality)
if result == QueueDownloadStatus.Finished:
self.s_queue_download_item_finished.emit(item)
elif result == QueueDownloadStatus.Skipped:
self.s_queue_download_item_skipped.emit(item)
except:
self.s_queue_download_item_failed.emit(item)
else:
time.sleep(2)
def on_queue_download_item_downloading(self, item: QtWidgets.QTreeWidgetItem) -> None:
2024-04-02 16:26:57 +02:00
self.queue_download_item_status(item, QueueDownloadStatus.Downloading)
def on_queue_download_item_finished(self, item: QtWidgets.QTreeWidgetItem) -> None:
2024-04-02 16:26:57 +02:00
self.queue_download_item_status(item, QueueDownloadStatus.Finished)
def on_queue_download_item_failed(self, item: QtWidgets.QTreeWidgetItem) -> None:
2024-04-02 16:26:57 +02:00
self.queue_download_item_status(item, QueueDownloadStatus.Failed)
def on_queue_download_item_skipped(self, item: QtWidgets.QTreeWidgetItem) -> None:
self.queue_download_item_status(item, QueueDownloadStatus.Skipped)
def queue_download_item_status(self, item: QtWidgets.QTreeWidgetItem, status: str) -> None:
item.setText(0, status)
def on_queue_download(
2024-04-21 22:42:19 +02:00
self, media: Track | Album | Playlist | Video | Mix | Artist, quality: Quality | QualityVideo | None = None
) -> QueueDownloadStatus:
result: QueueDownloadStatus
items_media: [Track | Album | Playlist | Video | Mix | Artist]
if isinstance(media, Artist):
items_media: [Album] = items_results_all(media)
else:
items_media = [media]
2023-12-19 23:58:57 +01:00
download_delay: bool = bool(isinstance(media, Track | Video) and self.settings.data.download_delay)
2023-12-19 23:58:57 +01:00
for item_media in items_media:
result = self.download(item_media, self.dl, delay_track=download_delay, quality=quality)
return result
2023-12-19 23:58:57 +01:00
2024-03-22 18:18:51 +01:00
def download(
self,
media: Track | Album | Playlist | Video | Mix | Artist,
dl: Download,
delay_track: bool = False,
2024-04-21 22:42:19 +02:00
quality: Quality | QualityVideo | None = None,
) -> QueueDownloadStatus:
result_dl: bool
path_file: str
result: QueueDownloadStatus
2024-04-21 22:42:19 +02:00
quality_audio: Quality | None
quality_video: QualityVideo | None
self.s_pb_reset.emit()
2024-01-19 07:25:41 +01:00
self.s_statusbar_message.emit(StatusbarMessage(message="Download started..."))
2023-12-19 23:58:57 +01:00
2024-01-14 23:09:43 +01:00
file_template = get_format_template(media, self.settings)
2023-12-19 23:58:57 +01:00
2024-01-12 11:20:10 +01:00
if isinstance(media, Track | Video):
2024-04-21 22:42:19 +02:00
if isinstance(media, Track):
quality_audio = quality
quality_video = None
else:
quality_audio = None
quality_video = quality
result_dl, path_file = dl.item(
2024-04-21 22:42:19 +02:00
media=media,
file_template=file_template,
download_delay=delay_track,
quality_audio=quality_audio,
quality_video=quality_video,
)
2024-01-12 11:20:10 +01:00
elif isinstance(media, Album | Playlist | Mix):
2024-04-21 22:42:19 +02:00
if isinstance(media, Album):
quality_audio = quality
quality_video = None
else:
quality_audio = None
quality_video = None
2024-01-12 10:21:34 +01:00
dl.items(
media=media,
2023-12-19 23:58:57 +01:00
file_template=file_template,
video_download=self.settings.data.video_download,
download_delay=self.settings.data.download_delay,
2024-04-21 22:42:19 +02:00
quality_audio=quality_audio,
quality_video=quality_video,
2023-12-19 23:58:57 +01:00
)
# Dummy values
result_dl = True
2024-04-20 15:38:50 +02:00
path_file = "dummy"
2024-01-19 07:25:41 +01:00
self.s_statusbar_message.emit(StatusbarMessage(message="Download finished.", timout=2000))
2023-12-19 23:58:57 +01:00
if result_dl and path_file:
result = QueueDownloadStatus.Finished
elif not result_dl and path_file:
result = QueueDownloadStatus.Skipped
else:
result = QueueDownloadStatus.Failed
return result
def on_version(
self, update_check: bool = False, update_available: bool = False, update_info: ReleaseLatest = None
) -> None:
DialogVersion(self, update_check, update_available, update_info)
2024-01-19 10:00:27 +01:00
2024-02-04 12:19:40 +01:00
def on_preferences(self) -> None:
2024-02-04 12:50:19 +01:00
DialogPreferences(settings=self.settings, settings_save=self.s_settings_save, parent=self)
2024-02-04 12:19:40 +01:00
def on_tr_results_expanded(self, list_item: QtWidgets.QTreeWidgetItem) -> None:
# If the child is a dummy the list_item has not been expanded before
load_children: bool = list_item.child(0).isDisabled()
if load_children:
list_item.removeChild(list_item.child(0))
2024-03-23 14:00:33 +01:00
media_list: [Mix | Album | Playlist | Artist] = list_item.data(1, QtCore.Qt.ItemDataRole.UserRole)
self.list_items_show_result(media_list=media_list, parent=list_item)
def button_reload_status(self, status: bool):
button_text: str = "Reloading..."
if status:
button_text = "Reload"
self.pb_reload_user_lists.setEnabled(status)
self.pb_reload_user_lists.setText(button_text)
2023-12-19 23:58:57 +01:00
# TODO: Comment with Google Docstrings.
def gui_activate(tidal: Tidal | None = None):
# Set dark theme and create QT app.
2023-12-19 23:58:57 +01:00
qdarktheme.enable_hi_dpi()
app = QtWidgets.QApplication(sys.argv)
# Fix for Windows: Tooltips have bright font color
# https://github.com/5yutan5/PyQtDarkTheme/issues/239
# qdarktheme.setup_theme()
qdarktheme.setup_theme(additional_qss="QToolTip { border: 0px; }")
2023-12-19 23:58:57 +01:00
# Create icon object and apply it to app window.
pixmap: QtGui.QPixmap = QtGui.QPixmap("tidal_dl_ng/ui/icon.png")
icon: QtGui.QIcon = QtGui.QIcon(pixmap)
app.setWindowIcon(icon)
2024-01-19 11:16:33 +01:00
# This bit gets the taskbar icon working properly in Windows
if sys.platform.startswith("win"):
import ctypes
# Make sure Pyinstaller icons are still grouped
if not sys.argv[0].endswith(".exe"):
# Arbitrary string
my_app_id: str = "exislow.tidal.dl-ng." + __version__
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(my_app_id)
2024-01-19 11:16:33 +01:00
window = MainWindow(tidal=tidal)
2023-12-19 23:58:57 +01:00
window.show()
# Check for updates
window.s_update_check.emit()
2023-12-19 23:58:57 +01:00
sys.exit(app.exec())
if __name__ == "__main__":
gui_activate()