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 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 18:48:25 +00:00
parent 00bc039964
commit 845448a754
3 changed files with 17 additions and 8 deletions

View File

@@ -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<Vec<i64>> {
/// 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<Vec<i64>> {
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();

View File

@@ -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<bool> {
@@ -283,8 +283,8 @@ async fn next(ctx: &Ctx) -> Result<bool> {
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));