Show that DeadAir is still working on an episode (#176)
A new download in a show with DeadAir on plays at full length until its copy is made, and nothing said so. Each file now carries deadair_pending while DeadAir is on for its show and the copy is still to come (not for one heard through, which it passes over), and the episode's line and details say "Cutting dead air…". The maker sends a deadair_done event when it finishes a file, and the page takes the list again, so the line becomes how much shorter the episode is without a reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
21
src/db.rs
21
src/db.rs
@@ -202,6 +202,7 @@ impl From<enclosures::Model> for EncRow {
|
||||
EncRow {
|
||||
id: e.id,
|
||||
deadair: None,
|
||||
deadair_pending: false,
|
||||
feed_id: e.feed_id,
|
||||
guid: e.guid,
|
||||
url: e.url,
|
||||
@@ -1184,6 +1185,10 @@ pub struct EncRow {
|
||||
/// (`deadair::for_reader`). None from the database.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deadair: Option<i64>,
|
||||
/// DeadAir is on for this person's show and the copy is still being made: the page says to
|
||||
/// wait rather than leave them guessing (`deadair::for_reader`).
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub deadair_pending: bool,
|
||||
pub feed_id: String,
|
||||
pub guid: String,
|
||||
pub url: String,
|
||||
@@ -2388,6 +2393,22 @@ impl Db {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Which of these enclosures DeadAir has dealt with since each was last downloaded, copy or
|
||||
/// none: the rest on disk are still waiting for it.
|
||||
pub async fn deadair_done(&self, enclosure_ids: &[i64]) -> Result<std::collections::HashSet<i64>> {
|
||||
if enclosure_ids.is_empty() {
|
||||
return Ok(Default::default());
|
||||
}
|
||||
let mut a = Args::default();
|
||||
let ids: Vec<String> = enclosure_ids.iter().map(|&i| a.p(i)).collect();
|
||||
let sql = format!(
|
||||
"SELECT d.enclosure_id FROM deadair d JOIN enclosures e ON e.id = d.enclosure_id
|
||||
WHERE d.enclosure_id IN ({}) AND d.made_at >= coalesce(e.downloaded_at, 0)",
|
||||
ids.join(", ")
|
||||
);
|
||||
self.rows(&sql, a.0).await?.iter().map(|r| Ok(r.try_get("", "enclosure_id")?)).collect()
|
||||
}
|
||||
|
||||
/// Counts an episode towards the time DeadAir has saved someone, once however often it is
|
||||
/// played.
|
||||
pub async fn count_deadair(&self, user_id: i64, feed_id: &str, guid: &str, saved_ms: i64) -> Result<()> {
|
||||
|
||||
@@ -189,9 +189,13 @@ pub async fn for_reader(ctx: &Ctx, user: i64, rows: &mut [crate::db::EntryRow])
|
||||
.collect();
|
||||
let copies: HashMap<i64, crate::db::Copy> =
|
||||
ctx.db.copies(&ids).await?.into_iter().map(|c| (c.enclosure_id, c)).collect();
|
||||
let done = ctx.db.deadair_done(&ids).await?;
|
||||
for r in rows.iter_mut() {
|
||||
// Heard through already: the maker passes it over, so nothing is coming.
|
||||
let finished = r.duration.is_some_and(|d| d > 0 && r.position * 10 >= d * 9);
|
||||
let mut converted = false;
|
||||
for e in &mut r.enclosures {
|
||||
e.deadair_pending = ids.contains(&e.id) && e.state == "done" && !done.contains(&e.id) && !finished;
|
||||
let Some(c) = copies.get(&e.id) else { continue };
|
||||
e.deadair = Some(c.saved_ms / 1000);
|
||||
// One position an item, however many files: the first with a copy sets the clock.
|
||||
@@ -308,8 +312,9 @@ async fn next(ctx: &Ctx, failed: &mut std::collections::HashSet<i64>) -> Result<
|
||||
};
|
||||
let saved_ms = saved(&cuts);
|
||||
ctx.db.store_deadair(t.id, path.as_deref(), &serde_json::to_string(&cuts)?, saved_ms).await?;
|
||||
ctx.out.emit(crate::ipc::Event::DeadairDone { feed: t.feed_id.clone(), enclosure: t.id, saved_s: saved_ms / 1000 });
|
||||
// The event says it too (ev="deadair_done"); this line has how long it took and how many cuts.
|
||||
tracing::info!(
|
||||
ev = "deadair",
|
||||
feed = t.feed_id,
|
||||
path = %src.display(),
|
||||
cuts = cuts.len(),
|
||||
|
||||
@@ -32,6 +32,8 @@ pub enum Event {
|
||||
DownloadDone { feed: String, enclosure: i64, url: String, path: String, bytes: u64 },
|
||||
DownloadError { feed: String, enclosure: i64, url: String, msg: String },
|
||||
TorrentDeferred { feed: String, url: String },
|
||||
/// DeadAir finished a file's copy (#171), so a page showing it as in progress can stop.
|
||||
DeadairDone { feed: String, enclosure: i64, saved_s: i64 },
|
||||
Reaped { path: String, bytes: u64 },
|
||||
/// Terminal: a client that asked for work stops reading here.
|
||||
ScanDone { feeds: usize },
|
||||
@@ -79,6 +81,7 @@ impl Event {
|
||||
// Noise in a terminal; a UI still gets them on the socket.
|
||||
Event::FeedStart { feed } => format!("{feed}: checking"),
|
||||
Event::TorrentDeferred { feed, .. } => format!("{feed}: torrent deferred"),
|
||||
Event::DeadairDone { feed, saved_s, .. } => format!("{feed}: DeadAir copy made, {saved_s} s shorter"),
|
||||
Event::ScanDone { feeds } => format!("scan complete, {feeds} feed(s)"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2515,9 +2515,15 @@ mod tests {
|
||||
};
|
||||
let off = page().await;
|
||||
assert_eq!((off.position, off.enclosures[0].deadair), (300, None), "off until turned on for the feed");
|
||||
assert!(!off.enclosures[0].deadair_pending);
|
||||
|
||||
db.set_deadair(u, "f", Some(true)).await.unwrap();
|
||||
db.sql("DELETE FROM deadair").await.unwrap();
|
||||
let waiting = page().await;
|
||||
assert!(waiting.enclosures[0].deadair_pending, "on, and the copy not made yet");
|
||||
db.store_deadair(7, Some("/x/a.deadair.mp3"), "[[60000,120000],[200000,260000]]", 120_000).await.unwrap();
|
||||
let on = page().await;
|
||||
assert!(!on.enclosures[0].deadair_pending);
|
||||
assert_eq!((on.position, on.duration, on.enclosures[0].deadair), (180, Some(480), Some(120)));
|
||||
|
||||
// Saved from the copy at 3:20 of its 8:00: 5:20 of the original's 10:00, short of 75%.
|
||||
|
||||
Reference in New Issue
Block a user