1+ import os
2+ import time
3+ import webbrowser
14from json .decoder import JSONDecodeError
25import confuse
36from 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):
5256token = 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+
55137class 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