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") 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 } } }