diff --git a/CHANGELOG.md b/CHANGELOG.md index 59751c8..48e845b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A feed's page in the Directory, and subscribing from it, no longer wait while the whole Directory is put together to check that the one feed is in it. - The feed list, and each live update of it, no longer asks the database for a group's title once for every feed in the group. - An episode that came as a torrent of several files plays its largest file, and can be deleted, by hand or to free space, instead of staying on disk for good. - Removing an account takes the feeds only that person read off the server, as unsubscribing does, instead of leaving them downloading every new episode for no one. diff --git a/src/web.rs b/src/web.rs index cfe8236..786fff2 100644 --- a/src/web.rs +++ b/src/web.rs @@ -954,20 +954,11 @@ async fn popular(state: &WebState, user_id: i64, everything: bool) -> Result = - catalogue.iter().map(|s| (s.id.as_str(), &s.cfg)).collect(); - let is_folder: std::collections::HashSet<&str> = - catalogue.iter().filter_map(|s| s.cfg.group.as_deref()).collect(); + let ok = listable(&catalogue); let mut out = vec![]; for s in &catalogue { let n = counts.get(&s.id).copied().unwrap_or(0); - // A feed inside an OPML that looks private is as private as the OPML. - let folder = s.cfg.group.as_deref().and_then(|g| by_id.get(g)); - if (n == 0 && !everything) - || is_folder.contains(s.id.as_str()) - || looks_private(&s.cfg) - || folder.is_some_and(|f| looks_private(f)) - { + if (n == 0 && !everything) || !ok.contains(s.id.as_str()) { continue; } let sum = listed.remove(&s.id).map(|l| l.summary).unwrap_or_default(); @@ -989,6 +980,26 @@ async fn popular(state: &WebState, user_id: i64, everything: bool) -> Result std::collections::HashSet<&str> { + let by_id: std::collections::HashMap<&str, &crate::config::Feed> = + catalogue.iter().map(|s| (s.id.as_str(), &s.cfg)).collect(); + let is_folder: std::collections::HashSet<&str> = catalogue.iter().filter_map(|s| s.cfg.group.as_deref()).collect(); + catalogue + .iter() + // A feed inside an OPML that looks private is as private as the OPML. + .filter(|s| { + !is_folder.contains(s.id.as_str()) + && !looks_private(&s.cfg) + && !s.cfg.group.as_deref().and_then(|g| by_id.get(g)).is_some_and(|f| looks_private(f)) + }) + .map(|s| s.id.as_str()) + .collect() +} + async fn get_popular( State(state): State, user: crate::db::User, @@ -1047,7 +1058,7 @@ async fn get_listed( user: crate::db::User, Path(id): Path, ) -> Result, ApiError> { - if !popular(&state, user.id, true).await?.iter().any(|p| p.id == id) { + if !listable(&crate::subscriptions(&state.ctx).await?).contains(id.as_str()) { return Err(ApiError::not_found(format!("{id:?} is not in the directory"))); } let order = crate::db::order_sql("published", "desc"); @@ -1082,7 +1093,7 @@ async fn subscribe_popular( user: crate::db::User, Path(id): Path, ) -> Result, ApiError> { - if !popular(&state, user.id, true).await?.iter().any(|p| p.id == id) { + if !listable(&crate::subscriptions(&state.ctx).await?).contains(id.as_str()) { return Err(ApiError::bad_request(format!("{id:?} is not in the directory"))); } state.ctx.db.subscribe(user.id, &id).await?; @@ -1248,6 +1259,25 @@ mod tests { assert!(looks_private(&basic), "a feed with a login configured"); } + #[test] + fn the_directory_lists_feeds_not_their_groups_and_nothing_private() { + let sub = |id: &str, url: &str, group: Option<&str>| crate::Sub { + id: id.into(), + cfg: crate::config::Feed { group: group.map(Into::into), ..feed(url) }, + managed: group.is_some(), + }; + let catalogue = [ + sub("plain", "https://example.com/rss", None), + sub("opml", "https://example.com/list.opml", None), + sub("kid", "https://kid.example/rss", Some("opml")), + sub("paid", "https://www.patreon.com/rss/x?auth=abc", None), + sub("paid-show", "https://www.patreon.com/rss/x?auth=abc&show=1", Some("paid")), + ]; + let mut got: Vec<&str> = listable(&catalogue).into_iter().collect(); + got.sort(); + assert_eq!(got, ["kid", "plain"], "the OPML as its feeds; a private feed and its shows never"); + } + #[test] fn only_the_last_admin_is_protected() { let u = |id, is_admin| crate::db::User {