DeadAir is off for every feed until turned on for it (#171)
There was one switch for everything, with a feed kept out by hand; any feed already subscribed would have been in it the moment the switch was turned on. Now each feed, or the group it came in, is turned on by itself, and a feed of a group turned on can still be kept out. The general switch and /api/me's deadair are gone; /api/me keeps deadair_saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- DeadAir, on the server: for anyone who turns it on, each episode gets a copy with its long silences shortened, so podcasts finish sooner without sounding sped up, and your place carries over between the copy and the original. The switch in the player comes next (#171).
|
||||
- DeadAir, on the server: in each feed you turn it on for, episodes get a copy with their long silences shortened, so podcasts finish sooner without sounding sped up, and your place carries over between the copy and the original. It is off for every feed until you do; the setting in the page comes next (#171).
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -2405,10 +2405,10 @@ mod tests {
|
||||
async fn deadair_copies_are_made_once_per_download_and_counted_once_per_episode() {
|
||||
let db = Db::memory().await.unwrap();
|
||||
let u = db.create_user("dee", None, false).await.unwrap();
|
||||
db.set_deadair(u, "", Some(true)).await.unwrap();
|
||||
db.set_deadair(u, "f", Some(false)).await.unwrap();
|
||||
db.set_deadair(u, "f", None).await.unwrap();
|
||||
assert_eq!(db.deadair_prefs(Some(u)).await.unwrap(), vec![(u, String::new(), true)]);
|
||||
db.set_deadair(u, "f", Some(true)).await.unwrap();
|
||||
db.set_deadair(u, "g", Some(false)).await.unwrap();
|
||||
db.set_deadair(u, "g", None).await.unwrap();
|
||||
assert_eq!(db.deadair_prefs(Some(u)).await.unwrap(), vec![(u, "f".to_string(), true)]);
|
||||
for sql in [
|
||||
"INSERT INTO enclosures (id, feed_id, guid, url, path, state, downloaded_at)
|
||||
VALUES (1, 'f', 'g', 'http://x/a.mp3', '/x/a.mp3', 'done', 100)",
|
||||
|
||||
@@ -33,7 +33,7 @@ const KEEP: f64 = 0.1;
|
||||
// shortcut: a file with more silences than this keeps only its longest; about 9 hours of
|
||||
// conversation at the rate measured. Pass the list some other way if a feed ever gets there.
|
||||
const MAX_CUTS: usize = 4000;
|
||||
/// How long the maker waits when there is nothing to do: a download or a switch turned on is
|
||||
/// How long the maker waits when there is nothing to do: a download or a feed turned on is
|
||||
/// picked up this long after, at most.
|
||||
const IDLE: std::time::Duration = std::time::Duration::from_secs(120);
|
||||
|
||||
@@ -130,15 +130,15 @@ pub fn saved(cuts: &[Cut]) -> i64 {
|
||||
cuts.iter().map(|Cut(s, e)| e - s).sum()
|
||||
}
|
||||
|
||||
/// Whether someone with these settings plays a feed from its copies: their switch on, and the
|
||||
/// feed not kept out, by itself or by the group it came in (an OPML or a Patreon creator).
|
||||
/// Whether someone with these settings plays a feed from its copies. Off for every feed until
|
||||
/// they turn it on for that one, or for the group it came in (an OPML or a Patreon creator),
|
||||
/// which a feed of the group can still say otherwise to.
|
||||
pub fn wants(prefs: &HashMap<String, bool>, feed: &str, group: Option<&str>) -> bool {
|
||||
prefs.get("").copied().unwrap_or(false)
|
||||
&& prefs.get(feed).or_else(|| group.and_then(|g| prefs.get(g))).copied().unwrap_or(true)
|
||||
prefs.get(feed).or_else(|| group.and_then(|g| prefs.get(g))).copied().unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Each person's settings who has the switch on, and each managed feed's group, which is what
|
||||
/// `wants` needs.
|
||||
/// The settings of each person with DeadAir on for something, and each managed feed's group,
|
||||
/// which is what `wants` needs.
|
||||
pub struct Wanting {
|
||||
pub people: HashMap<i64, HashMap<String, bool>>,
|
||||
pub groups: HashMap<String, String>,
|
||||
@@ -150,7 +150,7 @@ impl Wanting {
|
||||
for (user, feed, wanted) in ctx.db.deadair_prefs(only).await? {
|
||||
people.entry(user).or_default().insert(feed, wanted);
|
||||
}
|
||||
people.retain(|_, p| p.get("").copied().unwrap_or(false));
|
||||
people.retain(|_, p| p.values().any(|&w| w));
|
||||
let groups = if people.is_empty() {
|
||||
HashMap::new()
|
||||
} else {
|
||||
@@ -271,7 +271,7 @@ async fn have_ffmpeg() -> bool {
|
||||
// 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> {
|
||||
// Nobody with the switch on, which is most passes: nothing more to ask the database.
|
||||
// Nobody with DeadAir on for anything, which is most passes: nothing more to ask the database.
|
||||
let wanting = Wanting::load(ctx, None).await?;
|
||||
if wanting.people.is_empty() {
|
||||
return Ok(false);
|
||||
@@ -447,14 +447,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_feed_kept_out_by_itself_or_its_group() {
|
||||
fn off_for_every_feed_until_turned_on_for_it_or_its_group() {
|
||||
let p = |v: &[(&str, bool)]| v.iter().map(|(k, w)| (k.to_string(), *w)).collect::<HashMap<_, _>>();
|
||||
assert!(!wants(&p(&[]), "a", None), "off until turned on");
|
||||
assert!(wants(&p(&[("", true)]), "a", None));
|
||||
assert!(!wants(&p(&[("", true), ("a", false)]), "a", None));
|
||||
assert!(!wants(&p(&[("", true), ("g", false)]), "a", Some("g")));
|
||||
assert!(wants(&p(&[("", true), ("g", false), ("a", true)]), "a", Some("g")), "let back in");
|
||||
assert!(!wants(&p(&[("", false), ("a", true)]), "a", None), "the switch rules");
|
||||
assert!(!wants(&p(&[]), "a", None), "off until turned on for the feed");
|
||||
assert!(!wants(&p(&[("b", true)]), "a", None), "another feed's says nothing of this one");
|
||||
assert!(wants(&p(&[("a", true)]), "a", None));
|
||||
assert!(wants(&p(&[("g", true)]), "a", Some("g")), "on for its group");
|
||||
assert!(!wants(&p(&[("g", true), ("a", false)]), "a", Some("g")), "kept out of its group's");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -382,9 +382,8 @@ pub mod deadair {
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
|
||||
/// Who has DeadAir on: `feed_id` empty is the switch beside the speed, anything else a feed
|
||||
/// they keep out of it (`wanted` false) or let back in under a group kept out, as blocklists use
|
||||
/// an empty feed for everywhere.
|
||||
/// Who has DeadAir on, for which feeds: it is off for every feed until someone turns it on for
|
||||
/// that one or its group, and `wanted` false keeps one feed of a group out of the group's.
|
||||
pub mod deadair_prefs {
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
|
||||
@@ -2509,9 +2509,9 @@ mod tests {
|
||||
}
|
||||
};
|
||||
let off = page().await;
|
||||
assert_eq!((off.position, off.enclosures[0].deadair), (300, None), "off until turned on");
|
||||
assert_eq!((off.position, off.enclosures[0].deadair), (300, None), "off until turned on for the feed");
|
||||
|
||||
db.set_deadair(u, "", Some(true)).await.unwrap();
|
||||
db.set_deadair(u, "f", Some(true)).await.unwrap();
|
||||
let on = page().await;
|
||||
assert_eq!((on.position, on.duration, on.enclosures[0].deadair), (180, Some(480), Some(120)));
|
||||
|
||||
@@ -2531,7 +2531,7 @@ mod tests {
|
||||
playing.lock().unwrap().clear();
|
||||
assert_eq!(deadair::on_save(&ctx, &playing, u, "f", "g", 200, Some(600)).await.unwrap(), (200, Some(600)));
|
||||
|
||||
// Kept out: the original's clock again.
|
||||
// Turned off again: the original's clock.
|
||||
db.set_deadair(u, "f", Some(false)).await.unwrap();
|
||||
assert_eq!(page().await.enclosures[0].deadair, None);
|
||||
}
|
||||
|
||||
18
src/web.rs
18
src/web.rs
@@ -375,12 +375,10 @@ async fn me(
|
||||
let url = state.ctx.cfg().web.sign_out_url.clone();
|
||||
let sign_out = (by_proxy && !url.is_empty()).then_some(url);
|
||||
let blocked = state.ctx.db.blocklist(user.id, "").await.unwrap_or_default();
|
||||
let prefs = state.ctx.db.deadair_prefs(Some(user.id)).await.unwrap_or_default();
|
||||
let deadair = prefs.iter().any(|(_, f, on)| f.is_empty() && *on);
|
||||
let saved = state.ctx.db.deadair_total(user.id).await.unwrap_or(0) / 1000;
|
||||
Json(serde_json::json!({
|
||||
"name": user.name, "admin": user.is_admin, "sign_out": sign_out, "blocked": blocked,
|
||||
"deadair": deadair, "deadair_saved": saved,
|
||||
"deadair_saved": saved,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -388,8 +386,6 @@ async fn me(
|
||||
struct MePatch {
|
||||
/// Words that hide an item in every feed you read.
|
||||
blocked: Option<Vec<String>>,
|
||||
/// DeadAir's switch, beside the speed (#171).
|
||||
deadair: Option<bool>,
|
||||
}
|
||||
|
||||
/// Saves the block list for every feed. The theme is not the account's: each browser keeps its
|
||||
@@ -402,9 +398,6 @@ async fn patch_me(
|
||||
if let Some(words) = body.blocked {
|
||||
state.ctx.db.set_blocklist(user.id, "", &clean_words(words)?).await?;
|
||||
}
|
||||
if let Some(on) = body.deadair {
|
||||
state.ctx.db.set_deadair(user.id, "", Some(on)).await?;
|
||||
}
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -779,8 +772,7 @@ struct FeedRow {
|
||||
keywords: Vec<String>,
|
||||
/// Your words that hide an item of this feed, beside your list for every feed.
|
||||
blocked: Vec<String>,
|
||||
/// Played from DeadAir's copies when your switch is on: false for a feed you keep out, by
|
||||
/// itself or by its group.
|
||||
/// Played from DeadAir's copies: on for this feed or its group, off until you turn it on.
|
||||
deadair: bool,
|
||||
allow_explicit: bool,
|
||||
auto_download: bool,
|
||||
@@ -872,7 +864,7 @@ async fn feed_rows(state: &WebState, user: &crate::db::User, only: Option<&str>)
|
||||
.or_else(|| up.and_then(|u| u.keywords.clone()))
|
||||
.unwrap_or_else(|| feed.keywords.clone()),
|
||||
blocked,
|
||||
deadair: deadair.get(id).or_else(|| feed.group.as_deref().and_then(|g| deadair.get(g))).copied().unwrap_or(true),
|
||||
deadair: deadair.get(id).or_else(|| feed.group.as_deref().and_then(|g| deadair.get(g))).copied().unwrap_or(false),
|
||||
allow_explicit: mine
|
||||
.allow_explicit
|
||||
.or(up.and_then(|u| u.allow_explicit))
|
||||
@@ -1565,8 +1557,8 @@ struct FeedPatch {
|
||||
#[serde(default, deserialize_with = "double_option")]
|
||||
max_new_per_check: Option<Option<usize>>,
|
||||
pinned: Option<bool>,
|
||||
/// False keeps the feed out of DeadAir, true lets it in under a group kept out, null
|
||||
/// follows the group again.
|
||||
/// True plays the feed from DeadAir's copies, false keeps it out (of its group's too), null
|
||||
/// follows its group again (#171).
|
||||
#[serde(default, deserialize_with = "double_option")]
|
||||
deadair: Option<Option<bool>>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user