From e136b96b59fbeccf60cd976c65373951f08ae94a Mon Sep 17 00:00:00 2001 From: Yang Date: Mon, 25 Dec 2023 03:04:43 +1100 Subject: [PATCH] Change Talking Kotlin RSS feed to soundcloud. Update schema to include more fields for `TalkingKotlin`. --- .../kstreamlined/backend/client/FeedClient.kt | 20 +- .../backend/client/dto/CommonDTOs.kt | 1 + .../backend/client/dto/TalkingKotlinDTOs.kt | 34 +- .../mapper/TalkingKotlinEntryMapper.kt | 24 +- src/main/resources/application.properties | 2 +- .../resources/schema/kstreamlined.graphqls | 10 +- .../backend/client/FakeFeedClient.kt | 18 +- .../backend/client/RealFeedClientTest.kt | 39 +- .../datafetcher/FeedEntryDataFetcherTest.kt | 11 +- .../mapper/TalkingKotlinEntryMapperTest.kt | 29 +- .../talking_kotlin_rss_response_full.xml | 523 ++++++++++++------ .../talking_kotlin_rss_response_sample.xml | 100 ++-- 12 files changed, 499 insertions(+), 312 deletions(-) diff --git a/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/client/FeedClient.kt b/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/client/FeedClient.kt index a70bc0d..0011e19 100644 --- a/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/client/FeedClient.kt +++ b/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/client/FeedClient.kt @@ -72,12 +72,11 @@ class RealFeedClient( context(CacheContext) override suspend fun loadKotlinBlogFeed(): List = getFromCacheOrFetch { - httpClient.get(clientConfigs.kotlinBlogFeedUrl) - .body().channel.items.map { - it.copy( - description = StringEscapeUtils.unescapeXml(it.description).trim() - ) - } + httpClient.get(clientConfigs.kotlinBlogFeedUrl).body().channel.items.map { + it.copy( + description = StringEscapeUtils.unescapeXml(it.description).trim() + ) + } } context(CacheContext) @@ -89,7 +88,13 @@ class RealFeedClient( context(CacheContext) override suspend fun loadTalkingKotlinFeed(): List = getFromCacheOrFetch { - httpClient.get(clientConfigs.talkingKotlinFeedUrl).body().entries + httpClient.get(clientConfigs.talkingKotlinFeedUrl).body().channel.items + .take(TalkingKotlinFeedSize) + .map { + it.copy( + summary = StringEscapeUtils.unescapeXml(it.summary).trim() + ) + } } context(CacheContext) @@ -99,6 +104,7 @@ class RealFeedClient( companion object { private const val HttpTimeoutMillis = 30_000L + private const val TalkingKotlinFeedSize = 10 } } diff --git a/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/client/dto/CommonDTOs.kt b/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/client/dto/CommonDTOs.kt index 018df56..441bc6d 100644 --- a/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/client/dto/CommonDTOs.kt +++ b/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/client/dto/CommonDTOs.kt @@ -8,6 +8,7 @@ object Namespace { const val media = "http://search.yahoo.com/mrss/" const val content = "http://purl.org/rss/1.0/modules/content/" const val yt = "http://www.youtube.com/xml/schemas/2015" + const val itunes = "http://www.itunes.com/dtds/podcast-1.0.dtd" } @Serializable diff --git a/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/client/dto/TalkingKotlinDTOs.kt b/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/client/dto/TalkingKotlinDTOs.kt index 2bbbe80..77a48f8 100644 --- a/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/client/dto/TalkingKotlinDTOs.kt +++ b/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/client/dto/TalkingKotlinDTOs.kt @@ -4,29 +4,41 @@ import kotlinx.serialization.Serializable import nl.adaptivity.xmlutil.serialization.XmlElement import nl.adaptivity.xmlutil.serialization.XmlSerialName -@XmlSerialName("feed", Namespace.atom, "") +@XmlSerialName("rss", "", "") @Serializable data class TalkingKotlinRss( - val entries: List, + val channel: TalkingKotlinChannel, ) -@XmlSerialName("entry", Namespace.atom, "") +@XmlSerialName("channel", "", "") +@Serializable +data class TalkingKotlinChannel( + val items: List, +) + +@XmlSerialName("item", "", "") @Serializable data class TalkingKotlinItem( @XmlElement(true) - val id: String, + val guid: String, @XmlElement(true) val title: String, - @XmlSerialName("link", Namespace.atom, "") - val link: Link, @XmlElement(true) - val published: String, - val categories: List, + val pubDate: String, + @XmlElement(true) + val link: String, + @XmlElement(true) + @XmlSerialName(value = "duration", namespace = Namespace.itunes, prefix = "") + val duration: String, + @XmlElement(true) + @XmlSerialName(value = "summary", namespace = Namespace.itunes, prefix = "") + val summary: String, + val image: Image, ) { - @XmlSerialName("category", Namespace.atom, "") + @XmlSerialName("image", Namespace.itunes, "") @Serializable - data class Category( + data class Image( @XmlElement(false) - val term: String + val href: String, ) } diff --git a/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/datafetcher/mapper/TalkingKotlinEntryMapper.kt b/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/datafetcher/mapper/TalkingKotlinEntryMapper.kt index f2ebb22..419bbb3 100644 --- a/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/datafetcher/mapper/TalkingKotlinEntryMapper.kt +++ b/src/main/kotlin/io/github/reactivecircus/kstreamlined/backend/datafetcher/mapper/TalkingKotlinEntryMapper.kt @@ -2,20 +2,32 @@ package io.github.reactivecircus.kstreamlined.backend.datafetcher.mapper import io.github.reactivecircus.kstreamlined.backend.client.dto.TalkingKotlinItem import io.github.reactivecircus.kstreamlined.backend.schema.generated.types.TalkingKotlin +import java.time.Duration import java.time.ZonedDateTime import java.time.format.DateTimeFormatter fun TalkingKotlinItem.toTalkingKotlinEntry(): TalkingKotlin { return TalkingKotlin( - id = id, + id = guid, title = title, publishTime = ZonedDateTime - .parse(published, DateTimeFormatter.ISO_OFFSET_DATE_TIME) + .parse(pubDate, DateTimeFormatter.RFC_1123_DATE_TIME) .toInstant(), - contentUrl = link.href, - podcastLogoUrl = TalkingKotlinLogoUrl, - tags = categories.map { it.term }, + contentUrl = link, + thumbnailUrl = image.href, + summary = summary, + duration = duration.toFormattedDuration(), ) } -const val TalkingKotlinLogoUrl = "https://talkingkotlin.com/images/kotlin_talking_logo.png" +private fun String.toFormattedDuration(): String { + val parts = split(":").map { it.toInt() } + val duration = Duration.ofHours(parts[0].toLong()) + .plusMinutes(parts[1].toLong()) + .plusSeconds(parts[2].toLong()) + + val hours = duration.toHoursPart() + val minutes = duration.toMinutesPart() + + return if (hours > 0) "${hours}h ${minutes}min." else "${minutes}min." +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b17bbdb..f3e085b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,5 +3,5 @@ server.compression.enabled=true server.compression.mime-types=application/json ks.kotlin-blog-feed-url=https://blog.jetbrains.com/kotlin/feed/ ks.kotlin-youtube-feed-url=https://www.youtube.com/feeds/videos.xml?channel_id=UCP7uiEZIqci43m22KDl0sNw -ks.talking-kotlin-feed-url=https://talkingkotlin.com/feed +ks.talking-kotlin-feed-url=https://feeds.soundcloud.com/users/soundcloud:users:280353173/sounds.rss ks.kotlin-weekly-feed-url=https://us12.campaign-archive.com/feed?u=f39692e245b94f7fb693b6d82&id=93b2272cb6 diff --git a/src/main/resources/schema/kstreamlined.graphqls b/src/main/resources/schema/kstreamlined.graphqls index e42d7ee..915dbfc 100644 --- a/src/main/resources/schema/kstreamlined.graphqls +++ b/src/main/resources/schema/kstreamlined.graphqls @@ -75,10 +75,12 @@ type TalkingKotlin implements FeedEntry { publishTime: Instant! "Url of the content." contentUrl: String! - "Url of the podcast logo." - podcastLogoUrl: String! - "Tags of the episode." - tags: [String!]! + "Url of the podcast thumbnail." + thumbnailUrl: String! + "Summary of the podcast." + summary: String! + "Duration of the podcast." + duration: String! } type KotlinWeekly implements FeedEntry { diff --git a/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/client/FakeFeedClient.kt b/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/client/FakeFeedClient.kt index 33d4f6e..3fd5ecc 100644 --- a/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/client/FakeFeedClient.kt +++ b/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/client/FakeFeedClient.kt @@ -117,19 +117,13 @@ val DummyKotlinYouTubeItem = KotlinYouTubeItem( ) val DummyTalkingKotlinItem = TalkingKotlinItem( - id = "https://talkingkotlin.com/turbocharging-kotlin-arrow-analysis-optics-meta", + guid = "tag:soundcloud,2010:tracks/1295949565", title = "Turbocharging Kotlin: Arrow Analysis, Optics & Meta", - link = Link( - href = "https://talkingkotlin.com/turbocharging-kotlin-arrow-analysis-optics-meta/", - rel = "alternate", - type = "text/html", - title = "Turbocharging Kotlin: Arrow Analysis, Optics & Meta", - ), - published = "2022-06-28T00:00:00+02:00", - categories = listOf( - TalkingKotlinItem.Category("Arrow"), - TalkingKotlinItem.Category("Code Quality"), - ), + link = "https://soundcloud.com/user-38099918/arrow-analysis", + pubDate = "Tue, 28 Jun 2022 16:00:27 +0000", + summary = "We chat with Raul, Simon, and Alejandro to learn how Arrow adds functional paradigms and safety to Kotlin, and how it aims to influence the future of the language.", + duration = "00:57:44", + image = TalkingKotlinItem.Image(href = "https://i1.sndcdn.com/artworks-yEP8SdbEZOJcmVay-AWlLHQ-t3000x3000.jpg"), ) val DummyKotlinWeeklyItem = KotlinWeeklyItem( diff --git a/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/client/RealFeedClientTest.kt b/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/client/RealFeedClientTest.kt index 2fffd88..cab4f1a 100644 --- a/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/client/RealFeedClientTest.kt +++ b/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/client/RealFeedClientTest.kt @@ -200,42 +200,29 @@ class RealFeedClientTest { val mockEngine = MockEngine { respond( content = ByteReadChannel(mockTalkingKotlinRssResponse), - headers = headersOf(HttpHeaders.ContentType, "application/xml") + headers = headersOf(HttpHeaders.ContentType, "application/rss+xml") ) } val feedClient = RealFeedClient(mockEngine, TestClientConfigs) val expected = listOf( TalkingKotlinItem( - id = "https://talkingkotlin.com/turbocharging-kotlin-arrow-analysis-optics-meta", + guid = "tag:soundcloud,2010:tracks/1295949565", title = "Turbocharging Kotlin: Arrow Analysis, Optics & Meta", - link = Link( - href = "https://talkingkotlin.com/turbocharging-kotlin-arrow-analysis-optics-meta/", - rel = "alternate", - type = "text/html", - title = "Turbocharging Kotlin: Arrow Analysis, Optics & Meta", - ), - published = "2022-06-28T00:00:00+02:00", - categories = listOf( - TalkingKotlinItem.Category("Arrow"), - TalkingKotlinItem.Category("Code Quality"), - ), + link = "https://soundcloud.com/user-38099918/arrow-analysis", + pubDate = "Tue, 28 Jun 2022 16:00:27 +0000", + summary = "We chat with Raul, Simon, and Alejandro to learn how Arrow adds functional paradigms and safety to Kotlin, and how it aims to influence the future of the language.", + duration = "00:57:44", + image = TalkingKotlinItem.Image(href = "https://i1.sndcdn.com/artworks-yEP8SdbEZOJcmVay-AWlLHQ-t3000x3000.jpg"), ), TalkingKotlinItem( - id = "https://talkingkotlin.com/70-billion-events-per-day-adobe-and-kotlin", + guid = "tag:soundcloud,2010:tracks/1253069788", title = "70 Billion Events per Day – Adobe & Kotlin", - link = Link( - href = "https://talkingkotlin.com/70-billion-events-per-day-adobe-and-kotlin/", - rel = "alternate", - type = "text/html", - title = "70 Billion Events per Day – Adobe & Kotlin", - ), - published = "2022-04-19T00:00:00+02:00", - categories = listOf( - TalkingKotlinItem.Category("Kotlin Multiplatform"), - TalkingKotlinItem.Category("Ktor"), - TalkingKotlinItem.Category("Adobe"), - ), + link = "https://soundcloud.com/user-38099918/70-billion-events-per-day-adobe-kotlin", + pubDate = "Tue, 19 Apr 2022 16:00:24 +0000", + summary = "We talked to Rares Vlasceanu and Catalin Costache from Adobe about how they handle 70 000 000 000 events per day with the help of Kotlin and Ktor.", + duration = "00:51:09", + image = TalkingKotlinItem.Image(href = "https://i1.sndcdn.com/artworks-ANd7JtyjHYkhAUsQ-XU8I0Q-t3000x3000.jpg"), ), ) diff --git a/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/datafetcher/FeedEntryDataFetcherTest.kt b/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/datafetcher/FeedEntryDataFetcherTest.kt index 1860d73..1af9e7f 100644 --- a/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/datafetcher/FeedEntryDataFetcherTest.kt +++ b/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/datafetcher/FeedEntryDataFetcherTest.kt @@ -49,8 +49,9 @@ class FeedEntryDataFetcherTest { description } ... on TalkingKotlin { - podcastLogoUrl - tags + thumbnailUrl + summary + duration } ... on KotlinWeekly { __typename @@ -105,9 +106,9 @@ class FeedEntryDataFetcherTest { assertEquals(dummyTalkingKotlinEntry.title, context.read("data.feedEntries[3].title")) assertEquals(dummyTalkingKotlinEntry.publishTime, context.read("data.feedEntries[3].publishTime").toInstant()) assertEquals(dummyTalkingKotlinEntry.contentUrl, context.read("data.feedEntries[3].contentUrl")) - assertEquals(dummyTalkingKotlinEntry.podcastLogoUrl, context.read("data.feedEntries[3].podcastLogoUrl")) - assertEquals(dummyTalkingKotlinEntry.tags[0], context.read("data.feedEntries[3].tags[0]")) - assertEquals(dummyTalkingKotlinEntry.tags[1], context.read("data.feedEntries[3].tags[1]")) + assertEquals(dummyTalkingKotlinEntry.thumbnailUrl, context.read("data.feedEntries[3].thumbnailUrl")) + assertEquals(dummyTalkingKotlinEntry.summary, context.read("data.feedEntries[3].summary")) + assertEquals(dummyTalkingKotlinEntry.duration, context.read("data.feedEntries[3].duration")) } @Test diff --git a/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/datafetcher/mapper/TalkingKotlinEntryMapperTest.kt b/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/datafetcher/mapper/TalkingKotlinEntryMapperTest.kt index c7fdff5..0919322 100644 --- a/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/datafetcher/mapper/TalkingKotlinEntryMapperTest.kt +++ b/src/test/kotlin/io/github/reactivecircus/kstreamlined/backend/datafetcher/mapper/TalkingKotlinEntryMapperTest.kt @@ -1,6 +1,5 @@ package io.github.reactivecircus.kstreamlined.backend.datafetcher.mapper -import io.github.reactivecircus.kstreamlined.backend.client.dto.Link import io.github.reactivecircus.kstreamlined.backend.client.dto.TalkingKotlinItem import io.github.reactivecircus.kstreamlined.backend.schema.generated.types.TalkingKotlin import org.junit.jupiter.api.Test @@ -14,28 +13,20 @@ class TalkingKotlinEntryMapperTest { val expected = TalkingKotlin( id = "id", title = "Podcast title", - publishTime = Instant.parse("2022-06-27T22:00:00Z"), + publishTime = Instant.parse("2022-11-22T16:30:09Z"), contentUrl = "url", - podcastLogoUrl = TalkingKotlinLogoUrl, - tags = listOf( - "tag1", - "tag2", - ), + thumbnailUrl = "image-url", + summary = "summary", + duration = "43min.", ) val actual = TalkingKotlinItem( - id = "id", + guid = "id", title = "Podcast title", - link = Link( - href = "url", - rel = "alternate", - type = "text/html", - title = "Podcast title", - ), - published = "2022-06-28T00:00:00+02:00", - categories = listOf( - TalkingKotlinItem.Category("tag1"), - TalkingKotlinItem.Category("tag2"), - ), + pubDate = "Tue, 22 Nov 2022 16:30:09 +0000", + link = "url", + duration = "00:43:14", + summary = "summary", + image = TalkingKotlinItem.Image(href = "image-url"), ).toTalkingKotlinEntry() assertEquals(expected, actual) diff --git a/src/test/resources/talking_kotlin_rss_response_full.xml b/src/test/resources/talking_kotlin_rss_response_full.xml index 33229cb..a5cffe2 100644 --- a/src/test/resources/talking_kotlin_rss_response_full.xml +++ b/src/test/resources/talking_kotlin_rss_response_full.xml @@ -1,179 +1,344 @@ - - Jekyll - - - 2022-06-28T17:16:47+02:00 - https://talkingkotlin.com/feed.xml - Talking Kotlin - A Podcast on Kotlin and more - - Turbocharging Kotlin: Arrow Analysis, Optics & Meta - - 2022-06-28T00:00:00+02:00 - 2022-06-28T00:00:00+02:00 - https://talkingkotlin.com/turbocharging-kotlin-arrow-analysis-optics-meta - - - - - - - - - - - - 70 Billion Events per Day – Adobe & Kotlin - - 2022-04-19T00:00:00+02:00 - 2022-04-19T00:00:00+02:00 - https://talkingkotlin.com/70-billion-events-per-day-adobe-and-kotlin - - - - - - - - - - - - - Why iOS Developers at Todoist Wanted Kotlin Multiplatform - - 2022-02-17T00:00:00+01:00 - 2022-02-17T00:00:00+01:00 - https://talkingkotlin.com/why-ios-developers-at-todoist-wanted-kotlin-multiplatform - - - - - - - - - - - - - - The First Kotlin Commit in Android - - 2022-02-01T00:00:00+01:00 - 2022-02-01T00:00:00+01:00 - https://talkingkotlin.com/the-first-kotlin-commit-in-android - - - - - - - - - - - - 5 Years of Talking Kotlin Special - - 2022-01-10T00:00:00+01:00 - 2022-01-10T00:00:00+01:00 - https://talkingkotlin.com/five-years-of-talking-kotlin-special - - - - - - - - - - - - - Slacking with Zac Sweers - - 2021-12-27T00:00:00+01:00 - 2021-12-27T00:00:00+01:00 - https://talkingkotlin.com/slacking-with-zac-sweers - - - - - - - - - - - - - Building a static analyzer for Kotlin - - 2021-12-13T00:00:00+01:00 - 2021-12-13T00:00:00+01:00 - https://talkingkotlin.com/building-a-static-analyzer-for-kotlin - - - - - - - - - - - - - - Moving 1M users to Kotlin and Compose - - 2021-11-28T00:00:00+01:00 - 2021-11-28T00:00:00+01:00 - https://talkingkotlin.com/moving-one-million-users-to-Kotlin-and-Compose - - - - - - - - - - - - - What goes into a Kotlin Release - - 2021-11-11T00:00:00+01:00 - 2021-11-11T00:00:00+01:00 - https://talkingkotlin.com/what-goes-into-a-kotlin-release - - - - - - - - - - - - - From Java to Kotlin - - 2021-10-18T00:00:00+02:00 - 2021-10-18T00:00:00+02:00 - https://talkingkotlin.com/from-java-to-kotlin - - - - - - - - - - - - + + + + + diff --git a/src/test/resources/talking_kotlin_rss_response_sample.xml b/src/test/resources/talking_kotlin_rss_response_sample.xml index c17101e..198e03f 100644 --- a/src/test/resources/talking_kotlin_rss_response_sample.xml +++ b/src/test/resources/talking_kotlin_rss_response_sample.xml @@ -1,42 +1,58 @@ - - Jekyll - - - 2022-06-28T17:16:47+02:00 - https://talkingkotlin.com/feed.xml - Talking Kotlin - A Podcast on Kotlin and more - - Turbocharging Kotlin: Arrow Analysis, Optics & Meta - - 2022-06-28T00:00:00+02:00 - 2022-06-28T00:00:00+02:00 - https://talkingkotlin.com/turbocharging-kotlin-arrow-analysis-optics-meta - - - - - - - - - - - - 70 Billion Events per Day – Adobe & Kotlin - - 2022-04-19T00:00:00+02:00 - 2022-04-19T00:00:00+02:00 - https://talkingkotlin.com/70-billion-events-per-day-adobe-and-kotlin - - - - - - - - - - - - + + + + + +