Skip to content

Commit fa717bd

Browse files
committed
plex: Add auth v2
Closes #356, #245
1 parent 5123d27 commit fa717bd

File tree

2 files changed

+95
-43
lines changed

2 files changed

+95
-43
lines changed

trakt_scrobbler/cli/plex.py

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,11 @@
44
from rich.prompt import Prompt
55

66
from trakt_scrobbler import logger
7-
from trakt_scrobbler.utils import safe_request
8-
97
from .console import console
108

119
app = typer.Typer(help="Runs the authentication flow for plex.")
1210

1311

14-
def plex_token_auth(login, password):
15-
auth_params = {
16-
"url": "https://plex.tv/users/sign_in.json",
17-
"data": {"user[login]": login, "user[password]": password},
18-
"headers": {
19-
"X-Plex-Client-Identifier": "com.iamkroot.trakt_scrobbler",
20-
"X-Plex-Product": "Trakt Scrobbler",
21-
"Accept": "application/json",
22-
},
23-
}
24-
return safe_request("post", auth_params)
25-
26-
27-
def get_token():
28-
logger.info("Retrieving plex token")
29-
login = Prompt.ask("Plex login ID", console=console)
30-
pwd = Prompt.ask("Plex password", password=True, console=console)
31-
resp = plex_token_auth(login, pwd)
32-
if resp:
33-
return resp.json()["user"]["authToken"]
34-
elif resp is not None:
35-
err_msg = resp.json().get("error", resp.text)
36-
console.print(err_msg, style="error")
37-
logger.error(err_msg)
38-
return None
39-
else:
40-
logger.error("Unable to get access token")
41-
return None
42-
43-
4412
@app.command(name="auth", help="Runs the authentication flow for Plex")
4513
def auth(
4614
force: Annotated[
@@ -60,6 +28,7 @@ def auth(
6028
),
6129
] = False,
6230
):
31+
from trakt_scrobbler.player_monitors.plex import PlexAuth
6332
from trakt_scrobbler.player_monitors.plex import token as token_store
6433

6534
if force or token: # token implies force
@@ -69,14 +38,14 @@ def auth(
6938
if token:
7039
# TODO: Verify that token is valid
7140
token_store.data = Prompt.ask("[question]Enter token[/]", console=console)
41+
console.print("Plex token is saved.", style="info")
7242
elif not token_store:
73-
token_data = get_token()
74-
if token_data:
75-
token_store.data = token_data
43+
plex_auth = PlexAuth()
44+
if plex_auth.device_auth():
7645
logger.info("Saved plex token")
77-
78-
if token_store:
79-
console.print("Plex token is saved.", style="info")
46+
console.print("Plex token is saved.", style="info")
47+
else:
48+
console.print("Failed to retrieve plex token.", style="error")
49+
raise typer.Exit(1)
8050
else:
81-
console.print("Failed to retrieve plex token.", style="error")
82-
raise typer.Exit(1)
51+
console.print("Plex token is saved.", style="info")

trakt_scrobbler/player_monitors/plex.py

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import os
2+
import time
3+
import webbrowser
14
from json.decoder import JSONDecodeError
25
import confuse
36
from requests import HTTPError
@@ -15,6 +18,7 @@ class PlexToken:
1518
1619
TODO: Use some form of OS-provided encrypted storage
1720
"""
21+
1822
PATH = DATA_DIR / "plex_token.txt"
1923
OLD_PATH = DATA_DIR / "plex_token.json"
2024

@@ -52,6 +56,84 @@ def __bool__(self):
5256
token = PlexToken()
5357

5458

59+
class PlexAuth:
60+
# https://forums.plex.tv/t/authenticating-with-plex/609370
61+
API_URL = "https://plex.tv/api/v2"
62+
CLIENT_ID = (
63+
"com.iamkroot.trakt_scrobbler" # Reuse from CLI or consistently use this
64+
)
65+
PRODUCT = "Trakt Scrobbler"
66+
67+
def __init__(self):
68+
self._token_store = token
69+
70+
def get_pin(self):
71+
params = {
72+
"url": f"{self.API_URL}/pins",
73+
"headers": {"Accept": "application/json"},
74+
"data": {
75+
"strong": "true",
76+
"X-Plex-Product": self.PRODUCT,
77+
"X-Plex-Client-Identifier": self.CLIENT_ID,
78+
},
79+
}
80+
resp = safe_request("post", params)
81+
return resp.json() if resp else None
82+
83+
def device_auth(self):
84+
pin_data = self.get_pin()
85+
if not pin_data:
86+
logger.error("Could not get Plex PIN.")
87+
return False
88+
89+
auth_url = (
90+
f"https://app.plex.tv/auth#?clientID={self.CLIENT_ID}&code={pin_data['code']}"
91+
f"&context[device][product]={self.PRODUCT}"
92+
)
93+
94+
logger.info(f"Verification URL: {auth_url}")
95+
notify(
96+
"Opening browser for Plex authentication...", stdout=True, category="plex"
97+
)
98+
99+
term_bak = os.environ.pop("TERM", None)
100+
webbrowser.open(auth_url)
101+
if term_bak is not None:
102+
os.environ["TERM"] = term_bak
103+
104+
# Poll for token
105+
pin_id = pin_data["id"]
106+
check_url = f"{self.API_URL}/pins/{pin_id}"
107+
108+
start_time = time.time()
109+
poll_interval = 2
110+
# at max 2 minutes
111+
expires_in = min(pin_data.get("expiresIn", 120), 120)
112+
113+
while time.time() - start_time < expires_in:
114+
params = {
115+
"url": check_url,
116+
"headers": {"Accept": "application/json"},
117+
"params": {"X-Plex-Client-Identifier": self.CLIENT_ID},
118+
}
119+
resp = safe_request("get", params)
120+
if resp and resp.status_code == 200:
121+
data = resp.json()
122+
if data.get("authToken"):
123+
self._token_store.data = data["authToken"]
124+
logger.info("Plex authenticated successfully.")
125+
notify(
126+
"Plex authenticated successfully.", stdout=True, category="plex"
127+
)
128+
return True
129+
130+
time.sleep(poll_interval)
131+
132+
logger.error("Plex authentication timed out.")
133+
notify("Plex authentication timed out.", stdout=True, category="plex")
134+
return False
135+
136+
55137
class PlexMon(WebInterfaceMon):
56138
name = "plex"
57139
exclude_import = False
@@ -62,7 +144,7 @@ class PlexMon(WebInterfaceMon):
62144
"ip": confuse.String(default="localhost"),
63145
"port": confuse.String(default="32400"),
64146
"poll_interval": confuse.Number(default=10),
65-
"scrobble_user": confuse.String(default="")
147+
"scrobble_user": confuse.String(default=""),
66148
}
67149

68150
def __init__(self, scrobble_queue):
@@ -74,8 +156,9 @@ def __init__(self, scrobble_queue):
74156
self.token = token.data
75157
if not self.token:
76158
logger.error("Unable to retrieve plex token.")
77-
notify("Unable to retrieve plex token. Rerun plex auth.",
78-
category="exception")
159+
notify(
160+
"Unable to retrieve plex token. Rerun plex auth.", category="exception"
161+
)
79162
return
80163
super().__init__(scrobble_queue)
81164
self.sess.headers["Accept"] = "application/json"

0 commit comments

Comments
 (0)