From fdbe90aba734fae2bae6163a2425378c46ad3360 Mon Sep 17 00:00:00 2001 From: Cassio Rossi Date: Mon, 27 Jul 2026 23:18:08 +0100 Subject: [PATCH 1/2] fix(#298): only treat audio enclosures as podcast URLs in APIXMLParser WordPress's search feed emits an element with the featured image on every item, not just on podcast episodes. APIXMLParser stored podcastURL from any regardless of its type attribute, so FeedViewModel.search()'s podcastURL.isEmpty check classified every search result as a podcast. Now only audio/* enclosures populate podcastURL. Co-Authored-By: Claude --- .../Services/Parser/APIXMLParser.swift | 3 +- .../FeedLibraryTests/XMLParserTests.swift | 64 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/MacMagazine/Features/FeedLibrary/Sources/FeedLibrary/Services/Parser/APIXMLParser.swift b/MacMagazine/Features/FeedLibrary/Sources/FeedLibrary/Services/Parser/APIXMLParser.swift index cd427e9e..1b991a1b 100644 --- a/MacMagazine/Features/FeedLibrary/Sources/FeedLibrary/Services/Parser/APIXMLParser.swift +++ b/MacMagazine/Features/FeedLibrary/Sources/FeedLibrary/Services/Parser/APIXMLParser.swift @@ -84,7 +84,8 @@ class APIXMLParser: NSObject, XMLParserDelegate { } currentPost.artworkURL = url case "enclosure": - guard let url = attributes?["url"] else { + guard let url = attributes?["url"], + attributes?["type"]?.hasPrefix("audio/") == true else { return } currentPost.podcastURL = url diff --git a/MacMagazine/Features/FeedLibrary/Tests/FeedLibraryTests/XMLParserTests.swift b/MacMagazine/Features/FeedLibrary/Tests/FeedLibraryTests/XMLParserTests.swift index f1910ea4..1f66eab6 100644 --- a/MacMagazine/Features/FeedLibrary/Tests/FeedLibraryTests/XMLParserTests.swift +++ b/MacMagazine/Features/FeedLibrary/Tests/FeedLibraryTests/XMLParserTests.swift @@ -174,6 +174,70 @@ struct XMLParserTests { } } + @Test("Image enclosure from search feed is not treated as a podcast URL") + func parseSearchXMLImageEnclosureIsNotPodcastURL() async throws { + // Given + let xml = """ + + + 123 + News item + https://macmagazine.com.br/post + + + """ + let data = Data(xml.utf8) + + // When + let posts = try await withCheckedThrowingContinuation { continuation in + let parser = XMLParser(data: data) + let apiParser = APIXMLParser( + numberOfPosts: -1, + category: "", + parseFullContent: false, + continuation: continuation + ) + parser.delegate = apiParser + parser.parse() + } as [XMLPost] + + // Then + let post = try #require(posts.first) + #expect(post.podcastURL.isEmpty, "Image enclosure must not populate podcastURL") + } + + @Test("Audio enclosure from search feed is treated as a podcast URL") + func parseSearchXMLAudioEnclosureIsPodcastURL() async throws { + // Given + let xml = """ + + + 456 + Podcast item + https://macmagazine.com.br/podcast + + + """ + let data = Data(xml.utf8) + + // When + let posts = try await withCheckedThrowingContinuation { continuation in + let parser = XMLParser(data: data) + let apiParser = APIXMLParser( + numberOfPosts: -1, + category: "", + parseFullContent: false, + continuation: continuation + ) + parser.delegate = apiParser + parser.parse() + } as [XMLPost] + + // Then + let post = try #require(posts.first) + #expect(post.podcastURL == "https://feeds.soundcloud.com/stream/episode.mp3") + } + // MARK: - Error Handling Tests @Test("Parse invalid XML throws error") From 21caf88b9ccca9b0a283c86600babc83266e43f8 Mon Sep 17 00:00:00 2001 From: Cassio Rossi Date: Mon, 27 Jul 2026 23:31:46 +0100 Subject: [PATCH 2/2] fix(#298): match news categories by rawValue as well as filterKey Search results parse categories without injecting a feed-specific filterKey, so the raw XML category text (the Portuguese rawValue, e.g. "Destaques") never matched filterKeyToCategory, which only knew about filterKey strings like "NewsCategoryHighlights". Every search result fell back to .all and lost its highlights/rumors/reviews/etc. card styling. Matching on rawValue too fixes this, and the lookup is deduped since a regular feed's array can now match a category twice (once via the injected filterKey, once via the real XML text). Co-Authored-By: Claude --- .../Sources/NewsLibrary/Extensions/ArrayExtensions.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Extensions/ArrayExtensions.swift b/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Extensions/ArrayExtensions.swift index a3bb7073..c916ccaa 100644 --- a/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Extensions/ArrayExtensions.swift +++ b/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Extensions/ArrayExtensions.swift @@ -5,12 +5,14 @@ private let filterKeyToCategory: [String: NewsCategory] = { var map = [String: NewsCategory]() for category in NewsCategory.allCases { map[category.filterKey] = category + map[category.rawValue] = category } return map }() public extension Array where Element == String { var toNewsCategory: [NewsCategory] { - compactMap { filterKeyToCategory[$0] } + var seen = Set() + return compactMap { filterKeyToCategory[$0] }.filter { seen.insert($0).inserted } } }