diff options
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | tidal_dl_ng/cli.py | 2 | ||||
| -rw-r--r-- | tidal_dl_ng/config.py | 4 | ||||
| -rw-r--r-- | tidal_dl_ng/dialog.py | 31 | ||||
| -rw-r--r-- | tidal_dl_ng/gui.py | 29 | ||||
| -rw-r--r-- | tidal_dl_ng/ui/dialog_login.py | 110 | ||||
| -rw-r--r-- | tidal_dl_ng/ui/dialog_login.ui | 194 | ||||
| -rw-r--r-- | tidal_dl_ng/ui/dummy_register.py | 33 | ||||
| -rw-r--r-- | tidal_dl_ng/ui/dummy_wiggly.py | 68 |
9 files changed, 464 insertions, 12 deletions
@@ -5,8 +5,7 @@ [](https://img.shields.io/github/commit-activity/m/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.  @@ -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'<a href="{url_login}">{url_login}</a>') + 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() + + 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 - while True: - result = self.tidal.login(logger_gui.info) + 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() - if result: - break + 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>DialogLogin</class> + <widget class="QDialog" name="DialogLogin"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>451</width> + <height>400</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <widget class="QDialogButtonBox" name="bb_dialog"> + <property name="geometry"> + <rect> + <x>20</x> + <y>350</y> + <width>411</width> + <height>32</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="styleSheet"> + <string notr="true"/> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + <widget class="QWidget" name="verticalLayoutWidget"> + <property name="geometry"> + <rect> + <x>20</x> + <y>20</y> + <width>411</width> + <height>321</height> + </rect> + </property> + <layout class="QVBoxLayout" name="lv_main"> + <item> + <widget class="QLabel" name="l_header"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <pointsize>23</pointsize> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>TIDAL PKCE Login</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="l_description"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <italic>true</italic> + </font> + </property> + <property name="text"> + <string>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.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="l_url_login"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>50</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="styleSheet"> + <string notr="true">border: 2px solid red; padding: 6px</string> + </property> + <property name="text"> + <string>PLACEHOLDER URL...</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QTextEdit" name="te_url_redirect"> + <property name="placeholderText"> + <string>Copy and paste the redirect URL after login here...</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="l_hint"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <italic>true</italic> + </font> + </property> + <property name="text"> + <string>Waiting...</string> + </property> + </widget> + </item> + </layout> + </widget> + </widget> + <resources/> + <connections> + <connection> + <sender>bb_dialog</sender> + <signal>accepted()</signal> + <receiver>DialogLogin</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>bb_dialog</sender> + <signal>rejected()</signal> + <receiver>DialogLogin</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> 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 = """ +<ui language='c++'> + <widget class='WigglyWidget' name='wigglyWidget'> + <property name='geometry'> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>200</height> + </rect> + </property> + <property name='text'> + <string>Hello, world</string> + </property> + </widget> +</ui> +""" + +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) |
