Skip to content

Commit

Permalink
Data and UI fixes
Browse files Browse the repository at this point in the history
Add 'restart required' notice above language settings
Improve MediaItem serialisation system
Prepare to implement search page suggested correction
Fix playlist and artist loading edge cases
Fix jitter caused by WidthShrinkText
Handle invalid custom radio, and custom radio containing quotation marks
  • Loading branch information
toasterofbread committed May 7, 2023
1 parent 1ee7625 commit 6a63e49
Show file tree
Hide file tree
Showing 26 changed files with 371 additions and 178 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ fun getAppName(context: Context): String {
return if (string_id == 0) info.nonLocalizedLabel.toString() else context.getString(string_id)
}

actual class PlatformContext(context: Context) {
actual class PlatformContext(private val context: Context) {

private val context: WeakReference<Context> = WeakReference(context)
val ctx: Context get() = context.get()!!
// private val context: WeakReference<Context> = WeakReference(context)
val ctx: Context get() = context

actual fun getPrefs(): ProjectPreferences = ProjectPreferences.getInstance(ctx)

Expand Down
2 changes: 0 additions & 2 deletions shared/src/commonMain/kotlin/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.beust.klaxon.JsonObject
import com.beust.klaxon.Klaxon
import com.spectre7.spmp.PlayerServiceHost
import com.spectre7.spmp.platform.PlatformContext
import com.spectre7.spmp.api.DataApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ class SettingsGroup(var title: String?): SettingsItem() {
}
}

class SettingsItemComposable(val composable: @Composable () -> Unit): SettingsItem() {
override fun initialiseValueStates(prefs: ProjectPreferences, default_provider: (String) -> Any) {}
override fun resetValues() {}

@Composable
override fun GetItem(theme: Theme, openPage: (Int) -> Unit, openCustomPage: (SettingsPage) -> Unit) {
composable()
}
}

class SettingsItemInfoText(val text: String): SettingsItem() {
override fun initialiseValueStates(prefs: ProjectPreferences, default_provider: (String) -> Any) {}
override fun resetValues() {}
Expand Down
40 changes: 17 additions & 23 deletions shared/src/commonMain/kotlin/com/spectre7/spmp/api/DataApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,8 @@ fun <T> Result<T>.getOrReport(error_key: String): T? {
it
},
{
TODO()
// SpMp.error_manager.onError(error_key, it)
// null
SpMp.error_manager.onError(error_key, it)
null
}
)
}
Expand Down Expand Up @@ -96,16 +95,12 @@ class DataApi {
}

override fun fromJson(jv: JsonValue): Any? {
if (jv.obj == null && jv.type != Object::class.java) {
throw KlaxonException("Couldn't parse MediaItem as it isn't an object (${jv.type.name})")
}

if (jv.obj == null) {
if (jv.array == null) {
return null
}

try {
return MediaItem.fromJsonObject(jv.obj!!, klaxon)
return MediaItem.fromDataItems(jv.array!!.toList(), klaxon)
}
catch (e: Exception) {
throw RuntimeException("Couldn't parse MediaItem (${jv.obj})", e)
Expand All @@ -116,10 +111,16 @@ class DataApi {
if (value !is MediaItem) {
throw KlaxonException("Value $value is not a MediaItem")
}
return """{
"type": ${value.type.ordinal},
"id": "${value.id}",${value.getJsonMapValues(klaxon)}
}"""

val string = StringBuilder("[${value.type.ordinal},\"${value.id}\"")

for (item in value.getSerialisedData(klaxon)) {
string.append(',')
string.append(item)
}

string.append(']')
return string.toString()
}
}
private val mediaitem_ref_converter = object : Converter {
Expand All @@ -128,16 +129,12 @@ class DataApi {
}

override fun fromJson(jv: JsonValue): Any? {
if (jv.obj == null && jv.type != Object::class.java) {
throw KlaxonException("Couldn't parse MediaItem as it isn't an object (${jv.type.name})")
}

if (jv.obj == null) {
if (jv.array == null) {
return null
}

try {
return MediaItem.fromJsonObject(jv.obj!!, klaxon.converter(this), true)
return MediaItem.fromDataItems(jv.array!!.toList(), klaxon.converter(this))
}
catch (e: Exception) {
throw RuntimeException("Couldn't parse MediaItem ($jv)", e)
Expand All @@ -148,10 +145,7 @@ class DataApi {
if (value !is MediaItem) {
throw KlaxonException("Value $value is not a MediaItem")
}
return """{
"type": ${value.type.ordinal},
"id": "${value.id}"
}"""
return "[${value.type.ordinal},\"${value.id}\"]"
}
}

Expand Down
52 changes: 42 additions & 10 deletions shared/src/commonMain/kotlin/com/spectre7/spmp/api/HomeFeed.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.spectre7.spmp.api.DataApi.Companion.getStream
import com.spectre7.spmp.api.DataApi.Companion.ytUrl
import com.spectre7.spmp.model.*
import com.spectre7.spmp.ui.component.MediaItemLayout
import com.spectre7.spmp.ui.component.generateLayoutTitle
import okhttp3.Request
import java.io.InputStreamReader
import java.time.Duration
Expand Down Expand Up @@ -149,7 +148,7 @@ private fun processRows(rows: List<YoutubeiShelf>): List<MediaItemLayout> {
items = items,
thumbnail_source = thumbnail_source,
view_more = view_more,
media_item_type = media_item_type
thumbnail_item_type = media_item_type
))
}

Expand Down Expand Up @@ -267,7 +266,7 @@ data class YoutubeiBrowseResponse(
fun getHeaderChips(): List<Pair<Int, String>>? =
contents?.singleColumnBrowseResultsRenderer?.tabs?.first()?.tabRenderer?.content?.sectionListRenderer?.header?.chipCloudRenderer?.chips?.map {
Pair(
LocalisedYoutubeString.filterChip(it.chipCloudChipRenderer.text.first_text) ?: throw NotImplementedError(it.chipCloudChipRenderer.text.first_text),
LocalisedYoutubeString.filterChip(it.chipCloudChipRenderer.text!!.first_text) ?: throw NotImplementedError(it.chipCloudChipRenderer.text.first_text),
it.chipCloudChipRenderer.navigationEndpoint.browseEndpoint!!.params!!
)
}
Expand All @@ -282,16 +281,29 @@ data class YoutubeiBrowseResponse(
data class ContinuationContents(val sectionListContinuation: SectionListRenderer? = null, val musicPlaylistShelfContinuation: MusicShelfRenderer? = null)
}

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

data class YoutubeiShelf(
val musicShelfRenderer: MusicShelfRenderer? = null,
val musicCarouselShelfRenderer: MusicCarouselShelfRenderer? = null,
val musicDescriptionShelfRenderer: MusicDescriptionShelfRenderer? = null,
val musicPlaylistShelfRenderer: MusicShelfRenderer? = null,
val musicCardShelfRenderer: MusicCardShelfRenderer? = null,
val gridRenderer: GridRenderer? = null
val gridRenderer: GridRenderer? = null,
val itemSectionRenderer: ItemSectionRenderer? = null
) {
init {
assert(musicShelfRenderer != null || musicCarouselShelfRenderer != null || musicDescriptionShelfRenderer != null || musicPlaylistShelfRenderer != null || musicCardShelfRenderer != null || gridRenderer != null)
assert(
musicShelfRenderer != null
|| musicCarouselShelfRenderer != null
|| musicDescriptionShelfRenderer != null
|| musicPlaylistShelfRenderer != null
|| musicCardShelfRenderer != null
|| gridRenderer != null
|| itemSectionRenderer != null
)
}

val title: TextRun? get() =
Expand Down Expand Up @@ -507,7 +519,8 @@ data class MusicResponsiveListItemRenderer(
val playlistItemData: PlaylistItemData? = null,
val flexColumns: List<FlexColumn>? = null,
val thumbnail: ThumbnailRenderer? = null,
val navigationEndpoint: NavigationEndpoint? = null
val navigationEndpoint: NavigationEndpoint? = null,
val menu: YoutubeiNextResponse.Menu? = null
)
data class PlaylistItemData(val videoId: String)
data class FlexColumn(val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer)
Expand Down Expand Up @@ -585,14 +598,13 @@ data class ContentsItem(val musicTwoRowItemRenderer: MusicTwoRowItemRenderer? =
}
"MUSIC_PAGE_TYPE_ARTIST", "MUSIC_PAGE_TYPE_USER_CHANNEL" -> {
video_is_main = false
artist = Artist
.fromId(renderer.navigationEndpoint.browseEndpoint.browseId)
artist = Artist.fromId(renderer.navigationEndpoint.browseEndpoint.browseId)
}
}
}

if (renderer.flexColumns != null) {
for (column in renderer.flexColumns.withIndex()) {
for (column in renderer.flexColumns.withIndex()) {
val text = column.value.musicResponsiveListItemFlexColumnRenderer.text
if (text.runs == null) {
continue
Expand Down Expand Up @@ -640,7 +652,27 @@ data class ContentsItem(val musicTwoRowItemRenderer: MusicTwoRowItemRenderer? =
ret = playlist ?: artist ?: return null
}

return ret.supplyTitle(title).supplyArtist(artist ?: Artist.UNKNOWN).supplyThumbnailProvider(renderer.thumbnail?.toThumbnailProvider())
// Handle songs with no artist (or 'Various artists')
if (artist == null) {
if (renderer.flexColumns != null && renderer.flexColumns.size > 1) {
val text = renderer.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text
if (text.runs != null) {
artist = Artist.createForItem(ret).apply { supplyTitle(text.first_text) }
}
}

if (artist == null && renderer.menu != null) {
for (item in renderer.menu.menuRenderer.items) {
val browse_endpoint = (item.menuNavigationItemRenderer ?: continue).navigationEndpoint.browseEndpoint ?: continue
if (browse_endpoint.getMediaItemType() == MediaItem.Type.ARTIST) {
artist = Artist.fromId(browse_endpoint.browseId)
break
}
}
}
}

return ret.supplyTitle(title).supplyArtist(artist).supplyThumbnailProvider(renderer.thumbnail?.toThumbnailProvider())
}

throw NotImplementedError()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import com.spectre7.spmp.api.DataApi.Companion.getStream
import com.spectre7.spmp.api.DataApi.Companion.ytUrl
import com.spectre7.spmp.model.*
import com.spectre7.spmp.ui.component.MediaItemLayout
import com.spectre7.utils.printJson
import okhttp3.Request
import java.util.regex.Pattern

Expand Down Expand Up @@ -75,9 +74,11 @@ private fun unescape(input: String): String {
matcher.appendReplacement(sb, decimal.toChar().toString())
}
matcher.appendTail(sb)
return sb.toString()
return sb.toString().replace("\\\\\"", "\\\"")
}

class InvalidRadioException: Throwable()

fun loadMediaItemData(item: MediaItem): Result<MediaItem?> {
val lock = item.loading_lock
val item_id = item.id
Expand All @@ -103,7 +104,7 @@ fun loadMediaItemData(item: MediaItem): Result<MediaItem?> {
return Result.success(item)
}

if (item is Artist && item.unknown) {
if (item is Artist && item.is_for_item) {
return finish(true)
}

Expand Down Expand Up @@ -134,14 +135,15 @@ fun loadMediaItemData(item: MediaItem): Result<MediaItem?> {
null
else """{ "browseId": "$item_id" }"""

// TODO Fix language option not applying to radio
var request: Request = Request.Builder()
.ytUrl(url)
.addYtHeaders(body == null)
.apply {
if (body != null) post(DataApi.getYoutubeiRequestBody(body))
}
.build()

println(1)
val response = DataApi.request(request).getOrNull()
if (response != null) {
val response_body = response.getStream()
Expand All @@ -154,10 +156,14 @@ fun loadMediaItemData(item: MediaItem): Result<MediaItem?> {

val start_str = "JSON.parse('\\x7b\\x22browseId\\x22:\\x22$item_id\\x22\\x7d'), data:"
val start = string.indexOf(start_str)
check(start != -1)
if (start == -1) {
return Result.failure(InvalidRadioException())
}

val end = string.indexOf('}', start + start_str.length)
check(end != -1)
if (end == -1) {
return Result.failure(InvalidRadioException())
}

val json_reader = unescape(string.substring(start + start_str.length, end).trim().trim('\'')).reader()
val parsed = DataApi.klaxon.parse<YoutubeiBrowseResponse>(json_reader)!!
Expand All @@ -170,6 +176,7 @@ fun loadMediaItemData(item: MediaItem): Result<MediaItem?> {
.contents!![0]
.musicPlaylistShelfRenderer!!
json_reader.close()
println(7)

val continuation = parsed.continuations?.firstOrNull()?.nextRadioContinuationData?.continuation

Expand Down
36 changes: 26 additions & 10 deletions shared/src/commonMain/kotlin/com/spectre7/spmp/api/Search.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.spectre7.spmp.model.MediaItem
import com.spectre7.spmp.model.Playlist
import com.spectre7.spmp.model.Song
import com.spectre7.spmp.ui.component.MediaItemLayout
import com.spectre7.utils.getStringTODO
import okhttp3.Request

data class YoutubeiSearchResponse(
Expand All @@ -28,7 +29,7 @@ data class YoutubeiSearchResponse(
)
data class ChipCloudRenderer(val chips: List<Chip>)
data class Chip(val chipCloudChipRenderer: ChipCloudChipRenderer)
data class ChipCloudChipRenderer(val navigationEndpoint: NavigationEndpoint, val text: TextRuns)
data class ChipCloudChipRenderer(val navigationEndpoint: NavigationEndpoint, val text: TextRuns? = null)
}

data class ChipCloudRendererHeader(val chipCloudRenderer: YoutubeiSearchResponse.ChipCloudRenderer)
Expand Down Expand Up @@ -58,8 +59,9 @@ enum class SearchType {
}

data class SearchFilter(val type: SearchType, val params: String)
data class SearchResults(val suggested_correction: String?, val categories: List<Pair<MediaItemLayout, SearchFilter?>>)

fun searchYoutubeMusic(query: String, params: String?): Result<List<Pair<MediaItemLayout, SearchFilter?>>> {
fun searchYoutubeMusic(query: String, params: String?): Result<SearchResults> {
val params_str: String = if (params != null) "\"$params\"" else "null"
val request = Request.Builder()
.ytUrl("/youtubei/v1/search")
Expand All @@ -73,20 +75,34 @@ fun searchYoutubeMusic(query: String, params: String?): Result<List<Pair<MediaIt
}

val stream = result.getOrThrow().getStream()
val parsed: YoutubeiSearchResponse = DataApi.klaxon.parse(stream)!!
val str = stream.reader().readText()
val parsed: YoutubeiSearchResponse = DataApi.klaxon.parse(str)!!
stream.close()

val ret: MutableList<Pair<MediaItemLayout, SearchFilter?>> = mutableListOf()
val tab = parsed.contents.tabbedSearchResultsRenderer.tabs.first().tabRenderer

val chips = tab.content!!.sectionListRenderer.header!!.chipCloudRenderer.chips
var correction_suggestion: String? = null
val categories = tab.content!!.sectionListRenderer.contents!!.filter { shelf ->
if (shelf.itemSectionRenderer != null) {
shelf.itemSectionRenderer.contents.firstOrNull()?.didYouMeanRenderer?.correctedQuery?.first_text?.also {
correction_suggestion = it
}
false
}
else {
true
}
}

val category_layouts: MutableList<Pair<MediaItemLayout, SearchFilter?>> = mutableListOf()
val chips = tab.content.sectionListRenderer.header!!.chipCloudRenderer.chips

for (category in tab.content.sectionListRenderer.contents!!.withIndex()) {
for (category in categories.withIndex()) {

val card = category.value.musicCardShelfRenderer
if (card != null) {
ret.add(Pair(
MediaItemLayout(TODO(card.header.musicCardShelfHeaderBasicRenderer!!.title.first_text), null, items = mutableListOf(card.getMediaItem()), type = MediaItemLayout.Type.CARD),
category_layouts.add(Pair(
MediaItemLayout(LocalisedYoutubeString.raw(getStringTODO(card.header.musicCardShelfHeaderBasicRenderer!!.title.first_text)), null, items = mutableListOf(card.getMediaItem()), type = MediaItemLayout.Type.CARD),
null
))
continue
Expand All @@ -96,7 +112,7 @@ fun searchYoutubeMusic(query: String, params: String?): Result<List<Pair<MediaIt
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()

ret.add(Pair(
category_layouts.add(Pair(
MediaItemLayout(LocalisedYoutubeString.temp(shelf.title!!.first_text), null, items = items),
search_params?.let {
val item = items.firstOrNull() ?: return@let null
Expand All @@ -113,5 +129,5 @@ fun searchYoutubeMusic(query: String, params: String?): Result<List<Pair<MediaIt
))
}

return Result.success(ret)
return Result.success(SearchResults(correction_suggestion, category_layouts))
}
Loading

0 comments on commit 6a63e49

Please sign in to comment.