From 4f976fded79b0cb496785b9fc6f0a7153334f765 Mon Sep 17 00:00:00 2001 From: Marco Gomiero Date: Tue, 3 Feb 2026 12:13:42 +0100 Subject: [PATCH 1/2] Prefer Atom entry alternate links and ignore related media URLs --- .../rssparser/internal/atom/AtomParser.kt | 4 +- .../internal/atom/AtomFeedHandler.kt | 2 +- .../com/prof18/rssparser/model/RssItem.kt | 30 ++++++++++- .../atom/XmlParserAtomEntryRelatedLinkTest.kt | 50 +++++++++++++++++++ .../atom-feed-entry-related-link.xml | 17 +++++++ .../internal/atom/AtomFeedHandler.kt | 2 +- .../internal/mapper/RssChannelMapper.kt | 2 +- 7 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 rssparser/src/commonTest/kotlin/com/prof18/rssparser/atom/XmlParserAtomEntryRelatedLinkTest.kt create mode 100644 rssparser/src/commonTest/resources/atom-feed-entry-related-link.xml diff --git a/rssparser/src/androidMain/kotlin/com/prof18/rssparser/internal/atom/AtomParser.kt b/rssparser/src/androidMain/kotlin/com/prof18/rssparser/internal/atom/AtomParser.kt index 17911e8e..e8dc9418 100644 --- a/rssparser/src/androidMain/kotlin/com/prof18/rssparser/internal/atom/AtomParser.kt +++ b/rssparser/src/androidMain/kotlin/com/prof18/rssparser/internal/atom/AtomParser.kt @@ -172,8 +172,8 @@ internal fun CoroutineScope.extractAtomContent( rel != AtomKeyword.LINK_REL_ENCLOSURE.value ) { when { - insideItem -> channelFactory.articleBuilder.link(link) - else -> channelFactory.channelBuilder.link(link) + insideItem -> channelFactory.articleBuilder.link(link, rel) + !insideItem -> channelFactory.channelBuilder.link(link) } } } diff --git a/rssparser/src/appleMain/kotlin/com/prof18/rssparser/internal/atom/AtomFeedHandler.kt b/rssparser/src/appleMain/kotlin/com/prof18/rssparser/internal/atom/AtomFeedHandler.kt index 84724340..7b295655 100644 --- a/rssparser/src/appleMain/kotlin/com/prof18/rssparser/internal/atom/AtomFeedHandler.kt +++ b/rssparser/src/appleMain/kotlin/com/prof18/rssparser/internal/atom/AtomFeedHandler.kt @@ -56,7 +56,7 @@ internal class AtomFeedHandler( rel != AtomKeyword.LINK_REL_REPLIES.value ) { when { - isInsideItem -> channelFactory.articleBuilder.link(link) + isInsideItem -> channelFactory.articleBuilder.link(link, rel) else -> channelFactory.channelBuilder.link(link) } } diff --git a/rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RssItem.kt b/rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RssItem.kt index b1c7f2cc..828e1511 100644 --- a/rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RssItem.kt +++ b/rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RssItem.kt @@ -55,10 +55,20 @@ public data class RssItem( private var youtubeItemData: YoutubeItemData? = null, private var rawEnclosure: RawEnclosure? = null, ) { + private var linkPriority: Int = LINK_PRIORITY_NONE + fun guid(guid: String?) = apply { this.guid = guid } fun title(title: String?) = apply { this.title = title } fun author(author: String?) = apply { this.author = author } - fun link(link: String?) = apply { this.link = link } + + fun link(link: String?, rel: String? = null) = apply { + val candidate = link?.takeIf { it.isNotBlank() } ?: return@apply + val priority = linkPriorityFor(rel) + if (priority > linkPriority) { + this.link = candidate + linkPriority = priority + } + } fun pubDate(pubDate: String?) = apply { this.pubDate = pubDate } @@ -152,4 +162,22 @@ public data class RssItem( ) } } + + private companion object { + const val LINK_PRIORITY_NONE = -1 + const val LINK_PRIORITY_RELATED = 0 + const val LINK_PRIORITY_OTHER = 1 + const val LINK_PRIORITY_MISSING_REL = 2 + const val LINK_PRIORITY_ALTERNATE = 3 + + fun linkPriorityFor(rel: String?): Int { + val normalized = rel?.lowercase() + return when { + normalized == "alternate" -> LINK_PRIORITY_ALTERNATE + normalized.isNullOrBlank() -> LINK_PRIORITY_MISSING_REL + normalized == "related" -> LINK_PRIORITY_RELATED + else -> LINK_PRIORITY_OTHER + } + } + } } diff --git a/rssparser/src/commonTest/kotlin/com/prof18/rssparser/atom/XmlParserAtomEntryRelatedLinkTest.kt b/rssparser/src/commonTest/kotlin/com/prof18/rssparser/atom/XmlParserAtomEntryRelatedLinkTest.kt new file mode 100644 index 00000000..40c2013a --- /dev/null +++ b/rssparser/src/commonTest/kotlin/com/prof18/rssparser/atom/XmlParserAtomEntryRelatedLinkTest.kt @@ -0,0 +1,50 @@ +package com.prof18.rssparser.atom + +import com.prof18.rssparser.XmlParserTestExecutor +import com.prof18.rssparser.model.RssChannel +import com.prof18.rssparser.model.RssItem +import com.prof18.rssparser.parseFeed +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class XmlParserAtomEntryRelatedLinkTest : XmlParserTestExecutor() { + + private val expectedChannel = RssChannel( + title = "Visual Studio Code - Code Editing. Redefined.", + link = "https://code.visualstudio.com/", + description = null, + image = null, + lastBuildDate = "2026-02-04T17:00:00.000Z", + updatePeriod = null, + itunesChannelData = null, + youtubeChannelData = null, + items = listOf( + RssItem( + guid = "https://code.visualstudio.com/updates/v1_109", + title = "January 2026 Insiders (version 1.109)", + author = null, + link = "https://code.visualstudio.com/updates/v1_109", + pubDate = "2026-02-04T17:00:00.000Z", + description = null, + content = "Learn what is new.", + image = null, + audio = null, + video = null, + sourceName = null, + sourceUrl = null, + categories = listOf(), + itunesItemData = null, + commentsUrl = null, + youtubeItemData = null, + rawEnclosure = null, + ) + ) + ) + + @Test + fun channelIsParsedCorrectly() = runTest { + val channel = parseFeed("atom-feed-entry-related-link.xml") + assertEquals(expectedChannel, channel) + } +} diff --git a/rssparser/src/commonTest/resources/atom-feed-entry-related-link.xml b/rssparser/src/commonTest/resources/atom-feed-entry-related-link.xml new file mode 100644 index 00000000..fe450d7a --- /dev/null +++ b/rssparser/src/commonTest/resources/atom-feed-entry-related-link.xml @@ -0,0 +1,17 @@ + + + Visual Studio Code - Code Editing. Redefined. + + + 2026-02-04T17:00:00.000Z + https://code.visualstudio.com/ + + + January 2026 Insiders (version 1.109) + + + 2026-02-04T17:00:00.000Z + https://code.visualstudio.com/updates/v1_109 + Learn what is new. + + diff --git a/rssparser/src/jvmMain/kotlin/com/prof18/rssparser/internal/atom/AtomFeedHandler.kt b/rssparser/src/jvmMain/kotlin/com/prof18/rssparser/internal/atom/AtomFeedHandler.kt index bde71b03..3b9c5856 100644 --- a/rssparser/src/jvmMain/kotlin/com/prof18/rssparser/internal/atom/AtomFeedHandler.kt +++ b/rssparser/src/jvmMain/kotlin/com/prof18/rssparser/internal/atom/AtomFeedHandler.kt @@ -53,7 +53,7 @@ internal class AtomFeedHandler( href } when { - isInsideItem -> channelFactory.articleBuilder.link(link) + isInsideItem -> channelFactory.articleBuilder.link(link, rel) else -> channelFactory.channelBuilder.link(link) } } diff --git a/rssparser/src/webMain/kotlin/com/prof18/rssparser/internal/mapper/RssChannelMapper.kt b/rssparser/src/webMain/kotlin/com/prof18/rssparser/internal/mapper/RssChannelMapper.kt index 7d2ed9ab..dba045cf 100644 --- a/rssparser/src/webMain/kotlin/com/prof18/rssparser/internal/mapper/RssChannelMapper.kt +++ b/rssparser/src/webMain/kotlin/com/prof18/rssparser/internal/mapper/RssChannelMapper.kt @@ -186,7 +186,7 @@ private fun AtomFeedEntity.toRssChannel(baseFeedUrl: String?): RssChannel { link.rel != AtomKeyword.LINK_REL_ENCLOSURE.value && link.rel != AtomKeyword.LINK_REL_REPLIES.value ) { - link(link.generateLink(baseFeedUrl)) + link(link.generateLink(baseFeedUrl), link.rel) } } pubDate(entry.published) From 86a397fccb89ee309668f28983b680610b940d3e Mon Sep 17 00:00:00 2001 From: Marco Gomiero Date: Tue, 3 Feb 2026 12:29:22 +0100 Subject: [PATCH 2/2] Fix visibility --- .../kotlin/com/prof18/rssparser/model/RssItem.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RssItem.kt b/rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RssItem.kt index 828e1511..7031c937 100644 --- a/rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RssItem.kt +++ b/rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RssItem.kt @@ -164,13 +164,13 @@ public data class RssItem( } private companion object { - const val LINK_PRIORITY_NONE = -1 - const val LINK_PRIORITY_RELATED = 0 - const val LINK_PRIORITY_OTHER = 1 - const val LINK_PRIORITY_MISSING_REL = 2 - const val LINK_PRIORITY_ALTERNATE = 3 + private const val LINK_PRIORITY_NONE = -1 + private const val LINK_PRIORITY_RELATED = 0 + private const val LINK_PRIORITY_OTHER = 1 + private const val LINK_PRIORITY_MISSING_REL = 2 + private const val LINK_PRIORITY_ALTERNATE = 3 - fun linkPriorityFor(rel: String?): Int { + private fun linkPriorityFor(rel: String?): Int { val normalized = rel?.lowercase() return when { normalized == "alternate" -> LINK_PRIORITY_ALTERNATE