From 06219734c767819685db11370a5ccc4ce433e2ae Mon Sep 17 00:00:00 2001 From: Priveetee Date: Fri, 12 Jun 2026 20:59:36 +0200 Subject: [PATCH] feat: expose live stream state fields --- openapi/components/media.yaml | 5 ++- openapi/components/streams.yaml | 6 +++- .../typetype/server/models/StreamResponse.kt | 4 +++ .../dev/typetype/server/models/VideoItem.kt | 3 ++ .../server/services/ChannelTabResolver.kt | 17 +++++++++ .../server/services/LiveStreamTypes.kt | 36 +++++++++++++++++++ .../server/services/PipePipeChannelService.kt | 9 ----- .../server/services/RelatedItemMappers.kt | 6 +++- .../server/services/StreamInfoMappers.kt | 33 +++++------------ .../server/services/StreamTimelineMappers.kt | 24 +++++++++++++ .../typetype/server/ChannelTabResolverTest.kt | 26 ++++++++++++++ .../typetype/server/LiveStreamTypesTest.kt | 30 ++++++++++++++++ .../dev/typetype/server/StreamFieldsTest.kt | 14 +++++++- .../dev/typetype/server/TestFixtures.kt | 4 +++ 14 files changed, 179 insertions(+), 38 deletions(-) create mode 100644 src/main/kotlin/dev/typetype/server/services/ChannelTabResolver.kt create mode 100644 src/main/kotlin/dev/typetype/server/services/LiveStreamTypes.kt create mode 100644 src/main/kotlin/dev/typetype/server/services/StreamTimelineMappers.kt create mode 100644 src/test/kotlin/dev/typetype/server/ChannelTabResolverTest.kt create mode 100644 src/test/kotlin/dev/typetype/server/LiveStreamTypesTest.kt diff --git a/openapi/components/media.yaml b/openapi/components/media.yaml index 5c72bb80..f3f708fc 100644 --- a/openapi/components/media.yaml +++ b/openapi/components/media.yaml @@ -1,6 +1,6 @@ VideoItem: type: object - required: [id, title, url, thumbnailUrl, uploaderName, uploaderUrl, duration, viewCount, uploadDate, streamType, isShortFormContent, uploaderVerified] + required: [id, title, url, thumbnailUrl, uploaderName, uploaderUrl, duration, viewCount, uploadDate, streamType, isLive, isPostLive, isLiveContent, isShortFormContent, uploaderVerified] properties: id: { type: string } title: { type: string } @@ -14,6 +14,9 @@ VideoItem: uploadDate: { type: string } uploaded: { type: integer, format: int64, default: -1 } streamType: { type: string } + isLive: { type: boolean } + isPostLive: { type: boolean } + isLiveContent: { type: boolean } isShortFormContent: { type: boolean } uploaderVerified: { type: boolean } shortDescription: { type: string, nullable: true } diff --git a/openapi/components/streams.yaml b/openapi/components/streams.yaml index 5f0ae015..f8c7e78f 100644 --- a/openapi/components/streams.yaml +++ b/openapi/components/streams.yaml @@ -40,7 +40,7 @@ AudioStreamItem: isOriginal: { type: boolean } StreamResponse: type: object - required: [id, title, uploaderName, uploaderUrl, thumbnailUrl, description, duration, viewCount, uploadDate, uploaded, streamType, videoStreams, audioStreams, videoOnlyStreams, subtitles, relatedStreams] + required: [id, title, uploaderName, uploaderUrl, thumbnailUrl, description, duration, viewCount, uploadDate, uploaded, streamType, isLive, isPostLive, isLiveContent, hasLiveManifest, videoStreams, audioStreams, videoOnlyStreams, subtitles, relatedStreams] properties: id: { type: string } title: { type: string } @@ -57,6 +57,10 @@ StreamResponse: uploaded: { type: integer, format: int64 } publishedAt: { type: integer, format: int64, nullable: true } streamType: { type: string } + isLive: { type: boolean } + isPostLive: { type: boolean } + isLiveContent: { type: boolean } + hasLiveManifest: { type: boolean } isShortFormContent: { type: boolean } originalAudioTrackId: { type: string, nullable: true } preferredDefaultAudioTrackId: { type: string, nullable: true } diff --git a/src/main/kotlin/dev/typetype/server/models/StreamResponse.kt b/src/main/kotlin/dev/typetype/server/models/StreamResponse.kt index d55238df..c01772d9 100644 --- a/src/main/kotlin/dev/typetype/server/models/StreamResponse.kt +++ b/src/main/kotlin/dev/typetype/server/models/StreamResponse.kt @@ -40,4 +40,8 @@ data class StreamResponse( val sponsorBlockSegments: List, val relatedStreams: List, val publishedAt: Long? = null, + val isLive: Boolean = false, + val isPostLive: Boolean = false, + val isLiveContent: Boolean = false, + val hasLiveManifest: Boolean = false, ) diff --git a/src/main/kotlin/dev/typetype/server/models/VideoItem.kt b/src/main/kotlin/dev/typetype/server/models/VideoItem.kt index 57c5bc97..786e9cd4 100644 --- a/src/main/kotlin/dev/typetype/server/models/VideoItem.kt +++ b/src/main/kotlin/dev/typetype/server/models/VideoItem.kt @@ -20,4 +20,7 @@ data class VideoItem( val uploaderVerified: Boolean, val shortDescription: String?, val publishedAt: Long? = null, + val isLive: Boolean = false, + val isPostLive: Boolean = false, + val isLiveContent: Boolean = false, ) diff --git a/src/main/kotlin/dev/typetype/server/services/ChannelTabResolver.kt b/src/main/kotlin/dev/typetype/server/services/ChannelTabResolver.kt new file mode 100644 index 00000000..3abffd24 --- /dev/null +++ b/src/main/kotlin/dev/typetype/server/services/ChannelTabResolver.kt @@ -0,0 +1,17 @@ +package dev.typetype.server.services + +import org.schabi.newpipe.extractor.linkhandler.ChannelTabs + +internal fun String.toChannelTab(sort: String?): String? { + if (contains("/shorts", ignoreCase = true)) return ChannelTabs.SHORTS + if (contains("/streams", ignoreCase = true)) return ChannelTabs.LIVESTREAMS + if (contains("/livestreams", ignoreCase = true)) return ChannelTabs.LIVESTREAMS + if (contains("/search", ignoreCase = true)) return ChannelTabs.SEARCH + return if (sort != null) ChannelTabs.VIDEOS else null +} + +internal fun String.toBaseChannelUrl(tab: String): String = substringBefore("/$tab") + .substringBefore("/streams") + .substringBefore('?') + .substringBefore('#') + .trimEnd('/') diff --git a/src/main/kotlin/dev/typetype/server/services/LiveStreamTypes.kt b/src/main/kotlin/dev/typetype/server/services/LiveStreamTypes.kt new file mode 100644 index 00000000..ff5a46d3 --- /dev/null +++ b/src/main/kotlin/dev/typetype/server/services/LiveStreamTypes.kt @@ -0,0 +1,36 @@ +package dev.typetype.server.services + +import org.schabi.newpipe.extractor.stream.StreamType + +internal data class StreamLiveMetadata( + val streamType: String, + val hlsUrl: String, + val dashMpdUrl: String, + val isLive: Boolean, + val isPostLive: Boolean, + val isLiveContent: Boolean, + val hasLiveManifest: Boolean, +) + +internal fun StreamType?.toApiStreamType(fallback: String = ""): String = this?.name?.lowercase() ?: fallback + +internal fun streamLiveMetadata(streamType: StreamType?, hlsUrl: String?, dashMpdUrl: String?): StreamLiveMetadata { + val apiStreamType = streamType.toApiStreamType() + val apiHlsUrl = hlsUrl?.takeIf { it.startsWith("http") } ?: "" + val apiDashMpdUrl = dashMpdUrl?.takeIf { it.startsWith("http") } ?: "" + return StreamLiveMetadata( + streamType = apiStreamType, + hlsUrl = apiHlsUrl, + dashMpdUrl = apiDashMpdUrl, + isLive = apiStreamType.isLiveStreamType(), + isPostLive = apiStreamType.isPostLiveStreamType(), + isLiveContent = apiStreamType.isLiveContentType(), + hasLiveManifest = apiStreamType.isLiveStreamType() && (apiHlsUrl.isNotBlank() || apiDashMpdUrl.isNotBlank()), + ) +} + +internal fun String.isLiveStreamType(): Boolean = this == "live_stream" || this == "audio_live_stream" + +internal fun String.isPostLiveStreamType(): Boolean = this == "post_live_stream" || this == "post_live_audio_stream" + +internal fun String.isLiveContentType(): Boolean = isLiveStreamType() || isPostLiveStreamType() diff --git a/src/main/kotlin/dev/typetype/server/services/PipePipeChannelService.kt b/src/main/kotlin/dev/typetype/server/services/PipePipeChannelService.kt index 0333512f..34d4cbad 100644 --- a/src/main/kotlin/dev/typetype/server/services/PipePipeChannelService.kt +++ b/src/main/kotlin/dev/typetype/server/services/PipePipeChannelService.kt @@ -10,7 +10,6 @@ import org.schabi.newpipe.extractor.Page import org.schabi.newpipe.extractor.StreamingService import org.schabi.newpipe.extractor.channel.ChannelInfo import org.schabi.newpipe.extractor.channel.ChannelTabInfo -import org.schabi.newpipe.extractor.linkhandler.ChannelTabs class PipePipeChannelService : ChannelService { @@ -63,13 +62,5 @@ class PipePipeChannelService : ChannelService { return ChannelInfo.getMoreItems(service, url, page).toChannelResponse() } - private fun String.toChannelTab(sort: String?): String? { - if (contains("/shorts", ignoreCase = true)) return ChannelTabs.SHORTS - if (contains("/search", ignoreCase = true)) return ChannelTabs.SEARCH - return if (sort != null) ChannelTabs.VIDEOS else null - } - - private fun String.toBaseChannelUrl(tab: String): String = substringBefore("/$tab").substringBefore('?').substringBefore('#').trimEnd('/') - private fun channelId(url: String, service: StreamingService): String = service.channelLHFactory.fromUrl(url).id } diff --git a/src/main/kotlin/dev/typetype/server/services/RelatedItemMappers.kt b/src/main/kotlin/dev/typetype/server/services/RelatedItemMappers.kt index d8b743d1..0a61f9de 100644 --- a/src/main/kotlin/dev/typetype/server/services/RelatedItemMappers.kt +++ b/src/main/kotlin/dev/typetype/server/services/RelatedItemMappers.kt @@ -36,6 +36,7 @@ internal fun List.toSponsorBlockSegments(): Array