From 33778b33ddf1d7bf3d42a491d3d7f6155c062a43 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Wed, 28 Sep 2022 21:46:56 +0200 Subject: [PATCH] Start working on QuickPicks screen --- .../19.json | 670 ++++++++++++++++++ .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 18 +- .../it/vfsfitvnm/vimusic/models/Event.kt | 25 + .../vimusic/savers/DetailedSongSaver.kt | 2 +- .../it/vfsfitvnm/vimusic/savers/InfoSaver.kt | 5 +- .../vfsfitvnm/vimusic/savers/NullableSaver.kt | 13 + .../vfsfitvnm/vimusic/savers/ResultSaver.kt | 2 - .../vimusic/savers/YouTubeAlbumSaver.kt | 8 +- .../vimusic/savers/YouTubeArtistSaver.kt | 4 +- .../vimusic/savers/YouTubeBrowseInfoSaver.kt | 4 +- .../vimusic/savers/YouTubePlaylistSaver.kt | 8 +- .../vimusic/savers/YouTubeRelatedSaver.kt | 22 + .../vimusic/savers/YouTubeSongSaver.kt | 12 +- .../vimusic/savers/YouTubeVideoSaver.kt | 10 +- .../vimusic/savers/YouTubeWatchInfoSaver.kt | 4 +- .../vimusic/service/PlayerService.kt | 15 +- .../ui/components/themed/MediaItemMenu.kt | 10 +- .../vimusic/ui/screens/album/AlbumOverview.kt | 2 +- .../vimusic/ui/screens/artist/ArtistScreen.kt | 4 +- .../builtinplaylist/LocalPlaylistSongList.kt | 2 +- .../ui/screens/home/HomePlaylistList.kt | 10 +- .../vimusic/ui/screens/home/HomeScreen.kt | 29 +- .../vimusic/ui/screens/home/HomeSongList.kt | 2 +- .../vimusic/ui/screens/home/QuickPicks.kt | 214 ++++++ .../localplaylist/LocalPlaylistSongList.kt | 8 +- .../vimusic/ui/screens/player/Lyrics.kt | 2 +- .../ui/screens/player/PlayerBottomSheet.kt | 2 +- .../ui/screens/playlist/PlaylistSongList.kt | 10 +- .../ui/screens/search/LocalSongSearch.kt | 2 +- .../ui/screens/searchresult/SearchResult.kt | 2 +- .../searchresult/SearchResultScreen.kt | 10 +- .../it/vfsfitvnm/vimusic/ui/views/SongItem.kt | 16 +- .../vimusic/ui/views/YouTubeItems.kt | 16 +- .../it/vfsfitvnm/vimusic/utils/Utils.kt | 44 +- .../it/vfsfitvnm/youtubemusic/YouTube.kt | 412 +++++++---- .../models/MusicCarouselShelfRenderer.kt | 5 +- .../youtubemusic/models/NavigationEndpoint.kt | 2 +- 37 files changed, 1354 insertions(+), 272 deletions(-) create mode 100644 app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Event.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/NullableSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeRelatedSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json new file mode 100644 index 0000000..8196898 --- /dev/null +++ b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json @@ -0,0 +1,670 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "b9a9bb1674c7c50be2fab48de5afed43", + "entities": [ + { + "tableName": "Song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistsText", + "columnName": "artistsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durationText", + "columnName": "durationText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "synchronizedLyrics", + "columnName": "synchronizedLyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "likedAt", + "columnName": "likedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "totalPlayTimeMs", + "columnName": "totalPlayTimeMs", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongPlaylistMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlistId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "playlistId" + ] + }, + "indices": [ + { + "name": "index_SongPlaylistMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongPlaylistMap_playlistId", + "unique": false, + "columnNames": [ + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Playlist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "playlistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "browseId", + "columnName": "browseId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Artist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shuffleVideoId", + "columnName": "shuffleVideoId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shufflePlaylistId", + "columnName": "shufflePlaylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "radioVideoId", + "columnName": "radioVideoId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "radioPlaylistId", + "columnName": "radioPlaylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongArtistMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "artistId" + ] + }, + "indices": [ + { + "name": "index_SongArtistMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongArtistMap_artistId", + "unique": false, + "columnNames": [ + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Artist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "artistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Album", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorsText", + "columnName": "authorsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareUrl", + "columnName": "shareUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongAlbumMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "albumId" + ] + }, + "indices": [ + { + "name": "index_SongAlbumMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongAlbumMap_albumId", + "unique": false, + "columnNames": [ + "albumId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Album", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "albumId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SearchQuery", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_SearchQuery_query", + "unique": true, + "columnNames": [ + "query" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "QueuedMediaItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaItem", + "columnName": "mediaItem", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Format", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itag", + "columnName": "itag", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "contentLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "loudnessDb", + "columnName": "loudnessDb", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Event", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playTime", + "columnName": "playTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Event_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Event_songId` ON `${TABLE_NAME}` (`songId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [ + { + "viewName": "SortedSongPlaylistMap", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b9a9bb1674c7c50be2fab48de5afed43')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index aa9bd0a..2dc43b6 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -37,6 +37,7 @@ import it.vfsfitvnm.vimusic.models.Album import it.vfsfitvnm.vimusic.models.Artist import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength +import it.vfsfitvnm.vimusic.models.Event import it.vfsfitvnm.vimusic.models.Format import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.models.PlaylistPreview @@ -288,6 +289,19 @@ interface Database { @Query("SELECT EXISTS(SELECT 1 FROM Playlist WHERE browseId = :browseId)") fun isImportedPlaylist(browseId: String): Flow + @Transaction + @Query("SELECT Song.* FROM Event JOIN Song ON Song.id = songId GROUP BY songId ORDER BY SUM(playTime / ((:now - timestamp) / 86400000.0)) LIMIT 1") + @RewriteQueriesToDropUnusedColumns + fun trending(now: Long = System.currentTimeMillis()): Flow + +// @Transaction +// @Query("SELECT songId FROM Event GROUP BY songId ORDER BY SUM(playTime / ((:now - timestamp) / 86400000.0)) LIMIT 1") +// @RewriteQueriesToDropUnusedColumns +// fun trending(now: Long = System.currentTimeMillis()): Flow + + @Insert(onConflict = OnConflictStrategy.ABORT) + fun insert(event: Event) + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(format: Format) @@ -427,11 +441,12 @@ interface Database { SearchQuery::class, QueuedMediaItem::class, Format::class, + Event::class, ], views = [ SortedSongPlaylistMap::class ], - version = 18, + version = 19, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -448,6 +463,7 @@ interface Database { AutoMigration(from = 15, to = 16), AutoMigration(from = 16, to = 17), AutoMigration(from = 17, to = 18), + AutoMigration(from = 18, to = 19), ], ) @TypeConverters(Converters::class) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Event.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Event.kt new file mode 100644 index 0000000..912b88a --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Event.kt @@ -0,0 +1,25 @@ +package it.vfsfitvnm.vimusic.models + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Immutable +@Entity( + foreignKeys = [ + ForeignKey( + entity = Song::class, + parentColumns = ["id"], + childColumns = ["songId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class Event( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + @ColumnInfo(index = true) val songId: String, + val timestamp: Long, + val playTime: Long +) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt index db11ff5..5455701 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt @@ -26,6 +26,6 @@ object DetailedSongSaver : Saver> { thumbnailUrl = value[4] as String?, totalPlayTimeMs = value[5] as Long, albumId = value[6] as String?, - artists = InfoListSaver.restore(value[7] as List>) + artists = (value[7] as List>?)?.let(InfoListSaver::restore) ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt index 3f59e7b..9eae23b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt @@ -8,9 +8,6 @@ object InfoSaver : Saver> { override fun SaverScope.save(value: Info): List = listOf(value.id, value.name) override fun restore(value: List): Info? { - return if (value.size == 2) Info( - id = value[0], - name = value[1], - ) else null + return if (value.size == 2) Info(id = value[0], name = value[1]) else null } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/NullableSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/NullableSaver.kt new file mode 100644 index 0000000..4f56b9b --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/NullableSaver.kt @@ -0,0 +1,13 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope + +fun nullableSaver(saver: Saver) = + object : Saver { + override fun SaverScope.save(value: Original?): Saveable? = + value?.let { with(saver) { save(it) } } + + override fun restore(value: Saveable): Original? = + saver.restore(value) + } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt index 763d2c8..827f7eb 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt @@ -3,8 +3,6 @@ package it.vfsfitvnm.vimusic.savers import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.SaverScope -interface ResultSaver : Saver?, Pair> - fun resultSaver(saver: Saver) = object : Saver?, Pair> { override fun restore(value: Pair) = diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt index c4a5f8d..749db84 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt @@ -6,15 +6,15 @@ import it.vfsfitvnm.youtubemusic.YouTube object YouTubeAlbumSaver : Saver> { override fun SaverScope.save(value: YouTube.Item.Album): List = listOf( - with(YouTubeBrowseInfoSaver) { save(value.info) }, - with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } }, + value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } }, + value.authors?.let { with(YouTubeBrowseInfoListSaver) { save(it) } }, value.year, - with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } } ) @Suppress("UNCHECKED_CAST") override fun restore(value: List) = YouTube.Item.Album( - info = YouTubeBrowseInfoSaver.restore(value[0] as List), + info = (value[0] as List?)?.let(YouTubeBrowseInfoSaver::restore), authors = (value[1] as List>?)?.let(YouTubeBrowseInfoListSaver::restore), year = value[2] as String?, thumbnail = (value[3] as List?)?.let(YouTubeThumbnailSaver::restore) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt index 98a1965..7f602a7 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt @@ -6,13 +6,13 @@ import it.vfsfitvnm.youtubemusic.YouTube object YouTubeArtistSaver : Saver> { override fun SaverScope.save(value: YouTube.Item.Artist): List = listOf( - with(YouTubeBrowseInfoSaver) { save(value.info) }, + value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } }, value.subscribersCountText, with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } ) override fun restore(value: List) = YouTube.Item.Artist( - info = YouTubeBrowseInfoSaver.restore(value[0] as List), + info = (value[0] as List?)?.let(YouTubeBrowseInfoSaver::restore), subscribersCountText = value[1] as String?, thumbnail = (value[2] as List?)?.let(YouTubeThumbnailSaver::restore) ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt index a421e53..0e2bb9d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt @@ -8,11 +8,11 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint object YouTubeBrowseInfoSaver : Saver, List> { override fun SaverScope.save(value: YouTube.Info) = listOf( value.name, - with(YouTubeBrowseEndpointSaver) { value.endpoint?.let { save(it) } } + value.endpoint?.let { with(YouTubeBrowseEndpointSaver) { save(it) } } ) override fun restore(value: List) = YouTube.Info( - name = value[0] as String, + name = value[0] as String?, endpoint = (value[1] as List?)?.let(YouTubeBrowseEndpointSaver::restore) ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt index 9767efe..599e6f2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt @@ -6,14 +6,14 @@ import it.vfsfitvnm.youtubemusic.YouTube object YouTubePlaylistSaver : Saver> { override fun SaverScope.save(value: YouTube.Item.Playlist): List = listOf( - with(YouTubeBrowseInfoSaver) { save(value.info) }, - with(YouTubeBrowseInfoSaver) { value.channel?.let { save(it) } }, + value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } }, + value.channel?.let { with(YouTubeBrowseInfoSaver) { save(it) } }, value.songCount, - with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } } ) override fun restore(value: List) = YouTube.Item.Playlist( - info = YouTubeBrowseInfoSaver.restore(value[0] as List), + info = (value[0] as List?)?.let(YouTubeBrowseInfoSaver::restore), channel = (value[1] as List?)?.let(YouTubeBrowseInfoSaver::restore), songCount = value[2] as Int?, thumbnail = (value[3] as List?)?.let(YouTubeThumbnailSaver::restore) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeRelatedSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeRelatedSaver.kt new file mode 100644 index 0000000..6024b90 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeRelatedSaver.kt @@ -0,0 +1,22 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube + +object YouTubeRelatedSaver : Saver> { + override fun SaverScope.save(value: YouTube.Related): List = listOf( + value.songs?.let { with(YouTubeSongListSaver) { save(it) } }, + value.playlists?.let { with(YouTubePlaylistListSaver) { save(it) } }, + value.albums?.let { with(YouTubeAlbumListSaver) { save(it) } }, + value.artists?.let { with(YouTubeArtistListSaver) { save(it) } }, + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List) = YouTube.Related( + songs = (value[0] as List>?)?.let(YouTubeSongListSaver::restore), + playlists = (value[1] as List>?)?.let(YouTubePlaylistListSaver::restore), + albums = (value[2] as List>?)?.let(YouTubeAlbumListSaver::restore), + artists = (value[3] as List>?)?.let(YouTubeArtistListSaver::restore), + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt index 4b8cdf8..848efc0 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt @@ -6,17 +6,17 @@ import it.vfsfitvnm.youtubemusic.YouTube object YouTubeSongSaver : Saver> { override fun SaverScope.save(value: YouTube.Item.Song): List = listOf( - with(YouTubeWatchInfoSaver) { save(value.info) }, - with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } }, - with(YouTubeBrowseInfoSaver) { value.album?.let { save(it) } }, + value.info?.let { with(YouTubeWatchInfoSaver) { save(it) } }, + value.authors?.let { with(YouTubeBrowseInfoListSaver) { save(it) } }, + value.album?.let { with(YouTubeBrowseInfoSaver) { save(it) } }, value.durationText, - with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } } ) @Suppress("UNCHECKED_CAST") override fun restore(value: List) = YouTube.Item.Song( - info = YouTubeWatchInfoSaver.restore(value[0] as List), - authors = YouTubeBrowseInfoListSaver.restore(value[1] as List>), + info = (value[0] as List?)?.let(YouTubeWatchInfoSaver::restore), + authors = (value[1] as List>?)?.let(YouTubeBrowseInfoListSaver::restore), album = (value[2] as List?)?.let(YouTubeBrowseInfoSaver::restore), durationText = value[3] as String?, thumbnail = (value[4] as List?)?.let(YouTubeThumbnailSaver::restore) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt index b745939..1150385 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt @@ -6,17 +6,17 @@ import it.vfsfitvnm.youtubemusic.YouTube object YouTubeVideoSaver : Saver> { override fun SaverScope.save(value: YouTube.Item.Video): List = listOf( - with(YouTubeWatchInfoSaver) { save(value.info) }, - with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } }, + value.info?.let { with(YouTubeWatchInfoSaver) { save(it) } }, + value.authors?.let { with(YouTubeBrowseInfoListSaver) { save(it) } }, value.viewsText, value.durationText, - with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } } ) @Suppress("UNCHECKED_CAST") override fun restore(value: List) = YouTube.Item.Video( - info = YouTubeWatchInfoSaver.restore(value[0] as List), - authors = YouTubeBrowseInfoListSaver.restore(value[1] as List>), + info = (value[0] as List?)?.let(YouTubeWatchInfoSaver::restore), + authors = (value[1] as List>?)?.let(YouTubeBrowseInfoListSaver::restore), viewsText = value[2] as String?, durationText = value[3] as String?, thumbnail = (value[4] as List?)?.let(YouTubeThumbnailSaver::restore) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt index a563724..11c09f2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt @@ -8,11 +8,11 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint object YouTubeWatchInfoSaver : Saver, List> { override fun SaverScope.save(value: YouTube.Info) = listOf( value.name, - with(YouTubeWatchEndpointSaver) { value.endpoint?.let { save(it) } } + value.endpoint?.let { with(YouTubeWatchEndpointSaver) { save(it) } }, ) override fun restore(value: List) = YouTube.Info( - name = value[0] as String, + name = value[0] as String?, endpoint = (value[1] as List?)?.let(YouTubeWatchEndpointSaver::restore) ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt index 5225fbe..a815f6e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt @@ -65,6 +65,7 @@ import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.MainActivity import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.enums.ExoPlayerDiskCacheMaxSize +import it.vfsfitvnm.vimusic.models.Event import it.vfsfitvnm.vimusic.models.QueuedMediaItem import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.utils.InvincibleService @@ -285,11 +286,23 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene val totalPlayTimeMs = playbackStats.totalPlayTimeMs - if (totalPlayTimeMs > 2000) { + if (totalPlayTimeMs > 5000) { query { Database.incrementTotalPlayTimeMs(mediaItem.mediaId, totalPlayTimeMs) } } + + if (totalPlayTimeMs > 30000) { + query { + Database.insert( + Event( + songId = mediaItem.mediaId, + timestamp = System.currentTimeMillis(), + playTime = totalPlayTimeMs + ) + ) + } + } } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt index 6b9e7cf..43f6a01 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt @@ -1,5 +1,6 @@ package it.vfsfitvnm.vimusic.ui.components.themed +import android.content.Intent import android.text.format.DateUtils import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.ExperimentalAnimationApi @@ -57,7 +58,6 @@ import it.vfsfitvnm.vimusic.utils.color import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.semiBold -import it.vfsfitvnm.vimusic.utils.shareAsYouTubeSong import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOf @@ -241,7 +241,13 @@ fun BaseMediaItemMenu( onGoToAlbum = albumRoute::global, onGoToArtist = artistRoute::global, onShare = { - context.shareAsYouTubeSong(mediaItem) + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, "https://music.youtube.com/watch?v=${mediaItem.mediaId}") + } + + context.startActivity(Intent.createChooser(sendIntent, null)) }, modifier = modifier ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index bb04e88..75a430d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -95,7 +95,7 @@ fun AlbumOverview( title = youtubeAlbum.title, thumbnailUrl = youtubeAlbum.thumbnail?.url, year = youtubeAlbum.year, - authorsText = youtubeAlbum.authors?.joinToString("") { it.name }, + authorsText = youtubeAlbum.authors?.joinToString("") { it.name ?: "" }, shareUrl = youtubeAlbum.url, timestamp = System.currentTimeMillis() ), diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt index d9bf3e0..7f70d1a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt @@ -271,7 +271,7 @@ fun ArtistScreen2(browseId: String) { ) { index, song -> SongItem( song = song, - thumbnailSize = songThumbnailSizePx, + thumbnailSizePx = songThumbnailSizePx, onClick = { binder?.stopRadio() binder?.player?.forcePlayAtIndex( @@ -351,7 +351,7 @@ private suspend fun fetchArtist(browseId: String): Result? { ?.map { youtubeArtist -> Artist( id = browseId, - name = youtubeArtist.name, + name = youtubeArtist.name ?: "", thumbnailUrl = youtubeArtist.thumbnail?.url, info = youtubeArtist.description, shuffleVideoId = youtubeArtist.shuffleEndpoint?.videoId, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt index 50c9802..fc0cd19 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt @@ -103,7 +103,7 @@ fun BuiltInPlaylistSongList(builtInPlaylist: BuiltInPlaylist) { ) { index, song -> SongItem( song = song, - thumbnailSize = thumbnailSize, + thumbnailSizePx = thumbnailSize, onClick = { binder?.stopRadio() binder?.player?.forcePlayAtIndex( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt index 4fcdd18..3f391af 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt @@ -58,8 +58,8 @@ import kotlinx.coroutines.flow.flowOn @ExperimentalFoundationApi @Composable fun HomePlaylistList( - onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit, - onPlaylistClicked: (Playlist) -> Unit, + onBuiltInPlaylist: (BuiltInPlaylist) -> Unit, + onPlaylistClick: (Playlist) -> Unit, ) { val (colorPalette) = LocalAppearance.current @@ -186,7 +186,7 @@ fun HomePlaylistList( .clickable( indication = rememberRipple(bounded = true), interactionSource = remember { MutableInteractionSource() }, - onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Favorites) } + onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) } ) ) } @@ -200,7 +200,7 @@ fun HomePlaylistList( .clickable( indication = rememberRipple(bounded = true), interactionSource = remember { MutableInteractionSource() }, - onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Offline) } + onClick = { onBuiltInPlaylist(BuiltInPlaylist.Offline) } ) .animateItemPlacement() ) @@ -216,7 +216,7 @@ fun HomePlaylistList( .clickable( indication = rememberRipple(bounded = true), interactionSource = remember { MutableInteractionSource() }, - onClick = { onPlaylistClicked(playlistPreview.playlist) } + onClick = { onPlaylistClick(playlistPreview.playlist) } ) .animateItemPlacement() ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt index cbb720f..2be6d05 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt @@ -86,30 +86,27 @@ fun HomeScreen(onPlaylistUrl: (String) -> Unit) { tabIndex = tabIndex, onTabChanged = onTabChanged, tabColumnContent = { Item -> - Item(0, "Songs", R.drawable.musical_notes) - Item(1, "Playlists", R.drawable.playlist) - Item(2, "Artists", R.drawable.person) - Item(3, "Albums", R.drawable.disc) + Item(0, "Quick picks", R.drawable.sparkles) + Item(1, "Songs", R.drawable.musical_notes) + Item(2, "Playlists", R.drawable.playlist) + Item(3, "Artists", R.drawable.person) + Item(4, "Albums", R.drawable.disc) }, primaryIconButtonId = R.drawable.search, onPrimaryIconButtonClick = { searchRoute("") } ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { when (currentTabIndex) { - 1 -> HomePlaylistList( - onBuiltInPlaylistClicked = { builtInPlaylistRoute(it) }, - onPlaylistClicked = { localPlaylistRoute(it.id) } + 0 -> QuickPicks( + onAlbumClick = { albumRoute(it) }, ) - - 2 -> HomeArtistList( - onArtistClick = { artistRoute(it.id) } + 1 -> HomeSongList() + 2 -> HomePlaylistList( + onBuiltInPlaylist = { builtInPlaylistRoute(it) }, + onPlaylistClick = { localPlaylistRoute(it.id) } ) - - 3 -> HomeAlbumList( - onAlbumClick = { albumRoute(it.id) } - ) - - else -> HomeSongList() + 3 -> HomeArtistList(onArtistClick = { artistRoute(it.id) }) + 4 -> HomeAlbumList(onAlbumClick = { albumRoute(it.id) }) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt index 142065a..bd23931 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt @@ -162,7 +162,7 @@ fun HomeSongList() { ) { index, song -> SongItem( song = song, - thumbnailSize = thumbnailSize, + thumbnailSizePx = thumbnailSize, onClick = { binder?.stopRadio() binder?.player?.forcePlayAtIndex(items.map(DetailedSong::asMediaItem), index) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt new file mode 100644 index 0000000..6321a37 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt @@ -0,0 +1,214 @@ +package it.vfsfitvnm.vimusic.ui.screens.home + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder +import it.vfsfitvnm.vimusic.savers.DetailedSongSaver +import it.vfsfitvnm.vimusic.savers.YouTubeRelatedSaver +import it.vfsfitvnm.vimusic.savers.nullableSaver +import it.vfsfitvnm.vimusic.savers.resultSaver +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu +import it.vfsfitvnm.vimusic.ui.screens.albumRoute +import it.vfsfitvnm.vimusic.ui.styling.Dimensions +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.ui.styling.px +import it.vfsfitvnm.vimusic.ui.views.AlbumItem +import it.vfsfitvnm.vimusic.ui.views.SmallSongItem +import it.vfsfitvnm.vimusic.ui.views.SongItem +import it.vfsfitvnm.vimusic.utils.asMediaItem +import it.vfsfitvnm.vimusic.utils.forcePlay +import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState +import it.vfsfitvnm.vimusic.utils.produceSaveableState +import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.vimusic.utils.semiBold +import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOn + +@ExperimentalAnimationApi +@Composable +fun QuickPicks( + onAlbumClick: (String) -> Unit +) { + val (colorPalette, typography, thumbnailShape) = LocalAppearance.current + val binder = LocalPlayerServiceBinder.current + + val trending by produceSaveableState( + initialValue = null, + stateSaver = nullableSaver(DetailedSongSaver), + ) { + Database.trending() + .flowOn(Dispatchers.IO) + .filterNotNull() + .distinctUntilChanged() + .collect { value = it } + } + + val relatedResult by produceSaveableOneShotState( + initialValue = null, + stateSaver = resultSaver(nullableSaver(YouTubeRelatedSaver)), + trending?.id + ) { + println("trendingVideoId: ${trending?.id}") + trending?.id?.let { trendingVideoId -> + value = YouTube.related(trendingVideoId)?.map { related -> + related?.copy( + albums = related.albums?.map { album -> + album.copy( + authors = trending?.artists?.map { info -> + YouTube.Info( + name = info.name, + endpoint = NavigationEndpoint.Endpoint.Browse( + browseId = info.id, + params = null, + browseEndpointContextSupportedConfigs = null + ) + ) + } + ) + } + ) + } + } + } + + val songThumbnailSizePx = Dimensions.thumbnails.song.px + val albumThumbnailSizeDp = 108.dp + val albumThumbnailSizePx = albumThumbnailSizeDp.px +// val itemInHorizontalGridWidth = (LocalConfiguration.current.screenWidthDp.dp) * 0.8f + + LazyColumn( + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 + ) { + Header(title = "Quick picks") + } + + trending?.let { song -> + item(key = song.id) { + SongItem( + song = song, + thumbnailSizePx = songThumbnailSizePx, + onClick = { + val mediaItem = song.asMediaItem + binder?.stopRadio() + binder?.player?.forcePlay(mediaItem) + binder?.setupRadio( + NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) + ) + }, + menuContent = { + NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) + } + ) + } + } + + relatedResult?.getOrNull()?.let { related -> + items( + items = related.songs?.take(6) ?: emptyList(), + key = YouTube.Item::key + ) { song -> + SmallSongItem( + song = song, + thumbnailSizePx = songThumbnailSizePx, + onClick = { + val mediaItem = song.asMediaItem + binder?.stopRadio() + binder?.player?.forcePlay(mediaItem) + binder?.setupRadio( + NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) + ) + }, + ) + } + + item( + key = "albums", + contentType = "LazyRow" + ) { + LazyRow { + items( + items = related.albums ?: emptyList(), + key = YouTube.Item::key + ) { album -> + AlbumItem( + album = album, + thumbnailSizePx = albumThumbnailSizePx, + thumbnailSizeDp = albumThumbnailSizeDp, + modifier = Modifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { onAlbumClick(album.key) } + ) + .fillMaxWidth() + ) + } + } + } + + items( + items = related.songs?.drop(6) ?: emptyList(), + key = YouTube.Item::key + ) { song -> + SmallSongItem( + song = song, + thumbnailSizePx = songThumbnailSizePx, + onClick = { + val mediaItem = song.asMediaItem + binder?.stopRadio() + binder?.player?.forcePlay(mediaItem) + binder?.setupRadio( + NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) + ) + }, + ) + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt index 9153fc5..4ecd231 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt @@ -165,11 +165,7 @@ fun LocalPlaylistSongList( transaction { runBlocking(Dispatchers.IO) { withContext(Dispatchers.IO) { - YouTube.playlist(browseId)?.map { - it.next() - }?.map { playlist -> - playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null }) - } + YouTube.playlist(browseId)?.map { it.next() } } }?.getOrNull()?.let { remotePlaylist -> Database.clearPlaylist(playlistId) @@ -222,7 +218,7 @@ fun LocalPlaylistSongList( ) { index, song -> SongItem( song = song, - thumbnailSize = thumbnailSize, + thumbnailSizePx = thumbnailSize, onClick = { playlistWithSongs?.songs?.map(DetailedSong::asMediaItem) ?.let { mediaItems -> diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt index 2196223..6267680 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt @@ -135,7 +135,7 @@ fun Lyrics( )?.map { it?.value } } else { YouTube.next(mediaId, null) - ?.map { nextResult -> nextResult.lyrics?.text()?.getOrNull() } + ?.map { nextResult -> nextResult.lyrics()?.getOrNull() } }?.map { newLyrics -> onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "") state = state.copy(isLoading = false) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt index ba63067..04220bb 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt @@ -149,7 +149,7 @@ fun PlayerBottomSheet( SongItem( mediaItem = window.mediaItem, - thumbnailSize = thumbnailSize, + thumbnailSizePx = thumbnailSize, onClick = { if (isPlayingThisMediaItem) { if (shouldBePlaying) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt index 719ebd8..ca88584 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt @@ -81,11 +81,7 @@ fun PlaylistSongList( stateSaver = resultSaver(YouTubePlaylistOrAlbumSaver), ) { value = withContext(Dispatchers.IO) { - YouTube.playlist(browseId)?.map { - it.next() - }?.map { playlist -> - playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null }) - } + YouTube.playlist(browseId)?.map { it.next() } } } @@ -202,8 +198,8 @@ fun PlaylistSongList( itemsIndexed(items = playlist.songs ?: emptyList()) { index, song -> SongItem( - title = song.info.name, - authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name }, + title = song.info?.name, + authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name ?: "" }, durationText = song.durationText, onClick = { playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems -> diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt index 6abda8e..849b64e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt @@ -100,7 +100,7 @@ fun LocalSongSearch( ) { song -> SongItem( song = song, - thumbnailSize = thumbnailSize, + thumbnailSizePx = thumbnailSize, onClick = { val mediaItem = song.asMediaItem binder?.stopRadio() diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt index a291171..57b7015 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt @@ -92,7 +92,7 @@ inline fun SearchResult( items( items = items, - key = { it.key!! }, + key = YouTube.Item::key, itemContent = itemContent ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt index e46c1a7..1a75ddb 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt @@ -102,7 +102,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { onClick = { binder?.stopRadio() binder?.player?.forcePlay(song.asMediaItem) - binder?.setupRadio(song.info.endpoint) + binder?.setupRadio(song.info?.endpoint) } ) }, @@ -130,7 +130,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { .clickable( indication = rememberRipple(bounded = true), interactionSource = remember { MutableInteractionSource() }, - onClick = { albumRoute(album.info.endpoint?.browseId) } + onClick = { albumRoute(album.info?.endpoint?.browseId) } ) ) @@ -159,7 +159,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { .clickable( indication = rememberRipple(bounded = true), interactionSource = remember { MutableInteractionSource() }, - onClick = { artistRoute(artist.info.endpoint?.browseId) } + onClick = { artistRoute(artist.info?.endpoint?.browseId) } ) ) }, @@ -186,7 +186,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { onClick = { binder?.stopRadio() binder?.player?.forcePlay(video.asMediaItem) - binder?.setupRadio(video.info.endpoint) + binder?.setupRadio(video.info?.endpoint) } ) }, @@ -217,7 +217,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { .clickable( indication = rememberRipple(bounded = true), interactionSource = remember { MutableInteractionSource() }, - onClick = { playlistRoute(playlist.info.endpoint?.browseId) } + onClick = { playlistRoute(playlist.info?.endpoint?.browseId) } ) ) }, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt index a0b7672..7b6749c 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt @@ -40,7 +40,7 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @NonRestartableComposable fun SongItem( mediaItem: MediaItem, - thumbnailSize: Int, + thumbnailSizePx: Int, onClick: () -> Unit, menuContent: @Composable () -> Unit, modifier: Modifier = Modifier, @@ -48,7 +48,7 @@ fun SongItem( trailingContent: (@Composable () -> Unit)? = null ) { SongItem( - thumbnailModel = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSize), + thumbnailModel = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx), title = mediaItem.mediaMetadata.title!!.toString(), authors = mediaItem.mediaMetadata.artist.toString(), durationText = mediaItem.mediaMetadata.extras?.getString("durationText") ?: "?", @@ -65,7 +65,7 @@ fun SongItem( @NonRestartableComposable fun SongItem( song: DetailedSong, - thumbnailSize: Int, + thumbnailSizePx: Int, onClick: () -> Unit, menuContent: @Composable () -> Unit, modifier: Modifier = Modifier, @@ -73,7 +73,7 @@ fun SongItem( trailingContent: (@Composable () -> Unit)? = null ) { SongItem( - thumbnailModel = song.thumbnailUrl?.thumbnail(thumbnailSize), + thumbnailModel = song.thumbnailUrl?.thumbnail(thumbnailSizePx), title = song.title, authors = song.artistsText ?: "", durationText = song.durationText, @@ -90,8 +90,8 @@ fun SongItem( @NonRestartableComposable fun SongItem( thumbnailModel: Any?, - title: String, - authors: String, + title: String?, + authors: String?, durationText: String?, onClick: () -> Unit, menuContent: @Composable () -> Unit, @@ -131,7 +131,7 @@ fun SongItem( @ExperimentalAnimationApi @Composable fun SongItem( - title: String, + title: String?, authors: String?, durationText: String?, onClick: () -> Unit, @@ -167,7 +167,7 @@ fun SongItem( .weight(1f) ) { BasicText( - text = title, + text = title ?: "", style = typography.xs.semiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt index d62ad6c..35134f2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt @@ -79,8 +79,8 @@ fun SmallSongItem( ) { SongItem( thumbnailModel = song.thumbnail?.size(thumbnailSizePx), - title = song.info.name, - authors = song.authors?.joinToString("") { it.name } ?: "", + title = song.info?.name, + authors = song.authors?.joinToString("") { it.name ?: "" }, durationText = song.durationText, onClick = onClick, menuContent = { @@ -148,14 +148,14 @@ fun VideoItem( Column { BasicText( - text = video.info.name, + text = video.info?.name ?: "", style = typography.xs.semiBold, maxLines = 2, overflow = TextOverflow.Ellipsis, ) BasicText( - text = video.authors?.joinToString("") { it.name } ?: "", + text = video.authors?.joinToString("") { it.name ?: "" } ?: "", style = typography.xs.semiBold.secondary, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -252,7 +252,7 @@ fun PlaylistItem( Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { BasicText( - text = playlist.info.name, + text = playlist.info?.name ?: "", style = typography.xs.semiBold, maxLines = 2, overflow = TextOverflow.Ellipsis, @@ -322,14 +322,14 @@ fun AlbumItem( Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { BasicText( - text = album.info.name, + text = album.info?.name ?: "", style = typography.xs.semiBold, maxLines = 2, overflow = TextOverflow.Ellipsis, ) BasicText( - text = album.authors?.joinToString("") { it.name } ?: "", + text = album.authors?.joinToString("") { it.name ?: "" } ?: "", style = typography.xs.semiBold.secondary, maxLines = 2, overflow = TextOverflow.Ellipsis, @@ -406,7 +406,7 @@ fun ArtistItem( Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { BasicText( - text = artist.info.name, + text = artist.info?.name ?: "", style = typography.xs.semiBold, maxLines = 2, overflow = TextOverflow.Ellipsis diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt index 933d87e..8b83ac4 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt @@ -1,7 +1,5 @@ package it.vfsfitvnm.vimusic.utils -import android.content.Context -import android.content.Intent import android.net.Uri import androidx.core.net.toUri import androidx.core.os.bundleOf @@ -10,37 +8,23 @@ import androidx.media3.common.MediaMetadata import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.youtubemusic.YouTube -fun Context.shareAsYouTubeSong(mediaItem: MediaItem) { - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, "https://music.youtube.com/watch?v=${mediaItem.mediaId}") - } - - startActivity(Intent.createChooser(sendIntent, null)) -} - val YouTube.Item.Song.asMediaItem: MediaItem get() = MediaItem.Builder() - .also { -// println("$this") -// println(info.endpoint?.videoId) - } - .setMediaId(info.endpoint!!.videoId!!) - .setUri(info.endpoint!!.videoId) - .setCustomCacheKey(info.endpoint!!.videoId) + .setMediaId(key) + .setUri(key) + .setCustomCacheKey(key) .setMediaMetadata( MediaMetadata.Builder() - .setTitle(info.name) - .setArtist(authors?.joinToString("") { it.name }) + .setTitle(info?.name) + .setArtist(authors?.joinToString("") { it.name ?: "" }) .setAlbumTitle(album?.name) .setArtworkUri(thumbnail?.url?.toUri()) .setExtras( bundleOf( - "videoId" to info.endpoint!!.videoId, + "videoId" to key, "albumId" to album?.endpoint?.browseId, "durationText" to durationText, - "artistNames" to authors?.filter { it.endpoint != null }?.map { it.name }, + "artistNames" to authors?.filter { it.endpoint != null }?.mapNotNull { it.name }, "artistIds" to authors?.mapNotNull { it.endpoint?.browseId }, ) ) @@ -50,19 +34,19 @@ val YouTube.Item.Song.asMediaItem: MediaItem val YouTube.Item.Video.asMediaItem: MediaItem get() = MediaItem.Builder() - .setMediaId(info.endpoint!!.videoId!!) - .setUri(info.endpoint!!.videoId) - .setCustomCacheKey(info.endpoint!!.videoId) + .setMediaId(key) + .setUri(key) + .setCustomCacheKey(key) .setMediaMetadata( MediaMetadata.Builder() - .setTitle(info.name) - .setArtist(authors?.joinToString("") { it.name }) + .setTitle(info?.name) + .setArtist(authors?.joinToString("") { it.name ?: "" }) .setArtworkUri(thumbnail?.url?.toUri()) .setExtras( bundleOf( - "videoId" to info.endpoint!!.videoId, + "videoId" to key, "durationText" to durationText, - "artistNames" to if (isOfficialMusicVideo) authors?.filter { it.endpoint != null }?.map { it.name } else null, + "artistNames" to if (isOfficialMusicVideo) authors?.filter { it.endpoint != null }?.mapNotNull { it.name } else null, "artistIds" to if (isOfficialMusicVideo) authors?.mapNotNull { it.endpoint?.browseId } else null, ) ) diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt index fdc38e8..5317c1a 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt +++ b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt @@ -20,13 +20,16 @@ import it.vfsfitvnm.youtubemusic.models.BrowseResponse import it.vfsfitvnm.youtubemusic.models.ContinuationResponse import it.vfsfitvnm.youtubemusic.models.GetQueueResponse import it.vfsfitvnm.youtubemusic.models.GetSearchSuggestionsResponse +import it.vfsfitvnm.youtubemusic.models.MusicCarouselShelfRenderer import it.vfsfitvnm.youtubemusic.models.MusicResponsiveListItemRenderer import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer +import it.vfsfitvnm.youtubemusic.models.MusicTwoRowItemRenderer import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import it.vfsfitvnm.youtubemusic.models.NextResponse import it.vfsfitvnm.youtubemusic.models.PlayerResponse import it.vfsfitvnm.youtubemusic.models.Runs import it.vfsfitvnm.youtubemusic.models.SearchResponse +import it.vfsfitvnm.youtubemusic.models.SectionListRenderer import it.vfsfitvnm.youtubemusic.models.ThumbnailRenderer import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable @@ -36,7 +39,7 @@ import kotlinx.serialization.json.Json object YouTube { private const val Key = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" - val client = HttpClient(OkHttp) { + private val client = HttpClient(OkHttp) { BrowserUserAgent() expectSuccess = true @@ -162,37 +165,34 @@ object YouTube { } data class Info( - val name: String, + val name: String?, val endpoint: T? ) { - companion object { - inline fun from(run: Runs.Run): Info { - return Info( - name = run.text, - endpoint = run.navigationEndpoint?.endpoint as T? - ) - } - } + @Suppress("UNCHECKED_CAST") + constructor(run: Runs.Run) : this( + name = run.text, + endpoint = run.navigationEndpoint?.endpoint as T? + ) } sealed class Item { abstract val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? - abstract val key: String? + abstract val key: String data class Song( - val info: Info, + val info: Info?, val authors: List>?, val album: Info?, val durationText: String?, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? ) : Item() { - override val key: String? - get() = info.endpoint?.videoId + override val key: String + get() = info!!.endpoint!!.videoId!! - companion object : FromMusicShelfRendererContent { + companion object { val Filter = Filter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D") - override fun from(content: MusicShelfRenderer.Content): Song { + fun from(content: MusicShelfRenderer.Content): Song? { val (mainRuns, otherRuns) = content.runs // Possible configurations: @@ -210,21 +210,22 @@ object YouTube { ?.browseEndpoint ?.type == "MUSIC_PAGE_TYPE_ALBUM" } - ?.let(Info.Companion::from) + ?.let(::Info) return Song( - info = Info.from(mainRuns.first()), + info = mainRuns + .firstOrNull() + ?.let(::Info), authors = otherRuns .getOrNull(otherRuns.lastIndex - if (album == null) 1 else 2) - ?.map(Info.Companion::from) - ?: emptyList(), + ?.map(::Info), album = album, durationText = otherRuns .lastOrNull() ?.firstOrNull()?.text, thumbnail = content .thumbnail - ) + ).takeIf { it.info?.endpoint?.videoId != null } } fun from(renderer: MusicResponsiveListItemRenderer): Song? { @@ -236,15 +237,15 @@ object YouTube { ?.text ?.runs ?.getOrNull(0) - ?.let { Info.from(it) } ?: return null, + ?.let(::Info), authors = renderer .flexColumns .getOrNull(1) ?.musicResponsiveListItemFlexColumnRenderer ?.text ?.runs - ?.map { Info.from(it) } - ?.takeIf { it.isNotEmpty() }, + ?.map>(::Info) + ?.takeIf(List::isNotEmpty), durationText = renderer .fixedColumns ?.getOrNull(0) @@ -260,53 +261,55 @@ object YouTube { ?.text ?.runs ?.firstOrNull() - ?.let { Info.from(it) }, + ?.let(::Info), thumbnail = renderer .thumbnail ?.musicThumbnailRenderer ?.thumbnail ?.thumbnails ?.firstOrNull() - ) + ).takeIf { it.info?.endpoint?.videoId != null } } } } data class Video( - val info: Info, + val info: Info?, val authors: List>?, val viewsText: String?, val durationText: String?, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? ) : Item() { - override val key: String? - get() = info.endpoint?.videoId + override val key: String + get() = info!!.endpoint!!.videoId!! val isOfficialMusicVideo: Boolean get() = info - .endpoint + ?.endpoint ?.watchEndpointMusicSupportedConfigs ?.watchEndpointMusicConfig ?.musicVideoType == "MUSIC_VIDEO_TYPE_OMV" val isUserGeneratedContent: Boolean get() = info - .endpoint + ?.endpoint ?.watchEndpointMusicSupportedConfigs ?.watchEndpointMusicConfig ?.musicVideoType == "MUSIC_VIDEO_TYPE_UGC" - companion object : FromMusicShelfRendererContent