diff --git a/README.md b/README.md index 4ff0b0e..44f3ac1 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,7 @@ [![Commit activity](https://img.shields.io/github/commit-activity/m/exislow/tidal-dl-ng)](https://img.shields.io/github/commit-activity/m/exislow/tidal-dl-ng) [![License](https://img.shields.io/github/license/exislow/tidal-dl-ng)](https://img.shields.io/github/license/exislow/tidal-dl-ng) -This tool allows to download songs and videos from TIDAL (a paid plan is required!). You can use the command line or GUI -version of this tool. +This tool allows to download songs and videos from TIDAL. A paid plan is required! Audio quality varies up to HiRes / TIDAL MAX 24 Bit, 192 kHz depending on the song and your TIDAL plan. You can use the command line or GUI version of this tool. ![App Image](assets/app.png) @@ -93,7 +92,7 @@ tidal_ng_dl/gui.py The GUI is build with `PySide6` using the [Qt Designer](https://doc.qt.io/qt-6/qtdesigner-manual.html): ```bash -pyside6-designer +PYSIDE_DESIGNER_PLUGINS=tidal_dl_ng/ui pyside6-designer ``` After all changes are saved you need to translate the Qt Designer `*.ui` file into Python code: diff --git a/tidal_dl_ng/cli.py b/tidal_dl_ng/cli.py index 1266f98..e84eafc 100644 --- a/tidal_dl_ng/cli.py +++ b/tidal_dl_ng/cli.py @@ -95,7 +95,7 @@ def login(ctx: typer.Context) -> bool: settings = Settings() tidal = Tidal(settings) - result = tidal.login(print) + result = tidal.login(fn_print=print) ctx.obj[CTX_TIDAL] = tidal return result diff --git a/tidal_dl_ng/config.py b/tidal_dl_ng/config.py index 2557ae8..a5036fa 100644 --- a/tidal_dl_ng/config.py +++ b/tidal_dl_ng/config.py @@ -120,7 +120,7 @@ class Tidal(BaseConfig, metaclass=SingletonMeta): return result - def _login_finalize(self) -> bool: + def login_finalize(self) -> bool: result = self.session.check_login() if result: @@ -151,7 +151,7 @@ class Tidal(BaseConfig, metaclass=SingletonMeta): # Login method: PKCE authorization (enables HiRes streaming) self.session.login_pkce(fn_print) - is_login = self._login_finalize() + is_login = self.login_finalize() if is_login: fn_print("The login was successful. I have stored your credentials (token).") diff --git a/tidal_dl_ng/dialog.py b/tidal_dl_ng/dialog.py index 5370136..788aeba 100644 --- a/tidal_dl_ng/dialog.py +++ b/tidal_dl_ng/dialog.py @@ -1,6 +1,7 @@ from PySide6.QtWidgets import QDialog from tidal_dl_ng import __version__ +from tidal_dl_ng.ui.dialog_login import Ui_DialogLogin from tidal_dl_ng.ui.dialog_version import Ui_DialogVersion @@ -9,11 +10,39 @@ class DialogVersion(QDialog): def __init__(self, parent=None): super().__init__(parent) + # Create an instance of the GUI self.ui = Ui_DialogVersion() + # Run the .setupUi() method to show the GUI self.ui.setupUi(self) # Set the version. self.ui.l_version.setText("v" + __version__) - + # Show self.exec() + + +class DialogLogin(QDialog): + """Version dialog.""" + + url_redirect: str + return_code: int + + def __init__(self, url_login: str, hint: str, parent=None): + super().__init__(parent) + + # Create an instance of the GUI + self.ui = Ui_DialogLogin() + + # Run the .setupUi() method to show the GUI + self.ui.setupUi(self) + # Set data. + self.ui.l_url_login.setText(f'{url_login}') + self.ui.l_hint.setText(hint) + # Show + self.return_code = self.exec() + + self.url_redirect = self.ui.te_url_redirect.toPlainText() + + def accept1(self): + pass diff --git a/tidal_dl_ng/gui.py b/tidal_dl_ng/gui.py index dec6e4e..a16609b 100644 --- a/tidal_dl_ng/gui.py +++ b/tidal_dl_ng/gui.py @@ -2,8 +2,10 @@ import math import sys from collections.abc import Callable +from requests.exceptions import HTTPError + from tidal_dl_ng import __version__ -from tidal_dl_ng.dialog import DialogVersion +from tidal_dl_ng.dialog import DialogLogin, DialogVersion from tidal_dl_ng.helper.path import get_format_template from tidal_dl_ng.helper.tidal import items_results_all, search_results_all, user_media_lists @@ -85,12 +87,29 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): result = True else: self.tidal = Tidal(self.settings) + result = self.tidal.login_token() - while True: - result = self.tidal.login(logger_gui.info) + 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 - if result: - break + 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) if result: # Init `Download` object. diff --git a/tidal_dl_ng/ui/dialog_login.py b/tidal_dl_ng/ui/dialog_login.py new file mode 100644 index 0000000..ffb8e76 --- /dev/null +++ b/tidal_dl_ng/ui/dialog_login.py @@ -0,0 +1,110 @@ +################################################################################ +## Form generated from reading UI file 'dialog_login.ui' +## +## Created by: Qt User Interface Compiler version 6.6.1 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide6.QtCore import QCoreApplication, QMetaObject, QRect, QSize, Qt +from PySide6.QtGui import QFont +from PySide6.QtWidgets import QDialogButtonBox, QLabel, QSizePolicy, QTextEdit, QVBoxLayout, QWidget + + +class Ui_DialogLogin: + def setupUi(self, DialogLogin): + if not DialogLogin.objectName(): + DialogLogin.setObjectName("DialogLogin") + DialogLogin.resize(451, 400) + sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(DialogLogin.sizePolicy().hasHeightForWidth()) + DialogLogin.setSizePolicy(sizePolicy) + self.bb_dialog = QDialogButtonBox(DialogLogin) + self.bb_dialog.setObjectName("bb_dialog") + self.bb_dialog.setGeometry(QRect(20, 350, 411, 32)) + sizePolicy.setHeightForWidth(self.bb_dialog.sizePolicy().hasHeightForWidth()) + self.bb_dialog.setSizePolicy(sizePolicy) + self.bb_dialog.setStyleSheet("") + self.bb_dialog.setOrientation(Qt.Horizontal) + self.bb_dialog.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) + self.verticalLayoutWidget = QWidget(DialogLogin) + self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") + self.verticalLayoutWidget.setGeometry(QRect(20, 20, 411, 321)) + self.lv_main = QVBoxLayout(self.verticalLayoutWidget) + self.lv_main.setObjectName("lv_main") + self.lv_main.setContentsMargins(0, 0, 0, 0) + self.l_header = QLabel(self.verticalLayoutWidget) + self.l_header.setObjectName("l_header") + sizePolicy.setHeightForWidth(self.l_header.sizePolicy().hasHeightForWidth()) + self.l_header.setSizePolicy(sizePolicy) + font = QFont() + font.setPointSize(23) + font.setBold(True) + self.l_header.setFont(font) + + self.lv_main.addWidget(self.l_header) + + self.l_description = QLabel(self.verticalLayoutWidget) + self.l_description.setObjectName("l_description") + sizePolicy.setHeightForWidth(self.l_description.sizePolicy().hasHeightForWidth()) + self.l_description.setSizePolicy(sizePolicy) + font1 = QFont() + font1.setItalic(True) + self.l_description.setFont(font1) + self.l_description.setWordWrap(True) + + self.lv_main.addWidget(self.l_description) + + self.l_url_login = QLabel(self.verticalLayoutWidget) + self.l_url_login.setObjectName("l_url_login") + sizePolicy.setHeightForWidth(self.l_url_login.sizePolicy().hasHeightForWidth()) + self.l_url_login.setSizePolicy(sizePolicy) + self.l_url_login.setMinimumSize(QSize(0, 50)) + self.l_url_login.setBaseSize(QSize(0, 0)) + self.l_url_login.setStyleSheet("border: 2px solid red; padding: 6px") + self.l_url_login.setWordWrap(True) + self.l_url_login.setOpenExternalLinks(True) + + self.lv_main.addWidget(self.l_url_login) + + self.te_url_redirect = QTextEdit(self.verticalLayoutWidget) + self.te_url_redirect.setObjectName("te_url_redirect") + + self.lv_main.addWidget(self.te_url_redirect) + + self.l_hint = QLabel(self.verticalLayoutWidget) + self.l_hint.setObjectName("l_hint") + sizePolicy.setHeightForWidth(self.l_hint.sizePolicy().hasHeightForWidth()) + self.l_hint.setSizePolicy(sizePolicy) + self.l_hint.setFont(font1) + + self.lv_main.addWidget(self.l_hint) + + self.retranslateUi(DialogLogin) + self.bb_dialog.accepted.connect(DialogLogin.accept) + self.bb_dialog.rejected.connect(DialogLogin.reject) + + QMetaObject.connectSlotsByName(DialogLogin) + + # setupUi + + def retranslateUi(self, DialogLogin): + DialogLogin.setWindowTitle(QCoreApplication.translate("DialogLogin", "Dialog", None)) + self.l_header.setText(QCoreApplication.translate("DialogLogin", "TIDAL PKCE Login", None)) + self.l_description.setText( + QCoreApplication.translate( + "DialogLogin", + 'Click the link below and login with your TIDAL credentials. You will be redirected to an "Ooops" page' + ' after a successful login. Copy the URL of this "Ooops" page and post it in the box below.', + None, + ) + ) + self.l_url_login.setText(QCoreApplication.translate("DialogLogin", "PLACEHOLDER URL...", None)) + self.te_url_redirect.setPlaceholderText( + QCoreApplication.translate("DialogLogin", "Copy and paste the redirect URL after login here...", None) + ) + self.l_hint.setText(QCoreApplication.translate("DialogLogin", "Waiting...", None)) + + # retranslateUi diff --git a/tidal_dl_ng/ui/dialog_login.ui b/tidal_dl_ng/ui/dialog_login.ui new file mode 100644 index 0000000..ae0a25a --- /dev/null +++ b/tidal_dl_ng/ui/dialog_login.ui @@ -0,0 +1,194 @@ + + + DialogLogin + + + + 0 + 0 + 451 + 400 + + + + + 0 + 0 + + + + Dialog + + + + + 20 + 350 + 411 + 32 + + + + + 0 + 0 + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + 20 + 20 + 411 + 321 + + + + + + + + 0 + 0 + + + + + 23 + true + + + + TIDAL PKCE Login + + + + + + + + 0 + 0 + + + + + true + + + + Click the link below and login with your TIDAL credentials. You will be redirected to an "Ooops" page after a successful login. Copy the URL of this "Ooops" page and post it in the box below. + + + true + + + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 0 + 0 + + + + border: 2px solid red; padding: 6px + + + PLACEHOLDER URL... + + + true + + + true + + + + + + + Copy and paste the redirect URL after login here... + + + + + + + + 0 + 0 + + + + + true + + + + Waiting... + + + + + + + + + + bb_dialog + accepted() + DialogLogin + accept() + + + 248 + 254 + + + 157 + 274 + + + + + bb_dialog + rejected() + DialogLogin + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/tidal_dl_ng/ui/dummy_register.py b/tidal_dl_ng/ui/dummy_register.py new file mode 100644 index 0000000..dfffdc2 --- /dev/null +++ b/tidal_dl_ng/ui/dummy_register.py @@ -0,0 +1,33 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection + +from .dummy_wiggly import WigglyWidget + +# Set PYSIDE_DESIGNER_PLUGINS to point to this directory and load the plugin + + +TOOLTIP = "A cool wiggly widget (Python)" +DOM_XML = """ + + + + + 0 + 0 + 400 + 200 + + + + Hello, world + + + +""" + +if __name__ == "__main__": + QPyDesignerCustomWidgetCollection.registerCustomWidget( + WigglyWidget, module="wigglywidget", tool_tip=TOOLTIP, xml=DOM_XML + ) diff --git a/tidal_dl_ng/ui/dummy_wiggly.py b/tidal_dl_ng/ui/dummy_wiggly.py new file mode 100644 index 0000000..e6eb3d3 --- /dev/null +++ b/tidal_dl_ng/ui/dummy_wiggly.py @@ -0,0 +1,68 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +from PySide6.QtCore import Property, QBasicTimer +from PySide6.QtGui import QColor, QFontMetrics, QPainter, QPalette +from PySide6.QtWidgets import QWidget + + +class WigglyWidget(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self._step = 0 + self._text = "" + self.setBackgroundRole(QPalette.Midlight) + self.setAutoFillBackground(True) + + new_font = self.font() + new_font.setPointSize(new_font.pointSize() + 20) + self.setFont(new_font) + self._timer = QBasicTimer() + + def isRunning(self): + return self._timer.isActive() + + def setRunning(self, r): + if r == self.isRunning(): + return + if r: + self._timer.start(60, self) + else: + self._timer.stop() + + def paintEvent(self, event): + if not self._text: + return + + sineTable = [0, 38, 71, 92, 100, 92, 71, 38, 0, -38, -71, -92, -100, -92, -71, -38] + + metrics = QFontMetrics(self.font()) + x = (self.width() - metrics.horizontalAdvance(self.text)) / 2 + y = (self.height() + metrics.ascent() - metrics.descent()) / 2 + color = QColor() + + with QPainter(self) as painter: + for i in range(len(self.text)): + index = (self._step + i) % 16 + color.setHsv((15 - index) * 16, 255, 191) + painter.setPen(color) + dy = (sineTable[index] * metrics.height()) / 400 + c = self._text[i] + painter.drawText(x, y - dy, str(c)) + x += metrics.horizontalAdvance(c) + + def timerEvent(self, event): + if event.timerId() == self._timer.timerId(): + self._step += 1 + self.update() + else: + QWidget.timerEvent(event) + + def text(self): + return self._text + + def setText(self, text): + self._text = text + + running = Property(bool, isRunning, setRunning) + text = Property(str, text, setText)