99import sys
1010import time
1111import unicodedata
12+ import uuid
1213import warnings
1314import zipfile
1415from collections import deque
@@ -537,13 +538,17 @@ def createMyPlexDevice(headers, account, timeout=10): # pragma: no cover
537538
538539 Parameters:
539540 headers (dict): Provide the X-Plex- headers for the new device.
540- A unique X-Plex-Client-Identifier is required.
541+ A unique X-Plex-Client-Identifier is required or one will be generated if not provided .
541542 account (MyPlexAccount): The Plex account to create the device on.
542543 timeout (int): Timeout in seconds to wait for device login.
543544 """
544545 from plexapi .myplex import MyPlexPinLogin
545546
546- if 'X-Plex-Client-Identifier' not in headers :
547+ if not headers :
548+ client_identifier = generateUUID ()
549+ headers = {'X-Plex-Client-Identifier' : client_identifier }
550+ print (f'No X-Plex-Client-Identifier provided, generated: { client_identifier } ' )
551+ elif 'X-Plex-Client-Identifier' not in headers :
547552 raise BadRequest ('The X-Plex-Client-Identifier header is required.' )
548553
549554 clientIdentifier = headers ['X-Plex-Client-Identifier' ]
@@ -561,13 +566,17 @@ def plexOAuth(headers, forwardUrl=None, timeout=120): # pragma: no cover
561566
562567 Parameters:
563568 headers (dict): Provide the X-Plex- headers for the new device.
564- A unique X-Plex-Client-Identifier is required.
569+ A unique X-Plex-Client-Identifier is required or one will be generated if not provided .
565570 forwardUrl (str, optional): The url to redirect the client to after login.
566571 timeout (int, optional): Timeout in seconds to wait for device login. Default 120 seconds.
567572 """
568573 from plexapi .myplex import MyPlexAccount , MyPlexPinLogin
569574
570- if 'X-Plex-Client-Identifier' not in headers :
575+ if not headers :
576+ client_identifier = generateUUID ()
577+ headers = {'X-Plex-Client-Identifier' : client_identifier }
578+ print (f'No X-Plex-Client-Identifier provided, generated: { client_identifier } ' )
579+ elif 'X-Plex-Client-Identifier' not in headers :
571580 raise BadRequest ('The X-Plex-Client-Identifier header is required.' )
572581
573582 pinlogin = MyPlexPinLogin (headers = headers , oauth = True )
@@ -583,6 +592,56 @@ def plexOAuth(headers, forwardUrl=None, timeout=120): # pragma: no cover
583592 print ('Login failed.' )
584593
585594
595+ def plexJWTAuth (headers = None , token = None , jwtToken = None , keypair = (None , None ), scope = None ): # pragma: no cover
596+ """ Helper function for Plex JWT authentication using initial Plex OAuth.
597+
598+ Parameters:
599+ headers (dict, optional): Provide the X-Plex- headers for the new device.
600+ A unique X-Plex-Client-Identifier is required or one will be generated if not provided.
601+ token (str, optional): The Plex token to use for initial device registration.
602+ If not provided, Plex OAuth will be used to obtain a token.
603+ jwtToken (str, optional): The Plex JWT (JSON Web Token) to use for authentication.
604+ If provided, the JWT will be validated and refreshed if necessary.
605+ keypair (tuple, optional): A tuple of the ED25519 (privateKey, publicKey) to use for signing the JWT.
606+ If not provided, a new keypair will be generated and saved to 'private.key' and 'public.key'.
607+ scope (list[str], optional): List of scopes to request in the JWT.
608+ """
609+ from plexapi .myplex import MyPlexAccount , MyPlexJWTAuth
610+
611+ if not headers :
612+ client_identifier = generateUUID ()
613+ headers = {'X-Plex-Client-Identifier' : client_identifier }
614+ print (f'No X-Plex-Client-Identifier provided, generated: { client_identifier } ' )
615+ elif 'X-Plex-Client-Identifier' not in headers :
616+ raise BadRequest ('The X-Plex-Client-Identifier header is required.' )
617+
618+ if not token and not jwtToken :
619+ account = plexOAuth (headers )
620+ token = account .authenticationToken
621+
622+ jwtauth = MyPlexJWTAuth (headers = headers , token = token , jwtToken = jwtToken , keypair = keypair )
623+ if jwtToken and (not keypair [0 ] or not keypair [1 ]):
624+ raise BadRequest ('When providing a jwtToken, the corresponding ED25519 keypair is required.' )
625+
626+ if not keypair [0 ] or not keypair [1 ]:
627+ jwtauth .generateKeypair (keyfiles = ('private.key' , 'public.key' ))
628+ print ('Generated new ED25519 keypair and saved to "private.key" and "public.key".' )
629+
630+ if not jwtToken :
631+ jwtauth .registerDevice ()
632+ jwtauth .refreshJWT (scope = scope )
633+ print ('Registered new device and obtained JWT token.' )
634+ elif not jwtauth .verifyJWT ():
635+ jwtauth .refreshJWT (scope = scope )
636+ print ('Refreshed expired/invalid JWT token.' )
637+
638+ if jwtauth .jwtToken :
639+ print ('JWT authentication successful!' )
640+ return MyPlexAccount (token = jwtauth .jwtToken )
641+ else :
642+ print ('JWT authentication failed.' )
643+
644+
586645def choose (msg , items , attr ): # pragma: no cover
587646 """ Command line helper to display a list of choices, asking the
588647 user to choose one of the options.
@@ -735,3 +794,7 @@ def parseXMLString(s: str):
735794 except ElementTree .ParseError : # If it fails, clean the string and try again
736795 cleaned_s = cleanXMLString (s ).encode ('utf-8' )
737796 return ElementTree .fromstring (cleaned_s ) if cleaned_s .strip () else None
797+
798+
799+ def generateUUID () -> str :
800+ return str (uuid .uuid4 ())
0 commit comments