From b02153040bbb82757877861644bf5421af368b79 Mon Sep 17 00:00:00 2001 From: mattcarter11 <38189440+mattcarter11@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:34:07 +0200 Subject: [PATCH 1/2] show all account playlists, not only the first 24 --- .../java/com/zionhuang/innertube/YouTube.kt | 24 ++++++++++++++++--- .../innertube/models/GridRenderer.kt | 1 + .../models/response/BrowseResponse.kt | 8 +++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt index 4b386cfe0..5578d0f16 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt @@ -399,17 +399,35 @@ object YouTube { } suspend fun likedPlaylists(): Result> = runCatching { - val response = innerTube.browse( + var response = innerTube.browse( client = WEB_REMIX, browseId = "FEmusic_liked_playlists", setLogin = true ).body() - response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.gridRenderer?.items!! + val gridRenderer = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.gridRenderer + val playlists = gridRenderer?.items!! .drop(1) // the first item is "create new playlist" .mapNotNull(GridRenderer.Item::musicTwoRowItemRenderer) .mapNotNull { ArtistItemsPage.fromMusicTwoRowItemRenderer(it) as? PlaylistItem - } + }.toMutableList() + var continuation = gridRenderer?.continuations?.getContinuation() + while (continuation != null) { + response = innerTube.browse( + client = WEB_REMIX, + continuation = continuation, + setLogin = true + ).body() + val gridContinuation = response.continuationContents?.gridContinuation + playlists += gridContinuation?.items!! + .drop(1) // the first item is "create new playlist" + .mapNotNull(GridRenderer.Item::musicTwoRowItemRenderer) + .mapNotNull { + ArtistItemsPage.fromMusicTwoRowItemRenderer(it) as? PlaylistItem + } + continuation = gridContinuation?.continuations?.getContinuation() + } + playlists } suspend fun player(videoId: String, playlistId: String? = null): Result = runCatching { diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/GridRenderer.kt b/innertube/src/main/java/com/zionhuang/innertube/models/GridRenderer.kt index 795e8b277..783e7e3e5 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/models/GridRenderer.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/models/GridRenderer.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable data class GridRenderer( val header: Header?, val items: List, + val continuations: List? ) { @Serializable data class Header( diff --git a/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt b/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt index 2273fee40..616aa6b71 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/models/response/BrowseResponse.kt @@ -2,6 +2,7 @@ package com.zionhuang.innertube.models.response import com.zionhuang.innertube.models.Button import com.zionhuang.innertube.models.Continuation +import com.zionhuang.innertube.models.GridRenderer.Item import com.zionhuang.innertube.models.Menu import com.zionhuang.innertube.models.MusicShelfRenderer import com.zionhuang.innertube.models.ResponseContext @@ -49,6 +50,7 @@ data class BrowseResponse( data class ContinuationContents( val sectionListContinuation: SectionListContinuation?, val musicPlaylistShelfContinuation: MusicPlaylistShelfContinuation?, + val gridContinuation: GidContinuation ) { @Serializable data class SectionListContinuation( @@ -61,6 +63,12 @@ data class BrowseResponse( val contents: List, val continuations: List?, ) + + @Serializable + data class GidContinuation( + val items: List, + val continuations: List?, + ) } @Serializable From 90a9409e11da887d638f9242a6b48ff372c72890 Mon Sep 17 00:00:00 2001 From: mattcarter11 <38189440+mattcarter11@users.noreply.github.com> Date: Wed, 18 Sep 2024 20:39:08 +0200 Subject: [PATCH 2/2] add import playlist button, menu and logic --- .../zionhuang/music/ui/component/Dialog.kt | 7 ++- .../music/ui/menu/ImportPlaylistDialog.kt | 63 +++++++++++++++++++ .../music/ui/menu/YouTubePlaylistMenu.kt | 29 ++++++++- app/src/main/res/drawable/playlist_import.xml | 9 +++ 4 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/zionhuang/music/ui/menu/ImportPlaylistDialog.kt create mode 100644 app/src/main/res/drawable/playlist_import.xml diff --git a/app/src/main/java/com/zionhuang/music/ui/component/Dialog.kt b/app/src/main/java/com/zionhuang/music/ui/component/Dialog.kt index 7793ec1d0..f86dcb639 100644 --- a/app/src/main/java/com/zionhuang/music/ui/component/Dialog.kt +++ b/app/src/main/java/com/zionhuang/music/ui/component/Dialog.kt @@ -144,6 +144,7 @@ fun TextFieldDialog( initialTextFieldValue: TextFieldValue = TextFieldValue(), placeholder: @Composable (() -> Unit)? = null, singleLine: Boolean = true, + autoFocus: Boolean = true, maxLines: Int = if (singleLine) 1 else 10, isInputValid: (String) -> Boolean = { it.isNotEmpty() }, onDone: (String) -> Unit, @@ -158,8 +159,10 @@ fun TextFieldDialog( } LaunchedEffect(Unit) { - delay(300) - focusRequester.requestFocus() + if (autoFocus){ + delay(300) + focusRequester.requestFocus() + } } DefaultDialog( diff --git a/app/src/main/java/com/zionhuang/music/ui/menu/ImportPlaylistDialog.kt b/app/src/main/java/com/zionhuang/music/ui/menu/ImportPlaylistDialog.kt new file mode 100644 index 000000000..519801ab3 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/ui/menu/ImportPlaylistDialog.kt @@ -0,0 +1,63 @@ +package com.zionhuang.music.ui.menu + +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import com.zionhuang.music.LocalDatabase +import com.zionhuang.music.R +import com.zionhuang.music.db.entities.PlaylistEntity +import com.zionhuang.music.ui.component.TextFieldDialog +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch + +@Composable +fun ImportPlaylistDialog( + isVisible: Boolean, + onGetSong: suspend () -> List, // list of song ids. Songs should be inserted to database in this function. + playlistTitle: String, + onDismiss: () -> Unit, +) { + val database = LocalDatabase.current + val coroutineScope = rememberCoroutineScope() + + val textFieldValue by remember { mutableStateOf(TextFieldValue(text = playlistTitle)) } + var songIds by remember { + mutableStateOf?>(null) // list is not saveable + } + + if (isVisible) { + TextFieldDialog( + icon = { Icon(painter = painterResource(R.drawable.add), contentDescription = null) }, + title = { Text(text = stringResource(R.string.import_playlist)) }, + initialTextFieldValue = textFieldValue, + autoFocus = false, + onDismiss = onDismiss, + onDone = { finalName -> + val newPlaylist = PlaylistEntity( + name = finalName + ) + database.query { insert(newPlaylist) } + + coroutineScope.launch(Dispatchers.IO) { + val playlist = database.playlist(newPlaylist.id).firstOrNull() + + if (playlist != null){ + songIds = onGetSong() + database.addSongToPlaylist(playlist, songIds!!) + } + + onDismiss() + } + } + ) + } +} diff --git a/app/src/main/java/com/zionhuang/music/ui/menu/YouTubePlaylistMenu.kt b/app/src/main/java/com/zionhuang/music/ui/menu/YouTubePlaylistMenu.kt index c656a24bb..a596063b9 100644 --- a/app/src/main/java/com/zionhuang/music/ui/menu/YouTubePlaylistMenu.kt +++ b/app/src/main/java/com/zionhuang/music/ui/menu/YouTubePlaylistMenu.kt @@ -40,9 +40,8 @@ fun YouTubePlaylistMenu( val database = LocalDatabase.current val playerConnection = LocalPlayerConnection.current ?: return - var showChoosePlaylistDialog by rememberSaveable { - mutableStateOf(false) - } + var showChoosePlaylistDialog by rememberSaveable { mutableStateOf(false) } + var showImportPlaylistDialog by rememberSaveable { mutableStateOf(false) } AddToPlaylistDialog( isVisible = showChoosePlaylistDialog, @@ -61,6 +60,24 @@ fun YouTubePlaylistMenu( onDismiss = { showChoosePlaylistDialog = false } ) + ImportPlaylistDialog( + isVisible = showImportPlaylistDialog, + onGetSong = { + val allSongs = songs + .ifEmpty { + YouTube.playlist(playlist.id).completed().getOrNull()?.songs.orEmpty() + }.map { + it.toMediaMetadata() + } + database.transaction { + allSongs.forEach(::insert) + } + allSongs.map { it.id } + }, + playlistTitle = playlist.title, + onDismiss = { showImportPlaylistDialog = false } + ) + GridMenu( contentPadding = PaddingValues( start = 8.dp, @@ -124,6 +141,12 @@ fun YouTubePlaylistMenu( } onDismiss() } + GridMenuItem( + icon = R.drawable.playlist_import, + title = R.string.import_playlist + ) { + showImportPlaylistDialog = true + } GridMenuItem( icon = R.drawable.playlist_add, title = R.string.add_to_playlist diff --git a/app/src/main/res/drawable/playlist_import.xml b/app/src/main/res/drawable/playlist_import.xml new file mode 100644 index 000000000..8aa21ea72 --- /dev/null +++ b/app/src/main/res/drawable/playlist_import.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file