✨ Implemented login dialog for PKCE login method. Fixes #22.

This commit is contained in:
Robert Honz
2024-02-01 07:38:01 +01:00
parent a994aa629e
commit c1bf278fd7
9 changed files with 464 additions and 12 deletions
+2 -3
View File
@@ -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) [![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) [![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 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.
version of this tool.
![App Image](assets/app.png) ![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): The GUI is build with `PySide6` using the [Qt Designer](https://doc.qt.io/qt-6/qtdesigner-manual.html):
```bash ```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: After all changes are saved you need to translate the Qt Designer `*.ui` file into Python code:
+1 -1
View File
@@ -95,7 +95,7 @@ def login(ctx: typer.Context) -> bool:
settings = Settings() settings = Settings()
tidal = Tidal(settings) tidal = Tidal(settings)
result = tidal.login(print) result = tidal.login(fn_print=print)
ctx.obj[CTX_TIDAL] = tidal ctx.obj[CTX_TIDAL] = tidal
return result return result
+2 -2
View File
@@ -120,7 +120,7 @@ class Tidal(BaseConfig, metaclass=SingletonMeta):
return result return result
def _login_finalize(self) -> bool: def login_finalize(self) -> bool:
result = self.session.check_login() result = self.session.check_login()
if result: if result:
@@ -151,7 +151,7 @@ class Tidal(BaseConfig, metaclass=SingletonMeta):
# Login method: PKCE authorization (enables HiRes streaming) # Login method: PKCE authorization (enables HiRes streaming)
self.session.login_pkce(fn_print) self.session.login_pkce(fn_print)
is_login = self._login_finalize() is_login = self.login_finalize()
if is_login: if is_login:
fn_print("The login was successful. I have stored your credentials (token).") fn_print("The login was successful. I have stored your credentials (token).")
+30 -1
View File
@@ -1,6 +1,7 @@
from PySide6.QtWidgets import QDialog from PySide6.QtWidgets import QDialog
from tidal_dl_ng import __version__ 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 from tidal_dl_ng.ui.dialog_version import Ui_DialogVersion
@@ -9,11 +10,39 @@ class DialogVersion(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
# Create an instance of the GUI # Create an instance of the GUI
self.ui = Ui_DialogVersion() self.ui = Ui_DialogVersion()
# Run the .setupUi() method to show the GUI # Run the .setupUi() method to show the GUI
self.ui.setupUi(self) self.ui.setupUi(self)
# Set the version. # Set the version.
self.ui.l_version.setText("v" + __version__) self.ui.l_version.setText("v" + __version__)
# Show
self.exec() 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
+24 -5
View File
@@ -2,8 +2,10 @@ import math
import sys import sys
from collections.abc import Callable from collections.abc import Callable
from requests.exceptions import HTTPError
from tidal_dl_ng import __version__ 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.path import get_format_template
from tidal_dl_ng.helper.tidal import items_results_all, search_results_all, user_media_lists 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 result = True
else: else:
self.tidal = Tidal(self.settings) self.tidal = Tidal(self.settings)
result = self.tidal.login_token()
while True: if not result:
result = self.tidal.login(logger_gui.info) 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: if d_login.return_code == 1:
break 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: if result:
# Init `Download` object. # Init `Download` object.
+110
View File
@@ -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
+194
View File
@@ -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 &quot;Ooops&quot; page after a successful login. Copy the URL of this &quot;Ooops&quot; 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>
+33
View File
@@ -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
)
+68
View File
@@ -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)