Skip to content

Commit

Permalink
Begin artist and playlist page redesign
Browse files Browse the repository at this point in the history
Separate and begin redesigning artist and playlist pages
Add localisation for subscriber count and duration
  • Loading branch information
toasterofbread committed May 10, 2023
1 parent 4b587a6 commit 1ca6340
Show file tree
Hide file tree
Showing 21 changed files with 659 additions and 196 deletions.
File renamed without changes.
14 changes: 12 additions & 2 deletions shared/src/commonMain/kotlin/com/spectre7/spmp/api/DataApi.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.spectre7.spmp.api

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.beust.klaxon.*
import com.spectre7.spmp.model.Cache
import com.spectre7.spmp.model.MediaItem
Expand Down Expand Up @@ -157,6 +160,9 @@ class DataApi {
.converter(enum_converter)
.converter(mediaitem_converter)

fun getYtmAuth(): YoutubeMusicAuthInfo? =
Settings.get<Set<String>>(Settings.KEY_YTM_AUTH).let { if (it is YoutubeMusicAuthInfo) it else YoutubeMusicAuthInfo(it) }.initialisedOrNull()

enum class YoutubeiContextType {
BASE,
ALT,
Expand All @@ -171,6 +177,9 @@ class DataApi {
}
}

var ytm_authenticated: Boolean by mutableStateOf(false)
private set

private lateinit var youtubei_context: JsonObject
private lateinit var youtubei_context_alt: JsonObject
private lateinit var youtubei_context_android: JsonObject
Expand Down Expand Up @@ -238,8 +247,8 @@ class DataApi {
header_update_thread = thread {
val headers_builder = Headers.Builder().add("user-agent", user_agent)

val ytm_auth = Settings.get<Set<String>>(Settings.KEY_YTM_AUTH).let { if (it is YoutubeMusicAuthInfo) it else YoutubeMusicAuthInfo(it) }
if (ytm_auth.initialised) {
val ytm_auth = getYtmAuth()
if (ytm_auth != null) {
headers_builder["cookie"] = ytm_auth.cookie
for (header in ytm_auth.headers) {
headers_builder[header.key] = header.value
Expand All @@ -260,6 +269,7 @@ class DataApi {
headers_builder["user-agent"] = user_agent

youtubei_headers = headers_builder.build()
ytm_authenticated = ytm_auth != null
}
return header_update_thread!!
}
Expand Down
43 changes: 30 additions & 13 deletions shared/src/commonMain/kotlin/com/spectre7/spmp/api/HomeFeed.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ private val CACHE_LIFETIME = Duration.ofDays(1)

fun getHomeFeed(min_rows: Int = -1, allow_cached: Boolean = true, params: String? = null, continuation: String? = null): Result<Triple<List<MediaItemLayout>, String?, List<Pair<Int, String>>?>> {

val hl = SpMp.data_language
fun postRequest(ctoken: String?): Result<InputStreamReader> {
val endpoint = "/youtubei/v1/browse"
val request = Request.Builder()
Expand Down Expand Up @@ -71,7 +72,7 @@ fun getHomeFeed(min_rows: Int = -1, allow_cached: Boolean = true, params: String
var data: YoutubeiBrowseResponse = DataApi.klaxon.parse(response_reader)!!
response_reader.close()

val rows: MutableList<MediaItemLayout> = processRows(data.getShelves(continuation != null)).toMutableList()
val rows: MutableList<MediaItemLayout> = processRows(data.getShelves(continuation != null), hl).toMutableList()
check(rows.isNotEmpty())

val chips = data.getHeaderChips()
Expand All @@ -91,7 +92,7 @@ fun getHomeFeed(min_rows: Int = -1, allow_cached: Boolean = true, params: String

val shelves = data.getShelves(true)
check(shelves.isNotEmpty())
rows.addAll(processRows(shelves))
rows.addAll(processRows(shelves, hl))

ctoken = data.ctoken
}
Expand All @@ -105,7 +106,7 @@ fun getHomeFeed(min_rows: Int = -1, allow_cached: Boolean = true, params: String
return Result.success(Triple(rows, ctoken, chips))
}

private fun processRows(rows: List<YoutubeiShelf>): List<MediaItemLayout> {
private fun processRows(rows: List<YoutubeiShelf>, hl: String): List<MediaItemLayout> {
val ret = mutableListOf<MediaItemLayout>()
for (row in rows) {
when (val renderer = row.getRenderer()) {
Expand All @@ -124,7 +125,7 @@ private fun processRows(rows: List<YoutubeiShelf>): List<MediaItemLayout> {
view_more: MediaItemLayout.ViewMore? = null
) {

val items = row.getMediaItems().toMutableList()
val items = row.getMediaItems(hl).toMutableList()

// val final_title: String
// val final_subtitle: String?
Expand Down Expand Up @@ -282,7 +283,7 @@ data class YoutubeiBrowseResponse(
}

data class ItemSectionRenderer(val contents: List<ItemSectionRendererContent>)
data class ItemSectionRendererContent(val didYouMeanRenderer: DidYouMeanRenderer)
data class ItemSectionRendererContent(val didYouMeanRenderer: DidYouMeanRenderer? = null)
data class DidYouMeanRenderer(val correctedQuery: TextRuns)

data class YoutubeiShelf(
Expand Down Expand Up @@ -320,9 +321,9 @@ data class YoutubeiShelf(
return musicShelfRenderer?.bottomEndpoint ?: musicCarouselShelfRenderer?.header?.getRenderer()?.moreContentButton?.buttonRenderer?.navigationEndpoint
}

fun getMediaItems(): List<MediaItem> {
fun getMediaItems(hl: String): List<MediaItem> {
return (musicShelfRenderer?.contents ?: musicCarouselShelfRenderer?.contents ?: musicPlaylistShelfRenderer?.contents ?: gridRenderer!!.items).mapNotNull {
val item = it.toMediaItem()
val item = it.toMediaItem(hl)
item?.saveToCache()
return@mapNotNull item
}
Expand Down Expand Up @@ -418,6 +419,7 @@ data class HeaderRenderer(
val thumbnail: Thumbnails? = null,
val foregroundThumbnail: Thumbnails? = null,
val subtitle: TextRuns? = null,
val secondSubtitle: TextRuns? = null,
val moreContentButton: MoreContentButton? = null
) {
fun getThumbnails(): List<MediaItem.ThumbnailProvider.Thumbnail> {
Expand Down Expand Up @@ -452,7 +454,7 @@ data class TextRun(val text: String, val strapline: TextRuns? = null, val naviga

data class MusicShelfRenderer(
val title: TextRuns? = null,
val contents: List<ContentsItem>,
val contents: List<ContentsItem>? = null,
val continuations: List<YoutubeiNextResponse.Continuation>? = null,
val bottomEndpoint: NavigationEndpoint? = null
)
Expand Down Expand Up @@ -518,16 +520,19 @@ data class ThumbnailRenderer(val musicThumbnailRenderer: MusicThumbnailRenderer)
data class MusicResponsiveListItemRenderer(
val playlistItemData: PlaylistItemData? = 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 PlaylistItemData(val videoId: String)
data class FlexColumn(val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer)
data class MusicResponsiveListItemFlexColumnRenderer(val text: TextRuns)

data class FlexColumn(val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemColumnRenderer)
data class FixedColumn(val musicResponsiveListItemFixedColumnRenderer: MusicResponsiveListItemColumnRenderer)
data class MusicResponsiveListItemColumnRenderer(val text: TextRuns)

data class ContentsItem(val musicTwoRowItemRenderer: MusicTwoRowItemRenderer? = null, val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer? = null) {
fun toMediaItem(): MediaItem? {
fun toMediaItem(hl: String): MediaItem? {
if (musicTwoRowItemRenderer != null) {
val renderer = musicTwoRowItemRenderer

Expand Down Expand Up @@ -586,6 +591,7 @@ data class ContentsItem(val musicTwoRowItemRenderer: MusicTwoRowItemRenderer? =
var title: String? = null
var artist: Artist? = null
var playlist: Playlist? = null
var duration: Long? = null

if (video_id == null) {
val page_type = renderer.navigationEndpoint?.browseEndpoint?.getPageType()
Expand Down Expand Up @@ -638,9 +644,20 @@ data class ContentsItem(val musicTwoRowItemRenderer: MusicTwoRowItemRenderer? =
}
}

if (renderer.fixedColumns != null) {
for (column in renderer.fixedColumns) {
val text = column.musicResponsiveListItemFixedColumnRenderer.text.first_text
val parsed = parseYoutubeDurationString(text, hl)
if (parsed != null) {
duration = parsed
break
}
}
}

val ret: MediaItem
if (video_id != null) {
ret = Song.fromId(video_id)
ret = Song.fromId(video_id).supplyDuration(duration, true)
renderer.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.firstOrNull()?.also {
ret.supplySongType(if (it.height == it.width) Song.SongType.SONG else Song.SongType.VIDEO)
}
Expand All @@ -649,7 +666,7 @@ data class ContentsItem(val musicTwoRowItemRenderer: MusicTwoRowItemRenderer? =
return null
}
else {
ret = playlist ?: artist ?: return null
ret = (playlist?.supplyTotalDuration(duration, true)) ?: artist ?: return null
}

// Handle songs with no artist (or 'Various artists')
Expand Down
63 changes: 45 additions & 18 deletions shared/src/commonMain/kotlin/com/spectre7/spmp/api/LoadMediaitem.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import com.spectre7.spmp.ui.component.MediaItemLayout
import okhttp3.Request
import java.util.regex.Pattern

// TODO Organise and split

data class PlayerData(
val videoDetails: VideoDetails? = null,
// val streamingData: StreamingData? = null
Expand All @@ -23,6 +25,7 @@ data class VideoDetails(

fun loadBrowseId(browse_id: String, params: String? = null): Result<List<MediaItemLayout>> {
val params_str = if (params == null) "" else """, "params": "$params" """
val hl = SpMp.data_language
val request = Request.Builder()
.ytUrl("/youtubei/v1/browse")
.addYtHeaders()
Expand Down Expand Up @@ -54,7 +57,7 @@ fun loadBrowseId(browse_id: String, params: String? = null): Result<List<MediaIt
row.value.title?.text?.let { LocalisedYoutubeString.raw(it) },
null,
if (row.index == 0) MediaItemLayout.Type.NUMBERED_LIST else MediaItemLayout.Type.GRID,
row.value.getMediaItems().toMutableList(),
row.value.getMediaItems(hl).toMutableList(),
continuation = continuation,
view_more = view_more
))
Expand Down Expand Up @@ -112,6 +115,8 @@ fun loadMediaItemData(item: MediaItem): Result<MediaItem?> {
return result
}

println("Load $item_id $item")

if (item is Artist && item.is_for_item) {
return finish(true)
}
Expand Down Expand Up @@ -140,6 +145,7 @@ fun loadMediaItemData(item: MediaItem): Result<MediaItem?> {
}"""
else """{ "browseId": "$item_id" }"""

val hl = SpMp.data_language
var request: Request = Request.Builder()
.ytUrl(url)
.addYtHeaders()
Expand Down Expand Up @@ -173,8 +179,8 @@ fun loadMediaItemData(item: MediaItem): Result<MediaItem?> {
val layout = MediaItemLayout(
null, null,
MediaItemLayout.Type.LIST,
playlist_shelf.contents.mapNotNull { data ->
return@mapNotNull data.toMediaItem().also { check(it is Song) }
playlist_shelf.contents!!.mapNotNull { data ->
return@mapNotNull data.toMediaItem(hl).also { check(it is Song) }
}.toMutableList(),
continuation = continuation?.let { MediaItemLayout.Continuation(it, MediaItemLayout.Continuation.Type.SONG, item_id) }
)
Expand All @@ -189,22 +195,37 @@ fun loadMediaItemData(item: MediaItem): Result<MediaItem?> {
item.supplyDescription(header_renderer.description?.first_text, true)
item.supplyThumbnailProvider(MediaItem.ThumbnailProvider.fromThumbnails(header_renderer.getThumbnails()))

val artist = header_renderer.subtitle?.runs?.firstOrNull {
it.navigationEndpoint?.browseEndpoint?.getPageType() == "MUSIC_PAGE_TYPE_USER_CHANNEL"
header_renderer.subtitle?.runs?.also { subtitle ->
val artist_run = subtitle.firstOrNull {
it.navigationEndpoint?.browseEndpoint?.getPageType() == "MUSIC_PAGE_TYPE_USER_CHANNEL"
}
if (artist_run != null) {
item.supplyArtist(
Artist
.fromId(artist_run.navigationEndpoint!!.browseEndpoint!!.browseId)
.supplyTitle(artist_run.text, true) as Artist,
true
)
}

if (item is Playlist) {
item.supplyYear(subtitle.lastOrNull { it.text.all { it.isDigit() } }?.text?.toInt(), true)
}
}
if (artist != null) {
item.supplyArtist(
Artist
.fromId(artist.navigationEndpoint!!.browseEndpoint!!.browseId)
.supplyTitle(artist.text, true) as Artist,
true
)

if (item is Playlist) {
header_renderer.secondSubtitle?.runs?.also { second_subtitle ->
check(second_subtitle.size == 2) { second_subtitle.toString() }

item.supplyItemCount(second_subtitle[0].text.filter { it.isDigit() }.toInt(), true)
item.supplyTotalDuration(parseYoutubeDurationString(second_subtitle[1].text, hl), true)
}
}

if (header_renderer.subscriptionButton != null && item is Artist) {
val subscribe_button = header_renderer.subscriptionButton.subscribeButtonRenderer
item.supplySubscribeChannelId(subscribe_button.channelId, true)
item.supplySubscriberCountText(subscribe_button.subscriberCountText.first_text, true)
item.supplySubscriberCount(parseYoutubeSubscribersString(subscribe_button.subscriberCountText.first_text, hl), true)
item.subscribed = subscribe_button.subscribed
}
}
Expand All @@ -220,17 +241,23 @@ fun loadMediaItemData(item: MediaItem): Result<MediaItem?> {
val continuation: MediaItemLayout.Continuation? =
row.value.musicPlaylistShelfRenderer?.continuations?.firstOrNull()?.nextContinuationData?.continuation?.let { MediaItemLayout.Continuation(it, MediaItemLayout.Continuation.Type.PLAYLIST) }

val layout_title = row.value.title?.text?.let {
if (item is Artist && item.is_own_channel) LocalisedYoutubeString.ownChannel(it)
else LocalisedYoutubeString.mediaItemPage(it, item.type)
}

val view_more = row.value.getNavigationEndpoint()?.getViewMore()
view_more?.layout_type = MediaItemLayout.Type.LIST
if (view_more?.media_item != null && item is Artist) {
view_more.media_item.supplyArtist(item, true)
view_more.media_item.supplyTitle(layout_title?.getString(), false)
}

item_layouts.add(MediaItemLayout(
row.value.title?.text?.let {
if (item is Artist && item.is_own_channel) LocalisedYoutubeString.ownChannel(it)
else LocalisedYoutubeString.mediaItemPage(it, item.type)
},
layout_title,
null,
if (row.index == 0) MediaItemLayout.Type.NUMBERED_LIST else MediaItemLayout.Type.GRID,
row.value.getMediaItems().toMutableList(),
row.value.getMediaItems(hl).toMutableList(),
continuation = continuation,
view_more = view_more
))
Expand Down
3 changes: 2 additions & 1 deletion shared/src/commonMain/kotlin/com/spectre7/spmp/api/Search.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ data class SearchResults(val suggested_correction: String?, val categories: List

fun searchYoutubeMusic(query: String, params: String?): Result<SearchResults> {
val params_str: String = if (params != null) "\"$params\"" else "null"
val hl = SpMp.data_language
val request = Request.Builder()
.ytUrl("/youtubei/v1/search")
.addYtHeaders()
Expand Down Expand Up @@ -109,8 +110,8 @@ fun searchYoutubeMusic(query: String, params: String?): Result<SearchResults> {
}

val shelf = category.value.musicShelfRenderer ?: continue
val items = shelf.contents?.mapNotNull { it.toMediaItem(hl) }?.toMutableList() ?: continue
val search_params = if (category.index == 0) null else chips[category.index - 1].chipCloudChipRenderer.navigationEndpoint.searchEndpoint!!.params
val items = shelf.contents.mapNotNull { it.toMediaItem() }.toMutableList()

category_layouts.add(Pair(
MediaItemLayout(LocalisedYoutubeString.temp(shelf.title!!.first_text), null, items = items),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ fun isSubscribedToArtist(artist: Artist): Result<Boolean?> {
}

fun subscribeOrUnsubscribeArtist(artist: Artist, subscribe: Boolean): Result<Any> {
check(DataApi.ytm_authenticated)

val request: Request = Request.Builder()
.url("https://music.youtube.com/youtubei/v1/subscription/${if (subscribe) "subscribe" else "unsubscribe"}")
.addYtHeaders()
Expand Down
Loading

0 comments on commit 1ca6340

Please sign in to comment.