From 845448a754a5647e8eedd312b2c21aa1f1d94513 Mon Sep 17 00:00:00 2001 From: rays Date: Fri, 9 Oct 2026 18:48:25 +0000 Subject: [PATCH] DeadAir shortens episodes opened but not played (#173) The copy maker skips an episode everyone who would hear the copy has finished, and took finished to be the read mark. Opening an item sets it as well, so 359: Potatoes From the Sky, opened and downloaded with DeadAir on for its show, got no copy. Finished is now a place at 90% of the length or past it, as Currently Listening has it. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + src/db.rs | 18 +++++++++++++----- src/deadair.rs | 6 +++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 812f5e7..b5c07a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- DeadAir shortens an episode you have opened but not played yet, instead of passing it over as if you had finished it (#173). - Cover art up to 20 MB is shown, instead of nothing for anything over 5 MB, as for five of the Glass Cannon Network's feeds. - Seeking in an episode while many people listen answers in about 10 ms instead of 40, and files stream about four times as fast: each connection now sends what is ready without waiting for the browser to acknowledge the last part. - Artwork that could not be fetched is not fetched again for an hour, instead of on every page load, and the server's log says which image it was and why. diff --git a/src/db.rs b/src/db.rs index 536618d..9b008b9 100644 --- a/src/db.rs +++ b/src/db.rs @@ -2330,10 +2330,15 @@ impl Db { Ok(!self.rows(&sql, a.0).await?.is_empty()) } - /// Who has an item marked read, which playing it to the end or past 90% does. - pub async fn readers(&self, feed_id: &str, guid: &str) -> Result> { + /// Who has heard an episode through: their place at 90% of its length or past it, where the + /// page calls it played. Not the read mark, which opening an item sets too: going by it, + /// DeadAir passed over every episode opened before it was played (#173). + pub async fn finished(&self, feed_id: &str, guid: &str) -> Result> { self.rows( - "SELECT user_id FROM entry_state WHERE feed_id = $1 AND guid = $2 AND read", + "SELECT s.user_id FROM entry_state s + JOIN entries e ON e.feed_id = s.feed_id AND e.guid = s.guid + WHERE s.feed_id = $1 AND s.guid = $2 AND coalesce(s.duration, e.duration, 0) > 0 + AND s.position * 10 >= coalesce(s.duration, e.duration) * 9", vec![feed_id.into(), guid.into()], ) .await? @@ -2464,13 +2469,16 @@ mod tests { assert!(!db.subscribed_any(&[u], "f").await.unwrap()); db.subscribe(u, "f").await.unwrap(); assert!(db.subscribed_any(&[u], "f").await.unwrap()); - assert!(db.readers("f", "g").await.unwrap().is_empty()); + // Opened, which marks it read, and not played: not finished (#173). db.set_entry_flag(u, "f", "g", EntryFlag::Read, true).await.unwrap(); - assert_eq!(db.readers("f", "g").await.unwrap(), [u]); + assert!(db.finished("f", "g").await.unwrap().is_empty()); assert_eq!(db.entry_duration(u, "f", "g").await.unwrap(), Some(600)); db.set_position(u, "f", "g", 10, Some(590)).await.unwrap(); assert_eq!(db.entry_duration(u, "f", "g").await.unwrap(), Some(590), "what the player measured"); + assert!(db.finished("f", "g").await.unwrap().is_empty()); + db.set_position(u, "f", "g", 540, Some(590)).await.unwrap(); + assert_eq!(db.finished("f", "g").await.unwrap(), [u], "past 90%"); assert_eq!(db.deadair_total(u).await.unwrap(), 0); db.count_deadair(u, "f", "g", 2000).await.unwrap(); diff --git a/src/deadair.rs b/src/deadair.rs index ec047b5..29424e1 100644 --- a/src/deadair.rs +++ b/src/deadair.rs @@ -268,7 +268,7 @@ async fn have_ffmpeg() -> bool { } /// Makes one copy someone wants, if there is one to make. A file everyone who would play the -/// copy has already finished gets none: finished episodes are rarely played again. +/// copy has already heard through gets none: finished episodes are rarely played again. // shortcut: those stay on the list and are looked at again each pass, one query each; record // them as passed over if a large library makes the pass slow. async fn next(ctx: &Ctx) -> Result { @@ -283,8 +283,8 @@ async fn next(ctx: &Ctx) -> Result { if users.is_empty() || !ctx.db.subscribed_any(&users, &t.feed_id).await? { continue; } - let readers = ctx.db.readers(&t.feed_id, &t.guid).await?; - if users.iter().all(|u| readers.contains(u)) { + let finished = ctx.db.finished(&t.feed_id, &t.guid).await?; + if users.iter().all(|u| finished.contains(u)) { continue; } let src = crate::download::playable(Path::new(&t.path));