DeadAir in the page: a switch per show, one beside the speed, and the time saved (#172)
A DeadAir switch in each show's settings, and the same switch beside
the speed for the show that is playing, drawn as a waveform with its gap
closed up, outlined when off and solid when on, with aria-pressed. A
file the server gives a copy for plays from /media/{id}/deadair. An
episode's line says how much shorter it is, and Settings says how much
DeadAir has saved you.
Turning it on or off mid-episode saves the place with a request it waits
for, not a beacon, then asks for the entry again from the new
GET /api/entries/{feed}/{guid}, whose position is in the clock of the
version it now plays, and carries on there. A copy not made yet plays
from the next time. Turning a show on wakes the copy maker, so the copy
is there in seconds rather than at its next pass.
The browser test plays a real MP3 with long silences, quiet.mp3, from a
show it adds and removes itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
40
src/db.rs
40
src/db.rs
@@ -1061,12 +1061,22 @@ impl Args {
|
||||
/// The FROM and WHERE that `entries_in` and `count_in` share: whose read state, which feeds
|
||||
/// (one, or every feed the person subscribes to), the filter, and a case-insensitive search of
|
||||
/// title and description.
|
||||
fn entries_from(a: &mut Args, user_id: i64, feed_id: Option<&str>, filter: Filter, search: Option<&str>) -> String {
|
||||
fn entries_from(
|
||||
a: &mut Args,
|
||||
user_id: i64,
|
||||
feed_id: Option<&str>,
|
||||
filter: Filter,
|
||||
search: Option<&str>,
|
||||
guid: Option<&str>,
|
||||
) -> String {
|
||||
let user = a.p(user_id);
|
||||
let scope = match feed_id {
|
||||
let mut scope = match feed_id {
|
||||
Some(f) => format!("e.feed_id = {}", a.p(f)),
|
||||
None => format!("e.feed_id IN (SELECT feed_id FROM subscriptions WHERE user_id = {user})"),
|
||||
};
|
||||
if let Some(g) = guid {
|
||||
scope = format!("{scope} AND e.guid = {}", a.p(g));
|
||||
}
|
||||
let search = match search.map(|q| q.trim().to_lowercase()).filter(|q| !q.is_empty()) {
|
||||
Some(q) => {
|
||||
let like = a.p(format!("%{q}%"));
|
||||
@@ -1198,9 +1208,31 @@ impl Db {
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
order: &str,
|
||||
) -> Result<Vec<EntryRow>> {
|
||||
self.entries_where(user_id, feed_id, filter, search, None, offset, limit, order).await
|
||||
}
|
||||
|
||||
/// One entry as `entries_in` gives it: what the player asks for again when the clock it
|
||||
/// plays in changes (DeadAir, #171).
|
||||
pub async fn entry(&self, user_id: i64, feed_id: &str, guid: &str) -> Result<Option<EntryRow>> {
|
||||
let order = order_sql("published", "desc");
|
||||
Ok(self.entries_where(user_id, Some(feed_id), Filter::All, None, Some(guid), 0, 1, &order).await?.pop())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn entries_where(
|
||||
&self,
|
||||
user_id: i64,
|
||||
feed_id: Option<&str>,
|
||||
filter: Filter,
|
||||
search: Option<&str>,
|
||||
guid: Option<&str>,
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
order: &str,
|
||||
) -> Result<Vec<EntryRow>> {
|
||||
let mut a = Args::default();
|
||||
let from = entries_from(&mut a, user_id, feed_id, filter, search);
|
||||
let from = entries_from(&mut a, user_id, feed_id, filter, search, guid);
|
||||
let sql = format!(
|
||||
"SELECT e.guid, e.feed_id, e.title, e.link, e.published, e.description,
|
||||
coalesce(s.read, false) AS read, coalesce(s.flagged, false) AS flagged, e.image,
|
||||
@@ -1263,7 +1295,7 @@ impl Db {
|
||||
search: Option<&str>,
|
||||
) -> Result<i64> {
|
||||
let mut a = Args::default();
|
||||
let sql = format!("SELECT count(*) AS n {}", entries_from(&mut a, user_id, feed_id, filter, search));
|
||||
let sql = format!("SELECT count(*) AS n {}", entries_from(&mut a, user_id, feed_id, filter, search, None));
|
||||
let r = self.rows(&sql, a.0).await?;
|
||||
Ok(r.first().context("count(*) always returns a row")?.try_get("", "n")?)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user