The same feed over http and https is one feed (#362)

rays was subscribed to oldvcr's blog twice, once at
https://oldvcr.blogspot.com/feeds/posts/default and once at the same
address over http, and saw every post twice. same_feed compared
addresses exactly but for Patreon's, so add_to_catalogue found nothing
there already; check_url compared exactly too and now asks same_feed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 02:00:20 +00:00
parent eae0ef5061
commit 1d54f5141d
4 changed files with 31 additions and 6 deletions

View File

@@ -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"),

View File

@@ -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

View File

@@ -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

View File

@@ -1238,7 +1238,7 @@ fn check_url(url: &str, id: &str, feeds: &BTreeMap<String, crate::config::Feed>)
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())