Fix review-queue dead ends and harden grab/import/API paths
Some checks failed
check / check (push) Failing after 14m5s
dev release / build (push) Successful in 3m54s

Stop upgrade-search from queuing mid-confidence matches that approve
cannot honor (owned movies/episodes 409'd on the TUI). De-dupe pending
review rows, scope 1080p gates to the target episode, refuse unsafe
pack cleanup, and require a token for non-loopback binds.
This commit is contained in:
Breadway 2026-08-16 00:44:58 +08:00
parent 4a2adbc24d
commit 7ab28d30a7
31 changed files with 2536 additions and 328 deletions

View file

@ -7,6 +7,7 @@ use crate::dto::{
SearchResult, StuckReport, UpdateQualityProfileWeightsRequest, WeightsDto,
};
#[derive(Clone)]
pub struct DaemonClient {
base_url: String,
client: reqwest::Client,
@ -16,8 +17,9 @@ impl DaemonClient {
/// `api_token` mirrors `config.daemon.api_token` server-side — empty
/// means "no auth configured," so this stays a no-op default header
/// rather than sending a meaningless empty bearer token on every
/// request.
pub fn new(base_url: impl Into<String>, api_token: &str) -> Self {
/// request. A non-empty token that cannot be encoded as an HTTP header
/// is an error (not a silent unauthenticated client).
pub fn new(base_url: impl Into<String>, api_token: &str) -> Result<Self> {
let mut builder = reqwest::Client::builder()
// A default so no request can hang the TUI forever with zero
// feedback if the daemon is unreachable or a connection stalls.
@ -26,20 +28,23 @@ impl DaemonClient {
// which overrides this.
.timeout(std::time::Duration::from_secs(30));
if !api_token.is_empty() {
let token = api_token.replace(['\r', '\n'], "");
anyhow::ensure!(
!token.is_empty(),
"daemon.api_token is non-empty but contains only CR/LF"
);
let value = reqwest::header::HeaderValue::from_str(&format!("Bearer {token}"))
.context("daemon.api_token is not a valid HTTP header value")?;
let mut headers = reqwest::header::HeaderMap::new();
if let Ok(value) =
reqwest::header::HeaderValue::from_str(&format!("Bearer {api_token}"))
{
headers.insert(reqwest::header::AUTHORIZATION, value);
}
headers.insert(reqwest::header::AUTHORIZATION, value);
builder = builder.default_headers(headers);
}
Self {
Ok(Self {
base_url: base_url.into(),
client: builder
.build()
.expect("reqwest client builder should not fail with only a timeout/headers set"),
}
})
}
pub async fn health(&self) -> Result<bool> {
@ -59,7 +64,7 @@ impl DaemonClient {
pub async fn health_detail(&self) -> Result<HealthDetail> {
let resp = self
.client
.get(format!("{}/health", self.base_url))
.get(format!("{}/health/detail", self.base_url))
.timeout(std::time::Duration::from_secs(2))
.send()
.await