1.1.0: reach nntmux over its Newznab API (search + getnzb, metamesh key) with a per-release .nzb cache; drop the MariaDB/storage coupling
Publish image / publish (push) Failing after 13s

This commit is contained in:
Worph
2026-10-04 12:30:11 +00:00
parent 45f88dcdfe
commit 0d9d0132de
14 changed files with 1143 additions and 1506 deletions
+41 -20
View File
@@ -9,6 +9,10 @@
//!
//! Moved here from meta-share's `nzb/manifest_resolve.rs::grab_newznab`: the
//! indexer keys now live on this feeder, meta-share keeps only the NNTP pool.
//!
//! The error handling ([`fetch_nzb`], [`newznab_error`], [`classify`]) is shared
//! with the client for our own nntmux ([`crate::nntmux::api`]), which speaks the
//! same Newznab dialect.
use std::borrow::Cow;
@@ -155,44 +159,41 @@ pub async fn grab(
) -> Result<Vec<u8>, GatewayError> {
let base = api_base.trim_end_matches('/');
let url = format!("{scheme}://{base}/api?t=get&id={id}&apikey={api_key}");
fetch_nzb(http, &url, base).await
}
/// GET an `.nzb` and turn every way a Newznab server can refuse into the feeder
/// error contract: HTTP status, an HTML page (login wall, Cloudflare, a
/// permalink host), an oversize body, or a `<error code>` answered as `200`.
///
/// `label` names the server in error messages; `url` is never echoed (it
/// carries the API key). Shared by the external-indexer redeem ([`grab`]) and
/// the nntmux client ([`crate::nntmux::api`]).
pub async fn fetch_nzb(http: &reqwest::Client, url: &str, label: &str) -> Result<Vec<u8>, GatewayError> {
let resp = http
.get(&url)
.get(url)
.send()
.await
.map_err(|e| GatewayError::Transient(format!("newznab grab {base}: {}", e.without_url())))?;
.map_err(|e| GatewayError::Transient(format!("newznab grab {label}: {}", e.without_url())))?;
let status = resp.status();
if !status.is_success() {
return Err(match status.as_u16() {
404 => GatewayError::NotFound,
429 => GatewayError::RateLimited {
retry_after_s: resp
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse().ok())
.unwrap_or(QUOTA_RETRY_SECS),
},
_ if status.is_server_error() => {
GatewayError::Transient(format!("newznab grab {base}: HTTP {status}"))
}
_ => GatewayError::Permanent(format!("newznab grab {base}: HTTP {status}")),
});
return Err(status_error(&resp, label));
}
let body = resp
.bytes()
.await
.map_err(|e| GatewayError::Transient(format!("newznab grab {base}: {}", e.without_url())))?;
.map_err(|e| GatewayError::Transient(format!("newznab grab {label}: {}", e.without_url())))?;
if body.len() > MAX_NZB_BYTES {
return Err(GatewayError::Permanent(format!(
"newznab grab {base}: .nzb is {} bytes, over the {MAX_NZB_BYTES}-byte ceiling",
"newznab grab {label}: .nzb is {} bytes, over the {MAX_NZB_BYTES}-byte ceiling",
body.len()
)));
}
if looks_like_html(&body) {
return Err(GatewayError::Permanent(format!(
"indexer {base} answered the .nzb grab with an HTML page — check the API base and key"
"indexer {label} answered the .nzb grab with an HTML page — check the API base and key"
)));
}
let text: Cow<str> = String::from_utf8_lossy(&body);
@@ -202,6 +203,26 @@ pub async fn grab(
Ok(text.into_owned().into_bytes())
}
/// The feeder error for a non-2xx Newznab answer.
pub fn status_error(resp: &reqwest::Response, label: &str) -> GatewayError {
let status = resp.status();
match status.as_u16() {
404 => GatewayError::NotFound,
429 => GatewayError::RateLimited {
retry_after_s: resp
.headers()
.get(reqwest::header::RETRY_AFTER)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse().ok())
.unwrap_or(QUOTA_RETRY_SECS),
},
_ if status.is_server_error() => {
GatewayError::Transient(format!("newznab {label}: HTTP {status}"))
}
_ => GatewayError::Permanent(format!("newznab {label}: HTTP {status}")),
}
}
/// Drop the NZB `<head>` except `<meta type="password">`.
///
/// The grabbed file is seeded to the whole mesh, and indexers put