Skip to content

Commit

Permalink
Add playlist continuation, URI opening, jump buttons
Browse files Browse the repository at this point in the history
Implement playlist continuation (closes #112)
Add support for opening playlist URIs
Fix click-through on playlist page sticky top bar
Add jump buttons to playlist page
  • Loading branch information
toasterofbread committed Aug 16, 2023
1 parent 97b99cf commit 67ab12a
Show file tree
Hide file tree
Showing 14 changed files with 402 additions and 178 deletions.
4 changes: 4 additions & 0 deletions shared/src/commonMain/kotlin/SpMp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import com.toasterofbread.spmp.resources.uilocalisation.UnlocalisedStringCollect
import com.toasterofbread.spmp.ui.layout.mainpage.PlayerState
import com.toasterofbread.spmp.ui.layout.mainpage.PlayerStateImpl
import com.toasterofbread.spmp.ui.layout.mainpage.RootView
import com.toasterofbread.spmp.ui.layout.mainpage.ServiceNotConnectedView
import com.toasterofbread.spmp.ui.theme.ApplicationTheme
import com.toasterofbread.spmp.ui.theme.Theme
import com.toasterofbread.utils.*
Expand Down Expand Up @@ -140,6 +141,9 @@ object SpMp {
if (player_state.service_connected) {
RootView(player_state)
}
else {
ServiceNotConnectedView()
}
}
error_manager.Indicator(Theme.accent_provider)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.toasterofbread.spmp.model.mediaitem.ArtistRef
import com.toasterofbread.spmp.model.mediaitem.MediaItem
import com.toasterofbread.spmp.model.mediaitem.MediaItemData
import com.toasterofbread.spmp.model.mediaitem.MediaItemThumbnailProvider
import com.toasterofbread.spmp.model.mediaitem.Playlist
import com.toasterofbread.spmp.model.mediaitem.PlaylistData
import com.toasterofbread.spmp.model.mediaitem.Song
import com.toasterofbread.spmp.model.mediaitem.SongData
Expand All @@ -24,32 +25,52 @@ import com.toasterofbread.spmp.resources.uilocalisation.LocalisedYoutubeString
import com.toasterofbread.spmp.resources.uilocalisation.parseYoutubeDurationString
import com.toasterofbread.spmp.resources.uilocalisation.parseYoutubeSubscribersString
import com.toasterofbread.spmp.ui.component.mediaitemlayout.MediaItemLayout
import com.toasterofbread.utils.lazyAssert
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.job
import kotlinx.coroutines.withContext
import okhttp3.Request
import okhttp3.Response
import java.io.InputStream

data class PlayerData(
val videoDetails: VideoDetails? = null,
// val streamingData: StreamingData? = null
) {
// data class StreamingData(val formats: List<YoutubeVideoFormat>, val adaptiveFormats: List<YoutubeVideoFormat>)
}

data class VideoDetails(
val videoId: String,
val title: String,
val channelId: String,
)

suspend fun loadMediaItemData(
item: MediaItemData,
db: Database
db: Database,
continuation: MediaItemLayout.Continuation? = null
): Result<Unit> {
val item_id = item.id

if (continuation != null) {
check(item is PlaylistData && !item.isLocalPlaylist())

continuation.loadContinuation(db).fold(
{
val (items, ctoken) = it
item.items = item.items?.plus(items as List<SongData>)

println("ADDITEMS $items")

withContext(Dispatchers.IO) {
db.transaction {
for (playlist_item in items) {
playlist_item.saveToDatabase(db)
item.Items.addItem(playlist_item as Song, null, db)
}
item.Continuation.set(
ctoken?.let { token ->
continuation.copy(token = token)
},
db
)
}
}

return Result.success(Unit)
},
{ return Result.failure(it) }
)
}

val result =
if (item is ArtistData && item.is_for_item) Result.success(Unit)
else withContext(Dispatchers.IO) {
Expand All @@ -61,6 +82,10 @@ suspend fun loadMediaItemData(
"isAudioOnly" to true,
"videoId" to item_id,
)
else if (item is Playlist && !item.id.startsWith("VL"))
mapOf(
"browseId" to "VL$item_id"
)
else
mapOf(
"browseId" to item_id
Expand All @@ -77,17 +102,21 @@ suspend fun loadMediaItemData(
))
.build()

val response = Api.request(request).getOrNull()
coroutineContext.job.invokeOnCompletion {
response?.close()
}

if (response != null) {
val result = processDefaultResponse(item, response, hl, db)
if (result != null) {
return@withContext result
Api.request(request).fold(
{ response ->
val result = processDefaultResponse(item, response, hl, db)
if (result.isSuccess || item !is Song) {
return@withContext result
}
},
{ error ->
if (item !is Song) {
return@withContext Result.failure(error)
}
}
}
)

check(item is Song)

// 'next' endpoint has no artist, use 'player' instead
request = Request.Builder()
Expand Down Expand Up @@ -118,16 +147,18 @@ suspend fun loadMediaItemData(
}

result.onSuccess {
item.loaded = true
item.saveToDatabase(db)
withContext(Dispatchers.IO) {
item.loaded = true
item.saveToDatabase(db)
}
}

return result
}

class InvalidRadioException: Throwable()

suspend fun processSong(song: SongData, response_body: InputStream, db: Database): Result<Unit>? {
suspend fun processSong(song: SongData, response_body: InputStream, db: Database): Result<Unit> {
val tabs: List<YoutubeiNextResponse.Tab> = try {
Api.klaxon.parse<YoutubeiNextResponse>(response_body)!!
.contents
Expand Down Expand Up @@ -161,7 +192,7 @@ suspend fun processSong(song: SongData, response_body: InputStream, db: Database
)
}

suspend fun processDefaultResponse(item: MediaItemData, response: Response, hl: String, db: Database): Result<Unit>? {
suspend fun processDefaultResponse(item: MediaItemData, response: Response, hl: String, db: Database): Result<Unit> {
return withContext(Dispatchers.IO) {
val response_body = response.getStream()
return@withContext response_body.use {
Expand Down Expand Up @@ -264,10 +295,6 @@ suspend fun processDefaultResponse(item: MediaItemData, response: Response, hl:
}
}

if (item is PlaylistData) {

}

for (row in section_list_renderer.contents.orEmpty().withIndex()) {
val description = row.value.description
if (description != null) {
Expand All @@ -285,8 +312,7 @@ suspend fun processDefaultResponse(item: MediaItemData, response: Response, hl:
}

val continuation_token =
section_list_renderer.continuations?.firstOrNull()?.nextContinuationData?.continuation
?: row.value.musicPlaylistShelfRenderer?.continuations?.firstOrNull()?.nextContinuationData?.continuation
row.value.musicPlaylistShelfRenderer?.continuations?.firstOrNull()?.nextContinuationData?.continuation

if (item is PlaylistData) {
item.items = items_mapped.filterIsInstance<SongData>()
Expand Down Expand Up @@ -337,3 +363,13 @@ suspend fun processDefaultResponse(item: MediaItemData, response: Response, hl:
}
}
}

private data class PlayerData(
val videoDetails: VideoDetails? = null,
) {
class VideoDetails(
val videoId: String,
val title: String,
val channelId: String,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

internal abstract class ListenerLoader<K, V>: BasicLoader<K, V>() {
abstract val listeners: MutableList<Listener<K, V>>
protected abstract val listeners: MutableList<Listener<K, V>>

fun addListener(listener: Listener<K, V>) { listeners.add(listener) }
fun removeListener(listener: Listener<K, V>) { listeners.remove(listener) }
Expand Down Expand Up @@ -103,7 +103,7 @@ internal suspend inline fun <K, V> performSafeLoad(
instance_key: K,
lock: ReentrantLock,
running: MutableMap<K, Deferred<Result<V>>>,
listeners: List<ListenerLoader.Listener<K, V>>? = null,
listeners: List<ListenerLoader.Listener<K, in V>>? = null,
crossinline performLoad: suspend () -> Result<V>
): Result<V> {
val loading = lock.withLock {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.toasterofbread.spmp.model.mediaitem.loader

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
Expand All @@ -12,10 +13,11 @@ import com.toasterofbread.spmp.model.mediaitem.MediaItem
import com.toasterofbread.spmp.model.mediaitem.MediaItemData
import com.toasterofbread.spmp.model.mediaitem.PlaylistData
import com.toasterofbread.spmp.model.mediaitem.SongData
import com.toasterofbread.spmp.ui.component.mediaitemlayout.MediaItemLayout
import kotlinx.coroutines.Deferred
import java.util.concurrent.locks.ReentrantLock

internal object MediaItemLoader {
internal object MediaItemLoader: ListenerLoader<String, MediaItemData>() {
private val song_lock = ReentrantLock()
private val artist_lock = ReentrantLock()
private val playlist_lock = ReentrantLock()
Expand All @@ -38,36 +40,67 @@ internal object MediaItemLoader {
suspend fun loadArtist(artist: ArtistData, db: Database): Result<ArtistData> {
return loadItem(artist, loading_artists, artist_lock, db)
}
suspend fun loadPlaylist(playlist: PlaylistData, db: Database): Result<PlaylistData> {
return loadItem(playlist, loading_playlists, playlist_lock, db)
suspend fun loadPlaylist(playlist: PlaylistData, db: Database, continuation: MediaItemLayout.Continuation? = null): Result<PlaylistData> {
return loadItem(playlist, loading_playlists, playlist_lock, db, continuation)
}

override val listeners: MutableList<Listener<String, MediaItemData>> = mutableListOf()

private suspend fun <ItemType: MediaItemData> loadItem(
item: ItemType,
loading_items: MutableMap<String, Deferred<Result<ItemType>>>,
lock: ReentrantLock,
db: Database
db: Database,
continuation: MediaItemLayout.Continuation? = null
): Result<ItemType> {
return performSafeLoad(
item.id,
lock,
loading_items
loading_items,
listeners = listeners
) {
loadMediaItemData(item, db).fold(
loadMediaItemData(item, db, continuation).fold(
{ Result.success(item) },
{ Result.failure(it) }
)
}
}

}

@Composable
fun MediaItem.loadDataOnChange(db: Database): State<Boolean> {
val loading_state = remember(this) { mutableStateOf(false) }

DisposableEffect(this) {
val listener = object : ListenerLoader.Listener<String, MediaItemData> {
override fun onLoadStarted(key: String) {
if (key == id) {
loading_state.value = true
}
}
override fun onLoadFinished(key: String, value: MediaItemData) {
if (key == id) {
loading_state.value = false
}
}
override fun onLoadFailed(key: String, error: Throwable) {
if (key == id) {
loading_state.value = false
}
}
}

MediaItemLoader.addListener(listener)

onDispose {
MediaItemLoader.removeListener(listener)
}
}

LaunchedEffect(this) {
loading_state.value = true
loadData(db)
loading_state.value = false
}

return loading_state
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ data class MediaItemLayout(
}
}

suspend fun loadContinuation(database: Database, filters: List<RadioModifier> = emptyList()): Result<Pair<List<MediaItem>, String?>> {
suspend fun loadContinuation(database: Database, filters: List<RadioModifier> = emptyList()): Result<Pair<List<MediaItemData>, String?>> {
println("LOADCONT $this $type")
return when (type) {
Type.SONG -> loadSongContinuation(filters)
Type.PLAYLIST -> loadPlaylistContinuation(database, false)
Expand All @@ -131,12 +132,12 @@ data class MediaItemLayout(
private suspend fun loadSongContinuation(filters: List<RadioModifier>): Result<Pair<List<MediaItemData>, String?>> {
val result = getSongRadio(param as String, token, filters)
return result.fold(
{Result.success(Pair(it.items, it.continuation)) },
{ Result.success(Pair(it.items, it.continuation)) },
{ Result.failure(it) }
)
}

private suspend fun loadPlaylistContinuation(db: Database, initial: Boolean): Result<Pair<List<MediaItem>, String?>> = withContext(Dispatchers.IO) {
private suspend fun loadPlaylistContinuation(db: Database, initial: Boolean): Result<Pair<List<MediaItemData>, String?>> = withContext(Dispatchers.IO) {
if (initial) {
val playlist = AccountPlaylistRef(token)
playlist.loadData(db, false).onFailure {
Expand All @@ -146,7 +147,7 @@ data class MediaItemLayout(
val items = playlist.Items.get(db) ?: return@withContext Result.failure(IllegalStateException("Items for loaded $playlist is null"))

return@withContext Result.success(Pair(
items.subList(param as Int, items.size - 1),
items.subList(param as Int, items.size - 1).map { it.getEmptyData() },
playlist.Continuation.get(db)?.token
))
}
Expand Down
Loading

0 comments on commit 67ab12a

Please sign in to comment.