Inter, one file where WordPress listed two, and a pin heading on line
Inter (#11): the pages are set in Inter's variable font, served from the binary at /inter.woff2 as the icon is, with its OFL licence beside it in web/. Classic keeps Lucida Grande, the 2004 app's face. Double audio (#12): WordPress numbers each audio player on a page by adding ?_=N to its file's URL, so a post that embeds the file it encloses listed it twice, and it was downloaded twice. The parser keeps the first of an item's enclosures that differ only by that number. At startup the repeats already stored fold into the first; where only the repeat had been downloaded its file moves to the first rather than being deleted. Pin heading (#13): the rows' icon buttons kept the browser's side padding, which pushed their 16px icon 3px right of centre, and the heading's icon sat at the left of its column. Both are centred now, and the heading row takes the pixel of border the rows have, so every heading sits over its column. Closes #11, closes #12, closes #13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
46
src/feed.rs
46
src/feed.rs
@@ -480,7 +480,7 @@ fn from_rss(ch: rss::Channel, bytes: &[u8]) -> ParsedFeed {
|
||||
.filter_map(|(idx, item)| {
|
||||
// Straight from the XML, so an item with several keeps all of them. Falls
|
||||
// back to the parsed one if the scan and the parser disagree on item count.
|
||||
let enclosures: Vec<Enclosure> = per_item.get(idx).cloned().unwrap_or_else(|| {
|
||||
let mut enclosures: Vec<Enclosure> = per_item.get(idx).cloned().unwrap_or_else(|| {
|
||||
item.enclosure()
|
||||
.into_iter()
|
||||
.map(|e| Enclosure {
|
||||
@@ -491,6 +491,7 @@ fn from_rss(ch: rss::Channel, bytes: &[u8]) -> ParsedFeed {
|
||||
.filter(|e| !e.url.is_empty())
|
||||
.collect()
|
||||
});
|
||||
drop_player_repeats(&mut enclosures);
|
||||
|
||||
let guid = pick_guid(
|
||||
item.guid().map(|g| g.value()),
|
||||
@@ -555,7 +556,7 @@ fn from_atom(feed: atom_syndication::Feed) -> ParsedFeed {
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
// Atom carries enclosures as <link rel="enclosure">.
|
||||
let enclosures: Vec<Enclosure> = e
|
||||
let mut enclosures: Vec<Enclosure> = e
|
||||
.links()
|
||||
.iter()
|
||||
.filter(|l| l.rel() == "enclosure")
|
||||
@@ -566,6 +567,7 @@ fn from_atom(feed: atom_syndication::Feed) -> ParsedFeed {
|
||||
})
|
||||
.filter(|e| !e.url.is_empty())
|
||||
.collect();
|
||||
drop_player_repeats(&mut enclosures);
|
||||
|
||||
let alt = e
|
||||
.links()
|
||||
@@ -631,6 +633,31 @@ fn non_empty(s: Option<&str>) -> Option<String> {
|
||||
s.map(str::trim).filter(|s| !s.is_empty()).map(str::to_owned)
|
||||
}
|
||||
|
||||
/// WordPress numbers each audio player on a page by adding `?_=N` to its file's URL, so a post
|
||||
/// that embeds the file it encloses lists the same file twice: Rands in Repose's "The Promotion
|
||||
/// Paradox" was downloaded twice and offered two play buttons for one mp3. The first stays.
|
||||
fn drop_player_repeats(encs: &mut Vec<Enclosure>) {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
encs.retain(|e| seen.insert(same_file_key(&e.url)));
|
||||
}
|
||||
|
||||
/// An enclosure URL without WordPress's player number, for telling repeats of one file apart
|
||||
/// from different files.
|
||||
pub fn same_file_key(url: &str) -> String {
|
||||
let Ok(mut u) = url::Url::parse(url) else { return url.to_owned() };
|
||||
let kept: Vec<(String, String)> = u
|
||||
.query_pairs()
|
||||
.filter(|(k, v)| !(k == "_" && !v.is_empty() && v.bytes().all(|b| b.is_ascii_digit())))
|
||||
.map(|(k, v)| (k.into_owned(), v.into_owned()))
|
||||
.collect();
|
||||
if kept.is_empty() {
|
||||
u.set_query(None);
|
||||
} else {
|
||||
u.query_pairs_mut().clear().extend_pairs(kept);
|
||||
}
|
||||
u.to_string()
|
||||
}
|
||||
|
||||
/// A title as plain text. An Atom title of `type="html"`, or an RSS one in CDATA, comes through
|
||||
/// the XML parser with its HTML entities intact: The Verge's "Meta’s" reached the page as
|
||||
/// typed. Decoded one entity at a time, so an `&` that starts none, as in "Q&A", stays as it is
|
||||
@@ -777,6 +804,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_wordpress_lists_twice_is_one_enclosure() {
|
||||
let xml = br#"<?xml version="1.0"?><rss version="2.0"><channel><title>R</title>
|
||||
<item><title>The Promotion Paradox</title><guid>p</guid>
|
||||
<enclosure url="https://x/ep.mp3" length="1" type="audio/mpeg"/>
|
||||
<enclosure url="https://x/ep.mp3?_=2" length="1" type="audio/mpeg"/>
|
||||
<enclosure url="https://x/other.mp3?_=3&key=k" length="1" type="audio/mpeg"/>
|
||||
</item></channel></rss>"#;
|
||||
let urls: Vec<String> =
|
||||
parse(xml).unwrap().entries[0].enclosures.iter().map(|e| e.url.clone()).collect();
|
||||
assert_eq!(urls, ["https://x/ep.mp3", "https://x/other.mp3?_=3&key=k"], "the repeat goes, a different file stays");
|
||||
assert_eq!(same_file_key("https://x/a.mp3?key=k&_=2"), same_file_key("https://x/a.mp3?key=k"));
|
||||
assert_ne!(same_file_key("https://x/a.mp3?_=x"), same_file_key("https://x/a.mp3"), "only a number");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn titles_are_read_as_text_not_html() {
|
||||
// The Verge: an Atom title of type="html", its entity inside CDATA.
|
||||
|
||||
Reference in New Issue
Block a user