Skip to content

Commit

Permalink
Initial podcasts support (fixes #65)
Browse files Browse the repository at this point in the history
Add initial backend support for podcasts (podcast UI may be implemented in the future)
Fix artist page and playlist page bottom padding
  • Loading branch information
toasterofbread committed Jul 15, 2023
1 parent a67b672 commit 280716b
Show file tree
Hide file tree
Showing 21 changed files with 498 additions and 300 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ suspend fun getAccountPlaylists(): Result<List<AccountPlaylist>> = withContext(D

val playlist_data = parsed
.contents!!
.singleColumnBrowseResultsRenderer
.singleColumnBrowseResultsRenderer!!
.tabs
.first()
.tabRenderer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import kotlin.concurrent.thread
import org.schabi.newpipe.extractor.downloader.Request as NewPipeRequest
import org.schabi.newpipe.extractor.downloader.Response as NewPipeResponse

const val DEFAULT_CONNECT_TIMEOUT = 3000
const val DEFAULT_CONNECT_TIMEOUT = 10000
val PLAIN_HEADERS = listOf("accept-language", "user-agent", "accept-encoding", "content-encoding", "origin")

class JsonParseException(val json_obj: JsonObject, message: String? = null, cause: Throwable? = null): RuntimeException(message, cause)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import kotlinx.coroutines.withContext
import okhttp3.Request

suspend fun getGenericFeedViewMorePage(browse_id: String): Result<List<MediaItem>> = withContext(Dispatchers.IO) {

val hl = SpMp.data_language
val request = Request.Builder()
.ytUrl("/youtubei/v1/browse")
Expand All @@ -28,7 +27,7 @@ suspend fun getGenericFeedViewMorePage(browse_id: String): Result<List<MediaItem

val items = parsed
.contents!!
.singleColumnBrowseResultsRenderer
.singleColumnBrowseResultsRenderer!!
.tabs
.first()
.tabRenderer
Expand Down
150 changes: 65 additions & 85 deletions shared/src/commonMain/kotlin/com/toasterofbread/spmp/api/HomeFeed.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ import com.toasterofbread.spmp.api.radio.YoutubeiNextResponse
import com.toasterofbread.spmp.model.Cache
import com.toasterofbread.spmp.model.Settings
import com.toasterofbread.spmp.model.mediaitem.AccountPlaylist
import com.toasterofbread.spmp.model.mediaitem.Artist
import com.toasterofbread.spmp.model.mediaitem.MediaItem
import com.toasterofbread.spmp.model.mediaitem.MediaItemThumbnailProvider
import com.toasterofbread.spmp.model.mediaitem.Song
import com.toasterofbread.spmp.model.mediaitem.enums.MediaItemType
import com.toasterofbread.spmp.model.mediaitem.enums.SongType
import com.toasterofbread.spmp.resources.uilocalisation.LocalisedYoutubeString
import com.toasterofbread.spmp.ui.component.MediaItemLayout
import com.toasterofbread.spmp.ui.layout.mainpage.FilterChip
Expand Down Expand Up @@ -58,7 +56,11 @@ suspend fun getHomeFeed(
}
}

var last_request: Request? = null

fun performRequest(ctoken: String?): Result<YoutubeiBrowseResponse> {
last_request = null

val endpoint = "/youtubei/v1/browse"
val request = Request.Builder()
.ytUrl(if (ctoken == null) endpoint else "$endpoint?ctoken=$ctoken&continuation=$ctoken&type=next")
Expand All @@ -73,58 +75,19 @@ suspend fun getHomeFeed(
val result = Api.request(request)
val stream = result.getOrNull()?.getStream() ?: return result.cast()

last_request = request

try {
return stream.use {
Result.success(Api.klaxon.parse(it)!!)
}
}
catch (error: Throwable) {
val retry_result = Api.request(request)
val retry_stream = retry_result.getOrNull()?.getStream() ?: return retry_result.cast()

return retry_stream.use {
Result.failure(
JsonParseException(
Api.klaxon.parseJsonObject(it.reader()).apply {
// Remove unneeded keys from JSON object

remove("responseContext")

val items: MutableList<Any> = mutableListOf(this)
val keys_to_remove = listOf("trackingParams", "clickTrackingParams", "serializedShareEntity", "serializedContextData", "loggingContext")

while (items.isNotEmpty()) {
val obj = items.removeLast()

if (obj is Collection<*>) {
items.addAll(obj as Collection<Any>)
continue
}

check(obj is JsonObject)

for (key in keys_to_remove) {
obj.remove(key)
}

for (value in obj.values) {
if (value is JsonObject) {
items.add(value)
}
else if (value is Collection<*>) {
items.addAll(value.filterIsInstance<JsonObject>())
}
}
}
},
cause = error
)
)
}
catch (e: Throwable) {
return Result.failure(e)
}
}

return@withContext kotlin.runCatching {
try {
var data = performRequest(continuation).getOrThrow()

val rows: MutableList<MediaItemLayout> = processRows(data.getShelves(continuation != null), hl).toMutableList()
Expand Down Expand Up @@ -153,7 +116,53 @@ suspend fun getHomeFeed(
Cache.set(chips_cache_key, Api.klaxon.toJsonString(chips).reader(), CACHE_LIFETIME)
}

return@runCatching Triple(rows, ctoken, chips)
return@withContext Result.success(Triple(rows, ctoken, chips))
}
catch (error: Throwable) {
val request = last_request ?: return@withContext Result.failure(error)

val retry_result = Api.request(request)
val retry_stream = retry_result.getOrNull()?.getStream() ?: return@withContext retry_result.cast()

return@withContext retry_stream.use {
Result.failure(
JsonParseException(
Api.klaxon.parseJsonObject(it.reader()).apply {
// Remove unneeded keys from JSON object

remove("responseContext")

val items: MutableList<Any> = mutableListOf(this)
val keys_to_remove = listOf("trackingParams", "clickTrackingParams", "serializedShareEntity", "serializedContextData", "loggingContext")

while (items.isNotEmpty()) {
val obj = items.removeLast()

if (obj is Collection<*>) {
items.addAll(obj as Collection<Any>)
continue
}

check(obj is JsonObject)

for (key in keys_to_remove) {
obj.remove(key)
}

for (value in obj.values) {
if (value is JsonObject) {
items.add(value)
}
else if (value is Collection<*>) {
items.addAll(value.filterIsInstance<JsonObject>())
}
}
}
},
cause = error
)
)
}
}
}

Expand Down Expand Up @@ -261,7 +270,7 @@ data class YoutubeiBrowseResponse(
) {
val ctoken: String?
get() = continuationContents?.sectionListContinuation?.continuations?.firstOrNull()?.nextContinuationData?.continuation
?: contents!!.singleColumnBrowseResultsRenderer.tabs.first().tabRenderer.content?.sectionListRenderer?.continuations?.firstOrNull()?.nextContinuationData?.continuation
?: contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.continuations?.firstOrNull()?.nextContinuationData?.continuation

fun getShelves(has_continuation: Boolean): List<YoutubeiShelf> {
return if (has_continuation) continuationContents?.sectionListContinuation?.contents ?: emptyList()
Expand All @@ -276,13 +285,20 @@ data class YoutubeiBrowseResponse(
)
}

data class Contents(val singleColumnBrowseResultsRenderer: SingleColumnBrowseResultsRenderer)
data class Contents(
val singleColumnBrowseResultsRenderer: SingleColumnBrowseResultsRenderer? = null,
val twoColumnBrowseResultsRenderer: TwoColumnBrowseResultsRenderer? = null
)
data class SingleColumnBrowseResultsRenderer(val tabs: List<Tab>)
data class Tab(val tabRenderer: TabRenderer)
data class TabRenderer(val content: Content? = null)
data class Content(val sectionListRenderer: SectionListRenderer)
open class SectionListRenderer(val contents: List<YoutubeiShelf>? = null, val header: ChipCloudRendererHeader? = null, val continuations: List<YoutubeiNextResponse.Continuation>? = null)

class TwoColumnBrowseResultsRenderer(val tabs: List<Tab>, val secondaryContents: SecondaryContents) {
class SecondaryContents(val sectionListRenderer: SectionListRenderer)
}

data class ContinuationContents(val sectionListContinuation: SectionListRenderer? = null, val musicPlaylistShelfContinuation: MusicShelfRenderer? = null)
}

Expand Down Expand Up @@ -477,47 +493,11 @@ data class MusicCardShelfRenderer(
}
}

data class MusicTwoRowItemRenderer(
val navigationEndpoint: NavigationEndpoint,
val title: TextRuns,
val subtitle: TextRuns? = null,
val thumbnailRenderer: ThumbnailRenderer,
val menu: YoutubeiNextResponse.Menu? = null
) {
fun getArtist(host_item: MediaItem): Artist? {
for (run in subtitle?.runs ?: emptyList()) {
val browse_endpoint = run.navigationEndpoint?.browseEndpoint

val endpoint_type = browse_endpoint?.getMediaItemType()
if (endpoint_type == MediaItemType.ARTIST) {
return Artist.fromId(browse_endpoint.browseId).editArtistData { supplyTitle(run.text) }
}
}

if (host_item is Song) {
val index = if (host_item.song_type == SongType.VIDEO) 0 else 1
subtitle?.runs?.getOrNull(index)?.also {
return Artist.createForItem(host_item).editArtistData { supplyTitle(it.text) }
}
}

return null
}
}
data class ThumbnailRenderer(val musicThumbnailRenderer: MusicThumbnailRenderer) {
fun toThumbnailProvider(): MediaItemThumbnailProvider {
return MediaItemThumbnailProvider.fromThumbnails(musicThumbnailRenderer.thumbnail.thumbnails)!!
}
}
data class MusicResponsiveListItemRenderer(
val playlistItemData: RendererPlaylistItemData? = null,
val flexColumns: List<FlexColumn>? = null,
val fixedColumns: List<FixedColumn>? = null,
val thumbnail: ThumbnailRenderer? = null,
val navigationEndpoint: NavigationEndpoint? = null,
val menu: YoutubeiNextResponse.Menu? = null
)
data class RendererPlaylistItemData(val videoId: String, val playlistSetVideoId: String? = null)

data class FlexColumn(val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemColumnRenderer)
data class FixedColumn(val musicResponsiveListItemFixedColumnRenderer: MusicResponsiveListItemColumnRenderer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.toasterofbread.spmp.model.mediaitem.data.ArtistItemData
import com.toasterofbread.spmp.model.mediaitem.data.MediaItemData
import com.toasterofbread.spmp.model.mediaitem.data.SongItemData
import com.toasterofbread.spmp.model.mediaitem.enums.PlaylistType
import com.toasterofbread.spmp.model.mediaitem.enums.SongType
import com.toasterofbread.spmp.resources.uilocalisation.LocalisedYoutubeString
import com.toasterofbread.spmp.resources.uilocalisation.parseYoutubeDurationString
import com.toasterofbread.spmp.resources.uilocalisation.parseYoutubeSubscribersString
Expand Down Expand Up @@ -72,7 +73,7 @@ suspend fun loadBrowseId(browse_id: String, params: String? = null): Result<List
)

val ret: MutableList<MediaItemLayout> = mutableListOf()
for (row in parsed.contents!!.singleColumnBrowseResultsRenderer.tabs.first().tabRenderer.content!!.sectionListRenderer.contents!!.withIndex()) {
for (row in parsed.contents!!.singleColumnBrowseResultsRenderer!!.tabs.first().tabRenderer.content!!.sectionListRenderer.contents!!.withIndex()) {
if (row.value.description != null) {
continue
}
Expand Down Expand Up @@ -120,7 +121,7 @@ suspend fun processDefaultResponse(item: MediaItem, data: MediaItemData, respons
if (item is Playlist && item.playlist_type == PlaylistType.RADIO) {
val playlist_shelf = parsed
.contents!!
.singleColumnBrowseResultsRenderer
.singleColumnBrowseResultsRenderer!!
.tabs[0]
.tabRenderer
.content!!
Expand Down Expand Up @@ -196,7 +197,17 @@ suspend fun processDefaultResponse(item: MediaItem, data: MediaItemData, respons
}

val item_layouts: MutableList<MediaItemLayout> = mutableListOf()
for (row in parsed.contents!!.singleColumnBrowseResultsRenderer.tabs.first().tabRenderer.content!!.sectionListRenderer.contents!!.withIndex()) {

val rows = with (parsed.contents!!) {
if (singleColumnBrowseResultsRenderer != null) {
singleColumnBrowseResultsRenderer.tabs.first().tabRenderer.content!!.sectionListRenderer.contents!!
}
else {
twoColumnBrowseResultsRenderer!!.secondaryContents.sectionListRenderer.contents!!
}
}

for (row in rows.withIndex()) {
val description = row.value.description
if (description != null) {
data.supplyDescription(description, true)
Expand Down Expand Up @@ -232,7 +243,15 @@ suspend fun processDefaultResponse(item: MediaItem, data: MediaItemData, respons
layout_title,
null,
if (row.index == 0) MediaItemLayout.Type.NUMBERED_LIST else MediaItemLayout.Type.GRID,
items.map { it.first }.toMutableList(),
items.map {
if (item is Artist && it.first is Song && (it.first as Song).song_type == SongType.PODCAST) {
it.first.editData {
supplyArtist(item, true)
}
}

it.first
}.toMutableList(),
continuation = continuation,
view_more = view_more
)
Expand Down
Loading

0 comments on commit 280716b

Please sign in to comment.