@@ -421,6 +421,9 @@ pub fn run_setup_server() -> Result<SetupResult, Box<dyn std::error::Error>> {
421421 // The device code the active poller is tracking, returned to any resubmit so
422422 // the displayed code always matches what the poller is waiting on.
423423 let active_device_code: Arc < Mutex < Option < DeviceCodeResponse > > > = Arc :: new ( Mutex :: new ( None ) ) ;
424+ // Same idea for Jellyfin Quick Connect: the code the active poller is waiting
425+ // on, cleared when it expires so a later start mints a fresh one.
426+ let active_jellyfin_code: Arc < Mutex < Option < String > > > = Arc :: new ( Mutex :: new ( None ) ) ;
424427
425428 // Open browser to setup page
426429 let url = format ! ( "http://127.0.0.1:{port}" ) ;
@@ -808,14 +811,29 @@ pub fn run_setup_server() -> Result<SetupResult, Box<dyn std::error::Error>> {
808811
809812 // Claim the polling slot before initiating, mirroring the Trakt
810813 // path: a duplicate start (double-click, reload) must not mint a
811- // fresh code that no poller is waiting on. Jellyfin keeps no stored
812- // active code to hand back, so a duplicate is asked to retry.
814+ // fresh code that no poller is waiting on. Instead, hand back the
815+ // code the running poller is already tracking. The poller releases
816+ // the slot when its code expires, so a later start can begin fresh.
813817 if polling_started
814818 . compare_exchange ( false , true , Ordering :: SeqCst , Ordering :: SeqCst )
815819 . is_err ( )
816820 {
817- let response = Response :: from_string ( "Login already in progress, retry" )
818- . with_status_code ( StatusCode ( 503 ) ) ;
821+ let active = active_jellyfin_code. lock ( ) . ok ( ) . and_then ( |g| g. clone ( ) ) ;
822+ let response = match active {
823+ Some ( code) => {
824+ let json =
825+ serde_json:: json!( { "code" : code, "interval" : 2 } ) . to_string ( ) ;
826+ Response :: from_string ( json) . with_header (
827+ tiny_http:: Header :: from_bytes (
828+ & b"Content-Type" [ ..] ,
829+ & b"application/json" [ ..] ,
830+ )
831+ . unwrap ( ) ,
832+ )
833+ }
834+ None => Response :: from_string ( "Login already starting, retry" )
835+ . with_status_code ( StatusCode ( 503 ) ) ,
836+ } ;
819837 let _ = request. respond ( response) ;
820838 continue ;
821839 }
@@ -825,10 +843,15 @@ pub fn run_setup_server() -> Result<SetupResult, Box<dyn std::error::Error>> {
825843 if let Ok ( mut s) = oauth_state. lock ( ) {
826844 * s = OAuthState :: Pending ;
827845 }
846+ if let Ok ( mut guard) = active_jellyfin_code. lock ( ) {
847+ * guard = Some ( state. code . clone ( ) ) ;
848+ }
828849
829850 let oauth_state_clone = Arc :: clone ( & oauth_state) ;
830851 let setup_complete_clone = Arc :: clone ( & setup_complete) ;
831852 let result_clone = Arc :: clone ( & result) ;
853+ let polling_started_clone = Arc :: clone ( & polling_started) ;
854+ let active_code_clone = Arc :: clone ( & active_jellyfin_code) ;
832855 let state_clone = state. clone ( ) ;
833856 let server = server_url. clone ( ) ;
834857 let device = device_id. clone ( ) ;
@@ -840,6 +863,8 @@ pub fn run_setup_server() -> Result<SetupResult, Box<dyn std::error::Error>> {
840863 oauth_state_clone,
841864 setup_complete_clone,
842865 result_clone,
866+ polling_started_clone,
867+ active_code_clone,
843868 ) ;
844869 } ) ;
845870
@@ -1009,21 +1034,31 @@ pub fn run_setup_server() -> Result<SetupResult, Box<dyn std::error::Error>> {
10091034
10101035/// Poll for Jellyfin Quick Connect approval in the background, then exchange the
10111036/// secret for an access token, write the config, and signal completion.
1012- #[ allow( clippy:: needless_pass_by_value) ]
1037+ #[ allow( clippy:: needless_pass_by_value, clippy :: too_many_arguments ) ]
10131038fn poll_jellyfin_in_background (
10141039 server_url : String ,
10151040 device_id : String ,
10161041 state : QuickConnectState ,
10171042 oauth_state : Arc < Mutex < OAuthState > > ,
10181043 setup_complete : Arc < AtomicBool > ,
10191044 result : Arc < Mutex < Option < SetupResult > > > ,
1045+ polling_started : Arc < AtomicBool > ,
1046+ active_code : Arc < Mutex < Option < String > > > ,
10201047) {
10211048 let deadline = Instant :: now ( ) + Duration :: from_secs ( 300 ) ;
10221049 let set_state = |s : OAuthState | {
10231050 if let Ok ( mut guard) = oauth_state. lock ( ) {
10241051 * guard = s;
10251052 }
10261053 } ;
1054+ // On any terminal non-success outcome, free the slot and forget the code so a
1055+ // later start can begin a fresh flow instead of being stuck on a dead code.
1056+ let release = || {
1057+ polling_started. store ( false , Ordering :: SeqCst ) ;
1058+ if let Ok ( mut guard) = active_code. lock ( ) {
1059+ * guard = None ;
1060+ }
1061+ } ;
10271062
10281063 while Instant :: now ( ) < deadline {
10291064 thread:: sleep ( Duration :: from_secs ( 2 ) ) ;
@@ -1036,6 +1071,7 @@ fn poll_jellyfin_in_background(
10361071 Err ( e) => {
10371072 tracing:: error!( "Jellyfin authentication failed: {}" , e) ;
10381073 set_state ( OAuthState :: Error ( format ! ( "Authentication failed: {e}" ) ) ) ;
1074+ release ( ) ;
10391075 return ;
10401076 }
10411077 } ;
@@ -1049,6 +1085,7 @@ fn poll_jellyfin_in_background(
10491085 ) {
10501086 tracing:: error!( "Failed to write Jellyfin credentials: {}" , e) ;
10511087 set_state ( OAuthState :: Error ( format ! ( "Failed to save: {e}" ) ) ) ;
1088+ release ( ) ;
10521089 return ;
10531090 }
10541091
@@ -1066,6 +1103,7 @@ fn poll_jellyfin_in_background(
10661103 QuickConnectPoll :: Pending => { }
10671104 QuickConnectPoll :: Expired => {
10681105 set_state ( OAuthState :: Expired ) ;
1106+ release ( ) ;
10691107 return ;
10701108 }
10711109 QuickConnectPoll :: Error ( e) => {
@@ -1075,6 +1113,7 @@ fn poll_jellyfin_in_background(
10751113 }
10761114
10771115 set_state ( OAuthState :: Expired ) ;
1116+ release ( ) ;
10781117}
10791118
10801119/// Poll for "Login with Plex" authorization in the background.
0 commit comments