From a714e7754a3527bf32a0cd2f1fcba4e974944872 Mon Sep 17 00:00:00 2001 From: Marco Gomiero Date: Sun, 22 Mar 2026 13:46:56 +0100 Subject: [PATCH] Support parsing `media:content` with type and medium attributes --- rssparser/api/jvm/rssparser.api | 24 ++- rssparser/api/rssparser.klib.api | 26 ++- .../rssparser/internal/atom/AtomParser.kt | 33 +++- .../rssparser/internal/rss/RssParser.kt | 31 ++- .../internal/atom/AtomFeedHandler.kt | 32 ++- .../rssparser/internal/rss/RssFeedHandler.kt | 26 ++- .../prof18/rssparser/internal/AtomKeyword.kt | 2 +- .../rssparser/internal/ChannelFactory.kt | 4 + .../prof18/rssparser/internal/RssKeyword.kt | 1 + .../prof18/rssparser/model/RawMediaContent.kt | 32 +++ .../com/prof18/rssparser/model/RssItem.kt | 8 +- .../atom/XmlParserAtomIdeaRicirTest.kt | 6 + .../prof18/rssparser/rss/XmlParserDcDate.kt | 6 + .../rss/XmlParserMediaContentTypeTest.kt | 182 ++++++++++++++++++ .../rss/XmlParserStandardFeedTest.kt | 6 + .../resources/feed-media-content-type.xml | 63 ++++++ .../internal/atom/AtomFeedHandler.kt | 32 ++- .../rssparser/internal/rss/RssFeedHandler.kt | 26 ++- .../internal/entity/AtomFeedEntity.kt | 2 + .../internal/mapper/RssChannelMapper.kt | 62 +++++- 20 files changed, 562 insertions(+), 42 deletions(-) create mode 100644 rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RawMediaContent.kt create mode 100644 rssparser/src/commonTest/kotlin/com/prof18/rssparser/rss/XmlParserMediaContentTypeTest.kt create mode 100644 rssparser/src/commonTest/resources/feed-media-content-type.xml diff --git a/rssparser/api/jvm/rssparser.api b/rssparser/api/jvm/rssparser.api index 5759cd33..fff2699e 100644 --- a/rssparser/api/jvm/rssparser.api +++ b/rssparser/api/jvm/rssparser.api @@ -129,6 +129,21 @@ public final class com/prof18/rssparser/model/RawEnclosure { public fun toString ()Ljava/lang/String; } +public final class com/prof18/rssparser/model/RawMediaContent { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/prof18/rssparser/model/RawMediaContent; + public static synthetic fun copy$default (Lcom/prof18/rssparser/model/RawMediaContent;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/prof18/rssparser/model/RawMediaContent; + public fun equals (Ljava/lang/Object;)Z + public final fun getMedium ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public final fun getUrl ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/prof18/rssparser/model/RssChannel { public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/prof18/rssparser/model/RssImage;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/prof18/rssparser/model/ItunesChannelData;Lcom/prof18/rssparser/model/YoutubeChannelData;)V public final fun component1 ()Ljava/lang/String; @@ -174,7 +189,8 @@ public final class com/prof18/rssparser/model/RssImage { } public final class com/prof18/rssparser/model/RssItem { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/prof18/rssparser/model/ItunesItemData;Ljava/lang/String;Lcom/prof18/rssparser/model/YoutubeItemData;Lcom/prof18/rssparser/model/RawEnclosure;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/prof18/rssparser/model/ItunesItemData;Ljava/lang/String;Lcom/prof18/rssparser/model/YoutubeItemData;Lcom/prof18/rssparser/model/RawEnclosure;Lcom/prof18/rssparser/model/RawMediaContent;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/prof18/rssparser/model/ItunesItemData;Ljava/lang/String;Lcom/prof18/rssparser/model/YoutubeItemData;Lcom/prof18/rssparser/model/RawEnclosure;Lcom/prof18/rssparser/model/RawMediaContent;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component10 ()Ljava/lang/String; public final fun component11 ()Ljava/lang/String; @@ -184,6 +200,7 @@ public final class com/prof18/rssparser/model/RssItem { public final fun component15 ()Ljava/lang/String; public final fun component16 ()Lcom/prof18/rssparser/model/YoutubeItemData; public final fun component17 ()Lcom/prof18/rssparser/model/RawEnclosure; + public final fun component18 ()Lcom/prof18/rssparser/model/RawMediaContent; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; @@ -192,8 +209,8 @@ public final class com/prof18/rssparser/model/RssItem { public final fun component7 ()Ljava/lang/String; public final fun component8 ()Ljava/lang/String; public final fun component9 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/prof18/rssparser/model/ItunesItemData;Ljava/lang/String;Lcom/prof18/rssparser/model/YoutubeItemData;Lcom/prof18/rssparser/model/RawEnclosure;)Lcom/prof18/rssparser/model/RssItem; - public static synthetic fun copy$default (Lcom/prof18/rssparser/model/RssItem;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/prof18/rssparser/model/ItunesItemData;Ljava/lang/String;Lcom/prof18/rssparser/model/YoutubeItemData;Lcom/prof18/rssparser/model/RawEnclosure;ILjava/lang/Object;)Lcom/prof18/rssparser/model/RssItem; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/prof18/rssparser/model/ItunesItemData;Ljava/lang/String;Lcom/prof18/rssparser/model/YoutubeItemData;Lcom/prof18/rssparser/model/RawEnclosure;Lcom/prof18/rssparser/model/RawMediaContent;)Lcom/prof18/rssparser/model/RssItem; + public static synthetic fun copy$default (Lcom/prof18/rssparser/model/RssItem;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/prof18/rssparser/model/ItunesItemData;Ljava/lang/String;Lcom/prof18/rssparser/model/YoutubeItemData;Lcom/prof18/rssparser/model/RawEnclosure;Lcom/prof18/rssparser/model/RawMediaContent;ILjava/lang/Object;)Lcom/prof18/rssparser/model/RssItem; public fun equals (Ljava/lang/Object;)Z public final fun getAudio ()Ljava/lang/String; public final fun getAuthor ()Ljava/lang/String; @@ -207,6 +224,7 @@ public final class com/prof18/rssparser/model/RssItem { public final fun getLink ()Ljava/lang/String; public final fun getPubDate ()Ljava/lang/String; public final fun getRawEnclosure ()Lcom/prof18/rssparser/model/RawEnclosure; + public final fun getRawMediaContent ()Lcom/prof18/rssparser/model/RawMediaContent; public final fun getSourceName ()Ljava/lang/String; public final fun getSourceUrl ()Ljava/lang/String; public final fun getTitle ()Ljava/lang/String; diff --git a/rssparser/api/rssparser.klib.api b/rssparser/api/rssparser.klib.api index 84c0a472..49b55399 100644 --- a/rssparser/api/rssparser.klib.api +++ b/rssparser/api/rssparser.klib.api @@ -157,6 +157,25 @@ final class com.prof18.rssparser.model/RawEnclosure { // com.prof18.rssparser.mo final fun toString(): kotlin/String // com.prof18.rssparser.model/RawEnclosure.toString|toString(){}[0] } +final class com.prof18.rssparser.model/RawMediaContent { // com.prof18.rssparser.model/RawMediaContent|null[0] + constructor (kotlin/String?, kotlin/String?, kotlin/String?) // com.prof18.rssparser.model/RawMediaContent.|(kotlin.String?;kotlin.String?;kotlin.String?){}[0] + + final val medium // com.prof18.rssparser.model/RawMediaContent.medium|{}medium[0] + final fun (): kotlin/String? // com.prof18.rssparser.model/RawMediaContent.medium.|(){}[0] + final val type // com.prof18.rssparser.model/RawMediaContent.type|{}type[0] + final fun (): kotlin/String? // com.prof18.rssparser.model/RawMediaContent.type.|(){}[0] + final val url // com.prof18.rssparser.model/RawMediaContent.url|{}url[0] + final fun (): kotlin/String? // com.prof18.rssparser.model/RawMediaContent.url.|(){}[0] + + final fun component1(): kotlin/String? // com.prof18.rssparser.model/RawMediaContent.component1|component1(){}[0] + final fun component2(): kotlin/String? // com.prof18.rssparser.model/RawMediaContent.component2|component2(){}[0] + final fun component3(): kotlin/String? // com.prof18.rssparser.model/RawMediaContent.component3|component3(){}[0] + final fun copy(kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ...): com.prof18.rssparser.model/RawMediaContent // com.prof18.rssparser.model/RawMediaContent.copy|copy(kotlin.String?;kotlin.String?;kotlin.String?){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // com.prof18.rssparser.model/RawMediaContent.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // com.prof18.rssparser.model/RawMediaContent.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // com.prof18.rssparser.model/RawMediaContent.toString|toString(){}[0] +} + final class com.prof18.rssparser.model/RssChannel { // com.prof18.rssparser.model/RssChannel|null[0] constructor (kotlin/String?, kotlin/String?, kotlin/String?, com.prof18.rssparser.model/RssImage?, kotlin/String?, kotlin/String?, kotlin.collections/List, com.prof18.rssparser.model/ItunesChannelData?, com.prof18.rssparser.model/YoutubeChannelData?) // com.prof18.rssparser.model/RssChannel.|(kotlin.String?;kotlin.String?;kotlin.String?;com.prof18.rssparser.model.RssImage?;kotlin.String?;kotlin.String?;kotlin.collections.List;com.prof18.rssparser.model.ItunesChannelData?;com.prof18.rssparser.model.YoutubeChannelData?){}[0] @@ -217,7 +236,7 @@ final class com.prof18.rssparser.model/RssImage { // com.prof18.rssparser.model/ } final class com.prof18.rssparser.model/RssItem { // com.prof18.rssparser.model/RssItem|null[0] - constructor (kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin.collections/List, com.prof18.rssparser.model/ItunesItemData?, kotlin/String?, com.prof18.rssparser.model/YoutubeItemData?, com.prof18.rssparser.model/RawEnclosure?) // com.prof18.rssparser.model/RssItem.|(kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.collections.List;com.prof18.rssparser.model.ItunesItemData?;kotlin.String?;com.prof18.rssparser.model.YoutubeItemData?;com.prof18.rssparser.model.RawEnclosure?){}[0] + constructor (kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin/String?, kotlin.collections/List, com.prof18.rssparser.model/ItunesItemData?, kotlin/String?, com.prof18.rssparser.model/YoutubeItemData?, com.prof18.rssparser.model/RawEnclosure?, com.prof18.rssparser.model/RawMediaContent? = ...) // com.prof18.rssparser.model/RssItem.|(kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.collections.List;com.prof18.rssparser.model.ItunesItemData?;kotlin.String?;com.prof18.rssparser.model.YoutubeItemData?;com.prof18.rssparser.model.RawEnclosure?;com.prof18.rssparser.model.RawMediaContent?){}[0] final val audio // com.prof18.rssparser.model/RssItem.audio|{}audio[0] final fun (): kotlin/String? // com.prof18.rssparser.model/RssItem.audio.|(){}[0] @@ -243,6 +262,8 @@ final class com.prof18.rssparser.model/RssItem { // com.prof18.rssparser.model/R final fun (): kotlin/String? // com.prof18.rssparser.model/RssItem.pubDate.|(){}[0] final val rawEnclosure // com.prof18.rssparser.model/RssItem.rawEnclosure|{}rawEnclosure[0] final fun (): com.prof18.rssparser.model/RawEnclosure? // com.prof18.rssparser.model/RssItem.rawEnclosure.|(){}[0] + final val rawMediaContent // com.prof18.rssparser.model/RssItem.rawMediaContent|{}rawMediaContent[0] + final fun (): com.prof18.rssparser.model/RawMediaContent? // com.prof18.rssparser.model/RssItem.rawMediaContent.|(){}[0] final val sourceName // com.prof18.rssparser.model/RssItem.sourceName|{}sourceName[0] final fun (): kotlin/String? // com.prof18.rssparser.model/RssItem.sourceName.|(){}[0] final val sourceUrl // com.prof18.rssparser.model/RssItem.sourceUrl|{}sourceUrl[0] @@ -263,6 +284,7 @@ final class com.prof18.rssparser.model/RssItem { // com.prof18.rssparser.model/R final fun component15(): kotlin/String? // com.prof18.rssparser.model/RssItem.component15|component15(){}[0] final fun component16(): com.prof18.rssparser.model/YoutubeItemData? // com.prof18.rssparser.model/RssItem.component16|component16(){}[0] final fun component17(): com.prof18.rssparser.model/RawEnclosure? // com.prof18.rssparser.model/RssItem.component17|component17(){}[0] + final fun component18(): com.prof18.rssparser.model/RawMediaContent? // com.prof18.rssparser.model/RssItem.component18|component18(){}[0] final fun component2(): kotlin/String? // com.prof18.rssparser.model/RssItem.component2|component2(){}[0] final fun component3(): kotlin/String? // com.prof18.rssparser.model/RssItem.component3|component3(){}[0] final fun component4(): kotlin/String? // com.prof18.rssparser.model/RssItem.component4|component4(){}[0] @@ -271,7 +293,7 @@ final class com.prof18.rssparser.model/RssItem { // com.prof18.rssparser.model/R final fun component7(): kotlin/String? // com.prof18.rssparser.model/RssItem.component7|component7(){}[0] final fun component8(): kotlin/String? // com.prof18.rssparser.model/RssItem.component8|component8(){}[0] final fun component9(): kotlin/String? // com.prof18.rssparser.model/RssItem.component9|component9(){}[0] - final fun copy(kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin.collections/List = ..., com.prof18.rssparser.model/ItunesItemData? = ..., kotlin/String? = ..., com.prof18.rssparser.model/YoutubeItemData? = ..., com.prof18.rssparser.model/RawEnclosure? = ...): com.prof18.rssparser.model/RssItem // com.prof18.rssparser.model/RssItem.copy|copy(kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.collections.List;com.prof18.rssparser.model.ItunesItemData?;kotlin.String?;com.prof18.rssparser.model.YoutubeItemData?;com.prof18.rssparser.model.RawEnclosure?){}[0] + final fun copy(kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin/String? = ..., kotlin.collections/List = ..., com.prof18.rssparser.model/ItunesItemData? = ..., kotlin/String? = ..., com.prof18.rssparser.model/YoutubeItemData? = ..., com.prof18.rssparser.model/RawEnclosure? = ..., com.prof18.rssparser.model/RawMediaContent? = ...): com.prof18.rssparser.model/RssItem // com.prof18.rssparser.model/RssItem.copy|copy(kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.String?;kotlin.collections.List;com.prof18.rssparser.model.ItunesItemData?;kotlin.String?;com.prof18.rssparser.model.YoutubeItemData?;com.prof18.rssparser.model.RawEnclosure?;com.prof18.rssparser.model.RawMediaContent?){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // com.prof18.rssparser.model/RssItem.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // com.prof18.rssparser.model/RssItem.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // com.prof18.rssparser.model/RssItem.toString|toString(){}[0] 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 e8dc9418..6dcc0fcf 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 @@ -20,6 +20,7 @@ package com.prof18.rssparser.internal.atom import com.prof18.rssparser.internal.AtomKeyword import com.prof18.rssparser.internal.ChannelFactory import com.prof18.rssparser.internal.ParserInput +import com.prof18.rssparser.internal.RssKeyword import com.prof18.rssparser.internal.attributeValue import com.prof18.rssparser.internal.contains import com.prof18.rssparser.internal.nextTrimmedText @@ -27,7 +28,6 @@ import com.prof18.rssparser.model.RssChannel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.isActive import org.xmlpull.v1.XmlPullParser -import org.xmlpull.v1.XmlPullParserException internal fun CoroutineScope.extractAtomContent( xmlPullParser: XmlPullParser, @@ -203,10 +203,33 @@ internal fun CoroutineScope.extractAtomContent( } } - xmlPullParser.contains(AtomKeyword.YOUTUBE_MEDIA_GROUP_CONTENT) -> { - if (insideItem && insideYoutubeMediaGroup) { - val videoUrl = xmlPullParser.attributeValue(AtomKeyword.YOUTUBE_MEDIA_GROUP_CONTENT_URL) - channelFactory.youtubeItemDataBuilder.videoUrl(videoUrl) + xmlPullParser.contains(AtomKeyword.MEDIA_GROUP_CONTENT) -> { + if (insideItem) { + if (insideYoutubeMediaGroup) { + val videoUrl = xmlPullParser.attributeValue(AtomKeyword.YOUTUBE_MEDIA_GROUP_CONTENT_URL) + channelFactory.youtubeItemDataBuilder.videoUrl(videoUrl) + } else { + val url = xmlPullParser.attributeValue(RssKeyword.URL) + val type = xmlPullParser.attributeValue(RssKeyword.ITEM_TYPE) + val medium = xmlPullParser.attributeValue(RssKeyword.ITEM_MEDIUM) + + channelFactory.rawMediaContentBuilder.url(url) + channelFactory.rawMediaContentBuilder.type(type) + channelFactory.rawMediaContentBuilder.medium(medium) + + when { + !medium.isNullOrBlank() -> when { + medium.equals("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + medium.equals("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + medium.equals("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + !type.isNullOrBlank() -> when { + type.contains("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + type.contains("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + type.contains("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + } + } } } diff --git a/rssparser/src/androidMain/kotlin/com/prof18/rssparser/internal/rss/RssParser.kt b/rssparser/src/androidMain/kotlin/com/prof18/rssparser/internal/rss/RssParser.kt index 7d023bd8..c191f34b 100644 --- a/rssparser/src/androidMain/kotlin/com/prof18/rssparser/internal/rss/RssParser.kt +++ b/rssparser/src/androidMain/kotlin/com/prof18/rssparser/internal/rss/RssParser.kt @@ -126,11 +126,26 @@ internal fun CoroutineScope.extractRSSContent( xmlPullParser.contains(RssKeyword.ITEM_MEDIA_CONTENT) -> { if (insideItem) { - channelFactory.articleBuilder.image( - xmlPullParser.attributeValue( - RssKeyword.URL - ) - ) + val url = xmlPullParser.attributeValue(RssKeyword.URL) + val type = xmlPullParser.attributeValue(RssKeyword.ITEM_TYPE) + val medium = xmlPullParser.attributeValue(RssKeyword.ITEM_MEDIUM) + + channelFactory.rawMediaContentBuilder.url(url) + channelFactory.rawMediaContentBuilder.type(type) + channelFactory.rawMediaContentBuilder.medium(medium) + + when { + !medium.isNullOrBlank() -> when { + medium.equals("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + medium.equals("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + medium.equals("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + !type.isNullOrBlank() -> when { + type.contains("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + type.contains("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + type.contains("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + } } } @@ -150,17 +165,17 @@ internal fun CoroutineScope.extractRSSContent( channelFactory.rawEnclosureBuilder.url(url) when { - type != null && type.contains("image") -> { + type != null && type.contains("image", ignoreCase = true) -> { // If there are multiple elements, we take only the first channelFactory.articleBuilder.image(url) } - type != null && type.contains("audio") -> { + type != null && type.contains("audio", ignoreCase = true) -> { // If there are multiple elements, we take only the first channelFactory.articleBuilder.audioIfNull(url) } - type != null && type.contains("video") -> { + type != null && type.contains("video", ignoreCase = true) -> { // If there are multiple elements, we take only the first channelFactory.articleBuilder.videoIfNull(url) } 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 7b295655..19fe4bbd 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 @@ -3,6 +3,7 @@ package com.prof18.rssparser.internal.atom import com.prof18.rssparser.internal.AtomKeyword import com.prof18.rssparser.internal.ChannelFactory import com.prof18.rssparser.internal.FeedHandler +import com.prof18.rssparser.internal.RssKeyword import com.prof18.rssparser.internal.getValueOrNull import com.prof18.rssparser.model.RssChannel @@ -62,10 +63,33 @@ internal class AtomFeedHandler( } } - AtomKeyword.YOUTUBE_MEDIA_GROUP_CONTENT.value -> { - if (isInsideItem && isInsideYoutubeMediaGroup) { - val url = attributes.getValueOrNull(AtomKeyword.YOUTUBE_MEDIA_GROUP_CONTENT_URL.value) as? String - channelFactory.youtubeItemDataBuilder.videoUrl(url) + AtomKeyword.MEDIA_GROUP_CONTENT.value -> { + if (isInsideItem) { + if (isInsideYoutubeMediaGroup) { + val url = attributes.getValueOrNull(AtomKeyword.YOUTUBE_MEDIA_GROUP_CONTENT_URL.value) as? String + channelFactory.youtubeItemDataBuilder.videoUrl(url) + } else { + val url = attributes[RssKeyword.URL.value] as? String + val type = attributes[RssKeyword.ITEM_TYPE.value] as? String + val medium = attributes[RssKeyword.ITEM_MEDIUM.value] as? String + + channelFactory.rawMediaContentBuilder.url(url) + channelFactory.rawMediaContentBuilder.type(type) + channelFactory.rawMediaContentBuilder.medium(medium) + + when { + !medium.isNullOrBlank() -> when { + medium.equals("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + medium.equals("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + medium.equals("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + !type.isNullOrBlank() -> when { + type.contains("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + type.contains("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + type.contains("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + } + } } } diff --git a/rssparser/src/appleMain/kotlin/com/prof18/rssparser/internal/rss/RssFeedHandler.kt b/rssparser/src/appleMain/kotlin/com/prof18/rssparser/internal/rss/RssFeedHandler.kt index 07754f4d..fa191b3e 100644 --- a/rssparser/src/appleMain/kotlin/com/prof18/rssparser/internal/rss/RssFeedHandler.kt +++ b/rssparser/src/appleMain/kotlin/com/prof18/rssparser/internal/rss/RssFeedHandler.kt @@ -33,7 +33,25 @@ internal class RssFeedHandler : FeedHandler { RssKeyword.ITEM_MEDIA_CONTENT.value -> { if (isInsideItem) { val url = attributes.getValueOrNull(RssKeyword.URL.value) as? String - channelFactory.articleBuilder.image(url) + val type = attributes[RssKeyword.ITEM_TYPE.value] as? String + val medium = attributes[RssKeyword.ITEM_MEDIUM.value] as? String + + channelFactory.rawMediaContentBuilder.url(url) + channelFactory.rawMediaContentBuilder.type(type) + channelFactory.rawMediaContentBuilder.medium(medium) + + when { + !medium.isNullOrBlank() -> when { + medium.equals("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + medium.equals("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + medium.equals("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + !type.isNullOrBlank() -> when { + type.contains("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + type.contains("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + type.contains("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + } } } @@ -55,17 +73,17 @@ internal class RssFeedHandler : FeedHandler { channelFactory.rawEnclosureBuilder.url(url) when { - type != null && type.contains("image") -> { + type != null && type.contains("image", ignoreCase = true) -> { // If there are multiple elements, we take only the first channelFactory.articleBuilder.image(url) } - type != null && type.contains("audio") -> { + type != null && type.contains("audio", ignoreCase = true) -> { // If there are multiple elements, we take only the first channelFactory.articleBuilder.audioIfNull(url) } - type != null && type.contains("video") -> { + type != null && type.contains("video", ignoreCase = true) -> { // If there are multiple elements, we take only the first channelFactory.articleBuilder.videoIfNull(url) } diff --git a/rssparser/src/commonMain/kotlin/com/prof18/rssparser/internal/AtomKeyword.kt b/rssparser/src/commonMain/kotlin/com/prof18/rssparser/internal/AtomKeyword.kt index 56be6a6f..8bce4ff9 100644 --- a/rssparser/src/commonMain/kotlin/com/prof18/rssparser/internal/AtomKeyword.kt +++ b/rssparser/src/commonMain/kotlin/com/prof18/rssparser/internal/AtomKeyword.kt @@ -28,13 +28,13 @@ internal enum class AtomKeyword(val value: String) { ENTRY_DESCRIPTION("summary"), ENTRY_AUTHOR("name"), ENTRY_EMAIL("email"), + MEDIA_GROUP_CONTENT("media:content"), // YouTube YOUTUBE_CHANNEL_ID("yt:channelId"), YOUTUBE_VIDEO_ID("yt:videoId"), YOUTUBE_MEDIA_GROUP("media:group"), YOUTUBE_MEDIA_GROUP_TITLE("media:title"), - YOUTUBE_MEDIA_GROUP_CONTENT("media:content"), YOUTUBE_MEDIA_GROUP_CONTENT_URL("url"), YOUTUBE_MEDIA_GROUP_THUMBNAIL("media:thumbnail"), YOUTUBE_MEDIA_GROUP_THUMBNAIL_URL("url"), diff --git a/rssparser/src/commonMain/kotlin/com/prof18/rssparser/internal/ChannelFactory.kt b/rssparser/src/commonMain/kotlin/com/prof18/rssparser/internal/ChannelFactory.kt index 2ed89b97..9c349145 100644 --- a/rssparser/src/commonMain/kotlin/com/prof18/rssparser/internal/ChannelFactory.kt +++ b/rssparser/src/commonMain/kotlin/com/prof18/rssparser/internal/ChannelFactory.kt @@ -4,6 +4,7 @@ import com.prof18.rssparser.model.ItunesChannelData import com.prof18.rssparser.model.ItunesItemData import com.prof18.rssparser.model.ItunesOwner import com.prof18.rssparser.model.RawEnclosure +import com.prof18.rssparser.model.RawMediaContent import com.prof18.rssparser.model.RssChannel import com.prof18.rssparser.model.RssImage import com.prof18.rssparser.model.RssItem @@ -20,6 +21,7 @@ internal class ChannelFactory { var youtubeChannelDataBuilder = YoutubeChannelData.Builder() var youtubeItemDataBuilder = YoutubeItemData.Builder() var rawEnclosureBuilder = RawEnclosure.Builder() + var rawMediaContentBuilder = RawMediaContent.Builder() // This image url is extracted from the content and the description of the rss item. // It's a fallback just in case there aren't any images in the enclosure tag. @@ -33,6 +35,7 @@ internal class ChannelFactory { articleBuilder.itunesArticleData(itunesItemData) articleBuilder.youtubeItemData(youtubeItemDataBuilder.build()) articleBuilder.rawEnclosure(rawEnclosureBuilder.build()) + articleBuilder.rawMediaContent(rawMediaContentBuilder.build()) articleBuilder.build()?.let { channelBuilder.addItem(it) } // Reset temp data imageUrlFromContent = null @@ -40,6 +43,7 @@ internal class ChannelFactory { itunesArticleBuilder = ItunesItemData.Builder() youtubeItemDataBuilder = YoutubeItemData.Builder() rawEnclosureBuilder = RawEnclosure.Builder() + rawMediaContentBuilder = RawMediaContent.Builder() } fun buildItunesOwner() { diff --git a/rssparser/src/commonMain/kotlin/com/prof18/rssparser/internal/RssKeyword.kt b/rssparser/src/commonMain/kotlin/com/prof18/rssparser/internal/RssKeyword.kt index c97782fc..958d8289 100644 --- a/rssparser/src/commonMain/kotlin/com/prof18/rssparser/internal/RssKeyword.kt +++ b/rssparser/src/commonMain/kotlin/com/prof18/rssparser/internal/RssKeyword.kt @@ -52,6 +52,7 @@ internal enum class RssKeyword(val value: String) { ITEM_SOURCE("source"), ITEM_COMMENTS("comments"), ITEM_THUMB("thumb"), + ITEM_MEDIUM("medium"), // Item News ITEM_NEWS_IMAGE("News:Image"), diff --git a/rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RawMediaContent.kt b/rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RawMediaContent.kt new file mode 100644 index 00000000..8e83e1c2 --- /dev/null +++ b/rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RawMediaContent.kt @@ -0,0 +1,32 @@ +package com.prof18.rssparser.model + +public data class RawMediaContent( + val url: String?, + val type: String?, + val medium: String?, +) { + internal data class Builder( + private var url: String? = null, + private var type: String? = null, + private var medium: String? = null, + ) { + fun url(url: String?) = apply { this.url = url } + fun type(type: String?) = apply { this.type = type } + fun medium(medium: String?) = apply { this.medium = medium } + + fun build(): RawMediaContent? { + if ( + url.isNullOrBlank() && + type.isNullOrBlank() && + medium.isNullOrBlank() + ) { + return null + } + return RawMediaContent( + url = url, + type = type, + medium = medium, + ) + } + } +} 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 7031c937..a93f3c40 100644 --- a/rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RssItem.kt +++ b/rssparser/src/commonMain/kotlin/com/prof18/rssparser/model/RssItem.kt @@ -35,6 +35,7 @@ public data class RssItem( val commentsUrl: String?, val youtubeItemData: YoutubeItemData?, val rawEnclosure: RawEnclosure?, + val rawMediaContent: RawMediaContent? = null, ) { internal data class Builder( private var guid: String? = null, @@ -54,6 +55,7 @@ public data class RssItem( private var commentUrl: String? = null, private var youtubeItemData: YoutubeItemData? = null, private var rawEnclosure: RawEnclosure? = null, + private var rawMediaContent: RawMediaContent? = null, ) { private var linkPriority: Int = LINK_PRIORITY_NONE @@ -118,6 +120,8 @@ public data class RssItem( fun rawEnclosure(rawEnclosure: RawEnclosure?) = apply { this.rawEnclosure = rawEnclosure } + fun rawMediaContent(rawMediaContent: RawMediaContent?) = apply { this.rawMediaContent = rawMediaContent } + fun build(): RssItem? { if ( guid.isNullOrBlank() && @@ -136,7 +140,8 @@ public data class RssItem( itunesItemData == null && commentUrl.isNullOrBlank() && youtubeItemData == null && - rawEnclosure == null + rawEnclosure == null && + rawMediaContent == null ) { return null } @@ -159,6 +164,7 @@ public data class RssItem( commentsUrl = commentUrl, youtubeItemData = youtubeItemData, rawEnclosure = rawEnclosure, + rawMediaContent = rawMediaContent, ) } } diff --git a/rssparser/src/commonTest/kotlin/com/prof18/rssparser/atom/XmlParserAtomIdeaRicirTest.kt b/rssparser/src/commonTest/kotlin/com/prof18/rssparser/atom/XmlParserAtomIdeaRicirTest.kt index e9c3cc55..6a569e8e 100644 --- a/rssparser/src/commonTest/kotlin/com/prof18/rssparser/atom/XmlParserAtomIdeaRicirTest.kt +++ b/rssparser/src/commonTest/kotlin/com/prof18/rssparser/atom/XmlParserAtomIdeaRicirTest.kt @@ -1,6 +1,7 @@ package com.prof18.rssparser.atom import com.prof18.rssparser.XmlParserTestExecutor +import com.prof18.rssparser.model.RawMediaContent import com.prof18.rssparser.model.RssChannel import com.prof18.rssparser.model.RssItem import com.prof18.rssparser.parseFeed @@ -38,6 +39,11 @@ class XmlParserAtomIdeaRicirTest : XmlParserTestExecutor() { itunesItemData = null, youtubeItemData = null, rawEnclosure = null, + rawMediaContent = RawMediaContent( + url = "https://idea.ricir.net/assets/ComeSeguireAggiornamenti-social.png", + type = null, + medium = "image", + ), ) ), ) diff --git a/rssparser/src/commonTest/kotlin/com/prof18/rssparser/rss/XmlParserDcDate.kt b/rssparser/src/commonTest/kotlin/com/prof18/rssparser/rss/XmlParserDcDate.kt index d1a49a77..b51e3dff 100644 --- a/rssparser/src/commonTest/kotlin/com/prof18/rssparser/rss/XmlParserDcDate.kt +++ b/rssparser/src/commonTest/kotlin/com/prof18/rssparser/rss/XmlParserDcDate.kt @@ -1,6 +1,7 @@ package com.prof18.rssparser.rss import com.prof18.rssparser.XmlParserTestExecutor +import com.prof18.rssparser.model.RawMediaContent import com.prof18.rssparser.model.RssChannel import com.prof18.rssparser.model.RssImage import com.prof18.rssparser.model.RssItem @@ -93,6 +94,11 @@ Reporterre est un média indépendant qui publie chaque jour des articles, enqu itunesItemData = null, youtubeItemData = null, rawEnclosure = null, + rawMediaContent = RawMediaContent( + url = "https://reporterre.net/local/cache-vignettes/L700xH467/european_union_flag__4768764591_2_-cc8c4.jpg?1740588172", + type = "image/jpeg", + medium = "image", + ), ) ) ) diff --git a/rssparser/src/commonTest/kotlin/com/prof18/rssparser/rss/XmlParserMediaContentTypeTest.kt b/rssparser/src/commonTest/kotlin/com/prof18/rssparser/rss/XmlParserMediaContentTypeTest.kt new file mode 100644 index 00000000..81f3a07a --- /dev/null +++ b/rssparser/src/commonTest/kotlin/com/prof18/rssparser/rss/XmlParserMediaContentTypeTest.kt @@ -0,0 +1,182 @@ +package com.prof18.rssparser.rss + +import com.prof18.rssparser.XmlParserTestExecutor +import com.prof18.rssparser.model.RawMediaContent +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 XmlParserMediaContentTypeTest : XmlParserTestExecutor() { + + private val expectedChannel = RssChannel( + title = "Test Media Content Types", + link = "https://example.com", + description = "Feed testing media:content type filtering", + image = null, + lastBuildDate = null, + updatePeriod = null, + itunesChannelData = null, + youtubeChannelData = null, + items = listOf( + // application/java-archive: must NOT be assigned to image + RssItem( + guid = "https://example.com/files/large-file.zip/download", + title = "Non-image archive file", + author = null, + link = "https://example.com/files/large-file.zip/download", + pubDate = null, + description = "Item with application/java-archive media:content", + content = null, + image = null, + audio = null, + video = null, + sourceName = null, + sourceUrl = null, + categories = listOf(), + commentsUrl = null, + itunesItemData = null, + youtubeItemData = null, + rawEnclosure = null, + rawMediaContent = RawMediaContent( + url = "https://example.com/files/large-file.zip/download", + type = "application/java-archive; charset=binary", + medium = null, + ), + ), + // image/jpeg with medium=image: assigned to image + RssItem( + guid = "https://example.com/item2", + title = "Image with type and medium", + author = null, + link = "https://example.com/item2", + pubDate = null, + description = "Item with image/jpeg type and medium=image", + content = null, + image = "https://example.com/images/photo.jpg", + audio = null, + video = null, + sourceName = null, + sourceUrl = null, + categories = listOf(), + commentsUrl = null, + itunesItemData = null, + youtubeItemData = null, + rawEnclosure = null, + rawMediaContent = RawMediaContent( + url = "https://example.com/images/photo.jpg", + type = "image/jpeg", + medium = "image", + ), + ), + // video/mp4 with medium=video: assigned to video, NOT image + RssItem( + guid = "https://example.com/item3", + title = "Video with medium", + author = null, + link = "https://example.com/item3", + pubDate = null, + description = "Item with medium=video", + content = null, + image = null, + audio = null, + video = "https://example.com/videos/clip.mp4", + sourceName = null, + sourceUrl = null, + categories = listOf(), + commentsUrl = null, + itunesItemData = null, + youtubeItemData = null, + rawEnclosure = null, + rawMediaContent = RawMediaContent( + url = "https://example.com/videos/clip.mp4", + type = "video/mp4", + medium = "video", + ), + ), + // audio/mpeg with medium=audio: assigned to audio + RssItem( + guid = "https://example.com/item4", + title = "Audio with medium", + author = null, + link = "https://example.com/item4", + pubDate = null, + description = "Item with medium=audio", + content = null, + image = null, + audio = "https://example.com/audio/track.mp3", + video = null, + sourceName = null, + sourceUrl = null, + categories = listOf(), + commentsUrl = null, + itunesItemData = null, + youtubeItemData = null, + rawEnclosure = null, + rawMediaContent = RawMediaContent( + url = "https://example.com/audio/track.mp3", + type = "audio/mpeg", + medium = "audio", + ), + ), + // No type or medium: NOT assigned to image (safety) + RssItem( + guid = "https://example.com/item5", + title = "No type or medium", + author = null, + link = "https://example.com/item5", + pubDate = null, + description = "Item with media:content but no type or medium", + content = null, + image = null, + audio = null, + video = null, + sourceName = null, + sourceUrl = null, + categories = listOf(), + commentsUrl = null, + itunesItemData = null, + youtubeItemData = null, + rawEnclosure = null, + rawMediaContent = RawMediaContent( + url = "https://example.com/images/untyped.jpg", + type = null, + medium = null, + ), + ), + // medium=document: must NOT be assigned to image + RssItem( + guid = "https://example.com/item6", + title = "Document medium", + author = null, + link = "https://example.com/item6", + pubDate = null, + description = "Item with medium=document", + content = null, + image = null, + audio = null, + video = null, + sourceName = null, + sourceUrl = null, + categories = listOf(), + commentsUrl = null, + itunesItemData = null, + youtubeItemData = null, + rawEnclosure = null, + rawMediaContent = RawMediaContent( + url = "https://example.com/files/doc.pdf", + type = null, + medium = "document", + ), + ), + ), + ) + + @Test + fun channelIsParsedCorrectly() = runTest { + val channel = parseFeed("feed-media-content-type.xml") + assertEquals(expectedChannel, channel) + } +} diff --git a/rssparser/src/commonTest/kotlin/com/prof18/rssparser/rss/XmlParserStandardFeedTest.kt b/rssparser/src/commonTest/kotlin/com/prof18/rssparser/rss/XmlParserStandardFeedTest.kt index c53de57c..4fdc1fa3 100644 --- a/rssparser/src/commonTest/kotlin/com/prof18/rssparser/rss/XmlParserStandardFeedTest.kt +++ b/rssparser/src/commonTest/kotlin/com/prof18/rssparser/rss/XmlParserStandardFeedTest.kt @@ -18,6 +18,7 @@ package com.prof18.rssparser.rss import com.prof18.rssparser.XmlParserTestExecutor +import com.prof18.rssparser.model.RawMediaContent import com.prof18.rssparser.model.RssChannel import com.prof18.rssparser.model.RssItem import com.prof18.rssparser.parseFeed @@ -66,6 +67,11 @@ class XmlParserStandardFeedTest : XmlParserTestExecutor() { itunesItemData = null, youtubeItemData = null, rawEnclosure = null, + rawMediaContent = RawMediaContent( + url = "https://cdn57.androidauthority.net/wp-content/uploads/2019/02/Whats-next-with-5g--500x260.jpg", + type = null, + medium = "image", + ), ) ) ) diff --git a/rssparser/src/commonTest/resources/feed-media-content-type.xml b/rssparser/src/commonTest/resources/feed-media-content-type.xml new file mode 100644 index 00000000..dfc6f2ab --- /dev/null +++ b/rssparser/src/commonTest/resources/feed-media-content-type.xml @@ -0,0 +1,63 @@ + + + + Test Media Content Types + https://example.com + Feed testing media:content type filtering + + Non-image archive file + https://example.com/files/large-file.zip/download + https://example.com/files/large-file.zip/download + Item with application/java-archive media:content + + 298613a475d5b4aabd8f8590995a4de1 + + + + Image with type and medium + https://example.com/item2 + https://example.com/item2 + Item with image/jpeg type and medium=image + + + + Video with medium + https://example.com/item3 + https://example.com/item3 + Item with medium=video + + + + Audio with medium + https://example.com/item4 + https://example.com/item4 + Item with medium=audio + + + + No type or medium + https://example.com/item5 + https://example.com/item5 + Item with media:content but no type or medium + + + + Document medium + https://example.com/item6 + https://example.com/item6 + Item with medium=document + + + + 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 3b9c5856..dfc1b55a 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 @@ -3,6 +3,7 @@ package com.prof18.rssparser.internal.atom import com.prof18.rssparser.internal.AtomKeyword import com.prof18.rssparser.internal.ChannelFactory import com.prof18.rssparser.internal.FeedHandler +import com.prof18.rssparser.internal.RssKeyword import com.prof18.rssparser.model.RssChannel import org.xml.sax.Attributes @@ -59,10 +60,33 @@ internal class AtomFeedHandler( } } - AtomKeyword.YOUTUBE_MEDIA_GROUP_CONTENT.value -> { - if (isInsideItem && isInsideYoutubeMediaGroup) { - val url = attributes?.getValue(AtomKeyword.YOUTUBE_MEDIA_GROUP_CONTENT_URL.value) - channelFactory.youtubeItemDataBuilder.videoUrl(url) + AtomKeyword.MEDIA_GROUP_CONTENT.value -> { + if (isInsideItem) { + if (isInsideYoutubeMediaGroup) { + val url = attributes?.getValue(AtomKeyword.YOUTUBE_MEDIA_GROUP_CONTENT_URL.value) + channelFactory.youtubeItemDataBuilder.videoUrl(url) + } else { + val url = attributes?.getValue(RssKeyword.URL.value) + val type = attributes?.getValue(RssKeyword.ITEM_TYPE.value) + val medium = attributes?.getValue(RssKeyword.ITEM_MEDIUM.value) + + channelFactory.rawMediaContentBuilder.url(url) + channelFactory.rawMediaContentBuilder.type(type) + channelFactory.rawMediaContentBuilder.medium(medium) + + when { + !medium.isNullOrBlank() -> when { + medium.equals("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + medium.equals("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + medium.equals("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + !type.isNullOrBlank() -> when { + type.contains("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + type.contains("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + type.contains("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + } + } } } diff --git a/rssparser/src/jvmMain/kotlin/com/prof18/rssparser/internal/rss/RssFeedHandler.kt b/rssparser/src/jvmMain/kotlin/com/prof18/rssparser/internal/rss/RssFeedHandler.kt index 0cd11a4c..71667506 100644 --- a/rssparser/src/jvmMain/kotlin/com/prof18/rssparser/internal/rss/RssFeedHandler.kt +++ b/rssparser/src/jvmMain/kotlin/com/prof18/rssparser/internal/rss/RssFeedHandler.kt @@ -36,7 +36,25 @@ internal class RssFeedHandler : FeedHandler { RssKeyword.ITEM_MEDIA_CONTENT.value -> { if (isInsideItem) { val url = attributes?.getValue(RssKeyword.URL.value) - channelFactory.articleBuilder.image(url) + val type = attributes?.getValue(RssKeyword.ITEM_TYPE.value) + val medium = attributes?.getValue(RssKeyword.ITEM_MEDIUM.value) + + channelFactory.rawMediaContentBuilder.url(url) + channelFactory.rawMediaContentBuilder.type(type) + channelFactory.rawMediaContentBuilder.medium(medium) + + when { + !medium.isNullOrBlank() -> when { + medium.equals("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + medium.equals("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + medium.equals("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + !type.isNullOrBlank() -> when { + type.contains("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + type.contains("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + type.contains("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + } } } @@ -58,17 +76,17 @@ internal class RssFeedHandler : FeedHandler { channelFactory.rawEnclosureBuilder.url(url) when { - type != null && type.contains("image") -> { + type != null && type.contains("image", ignoreCase = true) -> { // If there are multiple elements, we take only the first channelFactory.articleBuilder.image(url) } - type != null && type.contains("audio") -> { + type != null && type.contains("audio", ignoreCase = true) -> { // If there are multiple elements, we take only the first channelFactory.articleBuilder.audioIfNull(url) } - type != null && type.contains("video") -> { + type != null && type.contains("video", ignoreCase = true) -> { // If there are multiple elements, we take only the first channelFactory.articleBuilder.videoIfNull(url) } diff --git a/rssparser/src/webMain/kotlin/com/prof18/rssparser/internal/entity/AtomFeedEntity.kt b/rssparser/src/webMain/kotlin/com/prof18/rssparser/internal/entity/AtomFeedEntity.kt index 3ed25207..10198e61 100644 --- a/rssparser/src/webMain/kotlin/com/prof18/rssparser/internal/entity/AtomFeedEntity.kt +++ b/rssparser/src/webMain/kotlin/com/prof18/rssparser/internal/entity/AtomFeedEntity.kt @@ -105,6 +105,8 @@ internal data class MediaGroupEntity( @Serializable internal data class AtomMediaContentEntity( val url: String? = null, + val type: String? = null, + val medium: String? = null, ) @XmlSerialName(value = "thumbnail", prefix = "media", namespace = "http://search.yahoo.com/mrss/") 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 dba045cf..531e58a5 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 @@ -3,9 +3,11 @@ package com.prof18.rssparser.internal.mapper import com.prof18.rssparser.internal.AtomKeyword import com.prof18.rssparser.internal.ChannelFactory import com.prof18.rssparser.internal.entity.AtomFeedEntity +import com.prof18.rssparser.internal.entity.AtomMediaContentEntity import com.prof18.rssparser.internal.entity.AtomLinkEntity import com.prof18.rssparser.internal.entity.FeedEntity import com.prof18.rssparser.internal.entity.RdfFeedEntity +import com.prof18.rssparser.internal.entity.RssMediaContentEntity import com.prof18.rssparser.internal.entity.RssFeedEntity import com.prof18.rssparser.model.RssChannel @@ -65,8 +67,8 @@ private fun RssFeedEntity.toRssChannel(): RssChannel { link(entry.link?.trim()) description(entry.description?.trim()) commentUrl(entry.comments?.trim()) - image(entry.mediaContent?.url?.trim()) - image(entry.mediaContent2?.url?.trim()) + generateMediaContent(entry.mediaContent, channelFactory) + generateMediaContent(entry.mediaContent2, channelFactory) image(entry.newsImage?.link?.trim()) image(entry.thumb?.trim()) image(entry.image?.link?.trim()) @@ -93,17 +95,17 @@ private fun RssFeedEntity.toRssChannel(): RssChannel { channelFactory.rawEnclosureBuilder.url(url) when { - type != null && type.contains("image") -> { + type != null && type.contains("image", ignoreCase = true) -> { // If there are multiple elements, we take only the first channelFactory.articleBuilder.image(url) } - type != null && type.contains("audio") -> { + type != null && type.contains("audio", ignoreCase = true) -> { // If there are multiple elements, we take only the first channelFactory.articleBuilder.audioIfNull(url) } - type != null && type.contains("video") -> { + type != null && type.contains("video", ignoreCase = true) -> { // If there are multiple elements, we take only the first channelFactory.articleBuilder.videoIfNull(url) } @@ -202,7 +204,7 @@ private fun AtomFeedEntity.toRssChannel(baseFeedUrl: String?): RssChannel { channelFactory.setImageFromContent(entry.content) channelFactory.setImageFromContent(entry.summary) image(entry.mediaThumbnail?.url) - image(entry.mediaContent?.url) + generateAtomMediaContent(entry.mediaContent, channelFactory) channelFactory.youtubeChannelDataBuilder.channelId(entry.youtubeChannelId) with(channelFactory.youtubeItemDataBuilder) { videoId(entry.youtubeVideoId) @@ -219,6 +221,54 @@ private fun AtomFeedEntity.toRssChannel(baseFeedUrl: String?): RssChannel { return channelFactory.build() } +private fun generateMediaContent(mediaContent: RssMediaContentEntity?, channelFactory: ChannelFactory) { + if (mediaContent == null) return + val url = mediaContent.url?.trim() + val type = mediaContent.type?.trim() + val medium = mediaContent.medium?.trim() + + channelFactory.rawMediaContentBuilder.url(url) + channelFactory.rawMediaContentBuilder.type(type) + channelFactory.rawMediaContentBuilder.medium(medium) + + when { + !medium.isNullOrBlank() -> when { + medium.equals("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + medium.equals("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + medium.equals("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + !type.isNullOrBlank() -> when { + type.contains("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + type.contains("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + type.contains("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + } +} + +private fun generateAtomMediaContent(mediaContent: AtomMediaContentEntity?, channelFactory: ChannelFactory) { + if (mediaContent == null) return + val url = mediaContent.url + val type = mediaContent.type?.trim() + val medium = mediaContent.medium?.trim() + + channelFactory.rawMediaContentBuilder.url(url) + channelFactory.rawMediaContentBuilder.type(type) + channelFactory.rawMediaContentBuilder.medium(medium) + + when { + !medium.isNullOrBlank() -> when { + medium.equals("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + medium.equals("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + medium.equals("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + !type.isNullOrBlank() -> when { + type.contains("image", ignoreCase = true) -> channelFactory.articleBuilder.image(url) + type.contains("audio", ignoreCase = true) -> channelFactory.articleBuilder.audioIfNull(url) + type.contains("video", ignoreCase = true) -> channelFactory.articleBuilder.videoIfNull(url) + } + } +} + private fun AtomLinkEntity.generateLink(baseFeedUrl: String?): String? { val rel = this.rel val href = this.href