Remove DeadAir copies unplayed for a week, and remake them on play (#179)

A copy is nearly the size of its original and was kept as long as the
original. Now `deadair.played_at` notes when one was last served (at
most once an hour, since a player asks again at every seek), and each
pass of the maker removes copies unplayed, or unmade, for 7 days. Only
the file goes: the row keeps its cuts with its path cleared, which
tells an expired copy apart from a file that cannot have one (no cuts)
and keeps the background queue from making it again straight away.
Serving the original of such an episode deletes the row, which puts it
back on the queue; the copy then comes back within a pass, though the
page does not switch to it mid-play (#178).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 02:51:17 +00:00
parent 9ea2a9250c
commit f29d493a0e
6 changed files with 125 additions and 0 deletions

View File

@@ -2547,6 +2547,47 @@ mod tests {
assert_eq!(page().await.enclosures[0].deadair, None);
}
#[tokio::test]
async fn deadair_removes_a_copy_unplayed_for_a_week_and_makes_it_again_when_played() {
let ctx = test_ctx(config::Config::default()).await;
let db = &ctx.db;
let dir = std::env::temp_dir().join(format!("ipx-deadair-ttl-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let (orig, copy) = (dir.join("a.mp3"), dir.join("a.deadair.mp3"));
std::fs::write(&orig, "original").unwrap();
std::fs::write(&copy, "copy").unwrap();
db.sql("INSERT INTO entries (feed_id, guid, first_seen) VALUES ('f', 'g', 1)").await.unwrap();
db.sql(&format!(
"INSERT INTO enclosures (id, feed_id, guid, url, path, state, downloaded_at)
VALUES (7, 'f', 'g', 'http://x/a.mp3', '{}', 'done', 100)",
orig.display()
))
.await
.unwrap();
db.store_deadair(7, Some(&copy.display().to_string()), "[[60000,120000]]", 60_000).await.unwrap();
let on_list = || async { db.deadair_todo().await.unwrap().iter().any(|t| t.id == 7) };
deadair::expire(&ctx).await.unwrap();
assert!(copy.exists(), "made just now");
db.sql("UPDATE deadair SET made_at = 100").await.unwrap();
db.deadair_played(7).await.unwrap();
deadair::expire(&ctx).await.unwrap();
assert!(copy.exists(), "made long ago, but played just now");
db.sql("UPDATE deadair SET played_at = 0").await.unwrap();
deadair::expire(&ctx).await.unwrap();
assert!(!copy.exists() && orig.exists(), "the copy goes, the original stays");
assert!(db.copies(&[7]).await.unwrap().is_empty());
assert!(!on_list().await, "not made again until someone plays it");
assert!(db.revive_copy(7).await.unwrap());
assert!(on_list().await, "played: made again");
// A file with no copy because it cannot have one is never put back on the list.
db.store_deadair(7, None, "[]", 0).await.unwrap();
assert!(!db.revive_copy(7).await.unwrap());
std::fs::remove_dir_all(&dir).unwrap();
}
#[tokio::test]
async fn two_changes_to_the_catalogue_at_once_both_keep() {
let ctx = Arc::new(test_ctx(config::Config::default()).await);