Skip to content

Commit

Permalink
85 unable to sign in (#87)
Browse files Browse the repository at this point in the history
* error handling

* update cli and tests to use browser auth and add option for oauth

* added test that will alert us by failing if/when oauth starts working again; also no need to pass in yt_auth object to tests since it will always use browser.json unless we specify --oauth

* formatted log statement

* updated GUI to use browser auth by default, and optionally use oauth

* re-enable passing in yt_auth in context as its needed in order to specify the path to the browser.json file in test/resources

* get correct path

* show generic person icon when signed in using browser auth

* remove redundant log

* update readmes
  • Loading branch information
apastel authored Nov 16, 2024
1 parent 152c01d commit e522d0d
Show file tree
Hide file tree
Showing 17 changed files with 573 additions and 145 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@ Find the .exe (or .deb) file in the [Releases](https://github.com/apastel/ytmusi
See [CLI README](ytmusic_deleter/README.md)

## Setup
Once installed and running, simply click the "Log In" button to authenticate to your YouTube Music account.
You will see your Google account name and profile photo to ensure you are logged into the correct account.
Once installed and running, click the "Sign In" button to authenticate to your YouTube Music account.

This login process uses the [Google API flow for TV devices](https://developers.google.com/youtube/v3/guides/auth/devices)
and is handled by the [ytmusicapi](https://ytmusicapi.readthedocs.io/en/stable/setup/oauth.html), which handles all of the
API interaction in this app.
Since Google has removed the ability to sign in using OAuth, you must now sign in using a cookie from your browser.

[Video Tutorial](https://youtu.be/oV-yLi1AW1c) on using YT Music Deleter
Follow the instructions in the [ytmusicapi](https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers)
project to copy your request headers to your clipboard. Then paste them into the window in YTMusic Deleter.
> Note: It's recommended to use Firefox for copying the headers since Firefox conveniently has a "Copy Request Headers" button.
[Video Tutorial](https://youtu.be/oV-yLi1AW1c) on using YTMusic Deleter


## Troubleshooting
Expand Down
105 changes: 105 additions & 0 deletions gui/src/main/python/browser_auth_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import shutil
from pathlib import Path

import ytmusicapi
from generated.ui_auth_dialog import Ui_AuthDialog
from PySide6.QtCore import QDir
from PySide6.QtCore import QObject
from PySide6.QtCore import QThread
from PySide6.QtCore import Signal
from PySide6.QtCore import Slot
from PySide6.QtWidgets import QDialog
from PySide6.QtWidgets import QDialogButtonBox
from PySide6.QtWidgets import QFileDialog
from PySide6.QtWidgets import QLineEdit
from PySide6.QtWidgets import QMessageBox
from PySide6.QtWidgets import QPlainTextEdit
from ytmusic_deleter import common
from ytmusicapi import YTMusic


class BrowserAuthDialog(QDialog, Ui_AuthDialog):
def __init__(self, parent):
super().__init__(parent)
# self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) # also closes parent window now for some reason
self.setupUi(self)

# Check again in case auth file was deleted/moved
self.parentWidget().is_signed_in()

self.enable_ok_button()
self.headersInputBox.textChanged.connect(self.enable_ok_button)
self.fileNameField.textChanged.connect(self.enable_ok_button)

self.browseButton.clicked.connect(self.choose_auth_file)

self.auth_setup = YTAuthSetup(self.headersInputBox, self.fileNameField, self.parentWidget().credential_dir)
self.auth_setup.auth_signal.connect(self.auth_finished)

self.headersInputBox.setPlaceholderText(
"""
Paste your raw request headers here and click OK.
See https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers
Alternatively, use Browse to select an existing browser.json file.
"""
)

def accept(self):
self.thread = QThread(self)
self.thread.started.connect(self.auth_setup.setup_auth)
self.thread.start()

@Slot()
def enable_ok_button(self):
self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(
self.headersInputBox.toPlainText() != "" or self.fileNameField.text() != ""
)

@Slot()
def choose_auth_file(self):
file_name, _ = QFileDialog.getOpenFileName(self, "Select Auth File", QDir.rootPath(), "*.json")
self.fileNameField.setText(file_name)

@Slot(str)
def auth_finished(self, auth_result):
if auth_result == "Success":
self.parentWidget().update_buttons()
self.close()
else:
error_dialog = QMessageBox()
error_dialog.setIcon(QMessageBox.Critical)
error_dialog.setText(auth_result)
error_dialog.setInformativeText(
"<html>See <a href=https://ytmusicapi.readthedocs.io/en/latest/setup.html#copy-authentication-headers>"
"https://ytmusicapi.readthedocs.io/en/latest/setup.html#copy-authentication-headers</a> for "
"instructions on obtaining your request headers.</html>"
)
error_dialog.setWindowTitle("Error")
error_dialog.exec()


class YTAuthSetup(QObject):
auth_signal = Signal(str)

def __init__(self, textarea: QPlainTextEdit, filename_field: QLineEdit, cred_dir):
super().__init__()
self.textarea = textarea
self.filename_field = filename_field
self.browser_file_path = Path(cred_dir) / common.BROWSER_FILENAME

@Slot()
def setup_auth(self):
try:
# Use selected headers_auth.json file
if self.filename_field.text():
YTMusic(self.filename_field.text())
shutil.copy2(self.filename_field.text(), self.browser_file_path)
# Use pasted headers
else:
user_input = self.textarea.toPlainText()
YTMusic(ytmusicapi.setup(filepath=self.browser_file_path, headers_raw=user_input))

self.auth_signal.emit("Success")
except Exception as e:
self.auth_signal.emit(str(e))
68 changes: 68 additions & 0 deletions gui/src/main/python/generated/ui_auth_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-

################################################################################
## Form generated from reading UI file 'auth_dialog.ui'
##
## Created by: Qt User Interface Compiler version 6.8.0
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox,
QLabel, QLineEdit, QPlainTextEdit, QPushButton,
QSizePolicy, QWidget)

class Ui_AuthDialog(object):
def setupUi(self, AuthDialog):
if not AuthDialog.objectName():
AuthDialog.setObjectName(u"AuthDialog")
AuthDialog.setWindowModality(Qt.ApplicationModal)
AuthDialog.resize(569, 511)
self.buttonBox = QDialogButtonBox(AuthDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setEnabled(True)
self.buttonBox.setGeometry(QRect(190, 470, 171, 32))
self.buttonBox.setOrientation(Qt.Horizontal)
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
self.buttonBox.setCenterButtons(True)
self.headersInputBox = QPlainTextEdit(AuthDialog)
self.headersInputBox.setObjectName(u"headersInputBox")
self.headersInputBox.setGeometry(QRect(60, 20, 451, 251))
self.orLabel = QLabel(AuthDialog)
self.orLabel.setObjectName(u"orLabel")
self.orLabel.setGeometry(QRect(130, 380, 331, 31))
self.browseButton = QPushButton(AuthDialog)
self.browseButton.setObjectName(u"browseButton")
self.browseButton.setGeometry(QRect(130, 420, 75, 23))
self.fileNameField = QLineEdit(AuthDialog)
self.fileNameField.setObjectName(u"fileNameField")
self.fileNameField.setEnabled(False)
self.fileNameField.setGeometry(QRect(220, 420, 241, 21))
self.helpLabel = QLabel(AuthDialog)
self.helpLabel.setObjectName(u"helpLabel")
self.helpLabel.setGeometry(QRect(60, 270, 451, 51))
self.helpLabel.setAlignment(Qt.AlignCenter)
self.helpLabel.setWordWrap(True)
self.helpLabel.setOpenExternalLinks(True)

self.retranslateUi(AuthDialog)
self.buttonBox.accepted.connect(AuthDialog.accept)
self.buttonBox.rejected.connect(AuthDialog.reject)

QMetaObject.connectSlotsByName(AuthDialog)
# setupUi

def retranslateUi(self, AuthDialog):
AuthDialog.setWindowTitle(QCoreApplication.translate("AuthDialog", u"Authentication", None))
self.orLabel.setText(QCoreApplication.translate("AuthDialog", u"Or select an existing browser.json file", None))
self.browseButton.setText(QCoreApplication.translate("AuthDialog", u"Browse", None))
self.helpLabel.setText(QCoreApplication.translate("AuthDialog", u"<html>See <a href=\"https://ytmusicapi.readthedocs.io/en/latest/setup.html#copy-authentication-headers\">https://ytmusicapi.readthedocs.io/en/latest/setup.html#copy-authentication-headers</a> for instructions on obtaining your request headers.</html>", None))
# retranslateUi

17 changes: 15 additions & 2 deletions gui/src/main/python/generated/ui_preferences_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,19 @@ def setupUi(self, PreferencesDialog):
if not PreferencesDialog.objectName():
PreferencesDialog.setObjectName(u"PreferencesDialog")
PreferencesDialog.setWindowModality(Qt.ApplicationModal)
PreferencesDialog.resize(400, 300)
PreferencesDialog.resize(497, 300)
PreferencesDialog.setModal(True)
self.buttonBox = QDialogButtonBox(PreferencesDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setGeometry(QRect(290, 20, 81, 241))
self.buttonBox.setGeometry(QRect(390, 20, 81, 241))
self.buttonBox.setOrientation(Qt.Vertical)
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
self.verboseCheckBox = QCheckBox(PreferencesDialog)
self.verboseCheckBox.setObjectName(u"verboseCheckBox")
self.verboseCheckBox.setGeometry(QRect(30, 20, 241, 41))
self.oauthCheckbox = QCheckBox(PreferencesDialog)
self.oauthCheckbox.setObjectName(u"oauthCheckbox")
self.oauthCheckbox.setGeometry(QRect(30, 60, 351, 41))

self.retranslateUi(PreferencesDialog)
self.buttonBox.accepted.connect(PreferencesDialog.accept)
Expand All @@ -53,5 +56,15 @@ def retranslateUi(self, PreferencesDialog):
self.verboseCheckBox.setWhatsThis(QCoreApplication.translate("PreferencesDialog", u"Enables verbose or \"debug\" logging in the application.", None))
#endif // QT_CONFIG(whatsthis)
self.verboseCheckBox.setText(QCoreApplication.translate("PreferencesDialog", u"Enable verbose logging", None))
#if QT_CONFIG(tooltip)
self.oauthCheckbox.setToolTip(QCoreApplication.translate("PreferencesDialog", u"Enables user-friendly authentication using OAuth (may not work)", None))
#endif // QT_CONFIG(tooltip)
#if QT_CONFIG(statustip)
self.oauthCheckbox.setStatusTip(QCoreApplication.translate("PreferencesDialog", u"Enables user-friendly authentication using OAuth (may not work)", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(whatsthis)
self.oauthCheckbox.setWhatsThis(QCoreApplication.translate("PreferencesDialog", u"Enables user-friendly authentication using OAuth (may not work)", None))
#endif // QT_CONFIG(whatsthis)
self.oauthCheckbox.setText(QCoreApplication.translate("PreferencesDialog", u"Enable OAuth login (may not work)", None))
# retranslateUi

Loading

0 comments on commit e522d0d

Please sign in to comment.