From 7ebf817fdbf208c898827b561e7963971f3d79f3 Mon Sep 17 00:00:00 2001 From: AE <98699388+A-EDev@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:44:19 +0300 Subject: [PATCH 1/5] fix(subscriptions): read whether an RSS entry is a Short from its link (#1393) --- .../flow/data/subscriptions/ChannelRssFeed.kt | 2 ++ .../data/subscriptions/ChannelRssParser.kt | 16 ++++++++++ .../subscriptions/ChannelRssParserTest.kt | 32 ++++++++++++++++++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/aedev/flow/data/subscriptions/ChannelRssFeed.kt b/app/src/main/java/io/github/aedev/flow/data/subscriptions/ChannelRssFeed.kt index 93c2c54332..22f26beabf 100644 --- a/app/src/main/java/io/github/aedev/flow/data/subscriptions/ChannelRssFeed.kt +++ b/app/src/main/java/io/github/aedev/flow/data/subscriptions/ChannelRssFeed.kt @@ -8,6 +8,8 @@ data class ChannelRssEntry( val publishedAtMillis: Long = 0L, val viewCount: Long = 0L, val description: String? = null, + /** From the entry's link: YouTube links a reel as `/shorts/`. Null when the link says neither. */ + val isShort: Boolean? = null, ) /** diff --git a/app/src/main/java/io/github/aedev/flow/data/subscriptions/ChannelRssParser.kt b/app/src/main/java/io/github/aedev/flow/data/subscriptions/ChannelRssParser.kt index 2e49f60595..531ef5cc73 100644 --- a/app/src/main/java/io/github/aedev/flow/data/subscriptions/ChannelRssParser.kt +++ b/app/src/main/java/io/github/aedev/flow/data/subscriptions/ChannelRssParser.kt @@ -52,6 +52,7 @@ object ChannelRssParser { var description: String? = null var publishedAt = 0L var viewCount = 0L + var isShort: Boolean? = null var eventType = parser.eventType while (eventType != XmlPullParser.END_DOCUMENT) { @@ -67,6 +68,7 @@ object ChannelRssParser { description = null publishedAt = 0L viewCount = 0L + isShort = null } else if (insideEntry) { when { tagName.equals("videoId", ignoreCase = true) -> { @@ -95,6 +97,10 @@ object ChannelRssParser { tagName.equals("statistics", ignoreCase = true) && viewCount == 0L -> { viewCount = parser.getAttributeValue(null, "views")?.toLongOrNull() ?: 0L } + + tagName.equals("link", ignoreCase = true) && isShort == null -> { + isShort = reelVerdict(parser.getAttributeValue(null, "href")) + } } } else if (tagName.equals("name", ignoreCase = true) && channelName == null) { // Only the feed-level carries a in a channel feed. @@ -116,6 +122,7 @@ object ChannelRssParser { publishedAtMillis = publishedAt, viewCount = viewCount, description = description, + isShort = isShort, ) } } @@ -129,6 +136,15 @@ object ChannelRssParser { return ChannelRssFeed(channelName = channelName?.takeIf { it.isNotBlank() }, entries = entries) } + private fun reelVerdict(href: String?): Boolean? { + val path = href?.substringAfter("://", "")?.substringAfter('/', "") ?: return null + return when { + path.startsWith("shorts/") -> true + path.startsWith("watch") -> false + else -> null + } + } + private fun parseTimestamp(value: String?): Long { val text = value?.trim().orEmpty() if (text.isEmpty()) return 0L diff --git a/app/src/test/java/io/github/aedev/flow/data/subscriptions/ChannelRssParserTest.kt b/app/src/test/java/io/github/aedev/flow/data/subscriptions/ChannelRssParserTest.kt index 031a2aee2a..bd02c00c2b 100644 --- a/app/src/test/java/io/github/aedev/flow/data/subscriptions/ChannelRssParserTest.kt +++ b/app/src/test/java/io/github/aedev/flow/data/subscriptions/ChannelRssParserTest.kt @@ -22,7 +22,10 @@ class ChannelRssParserTest { private fun entriesOf(xml: String): List = parse(xml).entries /** A channel feed shaped like YouTube's, entries newest first. */ - private fun feed(vararg ids: String): String = + private fun feed( + vararg ids: String, + links: Map = emptyMap(), + ): String = buildString { // No leading whitespace: an XML declaration must start at offset 0. append("") @@ -32,11 +35,13 @@ class ChannelRssParserTest { "xmlns=\"http://www.w3.org/2005/Atom\">", ) append("Channel Name") + append("") append("Uploader Namehttps://www.youtube.com/channel/x") ids.forEach { id -> append("") append("$id") append("Title $id") + links[id]?.let { append("") } append("2026-05-01T12:00:00+00:00") append("") append("Title $id") @@ -185,4 +190,29 @@ class ChannelRssParserTest { fun `empty feed announces nothing`() { assertEquals(emptyList(), ChannelRssParser.newEntriesSince(emptyList(), lastVideoId = "aaa")) } + + @Test + fun `entry link tells a reel from a video`() { + val entries = + entriesOf( + feed( + "reel", + "video", + links = + mapOf( + "reel" to "https://www.youtube.com/shorts/reel", + "video" to "https://www.youtube.com/watch?v=video", + ), + ), + ) + + assertEquals(listOf(true, false), entries.map { it.isShort }) + } + + @Test + fun `entry without a recognised link has no reel verdict`() { + val entries = entriesOf(feed("bare", "other", links = mapOf("other" to "https://www.youtube.com/live/other"))) + + assertEquals(listOf(null, null), entries.map { it.isShort }) + } } From 9389c292f8f98f123441b0a7e6aed3161ace375d Mon Sep 17 00:00:00 2001 From: AE <98699388+A-EDev@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:44:19 +0300 Subject: [PATCH 2/5] fix(subscriptions): classify RSS rows from the link before asking the Shorts tab (#1393) --- .../data/innertube/RssSubscriptionService.kt | 28 +++++---- .../subscriptions/SubscriptionReelVerdicts.kt | 18 ++++++ .../innertube/RssSubscriptionServiceTest.kt | 24 ++++++++ .../SubscriptionReelVerdictsTest.kt | 61 +++++++++++++++++++ 4 files changed, 118 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/io/github/aedev/flow/data/innertube/RssSubscriptionService.kt b/app/src/main/java/io/github/aedev/flow/data/innertube/RssSubscriptionService.kt index 3289fac57b..788a92eaf1 100644 --- a/app/src/main/java/io/github/aedev/flow/data/innertube/RssSubscriptionService.kt +++ b/app/src/main/java/io/github/aedev/flow/data/innertube/RssSubscriptionService.kt @@ -6,6 +6,7 @@ import io.github.aedev.flow.data.shorts.ChannelReelIndex import io.github.aedev.flow.data.subscriptions.ChannelRssClient import io.github.aedev.flow.data.subscriptions.ChannelRssEntry import io.github.aedev.flow.data.subscriptions.ChannelUploadsClient +import io.github.aedev.flow.data.subscriptions.classifiedFromVerdicts import io.github.aedev.flow.utils.ThumbnailUrlResolver import io.github.aedev.flow.utils.formatYouTubeRelativeTime import io.github.aedev.flow.utils.parsePremiereTimestamp @@ -111,7 +112,7 @@ class RssSubscriptionService .map { channelId -> async(Dispatchers.IO) { val result = fetchRssVideos(channelId, minimumDateMillis, knownVideoIds) - Triple(channelId, result, knownReels(channelId, result.videos, storedReelVerdicts)) + Triple(channelId, result, knownReels(channelId, result, storedReelVerdicts)) } }.awaitAll() } @@ -311,6 +312,7 @@ class RssSubscriptionService val videoTimestamps: Map, val videos: List