diff --git a/crates/feed/src/patreon.rs b/crates/feed/src/patreon.rs index 4dbc5a9..0436b25 100644 --- a/crates/feed/src/patreon.rs +++ b/crates/feed/src/patreon.rs @@ -22,13 +22,17 @@ pub fn is_patreon_creator(url: &str) -> bool { matches!(patreon_parts(url), Some((_, None))) } -/// Whether two URLs are the same feed. One Patreon show has several spellings -- by the -/// creator's name, by number, or with no creator at all -- and the token and show are what -/// identify it. +/// Whether two URLs are the same feed. The same address over http and https is one feed: +/// oldvcr's blog was subscribed under both, every post twice (#362). One Patreon show has several +/// spellings -- by the creator's name, by number, or with no creator at all -- and the token and +/// show are what identify it. #[must_use] pub fn same_feed(a: &str, b: &str) -> bool { - // Parsing both is the slow part, and only Patreon's addresses can differ and still match. - a == b + fn bare(u: &str) -> &str { + u.strip_prefix("https://").or_else(|| u.strip_prefix("http://")).unwrap_or(u) + } + // Parsing both is the slow part, and only Patreon's addresses can differ further and still match. + bare(a) == bare(b) || (a.contains("patreon.com") && b.contains("patreon.com") && patreon_parts(a).is_some_and(|p| Some(p) == patreon_parts(b))) @@ -117,6 +121,13 @@ mod tests { assert!(!is_patreon_creator(&format!("https://www.patreon.com/rss/x?auth={tok}&show=1")), "one show is a feed"); assert!(!is_patreon_creator(&format!("https://example.com/rss?auth={tok}"))); + assert!(same_feed( + "http://oldvcr.blogspot.com/feeds/posts/default", + "https://oldvcr.blogspot.com/feeds/posts/default" + )); + assert!(!same_feed("https://a.example/feed", "https://b.example/feed")); + assert!(!same_feed("https://a.example/feed", "https://a.example/feed/comments")); + // The show you already have by name is the one a bare token would add by number. assert!(same_feed( &format!("https://www.patreon.com/rss/glasscannon?auth={tok}&show=2073588"), diff --git a/crates/ipx/CHANGELOG.md b/crates/ipx/CHANGELOG.md index 17eb019..a8c18ea 100644 --- a/crates/ipx/CHANGELOG.md +++ b/crates/ipx/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A feed you already have, added again by the same address over http rather than https or the other way round, is the one you have, not a second subscription with every item twice. Changing a feed's address to another feed's over the other scheme is refused the same way (#362). - A podcast that was once in an OPML and is now a feed of its own is no longer hidden from All Subscriptions when that OPML is, so its unread items are listed there as the feed list counts them (#358). ## [1.5.1] - 2026-10-24 diff --git a/crates/ipx/src/catalogue.rs b/crates/ipx/src/catalogue.rs index ed19177..c63dca7 100644 --- a/crates/ipx/src/catalogue.rs +++ b/crates/ipx/src/catalogue.rs @@ -611,6 +611,19 @@ mod tests { assert!(ctx.db.feed_summary("has-file").await.unwrap().orphaned, "and flagged as orphaned"); } + #[tokio::test] + async fn a_feed_added_again_over_the_other_scheme_is_the_one_already_there() { + // oldvcr's blog, subscribed over https and again over http: every post twice (#362). + let mut cfg = config::Config::default(); + let mut f = feed(); + f.url = "https://oldvcr.blogspot.com/feeds/posts/default".into(); + cfg.feeds.insert("oldvcr".into(), f); + let ctx = test_ctx(cfg).await; + let got = add_to_catalogue(&ctx, "http://oldvcr.blogspot.com/feeds/posts/default", None, vec![]).await.unwrap(); + assert_eq!(got, ("oldvcr".to_owned(), true)); + assert_eq!(ctx.cfg().feeds.len(), 1); + } + #[tokio::test] async fn a_show_made_a_feed_of_its_own_is_not_hidden_with_its_list() { // Unsung: promoted out of a Hacker News OPML hidden from All Subscriptions, its unread diff --git a/crates/ipx/src/web.rs b/crates/ipx/src/web.rs index 9cc0980..129a656 100644 --- a/crates/ipx/src/web.rs +++ b/crates/ipx/src/web.rs @@ -1238,7 +1238,7 @@ fn check_url(url: &str, id: &str, feeds: &BTreeMap) Ok(u) => return Err(format!("{:?} is not an http(s) URL", u.scheme())), Err(e) => return Err(format!("that is not a valid URL: {e}")), } - if let Some((other, _)) = feeds.iter().find(|(k, f)| k.as_str() != id && f.url == url) { + if let Some((other, _)) = feeds.iter().find(|(k, f)| k.as_str() != id && crate::feed::same_feed(&f.url, url)) { return Err(format!("{other:?} is already subscribed to that URL")); } Ok(url.to_owned())