无需将URL粘贴到控制台的Spotipy OAuth2访问令牌

时间:2020-08-03 14:32:12

标签: python oauth-2.0 pyside2 spotipy

我目前正在研究Python Spotify管理应用程序。 我正在使用PySide2(Qt)创建GUI。 Qt具有浏览器功能来访问网站。我正在使用此代码进行身份验证:

import spotipy
from spotipy.oauth2 import SpotifyOAuth

auth = SpotifyOAuth(scope=scope, cache_path='user_cache',
                    client_id=client_id, client_secret=client_secret,
                    redirect_uri=redirect_uri)

sp = spotipy.Spotify(auth_manager=auth)

print(auth.get_auth_response())

运行此代码时,它将在Chrome中打开一个浏览器窗口,并要求我登录到我的Spotify帐户。然后它将我重定向到我的redirect_uri。我必须将此链接粘贴到控制台中。

Console Output

我的问题是我不想将URI粘贴到控制台中。我希望应用程序从PySide2(Qt)浏览器中获取url(我知道如何获取当前链接等),然后将其自动粘贴到控制台中。

我的问题是:

  1. Spotipy是否具有无需将链接粘贴到控制台即可制作OAuth2的功能?我想绕过输入并将重定向链接直接传递到Spotipy。

  2. 是否可以选择手动打开的浏览器?

我想不使用Flask,而只使用PySide2(PyQt,Qt等)来实现。最好的情况:只需从查询中获取令牌并将其用于api请求

1 个答案:

答案 0 :(得分:1)

可能的解决方案是在请求重定向的Qt WebEngine中实现新方案。

另一方面,Spotipy使用请求使请求阻塞事件循环,从而导致GUI冻结,因此我修改了请求以使其异步。

from functools import cached_property, partial
import threading
import types

import spotipy
from spotipy.oauth2 import SpotifyOAuth, SpotifyClientCredentials

from PySide2 import QtCore, QtWidgets, QtWebEngineCore, QtWebEngineWidgets


class ReplySpotify(QtCore.QObject):
    finished = QtCore.Signal()

    def __init__(self, func, args=(), kwargs=None, parent=None):
        super().__init__(parent)
        self._results = None
        self._is_finished = False
        self._error_str = ""
        threading.Thread(
            target=self._execute, args=(func, args, kwargs), daemon=True
        ).start()

    @property
    def results(self):
        return self._results

    @property
    def error_str(self):
        return self._error_str

    def is_finished(self):
        return self._is_finished

    def has_error(self):
        return bool(self._error_str)

    def _execute(self, func, args, kwargs):
        if kwargs is None:
            kwargs = {}
        try:
            self._results = func(*args, **kwargs)
        except Exception as e:
            self._error_str = str(e)
        self._is_finished = True
        self.finished.emit()


def convert_to_reply(func, *args, **kwargs):
    reply = ReplySpotify(func, args, kwargs)
    return reply


class ConvertToReply(type):
    def __call__(cls, *args, **kw):
        klass = super().__call__(*args, **kw)
        for key in dir(klass):
            value = getattr(klass, key)
            if isinstance(value, types.MethodType) and not key.startswith("_"):
                wrapped = partial(convert_to_reply, value)
                setattr(klass, key, wrapped)
        return klass


class QSpotify(spotipy.Spotify, metaclass=ConvertToReply):
    pass


class QOauthHandler(QtWebEngineCore.QWebEngineUrlSchemeHandler):
    authenticated = QtCore.Signal(str, dict)

    def __init__(self, parent=None):
        super().__init__(parent)
        self._html = ""

    @property
    def html(self):
        return self._html

    @html.setter
    def html(self, html):
        self._html = html

    def requestStarted(self, request):
        request_url = request.requestUrl()
        if request_url.host() == "oauth":
            query = QtCore.QUrlQuery(request_url.query())
            d = dict()
            for k, v in query.queryItems():
                d[k] = v
            self.authenticated.emit(request_url.path(), d)

            buf = QtCore.QBuffer(parent=self)
            request.destroyed.connect(buf.deleteLater)
            buf.open(QtCore.QIODevice.WriteOnly)
            buf.write(self.html.encode())
            buf.seek(0)
            buf.close()
            request.reply(b"text/html", buf)


class QSpotifyOAuth(QtCore.QObject, SpotifyOAuth):
    authenticationRequired = QtCore.Signal(QtCore.QUrl)
    codeChanged = QtCore.Signal()

    def __init__(
        self,
        client_id=None,
        client_secret=None,
        redirect_uri=None,
        state=None,
        scope=None,
        cache_path=None,
        username=None,
        proxies=None,
        show_dialog=False,
        requests_session=True,
        requests_timeout=None,
        parent=None,
    ):
        QtCore.QObject.__init__(self, parent=None)
        SpotifyOAuth.__init__(
            self,
            client_id,
            client_secret,
            redirect_uri,
            state,
            scope,
            cache_path,
            username,
            proxies,
            show_dialog,
            requests_session,
            requests_timeout,
        )
        self._code = ""

    def get_auth_response(self, state=None):
        url = QtCore.QUrl.fromUserInput(self.get_authorize_url())
        self.authenticationRequired.emit(url)
        loop = QtCore.QEventLoop()
        self.codeChanged.connect(loop.quit)
        loop.exec_()
        if state is None:
            state = self.state
        return state, self.code

    @property
    def code(self):
        return self._code

    def autenticate(self, values):
        self._code = values.get("code", "")
        self.codeChanged.emit()


class Widget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        lay = QtWidgets.QHBoxLayout(self)
        lay.addWidget(self.view, stretch=1)
        lay.addWidget(self.log, stretch=1)

        self.view.hide()

        client_id = ""
        client_secret = ""
        self.qauth = QSpotifyOAuth(
            cache_path="user_cache",
            client_id=client_id,
            client_secret=client_secret,
            redirect_uri="qt://oauth/spotify",
            scope="user-library-read",
        )
        self.qclient = QSpotify(auth_manager=self.qauth)
        self.qauth.authenticationRequired.connect(self.view.load)
        self.qauth.authenticationRequired.connect(self.view.show)

        reply = self.qclient.current_user_saved_tracks()
        reply.setParent(self)
        reply.finished.connect(partial(self.on_finished, reply))

    @cached_property
    def view(self):
        return QtWebEngineWidgets.QWebEngineView()

    @cached_property
    def log(self):
        return QtWidgets.QTextEdit(readOnly=True)

    def handle(self, path, values):
        self.qauth.autenticate(values)
        self.view.hide()

    def on_finished(self, reply):
        reply.deleteLater()
        for item in reply.results["items"]:
            track = item["track"]
            text = "<b>%s</b> %s" % (track["artists"][0]["name"], track["name"])
            self.log.append(text)

        if reply.results["items"]:
            new_reply = self.qclient.next(reply.results)
            new_reply.setParent(self)
            new_reply.finished.connect(partial(self.on_finished, new_reply))


def main():
    import sys

    scheme = QtWebEngineCore.QWebEngineUrlScheme(b"qt")
    QtWebEngineCore.QWebEngineUrlScheme.registerScheme(scheme)

    app = QtWidgets.QApplication(sys.argv)
    QtCore.QCoreApplication.setOrganizationName("qtspotify")
    QtCore.QCoreApplication.setApplicationName("Qt Spotify")

    handler = QOauthHandler()

    profile = QtWebEngineWidgets.QWebEngineProfile.defaultProfile()
    """profile.setPersistentCookiesPolicy(
        QtWebEngineWidgets.QWebEngineProfile.NoPersistentCookies
    )"""
    profile.installUrlSchemeHandler(b"qt", handler)

    w = Widget()
    w.resize(640, 480)
    w.show()

    handler.authenticated.connect(w.handle)

    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

注意:您必须将网址“ qt:// oauth / spotify”添加到仪表板中的项目设置中:

enter image description here