Skip to content

Commit 2782d23

Browse files
authored
Merge pull request #305 from werwolf2303/main
Implement oauth support
2 parents 322584b + 53d5115 commit 2782d23

File tree

3 files changed

+168
-3
lines changed

3 files changed

+168
-3
lines changed

README.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,43 @@ from librespot.zeroconf import ZeroconfServer
6262
zeroconf = ZeroconfServer.Builder().create()
6363
```
6464

65+
### Use OAuth for Login
66+
67+
#### Without auth url callback
68+
69+
```python
70+
from librespot.core import Session
71+
72+
# This will log an url in the terminal that you have to open
73+
74+
session = Session.Builder() \
75+
.oauth(None) \
76+
.create()
77+
```
78+
79+
#### With auth url callback
80+
81+
```python
82+
from librespot.core import Session
83+
84+
# This will pass the auth url to the method
85+
86+
def auth_url_callback(url):
87+
print(url)
88+
89+
session = Session.Builder() \
90+
.oauth(auth_url_callback) \
91+
.create()
92+
```
93+
6594
### Get Spotify's OAuth token
6695

6796
```python
6897
from librespot.core import Session
6998

7099

71100
session = Session.Builder() \
72-
.user_pass("Username", "Password") \
101+
.oauth(None) \
73102
.create()
74103

75104
access_token = session.tokens().get("playlist-read")
@@ -85,7 +114,7 @@ from librespot.metadata import TrackId
85114
from librespot.audio.decoders import AudioQuality, VorbisOnlyAudioQuality
86115

87116
session = Session.Builder() \
88-
.user_pass("Username", "Password") \
117+
.oauth(None) \
89118
.create()
90119

91120
track_id = TrackId.from_uri("spotify:track:xxxxxxxxxxxxxxxxxxxxxx")

librespot/core.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from Cryptodome.PublicKey import RSA
3030
from Cryptodome.Signature import PKCS1_v1_5
3131

32-
from librespot import util
32+
from librespot import util, oauth
3333
from librespot import Version
3434
from librespot.audio import AudioKeyManager
3535
from librespot.audio import CdnManager
@@ -48,6 +48,7 @@
4848
from librespot.metadata import PlaylistId
4949
from librespot.metadata import ShowId
5050
from librespot.metadata import TrackId
51+
from librespot.oauth import OAuth
5152
from librespot.proto import Authentication_pb2 as Authentication
5253
from librespot.proto import ClientToken_pb2 as ClientToken
5354
from librespot.proto import Connect_pb2 as Connect
@@ -1595,6 +1596,18 @@ def stored_file(self,
15951596
pass
15961597
return self
15971598

1599+
def oauth(self, oauth_url_callback) -> Session.Builder:
1600+
"""
1601+
Login via OAuth
1602+
1603+
You can supply an oauth_url_callback method that takes a string and returns the OAuth URL.
1604+
When oauth_url_callback is None, this will only log the auth url to the console.
1605+
"""
1606+
if os.path.isfile(self.conf.stored_credentials_file):
1607+
return self.stored_file(None)
1608+
self.login_credentials = OAuth(MercuryRequests.keymaster_client_id, "http://127.0.0.1:5588/login", oauth_url_callback).flow()
1609+
return self
1610+
15981611
def user_pass(self, username: str, password: str) -> Session.Builder:
15991612
"""Create credential from username and password
16001613

librespot/oauth.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import base64
2+
import logging
3+
import random
4+
import urllib
5+
from hashlib import sha256
6+
from http.server import HTTPServer, BaseHTTPRequestHandler
7+
from urllib.parse import urlparse
8+
from librespot.proto import Authentication_pb2 as Authentication
9+
import requests
10+
11+
12+
class OAuth:
13+
logger = logging.getLogger("Librespot:OAuth")
14+
__spotify_auth = "https://accounts.spotify.com/authorize?response_type=code&client_id=%s&redirect_uri=%s&code_challenge=%s&code_challenge_method=S256&scope=%s"
15+
__scopes = ["app-remote-control", "playlist-modify", "playlist-modify-private", "playlist-modify-public", "playlist-read", "playlist-read-collaborative", "playlist-read-private", "streaming", "ugc-image-upload", "user-follow-modify", "user-follow-read", "user-library-modify", "user-library-read", "user-modify", "user-modify-playback-state", "user-modify-private", "user-personalized", "user-read-birthdate", "user-read-currently-playing", "user-read-email", "user-read-play-history", "user-read-playback-position", "user-read-playback-state", "user-read-private", "user-read-recently-played", "user-top-read"]
16+
__spotify_token = "https://accounts.spotify.com/api/token"
17+
__spotify_token_data = {"grant_type": "authorization_code", "client_id": "", "redirect_uri": "", "code": "", "code_verifier": ""}
18+
__client_id = ""
19+
__redirect_url = ""
20+
__code_verifier = ""
21+
__code = ""
22+
__token = ""
23+
__server = None
24+
__oauth_url_callback = None
25+
26+
def __init__(self, client_id, redirect_url, oauth_url_callback):
27+
self.__client_id = client_id
28+
self.__redirect_url = redirect_url
29+
self.__oauth_url_callback = oauth_url_callback
30+
31+
def __generate_generate_code_verifier(self):
32+
possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
33+
verifier = ""
34+
for i in range(128):
35+
verifier += possible[random.randint(0, len(possible) - 1)]
36+
return verifier
37+
38+
def __generate_code_challenge(self, code_verifier):
39+
digest = sha256(code_verifier.encode('utf-8')).digest()
40+
return base64.urlsafe_b64encode(digest).decode('utf-8').rstrip('=')
41+
42+
def get_auth_url(self):
43+
self.__code_verifier = self.__generate_generate_code_verifier()
44+
auth_url = self.__spotify_auth % (self.__client_id, self.__redirect_url, self.__generate_code_challenge(self.__code_verifier), "+".join(self.__scopes))
45+
if self.__oauth_url_callback:
46+
self.__oauth_url_callback(auth_url)
47+
return auth_url
48+
49+
def set_code(self, code):
50+
self.__code = code
51+
52+
def request_token(self):
53+
if not self.__code:
54+
raise RuntimeError("You need to provide a code before!")
55+
request_data = self.__spotify_token_data
56+
request_data["client_id"] = self.__client_id
57+
request_data["redirect_uri"] = self.__redirect_url
58+
request_data["code"] = self.__code
59+
request_data["code_verifier"] = self.__code_verifier
60+
request = requests.post(
61+
self.__spotify_token,
62+
data=request_data,
63+
)
64+
if request.status_code != 200:
65+
raise RuntimeError("Received status code %d: %s" % (request.status_code, request.reason))
66+
self.__token = request.json()["access_token"]
67+
68+
def get_credentials(self):
69+
if not self.__token:
70+
raise RuntimeError("You need to request a token bore!")
71+
return Authentication.LoginCredentials(
72+
typ=Authentication.AuthenticationType.AUTHENTICATION_SPOTIFY_TOKEN,
73+
auth_data=self.__token.encode("utf-8")
74+
)
75+
76+
class CallbackServer(HTTPServer):
77+
callback_path = None
78+
79+
def __init__(self, server_address, RequestHandlerClass, callback_path, set_code):
80+
self.callback_path = callback_path
81+
self.set_code = set_code
82+
super().__init__(server_address, RequestHandlerClass)
83+
84+
class CallbackRequestHandler(BaseHTTPRequestHandler):
85+
def do_GET(self):
86+
if(self.path.startswith(self.server.callback_path)):
87+
query = urllib.parse.parse_qs(urlparse(self.path).query)
88+
if not query.__contains__("code"):
89+
self.wfile.write(b"Request doesn't contain 'code'")
90+
return
91+
self.server.set_code(query.get("code")[0])
92+
self.wfile.write(b"librespot-python received callback")
93+
pass
94+
95+
def __start_server(self):
96+
try:
97+
self.__server.handle_request()
98+
except KeyboardInterrupt:
99+
return
100+
if not self.__code:
101+
self.__start_server()
102+
103+
def run_callback_server(self):
104+
url = urlparse(self.__redirect_url)
105+
self.__server = self.CallbackServer(
106+
(url.hostname, url.port),
107+
self.CallbackRequestHandler,
108+
url.path,
109+
self.set_code
110+
)
111+
logging.info("OAuth: Waiting for callback on %s", url.hostname + ":" + str(url.port))
112+
self.__start_server()
113+
114+
def flow(self):
115+
logging.info("OAuth: Visit in your browser and log in: %s ", self.get_auth_url())
116+
self.run_callback_server()
117+
self.request_token()
118+
return self.get_credentials()
119+
120+
def __close(self):
121+
if self.__server:
122+
self.__server.shutdown()
123+

0 commit comments

Comments
 (0)