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

@@ -91,6 +91,10 @@ async fn create_missing(orm: &sea_orm::DatabaseConnection) -> Result<()> {
orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?;
}
}
if orm.execute_unprepared("SELECT played_at FROM deadair LIMIT 0").await.is_err() {
let sql = "ALTER TABLE deadair ADD COLUMN played_at BIGINT";
orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?;
}
Ok(())
}
@@ -2364,6 +2368,49 @@ impl Db {
Ok(())
}
/// Notes that someone is playing a copy, so it is kept. At most once an hour: the player asks
/// for more of the file at every seek.
pub async fn deadair_played(&self, enclosure_id: i64) -> Result<()> {
let now = now();
self.exec(
"UPDATE deadair SET played_at = $1 WHERE enclosure_id = $2 AND coalesce(played_at, 0) < $3",
vec![now.into(), enclosure_id.into(), (now - 3600).into()],
)
.await?;
Ok(())
}
/// Copies nobody has played, or made, since `before`, as (enclosure, path).
pub async fn stale_copies(&self, before: i64) -> Result<Vec<(i64, String)>> {
self.rows(
"SELECT enclosure_id, path FROM deadair WHERE path IS NOT NULL AND coalesce(played_at, made_at) < $1",
vec![before.into()],
)
.await?
.iter()
.map(|r| Ok((r.try_get("", "enclosure_id")?, r.try_get("", "path")?)))
.collect()
}
/// Records a copy's file as removed, keeping its cuts: they mark it as one to make again
/// (`revive_copy`).
pub async fn drop_copy_path(&self, enclosure_id: i64) -> Result<()> {
self.exec("UPDATE deadair SET path = NULL WHERE enclosure_id = $1", vec![enclosure_id.into()]).await?;
Ok(())
}
/// Puts an episode whose copy was removed back on DeadAir's list, and says whether it was one.
/// A file with no copy because it cannot have one has no cuts, and stays off the list.
pub async fn revive_copy(&self, enclosure_id: i64) -> Result<bool> {
let n = self
.exec(
"DELETE FROM deadair WHERE enclosure_id = $1 AND path IS NULL AND cuts <> '[]'",
vec![enclosure_id.into()],
)
.await?;
Ok(n > 0)
}
/// The copies there are of these enclosures, made since each was last downloaded.
pub async fn copies(&self, enclosure_ids: &[i64]) -> Result<Vec<Copy>> {
if enclosure_ids.is_empty() {