瀏覽代碼

Start working on QuickPicks screen

vfsfitvnm 2 年之前
父節點
當前提交
33778b33dd
共有 37 個文件被更改,包括 1354 次插入272 次删除
  1. 670 0
      app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json
  2. 17 1
      app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt
  3. 25 0
      app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Event.kt
  4. 1 1
      app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt
  5. 1 4
      app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt
  6. 13 0
      app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/NullableSaver.kt
  7. 0 2
      app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt
  8. 4 4
      app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt
  9. 2 2
      app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt
  10. 2 2
      app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt
  11. 4 4
      app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt
  12. 22 0
      app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeRelatedSaver.kt
  13. 6 6
      app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt
  14. 5 5
      app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt
  15. 2 2
      app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt
  16. 14 1
      app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt
  17. 8 2
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt
  18. 1 1
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt
  19. 2 2
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt
  20. 1 1
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt
  21. 5 5
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt
  22. 13 16
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt
  23. 1 1
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt
  24. 214 0
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt
  25. 2 6
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt
  26. 1 1
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt
  27. 1 1
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt
  28. 3 7
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt
  29. 1 1
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt
  30. 1 1
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt
  31. 5 5
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt
  32. 8 8
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt
  33. 8 8
      app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt
  34. 14 30
      app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt
  35. 274 138
      youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt
  36. 2 3
      youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicCarouselShelfRenderer.kt
  37. 1 1
      youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NavigationEndpoint.kt

+ 670 - 0
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')"
+    ]
+  }
+}

+ 17 - 1
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.Artist
 import it.vfsfitvnm.vimusic.models.DetailedSong
 import it.vfsfitvnm.vimusic.models.DetailedSong
 import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength
 import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength
+import it.vfsfitvnm.vimusic.models.Event
 import it.vfsfitvnm.vimusic.models.Format
 import it.vfsfitvnm.vimusic.models.Format
 import it.vfsfitvnm.vimusic.models.Playlist
 import it.vfsfitvnm.vimusic.models.Playlist
 import it.vfsfitvnm.vimusic.models.PlaylistPreview
 import it.vfsfitvnm.vimusic.models.PlaylistPreview
@@ -288,6 +289,19 @@ interface Database {
     @Query("SELECT EXISTS(SELECT 1 FROM Playlist WHERE browseId = :browseId)")
     @Query("SELECT EXISTS(SELECT 1 FROM Playlist WHERE browseId = :browseId)")
     fun isImportedPlaylist(browseId: String): Flow<Boolean>
     fun isImportedPlaylist(browseId: String): Flow<Boolean>
 
 
+    @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<DetailedSong?>
+
+//    @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<DetailedSong?>
+
+    @Insert(onConflict = OnConflictStrategy.ABORT)
+    fun insert(event: Event)
+
     @Insert(onConflict = OnConflictStrategy.REPLACE)
     @Insert(onConflict = OnConflictStrategy.REPLACE)
     fun insert(format: Format)
     fun insert(format: Format)
 
 
@@ -427,11 +441,12 @@ interface Database {
         SearchQuery::class,
         SearchQuery::class,
         QueuedMediaItem::class,
         QueuedMediaItem::class,
         Format::class,
         Format::class,
+        Event::class,
     ],
     ],
     views = [
     views = [
         SortedSongPlaylistMap::class
         SortedSongPlaylistMap::class
     ],
     ],
-    version = 18,
+    version = 19,
     exportSchema = true,
     exportSchema = true,
     autoMigrations = [
     autoMigrations = [
         AutoMigration(from = 1, to = 2),
         AutoMigration(from = 1, to = 2),
@@ -448,6 +463,7 @@ interface Database {
         AutoMigration(from = 15, to = 16),
         AutoMigration(from = 15, to = 16),
         AutoMigration(from = 16, to = 17),
         AutoMigration(from = 16, to = 17),
         AutoMigration(from = 17, to = 18),
         AutoMigration(from = 17, to = 18),
+        AutoMigration(from = 18, to = 19),
     ],
     ],
 )
 )
 @TypeConverters(Converters::class)
 @TypeConverters(Converters::class)

+ 25 - 0
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
+)

+ 1 - 1
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt

@@ -26,6 +26,6 @@ object DetailedSongSaver : Saver<DetailedSong, List<Any?>> {
         thumbnailUrl = value[4] as String?,
         thumbnailUrl = value[4] as String?,
         totalPlayTimeMs = value[5] as Long,
         totalPlayTimeMs = value[5] as Long,
         albumId = value[6] as String?,
         albumId = value[6] as String?,
-        artists = InfoListSaver.restore(value[7] as List<List<String>>)
+        artists = (value[7] as List<List<String>>?)?.let(InfoListSaver::restore)
     )
     )
 }
 }

+ 1 - 4
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt

@@ -8,9 +8,6 @@ object InfoSaver : Saver<Info, List<String>> {
     override fun SaverScope.save(value: Info): List<String> = listOf(value.id, value.name)
     override fun SaverScope.save(value: Info): List<String> = listOf(value.id, value.name)
 
 
     override fun restore(value: List<String>): Info? {
     override fun restore(value: List<String>): 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
     }
     }
 }
 }

+ 13 - 0
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 <Original, Saveable : Any> nullableSaver(saver: Saver<Original, Saveable>) =
+    object : Saver<Original?, Saveable> {
+        override fun SaverScope.save(value: Original?): Saveable? =
+            value?.let { with(saver) { save(it) } }
+
+        override fun restore(value: Saveable): Original? =
+            saver.restore(value)
+    }

+ 0 - 2
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.Saver
 import androidx.compose.runtime.saveable.SaverScope
 import androidx.compose.runtime.saveable.SaverScope
 
 
-interface ResultSaver<Original, Saveable> : Saver<Result<Original>?, Pair<Saveable?, Throwable?>>
-
 fun <Original, Saveable : Any> resultSaver(saver: Saver<Original, Saveable>) =
 fun <Original, Saveable : Any> resultSaver(saver: Saver<Original, Saveable>) =
     object : Saver<Result<Original>?, Pair<Saveable?, Throwable?>> {
     object : Saver<Result<Original>?, Pair<Saveable?, Throwable?>> {
         override fun restore(value: Pair<Saveable?, Throwable?>) =
         override fun restore(value: Pair<Saveable?, Throwable?>) =

+ 4 - 4
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt

@@ -6,15 +6,15 @@ import it.vfsfitvnm.youtubemusic.YouTube
 
 
 object YouTubeAlbumSaver : Saver<YouTube.Item.Album, List<Any?>> {
 object YouTubeAlbumSaver : Saver<YouTube.Item.Album, List<Any?>> {
     override fun SaverScope.save(value: YouTube.Item.Album): List<Any?> = listOf(
     override fun SaverScope.save(value: YouTube.Item.Album): List<Any?> = 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,
         value.year,
-        with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
+        value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
     )
     )
 
 
     @Suppress("UNCHECKED_CAST")
     @Suppress("UNCHECKED_CAST")
     override fun restore(value: List<Any?>) = YouTube.Item.Album(
     override fun restore(value: List<Any?>) = YouTube.Item.Album(
-        info = YouTubeBrowseInfoSaver.restore(value[0] as List<Any?>),
+        info = (value[0] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
         authors = (value[1] as List<List<Any?>>?)?.let(YouTubeBrowseInfoListSaver::restore),
         authors = (value[1] as List<List<Any?>>?)?.let(YouTubeBrowseInfoListSaver::restore),
         year = value[2] as String?,
         year = value[2] as String?,
         thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
         thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)

+ 2 - 2
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt

@@ -6,13 +6,13 @@ import it.vfsfitvnm.youtubemusic.YouTube
 
 
 object YouTubeArtistSaver : Saver<YouTube.Item.Artist, List<Any?>> {
 object YouTubeArtistSaver : Saver<YouTube.Item.Artist, List<Any?>> {
     override fun SaverScope.save(value: YouTube.Item.Artist): List<Any?> = listOf(
     override fun SaverScope.save(value: YouTube.Item.Artist): List<Any?> = listOf(
-        with(YouTubeBrowseInfoSaver) { save(value.info) },
+        value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } },
         value.subscribersCountText,
         value.subscribersCountText,
         with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
         with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
     )
     )
 
 
     override fun restore(value: List<Any?>) = YouTube.Item.Artist(
     override fun restore(value: List<Any?>) = YouTube.Item.Artist(
-        info = YouTubeBrowseInfoSaver.restore(value[0] as List<Any?>),
+        info = (value[0] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
         subscribersCountText = value[1] as String?,
         subscribersCountText = value[1] as String?,
         thumbnail = (value[2] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
         thumbnail = (value[2] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
     )
     )

+ 2 - 2
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt

@@ -8,11 +8,11 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
 object YouTubeBrowseInfoSaver : Saver<YouTube.Info<NavigationEndpoint.Endpoint.Browse>, List<Any?>> {
 object YouTubeBrowseInfoSaver : Saver<YouTube.Info<NavigationEndpoint.Endpoint.Browse>, List<Any?>> {
     override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Browse>) = listOf(
     override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Browse>) = listOf(
         value.name,
         value.name,
-        with(YouTubeBrowseEndpointSaver) { value.endpoint?.let { save(it) } }
+        value.endpoint?.let { with(YouTubeBrowseEndpointSaver) { save(it) } }
     )
     )
 
 
     override fun restore(value: List<Any?>) = YouTube.Info(
     override fun restore(value: List<Any?>) = YouTube.Info(
-        name = value[0] as String,
+        name = value[0] as String?,
         endpoint = (value[1] as List<Any?>?)?.let(YouTubeBrowseEndpointSaver::restore)
         endpoint = (value[1] as List<Any?>?)?.let(YouTubeBrowseEndpointSaver::restore)
     )
     )
 }
 }

+ 4 - 4
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt

@@ -6,14 +6,14 @@ import it.vfsfitvnm.youtubemusic.YouTube
 
 
 object YouTubePlaylistSaver : Saver<YouTube.Item.Playlist, List<Any?>> {
 object YouTubePlaylistSaver : Saver<YouTube.Item.Playlist, List<Any?>> {
     override fun SaverScope.save(value: YouTube.Item.Playlist): List<Any?> = listOf(
     override fun SaverScope.save(value: YouTube.Item.Playlist): List<Any?> = 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,
         value.songCount,
-        with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
+        value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
     )
     )
 
 
     override fun restore(value: List<Any?>) = YouTube.Item.Playlist(
     override fun restore(value: List<Any?>) = YouTube.Item.Playlist(
-        info = YouTubeBrowseInfoSaver.restore(value[0] as List<Any?>),
+        info = (value[0] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
         channel = (value[1] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
         channel = (value[1] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
         songCount = value[2] as Int?,
         songCount = value[2] as Int?,
         thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
         thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)

+ 22 - 0
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<YouTube.Related, List<Any?>> {
+    override fun SaverScope.save(value: YouTube.Related): List<Any?> = 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<Any?>) = YouTube.Related(
+        songs = (value[0] as List<List<Any?>>?)?.let(YouTubeSongListSaver::restore),
+        playlists = (value[1] as List<List<Any?>>?)?.let(YouTubePlaylistListSaver::restore),
+        albums = (value[2] as List<List<Any?>>?)?.let(YouTubeAlbumListSaver::restore),
+        artists = (value[3] as List<List<Any?>>?)?.let(YouTubeArtistListSaver::restore),
+    )
+}

+ 6 - 6
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt

@@ -6,17 +6,17 @@ import it.vfsfitvnm.youtubemusic.YouTube
 
 
 object YouTubeSongSaver : Saver<YouTube.Item.Song, List<Any?>> {
 object YouTubeSongSaver : Saver<YouTube.Item.Song, List<Any?>> {
     override fun SaverScope.save(value: YouTube.Item.Song): List<Any?> = listOf(
     override fun SaverScope.save(value: YouTube.Item.Song): List<Any?> = 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,
         value.durationText,
-        with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
+        value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
     )
     )
 
 
     @Suppress("UNCHECKED_CAST")
     @Suppress("UNCHECKED_CAST")
     override fun restore(value: List<Any?>) = YouTube.Item.Song(
     override fun restore(value: List<Any?>) = YouTube.Item.Song(
-        info = YouTubeWatchInfoSaver.restore(value[0] as List<Any?>),
-        authors = YouTubeBrowseInfoListSaver.restore(value[1] as List<List<Any?>>),
+        info = (value[0] as List<Any?>?)?.let(YouTubeWatchInfoSaver::restore),
+        authors = (value[1] as List<List<Any?>>?)?.let(YouTubeBrowseInfoListSaver::restore),
         album = (value[2] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
         album = (value[2] as List<Any?>?)?.let(YouTubeBrowseInfoSaver::restore),
         durationText = value[3] as String?,
         durationText = value[3] as String?,
         thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
         thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)

+ 5 - 5
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt

@@ -6,17 +6,17 @@ import it.vfsfitvnm.youtubemusic.YouTube
 
 
 object YouTubeVideoSaver : Saver<YouTube.Item.Video, List<Any?>> {
 object YouTubeVideoSaver : Saver<YouTube.Item.Video, List<Any?>> {
     override fun SaverScope.save(value: YouTube.Item.Video): List<Any?> = listOf(
     override fun SaverScope.save(value: YouTube.Item.Video): List<Any?> = 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.viewsText,
         value.durationText,
         value.durationText,
-        with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
+        value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
     )
     )
 
 
     @Suppress("UNCHECKED_CAST")
     @Suppress("UNCHECKED_CAST")
     override fun restore(value: List<Any?>) = YouTube.Item.Video(
     override fun restore(value: List<Any?>) = YouTube.Item.Video(
-        info = YouTubeWatchInfoSaver.restore(value[0] as List<Any?>),
-        authors = YouTubeBrowseInfoListSaver.restore(value[1] as List<List<Any?>>),
+        info = (value[0] as List<Any?>?)?.let(YouTubeWatchInfoSaver::restore),
+        authors = (value[1] as List<List<Any?>>?)?.let(YouTubeBrowseInfoListSaver::restore),
         viewsText = value[2] as String?,
         viewsText = value[2] as String?,
         durationText = value[3] as String?,
         durationText = value[3] as String?,
         thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
         thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)

+ 2 - 2
app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt

@@ -8,11 +8,11 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
 object YouTubeWatchInfoSaver : Saver<YouTube.Info<NavigationEndpoint.Endpoint.Watch>, List<Any?>> {
 object YouTubeWatchInfoSaver : Saver<YouTube.Info<NavigationEndpoint.Endpoint.Watch>, List<Any?>> {
     override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Watch>) = listOf(
     override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Watch>) = listOf(
         value.name,
         value.name,
-        with(YouTubeWatchEndpointSaver) { value.endpoint?.let { save(it) } }
+        value.endpoint?.let { with(YouTubeWatchEndpointSaver) { save(it) } },
     )
     )
 
 
     override fun restore(value: List<Any?>) = YouTube.Info(
     override fun restore(value: List<Any?>) = YouTube.Info(
-        name = value[0] as String,
+        name = value[0] as String?,
         endpoint = (value[1] as List<Any?>?)?.let(YouTubeWatchEndpointSaver::restore)
         endpoint = (value[1] as List<Any?>?)?.let(YouTubeWatchEndpointSaver::restore)
     )
     )
 }
 }

+ 14 - 1
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.MainActivity
 import it.vfsfitvnm.vimusic.R
 import it.vfsfitvnm.vimusic.R
 import it.vfsfitvnm.vimusic.enums.ExoPlayerDiskCacheMaxSize
 import it.vfsfitvnm.vimusic.enums.ExoPlayerDiskCacheMaxSize
+import it.vfsfitvnm.vimusic.models.Event
 import it.vfsfitvnm.vimusic.models.QueuedMediaItem
 import it.vfsfitvnm.vimusic.models.QueuedMediaItem
 import it.vfsfitvnm.vimusic.query
 import it.vfsfitvnm.vimusic.query
 import it.vfsfitvnm.vimusic.utils.InvincibleService
 import it.vfsfitvnm.vimusic.utils.InvincibleService
@@ -285,11 +286,23 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
 
 
         val totalPlayTimeMs = playbackStats.totalPlayTimeMs
         val totalPlayTimeMs = playbackStats.totalPlayTimeMs
 
 
-        if (totalPlayTimeMs > 2000) {
+        if (totalPlayTimeMs > 5000) {
             query {
             query {
                 Database.incrementTotalPlayTimeMs(mediaItem.mediaId, totalPlayTimeMs)
                 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) {
     override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {

+ 8 - 2
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt

@@ -1,5 +1,6 @@
 package it.vfsfitvnm.vimusic.ui.components.themed
 package it.vfsfitvnm.vimusic.ui.components.themed
 
 
+import android.content.Intent
 import android.text.format.DateUtils
 import android.text.format.DateUtils
 import androidx.compose.animation.AnimatedContentScope
 import androidx.compose.animation.AnimatedContentScope
 import androidx.compose.animation.ExperimentalAnimationApi
 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.enqueue
 import it.vfsfitvnm.vimusic.utils.forcePlay
 import it.vfsfitvnm.vimusic.utils.forcePlay
 import it.vfsfitvnm.vimusic.utils.semiBold
 import it.vfsfitvnm.vimusic.utils.semiBold
-import it.vfsfitvnm.vimusic.utils.shareAsYouTubeSong
 import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
 import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.flowOf
@@ -241,7 +241,13 @@ fun BaseMediaItemMenu(
         onGoToAlbum = albumRoute::global,
         onGoToAlbum = albumRoute::global,
         onGoToArtist = artistRoute::global,
         onGoToArtist = artistRoute::global,
         onShare = {
         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
         modifier = modifier
     )
     )

+ 1 - 1
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt

@@ -95,7 +95,7 @@ fun AlbumOverview(
                                 title = youtubeAlbum.title,
                                 title = youtubeAlbum.title,
                                 thumbnailUrl = youtubeAlbum.thumbnail?.url,
                                 thumbnailUrl = youtubeAlbum.thumbnail?.url,
                                 year = youtubeAlbum.year,
                                 year = youtubeAlbum.year,
-                                authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
+                                authorsText = youtubeAlbum.authors?.joinToString("") { it.name ?: "" },
                                 shareUrl = youtubeAlbum.url,
                                 shareUrl = youtubeAlbum.url,
                                 timestamp = System.currentTimeMillis()
                                 timestamp = System.currentTimeMillis()
                             ),
                             ),

+ 2 - 2
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt

@@ -271,7 +271,7 @@ fun ArtistScreen2(browseId: String) {
                 ) { index, song ->
                 ) { index, song ->
                     SongItem(
                     SongItem(
                         song = song,
                         song = song,
-                        thumbnailSize = songThumbnailSizePx,
+                        thumbnailSizePx = songThumbnailSizePx,
                         onClick = {
                         onClick = {
                             binder?.stopRadio()
                             binder?.stopRadio()
                             binder?.player?.forcePlayAtIndex(
                             binder?.player?.forcePlayAtIndex(
@@ -351,7 +351,7 @@ private suspend fun fetchArtist(browseId: String): Result<Artist>? {
         ?.map { youtubeArtist ->
         ?.map { youtubeArtist ->
             Artist(
             Artist(
                 id = browseId,
                 id = browseId,
-                name = youtubeArtist.name,
+                name = youtubeArtist.name ?: "",
                 thumbnailUrl = youtubeArtist.thumbnail?.url,
                 thumbnailUrl = youtubeArtist.thumbnail?.url,
                 info = youtubeArtist.description,
                 info = youtubeArtist.description,
                 shuffleVideoId = youtubeArtist.shuffleEndpoint?.videoId,
                 shuffleVideoId = youtubeArtist.shuffleEndpoint?.videoId,

+ 1 - 1
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt

@@ -103,7 +103,7 @@ fun BuiltInPlaylistSongList(builtInPlaylist: BuiltInPlaylist) {
             ) { index, song ->
             ) { index, song ->
                 SongItem(
                 SongItem(
                     song = song,
                     song = song,
-                    thumbnailSize = thumbnailSize,
+                    thumbnailSizePx = thumbnailSize,
                     onClick = {
                     onClick = {
                         binder?.stopRadio()
                         binder?.stopRadio()
                         binder?.player?.forcePlayAtIndex(
                         binder?.player?.forcePlayAtIndex(

+ 5 - 5
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt

@@ -58,8 +58,8 @@ import kotlinx.coroutines.flow.flowOn
 @ExperimentalFoundationApi
 @ExperimentalFoundationApi
 @Composable
 @Composable
 fun HomePlaylistList(
 fun HomePlaylistList(
-    onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit,
-    onPlaylistClicked: (Playlist) -> Unit,
+    onBuiltInPlaylist: (BuiltInPlaylist) -> Unit,
+    onPlaylistClick: (Playlist) -> Unit,
 ) {
 ) {
     val (colorPalette) = LocalAppearance.current
     val (colorPalette) = LocalAppearance.current
 
 
@@ -186,7 +186,7 @@ fun HomePlaylistList(
                     .clickable(
                     .clickable(
                         indication = rememberRipple(bounded = true),
                         indication = rememberRipple(bounded = true),
                         interactionSource = remember { MutableInteractionSource() },
                         interactionSource = remember { MutableInteractionSource() },
-                        onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Favorites) }
+                        onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) }
                     )
                     )
             )
             )
         }
         }
@@ -200,7 +200,7 @@ fun HomePlaylistList(
                     .clickable(
                     .clickable(
                         indication = rememberRipple(bounded = true),
                         indication = rememberRipple(bounded = true),
                         interactionSource = remember { MutableInteractionSource() },
                         interactionSource = remember { MutableInteractionSource() },
-                        onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Offline) }
+                        onClick = { onBuiltInPlaylist(BuiltInPlaylist.Offline) }
                     )
                     )
                     .animateItemPlacement()
                     .animateItemPlacement()
             )
             )
@@ -216,7 +216,7 @@ fun HomePlaylistList(
                     .clickable(
                     .clickable(
                         indication = rememberRipple(bounded = true),
                         indication = rememberRipple(bounded = true),
                         interactionSource = remember { MutableInteractionSource() },
                         interactionSource = remember { MutableInteractionSource() },
-                        onClick = { onPlaylistClicked(playlistPreview.playlist) }
+                        onClick = { onPlaylistClick(playlistPreview.playlist) }
                     )
                     )
                     .animateItemPlacement()
                     .animateItemPlacement()
             )
             )

+ 13 - 16
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt

@@ -86,30 +86,27 @@ fun HomeScreen(onPlaylistUrl: (String) -> Unit) {
                 tabIndex = tabIndex,
                 tabIndex = tabIndex,
                 onTabChanged = onTabChanged,
                 onTabChanged = onTabChanged,
                 tabColumnContent = { Item ->
                 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,
                 primaryIconButtonId = R.drawable.search,
                 onPrimaryIconButtonClick = { searchRoute("") }
                 onPrimaryIconButtonClick = { searchRoute("") }
             ) { currentTabIndex ->
             ) { currentTabIndex ->
                 saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
                 saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
                     when (currentTabIndex) {
                     when (currentTabIndex) {
-                        1 -> HomePlaylistList(
-                            onBuiltInPlaylistClicked = { builtInPlaylistRoute(it) },
-                            onPlaylistClicked = { localPlaylistRoute(it.id) }
+                        0 -> QuickPicks(
+                            onAlbumClick = { albumRoute(it) },
                         )
                         )
-
-                        2 -> HomeArtistList(
-                            onArtistClick = { artistRoute(it.id) }
-                        )
-
-                        3 -> HomeAlbumList(
-                            onAlbumClick = { albumRoute(it.id) }
+                        1 -> HomeSongList()
+                        2 -> HomePlaylistList(
+                            onBuiltInPlaylist = { builtInPlaylistRoute(it) },
+                            onPlaylistClick = { localPlaylistRoute(it.id) }
                         )
                         )
-
-                        else -> HomeSongList()
+                        3 -> HomeArtistList(onArtistClick = { artistRoute(it.id) })
+                        4 -> HomeAlbumList(onAlbumClick = { albumRoute(it.id) })
                     }
                     }
                 }
                 }
             }
             }

+ 1 - 1
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt

@@ -162,7 +162,7 @@ fun HomeSongList() {
             ) { index, song ->
             ) { index, song ->
                 SongItem(
                 SongItem(
                     song = song,
                     song = song,
-                    thumbnailSize = thumbnailSize,
+                    thumbnailSizePx = thumbnailSize,
                     onClick = {
                     onClick = {
                         binder?.stopRadio()
                         binder?.stopRadio()
                         binder?.player?.forcePlayAtIndex(items.map(DetailedSong::asMediaItem), index)
                         binder?.player?.forcePlayAtIndex(items.map(DetailedSong::asMediaItem), index)

+ 214 - 0
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)
+                        )
+                    },
+                )
+            }
+        }
+    }
+}

+ 2 - 6
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt

@@ -165,11 +165,7 @@ fun LocalPlaylistSongList(
                                     transaction {
                                     transaction {
                                         runBlocking(Dispatchers.IO) {
                                         runBlocking(Dispatchers.IO) {
                                             withContext(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 ->
                                         }?.getOrNull()?.let { remotePlaylist ->
                                             Database.clearPlaylist(playlistId)
                                             Database.clearPlaylist(playlistId)
@@ -222,7 +218,7 @@ fun LocalPlaylistSongList(
             ) { index, song ->
             ) { index, song ->
                 SongItem(
                 SongItem(
                     song = song,
                     song = song,
-                    thumbnailSize = thumbnailSize,
+                    thumbnailSizePx = thumbnailSize,
                     onClick = {
                     onClick = {
                         playlistWithSongs?.songs?.map(DetailedSong::asMediaItem)
                         playlistWithSongs?.songs?.map(DetailedSong::asMediaItem)
                             ?.let { mediaItems ->
                             ?.let { mediaItems ->

+ 1 - 1
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt

@@ -135,7 +135,7 @@ fun Lyrics(
                     )?.map { it?.value }
                     )?.map { it?.value }
                 } else {
                 } else {
                     YouTube.next(mediaId, null)
                     YouTube.next(mediaId, null)
-                        ?.map { nextResult -> nextResult.lyrics?.text()?.getOrNull() }
+                        ?.map { nextResult -> nextResult.lyrics()?.getOrNull() }
                 }?.map { newLyrics ->
                 }?.map { newLyrics ->
                     onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "")
                     onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "")
                     state = state.copy(isLoading = false)
                     state = state.copy(isLoading = false)

+ 1 - 1
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt

@@ -149,7 +149,7 @@ fun PlayerBottomSheet(
 
 
                     SongItem(
                     SongItem(
                         mediaItem = window.mediaItem,
                         mediaItem = window.mediaItem,
-                        thumbnailSize = thumbnailSize,
+                        thumbnailSizePx = thumbnailSize,
                         onClick = {
                         onClick = {
                             if (isPlayingThisMediaItem) {
                             if (isPlayingThisMediaItem) {
                                 if (shouldBePlaying) {
                                 if (shouldBePlaying) {

+ 3 - 7
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt

@@ -81,11 +81,7 @@ fun PlaylistSongList(
         stateSaver = resultSaver(YouTubePlaylistOrAlbumSaver),
         stateSaver = resultSaver(YouTubePlaylistOrAlbumSaver),
     ) {
     ) {
         value = withContext(Dispatchers.IO) {
         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 ->
                 itemsIndexed(items = playlist.songs ?: emptyList()) { index, song ->
                     SongItem(
                     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,
                         durationText = song.durationText,
                         onClick = {
                         onClick = {
                             playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
                             playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems ->

+ 1 - 1
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt

@@ -100,7 +100,7 @@ fun LocalSongSearch(
         ) { song ->
         ) { song ->
             SongItem(
             SongItem(
                 song = song,
                 song = song,
-                thumbnailSize = thumbnailSize,
+                thumbnailSizePx = thumbnailSize,
                 onClick = {
                 onClick = {
                     val mediaItem = song.asMediaItem
                     val mediaItem = song.asMediaItem
                     binder?.stopRadio()
                     binder?.stopRadio()

+ 1 - 1
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt

@@ -92,7 +92,7 @@ inline fun <T : YouTube.Item> SearchResult(
 
 
         items(
         items(
             items = items,
             items = items,
-            key = { it.key!! },
+            key = YouTube.Item::key,
             itemContent = itemContent
             itemContent = itemContent
         )
         )
 
 

+ 5 - 5
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt

@@ -102,7 +102,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
                                         onClick = {
                                         onClick = {
                                             binder?.stopRadio()
                                             binder?.stopRadio()
                                             binder?.player?.forcePlay(song.asMediaItem)
                                             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(
                                             .clickable(
                                                 indication = rememberRipple(bounded = true),
                                                 indication = rememberRipple(bounded = true),
                                                 interactionSource = remember { MutableInteractionSource() },
                                                 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(
                                             .clickable(
                                                 indication = rememberRipple(bounded = true),
                                                 indication = rememberRipple(bounded = true),
                                                 interactionSource = remember { MutableInteractionSource() },
                                                 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 = {
                                         onClick = {
                                             binder?.stopRadio()
                                             binder?.stopRadio()
                                             binder?.player?.forcePlay(video.asMediaItem)
                                             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(
                                             .clickable(
                                                 indication = rememberRipple(bounded = true),
                                                 indication = rememberRipple(bounded = true),
                                                 interactionSource = remember { MutableInteractionSource() },
                                                 interactionSource = remember { MutableInteractionSource() },
-                                                onClick = { playlistRoute(playlist.info.endpoint?.browseId) }
+                                                onClick = { playlistRoute(playlist.info?.endpoint?.browseId) }
                                             )
                                             )
                                     )
                                     )
                                 },
                                 },

+ 8 - 8
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt

@@ -40,7 +40,7 @@ import it.vfsfitvnm.vimusic.utils.thumbnail
 @NonRestartableComposable
 @NonRestartableComposable
 fun SongItem(
 fun SongItem(
     mediaItem: MediaItem,
     mediaItem: MediaItem,
-    thumbnailSize: Int,
+    thumbnailSizePx: Int,
     onClick: () -> Unit,
     onClick: () -> Unit,
     menuContent: @Composable () -> Unit,
     menuContent: @Composable () -> Unit,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
@@ -48,7 +48,7 @@ fun SongItem(
     trailingContent: (@Composable () -> Unit)? = null
     trailingContent: (@Composable () -> Unit)? = null
 ) {
 ) {
     SongItem(
     SongItem(
-        thumbnailModel = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSize),
+        thumbnailModel = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx),
         title = mediaItem.mediaMetadata.title!!.toString(),
         title = mediaItem.mediaMetadata.title!!.toString(),
         authors = mediaItem.mediaMetadata.artist.toString(),
         authors = mediaItem.mediaMetadata.artist.toString(),
         durationText = mediaItem.mediaMetadata.extras?.getString("durationText") ?: "?",
         durationText = mediaItem.mediaMetadata.extras?.getString("durationText") ?: "?",
@@ -65,7 +65,7 @@ fun SongItem(
 @NonRestartableComposable
 @NonRestartableComposable
 fun SongItem(
 fun SongItem(
     song: DetailedSong,
     song: DetailedSong,
-    thumbnailSize: Int,
+    thumbnailSizePx: Int,
     onClick: () -> Unit,
     onClick: () -> Unit,
     menuContent: @Composable () -> Unit,
     menuContent: @Composable () -> Unit,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
@@ -73,7 +73,7 @@ fun SongItem(
     trailingContent: (@Composable () -> Unit)? = null
     trailingContent: (@Composable () -> Unit)? = null
 ) {
 ) {
     SongItem(
     SongItem(
-        thumbnailModel = song.thumbnailUrl?.thumbnail(thumbnailSize),
+        thumbnailModel = song.thumbnailUrl?.thumbnail(thumbnailSizePx),
         title = song.title,
         title = song.title,
         authors = song.artistsText ?: "",
         authors = song.artistsText ?: "",
         durationText = song.durationText,
         durationText = song.durationText,
@@ -90,8 +90,8 @@ fun SongItem(
 @NonRestartableComposable
 @NonRestartableComposable
 fun SongItem(
 fun SongItem(
     thumbnailModel: Any?,
     thumbnailModel: Any?,
-    title: String,
-    authors: String,
+    title: String?,
+    authors: String?,
     durationText: String?,
     durationText: String?,
     onClick: () -> Unit,
     onClick: () -> Unit,
     menuContent: @Composable () -> Unit,
     menuContent: @Composable () -> Unit,
@@ -131,7 +131,7 @@ fun SongItem(
 @ExperimentalAnimationApi
 @ExperimentalAnimationApi
 @Composable
 @Composable
 fun SongItem(
 fun SongItem(
-    title: String,
+    title: String?,
     authors: String?,
     authors: String?,
     durationText: String?,
     durationText: String?,
     onClick: () -> Unit,
     onClick: () -> Unit,
@@ -167,7 +167,7 @@ fun SongItem(
                 .weight(1f)
                 .weight(1f)
         ) {
         ) {
             BasicText(
             BasicText(
-                text = title,
+                text = title ?: "",
                 style = typography.xs.semiBold,
                 style = typography.xs.semiBold,
                 maxLines = 1,
                 maxLines = 1,
                 overflow = TextOverflow.Ellipsis,
                 overflow = TextOverflow.Ellipsis,

+ 8 - 8
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt

@@ -79,8 +79,8 @@ fun SmallSongItem(
 ) {
 ) {
     SongItem(
     SongItem(
         thumbnailModel = song.thumbnail?.size(thumbnailSizePx),
         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,
         durationText = song.durationText,
         onClick = onClick,
         onClick = onClick,
         menuContent = {
         menuContent = {
@@ -148,14 +148,14 @@ fun VideoItem(
 
 
         Column {
         Column {
             BasicText(
             BasicText(
-                text = video.info.name,
+                text = video.info?.name ?: "",
                 style = typography.xs.semiBold,
                 style = typography.xs.semiBold,
                 maxLines = 2,
                 maxLines = 2,
                 overflow = TextOverflow.Ellipsis,
                 overflow = TextOverflow.Ellipsis,
             )
             )
 
 
             BasicText(
             BasicText(
-                text = video.authors?.joinToString("") { it.name } ?: "",
+                text = video.authors?.joinToString("") { it.name ?: "" } ?: "",
                 style = typography.xs.semiBold.secondary,
                 style = typography.xs.semiBold.secondary,
                 maxLines = 1,
                 maxLines = 1,
                 overflow = TextOverflow.Ellipsis,
                 overflow = TextOverflow.Ellipsis,
@@ -252,7 +252,7 @@ fun PlaylistItem(
 
 
         Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
         Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
             BasicText(
             BasicText(
-                text = playlist.info.name,
+                text = playlist.info?.name ?: "",
                 style = typography.xs.semiBold,
                 style = typography.xs.semiBold,
                 maxLines = 2,
                 maxLines = 2,
                 overflow = TextOverflow.Ellipsis,
                 overflow = TextOverflow.Ellipsis,
@@ -322,14 +322,14 @@ fun AlbumItem(
 
 
         Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
         Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
             BasicText(
             BasicText(
-                text = album.info.name,
+                text = album.info?.name ?: "",
                 style = typography.xs.semiBold,
                 style = typography.xs.semiBold,
                 maxLines = 2,
                 maxLines = 2,
                 overflow = TextOverflow.Ellipsis,
                 overflow = TextOverflow.Ellipsis,
             )
             )
 
 
             BasicText(
             BasicText(
-                text = album.authors?.joinToString("") { it.name } ?: "",
+                text = album.authors?.joinToString("") { it.name ?: "" } ?: "",
                 style = typography.xs.semiBold.secondary,
                 style = typography.xs.semiBold.secondary,
                 maxLines = 2,
                 maxLines = 2,
                 overflow = TextOverflow.Ellipsis,
                 overflow = TextOverflow.Ellipsis,
@@ -406,7 +406,7 @@ fun ArtistItem(
 
 
         Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
         Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
             BasicText(
             BasicText(
-                text = artist.info.name,
+                text = artist.info?.name ?: "",
                 style = typography.xs.semiBold,
                 style = typography.xs.semiBold,
                 maxLines = 2,
                 maxLines = 2,
                 overflow = TextOverflow.Ellipsis
                 overflow = TextOverflow.Ellipsis

+ 14 - 30
app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt

@@ -1,7 +1,5 @@
 package it.vfsfitvnm.vimusic.utils
 package it.vfsfitvnm.vimusic.utils
 
 
-import android.content.Context
-import android.content.Intent
 import android.net.Uri
 import android.net.Uri
 import androidx.core.net.toUri
 import androidx.core.net.toUri
 import androidx.core.os.bundleOf
 import androidx.core.os.bundleOf
@@ -10,37 +8,23 @@ import androidx.media3.common.MediaMetadata
 import it.vfsfitvnm.vimusic.models.DetailedSong
 import it.vfsfitvnm.vimusic.models.DetailedSong
 import it.vfsfitvnm.youtubemusic.YouTube
 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
 val YouTube.Item.Song.asMediaItem: MediaItem
     get() = MediaItem.Builder()
     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(
         .setMediaMetadata(
             MediaMetadata.Builder()
             MediaMetadata.Builder()
-                .setTitle(info.name)
-                .setArtist(authors?.joinToString("") { it.name })
+                .setTitle(info?.name)
+                .setArtist(authors?.joinToString("") { it.name ?: "" })
                 .setAlbumTitle(album?.name)
                 .setAlbumTitle(album?.name)
                 .setArtworkUri(thumbnail?.url?.toUri())
                 .setArtworkUri(thumbnail?.url?.toUri())
                 .setExtras(
                 .setExtras(
                     bundleOf(
                     bundleOf(
-                        "videoId" to info.endpoint!!.videoId,
+                        "videoId" to key,
                         "albumId" to album?.endpoint?.browseId,
                         "albumId" to album?.endpoint?.browseId,
                         "durationText" to durationText,
                         "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 },
                         "artistIds" to authors?.mapNotNull { it.endpoint?.browseId },
                     )
                     )
                 )
                 )
@@ -50,19 +34,19 @@ val YouTube.Item.Song.asMediaItem: MediaItem
 
 
 val YouTube.Item.Video.asMediaItem: MediaItem
 val YouTube.Item.Video.asMediaItem: MediaItem
     get() = MediaItem.Builder()
     get() = MediaItem.Builder()
-        .setMediaId(info.endpoint!!.videoId!!)
-        .setUri(info.endpoint!!.videoId)
-        .setCustomCacheKey(info.endpoint!!.videoId)
+        .setMediaId(key)
+        .setUri(key)
+        .setCustomCacheKey(key)
         .setMediaMetadata(
         .setMediaMetadata(
             MediaMetadata.Builder()
             MediaMetadata.Builder()
-                .setTitle(info.name)
-                .setArtist(authors?.joinToString("") { it.name })
+                .setTitle(info?.name)
+                .setArtist(authors?.joinToString("") { it.name ?: "" })
                 .setArtworkUri(thumbnail?.url?.toUri())
                 .setArtworkUri(thumbnail?.url?.toUri())
                 .setExtras(
                 .setExtras(
                     bundleOf(
                     bundleOf(
-                        "videoId" to info.endpoint!!.videoId,
+                        "videoId" to key,
                         "durationText" to durationText,
                         "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,
                         "artistIds" to if (isOfficialMusicVideo) authors?.mapNotNull { it.endpoint?.browseId } else null,
                     )
                     )
                 )
                 )

+ 274 - 138
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.ContinuationResponse
 import it.vfsfitvnm.youtubemusic.models.GetQueueResponse
 import it.vfsfitvnm.youtubemusic.models.GetQueueResponse
 import it.vfsfitvnm.youtubemusic.models.GetSearchSuggestionsResponse
 import it.vfsfitvnm.youtubemusic.models.GetSearchSuggestionsResponse
+import it.vfsfitvnm.youtubemusic.models.MusicCarouselShelfRenderer
 import it.vfsfitvnm.youtubemusic.models.MusicResponsiveListItemRenderer
 import it.vfsfitvnm.youtubemusic.models.MusicResponsiveListItemRenderer
 import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer
 import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer
+import it.vfsfitvnm.youtubemusic.models.MusicTwoRowItemRenderer
 import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
 import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
 import it.vfsfitvnm.youtubemusic.models.NextResponse
 import it.vfsfitvnm.youtubemusic.models.NextResponse
 import it.vfsfitvnm.youtubemusic.models.PlayerResponse
 import it.vfsfitvnm.youtubemusic.models.PlayerResponse
 import it.vfsfitvnm.youtubemusic.models.Runs
 import it.vfsfitvnm.youtubemusic.models.Runs
 import it.vfsfitvnm.youtubemusic.models.SearchResponse
 import it.vfsfitvnm.youtubemusic.models.SearchResponse
+import it.vfsfitvnm.youtubemusic.models.SectionListRenderer
 import it.vfsfitvnm.youtubemusic.models.ThumbnailRenderer
 import it.vfsfitvnm.youtubemusic.models.ThumbnailRenderer
 import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.Serializable
@@ -36,7 +39,7 @@ import kotlinx.serialization.json.Json
 object YouTube {
 object YouTube {
     private const val Key = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
     private const val Key = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
 
 
-    val client = HttpClient(OkHttp) {
+    private val client = HttpClient(OkHttp) {
         BrowserUserAgent()
         BrowserUserAgent()
 
 
         expectSuccess = true
         expectSuccess = true
@@ -162,37 +165,34 @@ object YouTube {
     }
     }
 
 
     data class Info<T : NavigationEndpoint.Endpoint>(
     data class Info<T : NavigationEndpoint.Endpoint>(
-        val name: String,
+        val name: String?,
         val endpoint: T?
         val endpoint: T?
     ) {
     ) {
-        companion object {
-            inline fun <reified T : NavigationEndpoint.Endpoint> from(run: Runs.Run): Info<T> {
-                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 {
     sealed class Item {
         abstract val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
         abstract val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
-        abstract val key: String?
+        abstract val key: String
 
 
         data class Song(
         data class Song(
-            val info: Info<NavigationEndpoint.Endpoint.Watch>,
+            val info: Info<NavigationEndpoint.Endpoint.Watch>?,
             val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
             val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
             val album: Info<NavigationEndpoint.Endpoint.Browse>?,
             val album: Info<NavigationEndpoint.Endpoint.Browse>?,
             val durationText: String?,
             val durationText: String?,
             override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
             override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
         ) : Item() {
         ) : Item() {
-            override val key: String?
-                get() = info.endpoint?.videoId
+            override val key: String
+                get() = info!!.endpoint!!.videoId!!
 
 
-            companion object : FromMusicShelfRendererContent<Song> {
+            companion object {
                 val Filter = Filter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D")
                 val Filter = Filter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D")
 
 
-                override fun from(content: MusicShelfRenderer.Content): Song {
+                fun from(content: MusicShelfRenderer.Content): Song? {
                     val (mainRuns, otherRuns) = content.runs
                     val (mainRuns, otherRuns) = content.runs
 
 
                     // Possible configurations:
                     // Possible configurations:
@@ -210,21 +210,22 @@ object YouTube {
                                 ?.browseEndpoint
                                 ?.browseEndpoint
                                 ?.type == "MUSIC_PAGE_TYPE_ALBUM"
                                 ?.type == "MUSIC_PAGE_TYPE_ALBUM"
                         }
                         }
-                        ?.let(Info.Companion::from)
+                        ?.let(::Info)
 
 
                     return Song(
                     return Song(
-                        info = Info.from(mainRuns.first()),
+                        info = mainRuns
+                            .firstOrNull()
+                            ?.let(::Info),
                         authors = otherRuns
                         authors = otherRuns
                             .getOrNull(otherRuns.lastIndex - if (album == null) 1 else 2)
                             .getOrNull(otherRuns.lastIndex - if (album == null) 1 else 2)
-                            ?.map(Info.Companion::from)
-                            ?: emptyList(),
+                            ?.map(::Info),
                         album = album,
                         album = album,
                         durationText = otherRuns
                         durationText = otherRuns
                             .lastOrNull()
                             .lastOrNull()
                             ?.firstOrNull()?.text,
                             ?.firstOrNull()?.text,
                         thumbnail = content
                         thumbnail = content
                             .thumbnail
                             .thumbnail
-                    )
+                    ).takeIf { it.info?.endpoint?.videoId != null }
                 }
                 }
 
 
                 fun from(renderer: MusicResponsiveListItemRenderer): Song? {
                 fun from(renderer: MusicResponsiveListItemRenderer): Song? {
@@ -236,15 +237,15 @@ object YouTube {
                             ?.text
                             ?.text
                             ?.runs
                             ?.runs
                             ?.getOrNull(0)
                             ?.getOrNull(0)
-                            ?.let { Info.from(it) } ?: return null,
+                            ?.let(::Info),
                         authors = renderer
                         authors = renderer
                             .flexColumns
                             .flexColumns
                             .getOrNull(1)
                             .getOrNull(1)
                             ?.musicResponsiveListItemFlexColumnRenderer
                             ?.musicResponsiveListItemFlexColumnRenderer
                             ?.text
                             ?.text
                             ?.runs
                             ?.runs
-                            ?.map { Info.from<NavigationEndpoint.Endpoint.Browse>(it) }
-                            ?.takeIf { it.isNotEmpty() },
+                            ?.map<Runs.Run, Info<NavigationEndpoint.Endpoint.Browse>>(::Info)
+                            ?.takeIf(List<Any>::isNotEmpty),
                         durationText = renderer
                         durationText = renderer
                             .fixedColumns
                             .fixedColumns
                             ?.getOrNull(0)
                             ?.getOrNull(0)
@@ -260,53 +261,55 @@ object YouTube {
                             ?.text
                             ?.text
                             ?.runs
                             ?.runs
                             ?.firstOrNull()
                             ?.firstOrNull()
-                            ?.let { Info.from(it) },
+                            ?.let(::Info),
                         thumbnail = renderer
                         thumbnail = renderer
                             .thumbnail
                             .thumbnail
                             ?.musicThumbnailRenderer
                             ?.musicThumbnailRenderer
                             ?.thumbnail
                             ?.thumbnail
                             ?.thumbnails
                             ?.thumbnails
                             ?.firstOrNull()
                             ?.firstOrNull()
-                    )
+                    ).takeIf { it.info?.endpoint?.videoId != null }
                 }
                 }
             }
             }
         }
         }
 
 
         data class Video(
         data class Video(
-            val info: Info<NavigationEndpoint.Endpoint.Watch>,
+            val info: Info<NavigationEndpoint.Endpoint.Watch>?,
             val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
             val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
             val viewsText: String?,
             val viewsText: String?,
             val durationText: String?,
             val durationText: String?,
             override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
             override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
         ) : Item() {
         ) : Item() {
-            override val key: String?
-                get() = info.endpoint?.videoId
+            override val key: String
+                get() = info!!.endpoint!!.videoId!!
 
 
             val isOfficialMusicVideo: Boolean
             val isOfficialMusicVideo: Boolean
                 get() = info
                 get() = info
-                    .endpoint
+                    ?.endpoint
                     ?.watchEndpointMusicSupportedConfigs
                     ?.watchEndpointMusicSupportedConfigs
                     ?.watchEndpointMusicConfig
                     ?.watchEndpointMusicConfig
                     ?.musicVideoType == "MUSIC_VIDEO_TYPE_OMV"
                     ?.musicVideoType == "MUSIC_VIDEO_TYPE_OMV"
 
 
             val isUserGeneratedContent: Boolean
             val isUserGeneratedContent: Boolean
                 get() = info
                 get() = info
-                    .endpoint
+                    ?.endpoint
                     ?.watchEndpointMusicSupportedConfigs
                     ?.watchEndpointMusicSupportedConfigs
                     ?.watchEndpointMusicConfig
                     ?.watchEndpointMusicConfig
                     ?.musicVideoType == "MUSIC_VIDEO_TYPE_UGC"
                     ?.musicVideoType == "MUSIC_VIDEO_TYPE_UGC"
 
 
-            companion object : FromMusicShelfRendererContent<Video> {
+            companion object {
                 val Filter = Filter("EgWKAQIQAWoKEAkQChAFEAMQBA%3D%3D")
                 val Filter = Filter("EgWKAQIQAWoKEAkQChAFEAMQBA%3D%3D")
 
 
-                override fun from(content: MusicShelfRenderer.Content): Video {
+                fun from(content: MusicShelfRenderer.Content): Video? {
                     val (mainRuns, otherRuns) = content.runs
                     val (mainRuns, otherRuns) = content.runs
 
 
                     return Video(
                     return Video(
-                        info = Info.from(mainRuns.first()),
+                        info = mainRuns
+                            .firstOrNull()
+                            ?.let(::Info),
                         authors = otherRuns
                         authors = otherRuns
                             .getOrNull(otherRuns.lastIndex - 2)
                             .getOrNull(otherRuns.lastIndex - 2)
-                            ?.map(Info.Companion::from),
+                            ?.map(::Info),
                         viewsText = otherRuns
                         viewsText = otherRuns
                             .getOrNull(otherRuns.lastIndex - 1)
                             .getOrNull(otherRuns.lastIndex - 1)
                             ?.firstOrNull()
                             ?.firstOrNull()
@@ -317,31 +320,31 @@ object YouTube {
                             ?.text,
                             ?.text,
                         thumbnail = content
                         thumbnail = content
                             .thumbnail
                             .thumbnail
-                    )
+                    ).takeIf { it.info?.endpoint?.videoId != null }
                 }
                 }
             }
             }
         }
         }
 
 
         data class Album(
         data class Album(
-            val info: Info<NavigationEndpoint.Endpoint.Browse>,
+            val info: Info<NavigationEndpoint.Endpoint.Browse>?,
             val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
             val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
             val year: String?,
             val year: String?,
             override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
             override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
         ) : Item() {
         ) : Item() {
-            override val key: String?
-                get() = info.endpoint?.browseId
+            override val key: String
+                get() = info!!.endpoint!!.browseId!!
 
 
-            companion object : FromMusicShelfRendererContent<Album> {
+            companion object {
                 val Filter = Filter("EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D")
                 val Filter = Filter("EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D")
 
 
-                override fun from(content: MusicShelfRenderer.Content): Album {
+                fun from(content: MusicShelfRenderer.Content): Album? {
                     val (mainRuns, otherRuns) = content.runs
                     val (mainRuns, otherRuns) = content.runs
 
 
                     return Album(
                     return Album(
                         info = Info(
                         info = Info(
                             name = mainRuns
                             name = mainRuns
-                                .first()
-                                .text,
+                                .firstOrNull()
+                                ?.text,
                             endpoint = content
                             endpoint = content
                                 .musicResponsiveListItemRenderer
                                 .musicResponsiveListItemRenderer
                                 .navigationEndpoint
                                 .navigationEndpoint
@@ -349,37 +352,59 @@ object YouTube {
                         ),
                         ),
                         authors = otherRuns
                         authors = otherRuns
                             .getOrNull(otherRuns.lastIndex - 1)
                             .getOrNull(otherRuns.lastIndex - 1)
-                            ?.map(Info.Companion::from),
+                            ?.map(::Info),
                         year = otherRuns
                         year = otherRuns
                             .getOrNull(otherRuns.lastIndex)
                             .getOrNull(otherRuns.lastIndex)
                             ?.firstOrNull()
                             ?.firstOrNull()
                             ?.text,
                             ?.text,
                         thumbnail = content
                         thumbnail = content
                             .thumbnail
                             .thumbnail
-                    )
+                    ).takeIf { it.info?.endpoint?.browseId != null }
+                }
+
+                fun from(renderer: MusicTwoRowItemRenderer): Album? {
+                    return Album(
+                        info = renderer
+                            .title
+                            .runs
+                            .firstOrNull()
+                            ?.let(::Info),
+                        authors = null,
+                        year = renderer
+                            .subtitle
+                            .runs
+                            .lastOrNull()
+                            ?.text,
+                        thumbnail = renderer
+                            .thumbnailRenderer
+                            .musicThumbnailRenderer
+                            .thumbnail
+                            .thumbnails
+                            .firstOrNull()
+                    ).takeIf { it.info?.endpoint?.browseId != null }
                 }
                 }
             }
             }
         }
         }
 
 
         data class Artist(
         data class Artist(
-            val info: Info<NavigationEndpoint.Endpoint.Browse>,
+            val info: Info<NavigationEndpoint.Endpoint.Browse>?,
             val subscribersCountText: String?,
             val subscribersCountText: String?,
             override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
             override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
         ) : Item() {
         ) : Item() {
-            override val key: String?
-                get() = info.endpoint?.browseId
+            override val key: String
+                get() = info!!.endpoint!!.browseId!!
 
 
-            companion object : FromMusicShelfRendererContent<Artist> {
+            companion object {
                 val Filter = Filter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D")
                 val Filter = Filter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D")
 
 
-                override fun from(content: MusicShelfRenderer.Content): Artist {
+                fun from(content: MusicShelfRenderer.Content): Artist? {
                     val (mainRuns, otherRuns) = content.runs
                     val (mainRuns, otherRuns) = content.runs
 
 
                     return Artist(
                     return Artist(
                         info = Info(
                         info = Info(
                             name = mainRuns
                             name = mainRuns
-                                .first()
-                                .text,
+                                .firstOrNull()
+                                ?.text,
                             endpoint = content
                             endpoint = content
                                 .musicResponsiveListItemRenderer
                                 .musicResponsiveListItemRenderer
                                 .navigationEndpoint
                                 .navigationEndpoint
@@ -391,22 +416,43 @@ object YouTube {
                             ?.text,
                             ?.text,
                         thumbnail = content
                         thumbnail = content
                             .thumbnail
                             .thumbnail
-                    )
+                    ).takeIf { it.info?.endpoint?.browseId != null }
+                }
+
+                fun from(renderer: MusicTwoRowItemRenderer): Artist? {
+                    return Artist(
+                        info = renderer
+                            .title
+                            .runs
+                            .firstOrNull()
+                            ?.let(::Info),
+                        subscribersCountText = renderer
+                            .subtitle
+                            .runs
+                            .firstOrNull()
+                            ?.text,
+                        thumbnail = renderer
+                            .thumbnailRenderer
+                            .musicThumbnailRenderer
+                            .thumbnail
+                            .thumbnails
+                            .firstOrNull()
+                    ).takeIf { it.info?.endpoint?.browseId != null }
                 }
                 }
             }
             }
         }
         }
 
 
         data class Playlist(
         data class Playlist(
-            val info: Info<NavigationEndpoint.Endpoint.Browse>,
+            val info: Info<NavigationEndpoint.Endpoint.Browse>?,
             val channel: Info<NavigationEndpoint.Endpoint.Browse>?,
             val channel: Info<NavigationEndpoint.Endpoint.Browse>?,
             val songCount: Int?,
             val songCount: Int?,
             override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
             override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
         ) : Item() {
         ) : Item() {
-            override val key: String?
-                get() = info.endpoint?.browseId
+            override val key: String
+                get() = info!!.endpoint!!.browseId!!
 
 
-            companion object : FromMusicShelfRendererContent<Playlist> {
-                override fun from(content: MusicShelfRenderer.Content): Playlist {
+            companion object {
+                fun from(content: MusicShelfRenderer.Content): Playlist? {
                     val (mainRuns, otherRuns) = content.runs
                     val (mainRuns, otherRuns) = content.runs
 
 
                     return Playlist(
                     return Playlist(
@@ -422,7 +468,7 @@ object YouTube {
                         channel = otherRuns
                         channel = otherRuns
                             .firstOrNull()
                             .firstOrNull()
                             ?.firstOrNull()
                             ?.firstOrNull()
-                            ?.let { Info.from(it) },
+                            ?.let(::Info),
                         songCount = otherRuns
                         songCount = otherRuns
                             .lastOrNull()
                             .lastOrNull()
                             ?.firstOrNull()
                             ?.firstOrNull()
@@ -432,7 +478,36 @@ object YouTube {
                             ?.toIntOrNull(),
                             ?.toIntOrNull(),
                         thumbnail = content
                         thumbnail = content
                             .thumbnail
                             .thumbnail
-                    )
+                    ).takeIf { it.info?.endpoint?.browseId != null }
+                }
+
+                fun from(renderer: MusicTwoRowItemRenderer): Playlist? {
+                    return Playlist(
+                        info = renderer
+                            .title
+                            .runs
+                            .firstOrNull()
+                            ?.let(::Info),
+                        channel = renderer
+                            .subtitle
+                            .runs
+                            .getOrNull(2)
+                            ?.let(::Info),
+                        songCount = renderer
+                            .subtitle
+                            .runs
+                            .getOrNull(4)
+                            ?.text
+                            ?.split(' ')
+                            ?.firstOrNull()
+                            ?.toIntOrNull(),
+                        thumbnail = renderer
+                            .thumbnailRenderer
+                            .musicThumbnailRenderer
+                            .thumbnail
+                            .thumbnails
+                            .firstOrNull()
+                    ).takeIf { it.info?.endpoint?.browseId != null }
                 }
                 }
             }
             }
         }
         }
@@ -445,15 +520,11 @@ object YouTube {
             val Filter = Filter("EgeKAQQoADgBagwQDhAKEAMQBRAJEAQ%3D")
             val Filter = Filter("EgeKAQQoADgBagwQDhAKEAMQBRAJEAQ%3D")
         }
         }
 
 
-        interface FromMusicShelfRendererContent<out T : Item> {
-            fun from(content: MusicShelfRenderer.Content): T
-        }
-
         @JvmInline
         @JvmInline
         value class Filter(val value: String)
         value class Filter(val value: String)
     }
     }
 
 
-    class SearchResult(val items: List<Item>, val continuation: String?)
+    class SearchResult(val items: List<Item>?, val continuation: String?)
 
 
     suspend fun search(
     suspend fun search(
         query: String,
         query: String,
@@ -495,7 +566,7 @@ object YouTube {
             SearchResult(
             SearchResult(
                 items = musicShelfRenderer
                 items = musicShelfRenderer
                     ?.contents
                     ?.contents
-                    ?.map(
+                    ?.mapNotNull(
                         when (filter) {
                         when (filter) {
                             Item.Song.Filter.value -> Item.Song.Companion::from
                             Item.Song.Filter.value -> Item.Song.Companion::from
                             Item.Album.Filter.value -> Item.Album.Companion::from
                             Item.Album.Filter.value -> Item.Album.Companion::from
@@ -505,7 +576,7 @@ object YouTube {
                             Item.FeaturedPlaylist.Filter.value -> Item.Playlist.Companion::from
                             Item.FeaturedPlaylist.Filter.value -> Item.Playlist.Companion::from
                             else -> error("Unknown filter: $filter")
                             else -> error("Unknown filter: $filter")
                         }
                         }
-                    ) ?: emptyList(),
+                    ),
                 continuation = musicShelfRenderer
                 continuation = musicShelfRenderer
                     ?.continuations
                     ?.continuations
                     ?.firstOrNull()
                     ?.firstOrNull()
@@ -623,7 +694,7 @@ object YouTube {
                         info = Info(
                         info = Info(
                             name = renderer
                             name = renderer
                                 .title
                                 .title
-                                ?.text ?: return@let null,
+                                ?.text,
                             endpoint = renderer
                             endpoint = renderer
                                 .navigationEndpoint
                                 .navigationEndpoint
                                 .watchEndpoint
                                 .watchEndpoint
@@ -632,14 +703,13 @@ object YouTube {
                             .longBylineText
                             .longBylineText
                             ?.splitBySeparator()
                             ?.splitBySeparator()
                             ?.getOrNull(0)
                             ?.getOrNull(0)
-                            ?.map { Info.from(it) }
-                            ?: emptyList(),
+                            ?.map(::Info),
                         album = renderer
                         album = renderer
                             .longBylineText
                             .longBylineText
                             ?.splitBySeparator()
                             ?.splitBySeparator()
                             ?.getOrNull(1)
                             ?.getOrNull(1)
                             ?.getOrNull(0)
                             ?.getOrNull(0)
-                            ?.let { Info.from(it) },
+                            ?.let(::Info),
                         thumbnail = renderer
                         thumbnail = renderer
                             .thumbnail
                             .thumbnail
                             .thumbnails
                             .thumbnails
@@ -647,7 +717,7 @@ object YouTube {
                         durationText = renderer
                         durationText = renderer
                             .lengthText
                             .lengthText
                             ?.text
                             ?.text
-                    )
+                    ).takeIf { it.info?.endpoint?.videoId != null }
                 }
                 }
             }
             }
         }.recoverIfCancelled()
         }.recoverIfCancelled()
@@ -663,16 +733,6 @@ object YouTube {
         )?.map { it?.firstOrNull() }
         )?.map { it?.firstOrNull() }
     }
     }
 
 
-    suspend fun queue(playlistId: String): Result<List<Item.Song>?>? {
-        return getQueue(
-            GetQueueBody(
-                context = Context.DefaultWeb,
-                videoIds = null,
-                playlistId = playlistId
-            )
-        )
-    }
-
     suspend fun next(
     suspend fun next(
         videoId: String?,
         videoId: String?,
         playlistId: String?,
         playlistId: String?,
@@ -759,7 +819,7 @@ object YouTube {
                             info = Info(
                             info = Info(
                                 name = renderer
                                 name = renderer
                                     .title
                                     .title
-                                    ?.text ?: return@mapNotNull null,
+                                    ?.text,
                                 endpoint = renderer
                                 endpoint = renderer
                                     .navigationEndpoint
                                     .navigationEndpoint
                                     .watchEndpoint
                                     .watchEndpoint
@@ -768,14 +828,13 @@ object YouTube {
                                 .longBylineText
                                 .longBylineText
                                 ?.splitBySeparator()
                                 ?.splitBySeparator()
                                 ?.getOrNull(0)
                                 ?.getOrNull(0)
-                                ?.map { run -> Info.from(run) }
-                                ?: emptyList(),
+                                ?.map(::Info),
                             album = renderer
                             album = renderer
                                 .longBylineText
                                 .longBylineText
                                 ?.splitBySeparator()
                                 ?.splitBySeparator()
                                 ?.getOrNull(1)
                                 ?.getOrNull(1)
                                 ?.getOrNull(0)
                                 ?.getOrNull(0)
-                                ?.let { run -> Info.from(run) },
+                                ?.let(::Info),
                             thumbnail = renderer
                             thumbnail = renderer
                                 .thumbnail
                                 .thumbnail
                                 .thumbnails
                                 .thumbnails
@@ -783,24 +842,14 @@ object YouTube {
                             durationText = renderer
                             durationText = renderer
                                 .lengthText
                                 .lengthText
                                 ?.text
                                 ?.text
-                        )
+                        ).takeIf { it.info?.endpoint?.videoId != null }
                     },
                     },
-                lyrics = NextResult.Lyrics(
-                    browseId = tabs
-                        .getOrNull(1)
-                        ?.tabRenderer
-                        ?.endpoint
-                        ?.browseEndpoint
-                        ?.browseId
-                ),
-                related = NextResult.Related(
-                    browseId = tabs
-                        .getOrNull(2)
-                        ?.tabRenderer
-                        ?.endpoint
-                        ?.browseEndpoint
-                        ?.browseId
-                )
+                lyricsBrowseId = tabs
+                    .getOrNull(1)
+                    ?.tabRenderer
+                    ?.endpoint
+                    ?.browseEndpoint
+                    ?.browseId,
             )
             )
         }.recoverIfCancelled()
         }.recoverIfCancelled()
     }
     }
@@ -811,32 +860,23 @@ object YouTube {
         val params: String? = null,
         val params: String? = null,
         val playlistSetVideoId: String? = null,
         val playlistSetVideoId: String? = null,
         val items: List<Item.Song>?,
         val items: List<Item.Song>?,
-        val lyrics: Lyrics?,
-        val related: Related?,
+        val lyricsBrowseId: String?
     ) {
     ) {
-        class Lyrics(
-            val browseId: String?,
-        ) {
-            suspend fun text(): Result<String?>? {
-                return if (browseId == null) {
-                    Result.success(null)
-                } else {
-                    browse(browseId)?.map { body ->
-                        body.contents
-                            .sectionListRenderer
-                            ?.contents
-                            ?.first()
-                            ?.musicDescriptionShelfRenderer
-                            ?.description
-                            ?.text
-                    }
+        suspend fun lyrics(): Result<String?>? {
+            return if (lyricsBrowseId == null) {
+                Result.success(null)
+            } else {
+                browse(lyricsBrowseId)?.map { body ->
+                    body.contents
+                        .sectionListRenderer
+                        ?.contents
+                        ?.first()
+                        ?.musicDescriptionShelfRenderer
+                        ?.description
+                        ?.text
                 }
                 }
             }
             }
         }
         }
-
-        class Related(
-            val browseId: String?,
-        )
     }
     }
 
 
     suspend fun browse(browseId: String): Result<BrowseResponse>? {
     suspend fun browse(browseId: String): Result<BrowseResponse>? {
@@ -875,12 +915,14 @@ object YouTube {
                         parameter("continuation", continuation)
                         parameter("continuation", continuation)
                     }.body<ContinuationResponse>().let { continuationResponse ->
                     }.body<ContinuationResponse>().let { continuationResponse ->
                         copy(
                         copy(
-                            songs = songs?.plus(continuationResponse
-                                .continuationContents
-                                .musicShelfContinuation
-                                ?.contents
-                                ?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
-                                ?.mapNotNull(Item.Song.Companion::from) ?: emptyList()),
+                            songs = songs?.plus(
+                                continuationResponse
+                                    .continuationContents
+                                    .musicShelfContinuation
+                                    ?.contents
+                                    ?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
+                                    ?.mapNotNull(Item.Song.Companion::from) ?: emptyList()
+                            ),
                             continuation = continuationResponse
                             continuation = continuationResponse
                                 .continuationContents
                                 .continuationContents
                                 .musicShelfContinuation
                                 .musicShelfContinuation
@@ -897,7 +939,7 @@ object YouTube {
 
 
     suspend fun album(browseId: String): Result<PlaylistOrAlbum>? {
     suspend fun album(browseId: String): Result<PlaylistOrAlbum>? {
         return playlistOrAlbum(browseId)?.map { album ->
         return playlistOrAlbum(browseId)?.map { album ->
-           album.url?.let { Url(it).parameters["list"] }?.let { playlistId ->
+            album.url?.let { Url(it).parameters["list"] }?.let { playlistId ->
                 playlistOrAlbum("VL$playlistId")?.getOrNull()?.let { playlist ->
                 playlistOrAlbum("VL$playlistId")?.getOrNull()?.let { playlist ->
                     album.copy(songs = playlist.songs)
                     album.copy(songs = playlist.songs)
                 }
                 }
@@ -950,7 +992,7 @@ object YouTube {
                     ?.subtitle
                     ?.subtitle
                     ?.splitBySeparator()
                     ?.splitBySeparator()
                     ?.getOrNull(1)
                     ?.getOrNull(1)
-                    ?.map { Info.from(it) },
+                    ?.map(::Info),
                 year = body
                 year = body
                     .header
                     .header
                     ?.musicDetailHeaderRenderer
                     ?.musicDetailHeaderRenderer
@@ -972,9 +1014,7 @@ object YouTube {
                     ?.musicShelfRenderer
                     ?.musicShelfRenderer
                     ?.contents
                     ?.contents
                     ?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
                     ?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
-                    ?.mapNotNull(Item.Song.Companion::from)
-//                    ?.filter { it.info.endpoint != null }
-                ,
+                    ?.mapNotNull(Item.Song.Companion::from),
                 url = body
                 url = body
                     .microformat
                     .microformat
                     ?.microformatDataRenderer
                     ?.microformatDataRenderer
@@ -999,7 +1039,7 @@ object YouTube {
     }
     }
 
 
     data class Artist(
     data class Artist(
-        val name: String,
+        val name: String?,
         val description: String?,
         val description: String?,
         val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?,
         val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?,
         val shuffleEndpoint: NavigationEndpoint.Endpoint.Watch?,
         val shuffleEndpoint: NavigationEndpoint.Endpoint.Watch?,
@@ -1013,7 +1053,7 @@ object YouTube {
                     .header
                     .header
                     ?.musicImmersiveHeaderRenderer
                     ?.musicImmersiveHeaderRenderer
                     ?.title
                     ?.title
-                    ?.text ?: "Unknown",
+                    ?.text,
                 description = body
                 description = body
                     .header
                     .header
                     ?.musicImmersiveHeaderRenderer
                     ?.musicImmersiveHeaderRenderer
@@ -1045,4 +1085,100 @@ object YouTube {
             )
             )
         }
         }
     }
     }
+
+    data class Related(
+        val songs: List<Item.Song>? = null,
+        val playlists: List<Item.Playlist>? = null,
+        val albums: List<Item.Album>? = null,
+        val artists: List<Item.Artist>? = null,
+    )
+
+    suspend fun related(videoId: String): Result<Related?>? {
+        return runCatching {
+            val body = client.post("/youtubei/v1/next") {
+                contentType(ContentType.Application.Json)
+                setBody(
+                    NextBody(
+                        context = Context.DefaultWeb,
+                        videoId = videoId,
+                        playlistId = null,
+                        isAudioOnly = true,
+                        tunerSettingValue = "AUTOMIX_SETTING_NORMAL",
+                        watchEndpointMusicSupportedConfigs = NextBody.WatchEndpointMusicSupportedConfigs(
+                            musicVideoType = "MUSIC_VIDEO_TYPE_ATV"
+                        ),
+                        index = 0,
+                        playlistSetVideoId = null,
+                        params = null,
+                        continuation = null
+                    )
+                )
+                parameter("key", Key)
+                parameter("prettyPrint", false)
+            }.body<NextResponse>()
+
+            body
+                .contents
+                .singleColumnMusicWatchNextResultsRenderer
+                .tabbedRenderer
+                .watchNextTabbedResultsRenderer
+                .tabs
+                .getOrNull(2)
+                ?.tabRenderer
+                ?.endpoint
+                ?.browseEndpoint
+                ?.browseId
+                ?.let { browseId ->
+                    browse(browseId)?.getOrThrow()?.let { browseResponse ->
+                        browseResponse
+                            .contents
+                            .sectionListRenderer
+                            ?.contents
+                            ?.mapNotNull(SectionListRenderer.Content::musicCarouselShelfRenderer)
+                            ?.map(MusicCarouselShelfRenderer::contents)
+                    }
+                }?.let { contents ->
+                    Related(
+                        songs = contents.find { items ->
+                            items.firstOrNull()?.musicResponsiveListItemRenderer != null
+                        }?.mapNotNull { content ->
+                            Item.Song.from(content.musicResponsiveListItemRenderer!!)
+                        },
+                        playlists = contents.find { items ->
+                            items.firstOrNull()
+                                ?.musicTwoRowItemRenderer
+                                ?.navigationEndpoint
+                                ?.browseEndpoint
+                                ?.browseEndpointContextSupportedConfigs
+                                ?.browseEndpointContextMusicConfig
+                                ?.pageType == "MUSIC_PAGE_TYPE_PLAYLIST"
+                        }
+                            ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
+                            ?.mapNotNull(Item.Playlist.Companion::from),
+                        albums = contents.find { items ->
+                            items.firstOrNull()
+                                ?.musicTwoRowItemRenderer
+                                ?.navigationEndpoint
+                                ?.browseEndpoint
+                                ?.browseEndpointContextSupportedConfigs
+                                ?.browseEndpointContextMusicConfig
+                                ?.pageType == "MUSIC_PAGE_TYPE_ALBUM"
+                        }
+                            ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
+                            ?.mapNotNull(Item.Album.Companion::from),
+                        artists = contents.find { items ->
+                            items.firstOrNull()
+                                ?.musicTwoRowItemRenderer
+                                ?.navigationEndpoint
+                                ?.browseEndpoint
+                                ?.browseEndpointContextSupportedConfigs
+                                ?.browseEndpointContextMusicConfig
+                                ?.pageType == "MUSIC_PAGE_TYPE_ARTIST"
+                        }
+                            ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
+                            ?.mapNotNull(Item.Artist.Companion::from),
+                    )
+                }
+        }.recoverIfCancelled()
+    }
 }
 }

+ 2 - 3
youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/MusicCarouselShelfRenderer.kt

@@ -1,9 +1,7 @@
 package it.vfsfitvnm.youtubemusic.models
 package it.vfsfitvnm.youtubemusic.models
 
 
-import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.Serializable
 
 
-@OptIn(ExperimentalSerializationApi::class)
 @Serializable
 @Serializable
 data class MusicCarouselShelfRenderer(
 data class MusicCarouselShelfRenderer(
     val header: Header,
     val header: Header,
@@ -12,7 +10,8 @@ data class MusicCarouselShelfRenderer(
     @Serializable
     @Serializable
     data class Content(
     data class Content(
         val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?,
         val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?,
-        val musicNavigationButtonRenderer: MusicNavigationButtonRenderer?
+        val musicNavigationButtonRenderer: MusicNavigationButtonRenderer?,
+        val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
     )
     )
 
 
     @Serializable
     @Serializable

+ 1 - 1
youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/models/NavigationEndpoint.kt

@@ -93,7 +93,7 @@ data class NavigationEndpoint(
         @Serializable
         @Serializable
         data class Browse(
         data class Browse(
             val params: String?,
             val params: String?,
-            val browseId: String,
+            val browseId: String?,
             val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs?,
             val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs?,
         ) : Endpoint() {
         ) : Endpoint() {
             val type: String?
             val type: String?