Skip to content

Commit 149801a

Browse files
committed
fix(setup): reuse the in-flight Quick Connect code and free the slot on expiry
1 parent 4fd4816 commit 149801a

1 file changed

Lines changed: 44 additions & 5 deletions

File tree

src/setup/server.rs

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
10131038
fn 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

Comments
 (0)