fix init angain again again (hopefully last time)

This commit is contained in:
2026-06-30 17:36:39 +02:00
parent 260a2b1c69
commit 0c2638fec3
5 changed files with 65 additions and 15 deletions
Binary file not shown.
+8 -3
View File
@@ -139,7 +139,7 @@ def join_query(args) -> str:
return " ".join(parts).strip() return " ".join(parts).strip()
def call(host, port, method, route, body=None, query=None): def call(host, port, method, route, body=None, query=None, timeout=15):
qs = "" qs = ""
if query: if query:
qs = "?" + urllib.parse.urlencode(query) qs = "?" + urllib.parse.urlencode(query)
@@ -153,7 +153,7 @@ def call(host, port, method, route, body=None, query=None):
req = urllib.request.Request(url=url, data=data, headers=headers, method=method) req = urllib.request.Request(url=url, data=data, headers=headers, method=method)
try: try:
with urllib.request.urlopen(req, timeout=15) as resp: with urllib.request.urlopen(req, timeout=timeout) as resp:
payload = resp.read().decode("utf-8") payload = resp.read().decode("utf-8")
return resp.status, payload return resp.status, payload
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
@@ -333,8 +333,13 @@ def main(argv=None):
query = {"q": q} query = {"q": q}
if limit is not None and limit > 0: if limit is not None and limit > 0:
query["limit"] = str(limit) query["limit"] = str(limit)
# yt-dlp searches can easily take 30+ seconds on a slow network
# (the daemon calls `ytsearchN:QUERY -j` which hits YouTube).
# The default 15 s `urllib` timeout short-circuits the picker
# before the daemon even returns, so give this one endpoint a
# generous budget. All other endpoints keep the 15 s default.
status, payload = call( status, payload = call(
host, port, "GET", "/search/yt", query=query host, port, "GET", "/search/yt", query=query, timeout=120
) )
obj = try_parse(payload) obj = try_parse(payload)
results = (obj or {}).get("results") or [] results = (obj or {}).get("results") or []
+9 -1
View File
@@ -180,7 +180,15 @@ fn handle(
.get("limit") .get("limit")
.and_then(|s| s.parse::<usize>().ok()) .and_then(|s| s.parse::<usize>().ok())
.unwrap_or(state.search_count); .unwrap_or(state.search_count);
match state.search_yt_sync(&q, limit) { // Release the synchronous state guard BEFORE calling
// search_yt_sync — yt-dlp can take 30+ seconds on a slow
// network and holding the daemon mutex here would freeze
// every other endpoint (including the GUI's `/init`,
// `/downloads`, `/now-playing` polls) for the entire
// duration. Mirrors the drop-then-call pattern that
// `init_sync` already uses.
drop(state);
match DaemonState::search_yt_sync(&q, limit) {
Ok(results) => json_response(200, &serde_json::json!({ Ok(results) => json_response(200, &serde_json::json!({
"query": q, "query": q,
"limit": limit, "limit": limit,
+10 -1
View File
@@ -556,7 +556,16 @@ impl DaemonState {
/// non-zero exit are tolerated as much as possible: an empty list is /// non-zero exit are tolerated as much as possible: an empty list is
/// returned when nothing parsed, while a yt-dlp startup failure surfaces /// returned when nothing parsed, while a yt-dlp startup failure surfaces
/// a descriptive error string so the CLI can show it to the user. /// a descriptive error string so the CLI can show it to the user.
pub fn search_yt_sync(&self, query: &str, limit: usize) -> Result<Vec<YtCandidate>, String> { ///
/// Lives as an associated function (no `&self`) so the HTTP handler can
/// release the daemon state mutex before spawning yt-dlp. Earlier
/// revisions invoked this as `state.search_yt_sync(...)` *while*
/// holding the single-threaded `tiny_http` request handler's state
/// guard, which froze the entire daemon (including `/init`, `/queue`,
/// `/now-playing`) for the full duration of a yt-dlp search — typically
/// 30+ seconds on a slow network. Mirrors the lock-free pattern already
/// used for `init_sync` below.
pub fn search_yt_sync(query: &str, limit: usize) -> Result<Vec<YtCandidate>, String> {
let limit = limit.max(1); let limit = limit.max(1);
let src = format!("ytsearch{limit}:{query}"); let src = format!("ytsearch{limit}:{query}");
debug!("yt-dlp search: {src}"); debug!("yt-dlp search: {src}");
+35 -7
View File
@@ -330,12 +330,23 @@ impl DaemonClient {
} }
pub fn search_yt(&self, q: &str, limit: usize) -> Result<Vec<YtCandidate>, DaemonError> { pub fn search_yt(&self, q: &str, limit: usize) -> Result<Vec<YtCandidate>, DaemonError> {
// `/search/yt` shells out to `yt-dlp ytsearchN:...` on the daemon
// side, which can easily take 30+ seconds on a slow network — well
// past the agent's 8 s default. Override per-request so the picker
// doesn't ghost-fail with a transport timeout. All other
// endpoints keep the default; the daemon's state lock is no
// longer held during the search (see daemon state.rs), so other
// GETs (now-playing, downloads, queue) keep flowing while a
// search is in flight.
Ok(self Ok(self
.get::<SearchYtResult>(&format!( .get_with_timeout::<SearchYtResult>(
&format!(
"/search/yt?q={}&limit={}", "/search/yt?q={}&limit={}",
urlencoding::encode(q), urlencoding::encode(q),
limit limit
))? ),
Duration::from_secs(120),
)?
.results) .results)
} }
@@ -484,7 +495,19 @@ impl DaemonClient {
// ---------------------------------------------------------------- // ----------------------------------------------------------------
fn get<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T, DaemonError> { fn get<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T, DaemonError> {
self._request_json("GET", path, None) self._request_json("GET", path, None, None)
}
/// Like `get` but lets callers override the per-request timeout. Used
/// only for endpoints that back onto slow subprocess calls (currently
/// just `/search/yt`, since yt-dlp's first network round-trip can blow
/// past the 8 s agent default).
fn get_with_timeout<T: for<'de> Deserialize<'de>>(
&self,
path: &str,
timeout: Duration,
) -> Result<T, DaemonError> {
self._request_json("GET", path, None, Some(timeout))
} }
fn post_json<T: for<'de> Deserialize<'de>>( fn post_json<T: for<'de> Deserialize<'de>>(
@@ -492,7 +515,7 @@ impl DaemonClient {
path: &str, path: &str,
body: &serde_json::Value, body: &serde_json::Value,
) -> Result<T, DaemonError> { ) -> Result<T, DaemonError> {
self._request_json("POST", path, Some(body)) self._request_json("POST", path, Some(body), None)
} }
fn patch<T: for<'de> Deserialize<'de>>( fn patch<T: for<'de> Deserialize<'de>>(
@@ -500,7 +523,7 @@ impl DaemonClient {
path: &str, path: &str,
body: &serde_json::Value, body: &serde_json::Value,
) -> Result<T, DaemonError> { ) -> Result<T, DaemonError> {
self._request_json("PATCH", path, Some(body)) self._request_json("PATCH", path, Some(body), None)
} }
fn del_json<T: for<'de> Deserialize<'de>>( fn del_json<T: for<'de> Deserialize<'de>>(
@@ -508,11 +531,11 @@ impl DaemonClient {
path: &str, path: &str,
body: &serde_json::Value, body: &serde_json::Value,
) -> Result<T, DaemonError> { ) -> Result<T, DaemonError> {
self._request_json("DELETE", path, Some(body)) self._request_json("DELETE", path, Some(body), None)
} }
fn del<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T, DaemonError> { fn del<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T, DaemonError> {
self._request_json("DELETE", path, None) self._request_json("DELETE", path, None, None)
} }
fn _request_json<T: for<'de> Deserialize<'de>>( fn _request_json<T: for<'de> Deserialize<'de>>(
@@ -520,6 +543,7 @@ impl DaemonClient {
method: &str, method: &str,
path: &str, path: &str,
body: Option<&serde_json::Value>, body: Option<&serde_json::Value>,
timeout: Option<Duration>,
) -> Result<T, DaemonError> { ) -> Result<T, DaemonError> {
let url = format!("{}{}", self.base, path); let url = format!("{}{}", self.base, path);
let req = match method { let req = match method {
@@ -535,6 +559,10 @@ impl DaemonClient {
))) )))
} }
}; };
let req = match timeout {
Some(t) => req.timeout(t),
None => req,
};
let req = match body { let req = match body {
Some(b) => req.send_json(ureq::json!(b.clone())), Some(b) => req.send_json(ureq::json!(b.clone())),
None => req.call(), None => req.call(),