Start working on QuickPicks screen
This commit is contained in:
parent
7a3c0ca110
commit
33778b33dd
37 changed files with 1354 additions and 272 deletions
670
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json
Normal file
670
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json
Normal file
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -37,6 +37,7 @@ import it.vfsfitvnm.vimusic.models.Album
|
|||
import it.vfsfitvnm.vimusic.models.Artist
|
||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||
import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength
|
||||
import it.vfsfitvnm.vimusic.models.Event
|
||||
import it.vfsfitvnm.vimusic.models.Format
|
||||
import it.vfsfitvnm.vimusic.models.Playlist
|
||||
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
||||
|
@ -288,6 +289,19 @@ interface Database {
|
|||
@Query("SELECT EXISTS(SELECT 1 FROM Playlist WHERE browseId = :browseId)")
|
||||
fun isImportedPlaylist(browseId: String): Flow<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)
|
||||
fun insert(format: Format)
|
||||
|
||||
|
@ -427,11 +441,12 @@ interface Database {
|
|||
SearchQuery::class,
|
||||
QueuedMediaItem::class,
|
||||
Format::class,
|
||||
Event::class,
|
||||
],
|
||||
views = [
|
||||
SortedSongPlaylistMap::class
|
||||
],
|
||||
version = 18,
|
||||
version = 19,
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 1, to = 2),
|
||||
|
@ -448,6 +463,7 @@ interface Database {
|
|||
AutoMigration(from = 15, to = 16),
|
||||
AutoMigration(from = 16, to = 17),
|
||||
AutoMigration(from = 17, to = 18),
|
||||
AutoMigration(from = 18, to = 19),
|
||||
],
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
25
app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Event.kt
Normal file
25
app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Event.kt
Normal file
|
@ -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
|
||||
)
|
|
@ -26,6 +26,6 @@ object DetailedSongSaver : Saver<DetailedSong, List<Any?>> {
|
|||
thumbnailUrl = value[4] as String?,
|
||||
totalPlayTimeMs = value[5] as Long,
|
||||
albumId = value[6] as String?,
|
||||
artists = InfoListSaver.restore(value[7] as List<List<String>>)
|
||||
artists = (value[7] as List<List<String>>?)?.let(InfoListSaver::restore)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -3,8 +3,6 @@ package it.vfsfitvnm.vimusic.savers
|
|||
import androidx.compose.runtime.saveable.Saver
|
||||
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>) =
|
||||
object : Saver<Result<Original>?, Pair<Saveable?, Throwable?>> {
|
||||
override fun restore(value: Pair<Saveable?, Throwable?>) =
|
||||
|
|
|
@ -6,15 +6,15 @@ import it.vfsfitvnm.youtubemusic.YouTube
|
|||
|
||||
object YouTubeAlbumSaver : Saver<YouTube.Item.Album, List<Any?>> {
|
||||
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,
|
||||
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
|
||||
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
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),
|
||||
year = value[2] as String?,
|
||||
thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
|
||||
|
|
|
@ -6,13 +6,13 @@ import it.vfsfitvnm.youtubemusic.YouTube
|
|||
|
||||
object YouTubeArtistSaver : Saver<YouTube.Item.Artist, List<Any?>> {
|
||||
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,
|
||||
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
|
||||
)
|
||||
|
||||
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?,
|
||||
thumbnail = (value[2] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
|
||||
)
|
||||
|
|
|
@ -8,11 +8,11 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
|||
object YouTubeBrowseInfoSaver : Saver<YouTube.Info<NavigationEndpoint.Endpoint.Browse>, List<Any?>> {
|
||||
override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Browse>) = listOf(
|
||||
value.name,
|
||||
with(YouTubeBrowseEndpointSaver) { value.endpoint?.let { save(it) } }
|
||||
value.endpoint?.let { with(YouTubeBrowseEndpointSaver) { save(it) } }
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,14 +6,14 @@ import it.vfsfitvnm.youtubemusic.YouTube
|
|||
|
||||
object YouTubePlaylistSaver : Saver<YouTube.Item.Playlist, List<Any?>> {
|
||||
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,
|
||||
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
|
||||
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
|
||||
)
|
||||
|
||||
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),
|
||||
songCount = value[2] as Int?,
|
||||
thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
|
||||
|
|
|
@ -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,17 +6,17 @@ import it.vfsfitvnm.youtubemusic.YouTube
|
|||
|
||||
object YouTubeSongSaver : Saver<YouTube.Item.Song, List<Any?>> {
|
||||
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,
|
||||
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
|
||||
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
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),
|
||||
durationText = value[3] as String?,
|
||||
thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
|
||||
|
|
|
@ -6,17 +6,17 @@ import it.vfsfitvnm.youtubemusic.YouTube
|
|||
|
||||
object YouTubeVideoSaver : Saver<YouTube.Item.Video, List<Any?>> {
|
||||
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.durationText,
|
||||
with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } }
|
||||
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } }
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
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?,
|
||||
durationText = value[3] as String?,
|
||||
thumbnail = (value[4] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore)
|
||||
|
|
|
@ -8,11 +8,11 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
|||
object YouTubeWatchInfoSaver : Saver<YouTube.Info<NavigationEndpoint.Endpoint.Watch>, List<Any?>> {
|
||||
override fun SaverScope.save(value: YouTube.Info<NavigationEndpoint.Endpoint.Watch>) = listOf(
|
||||
value.name,
|
||||
with(YouTubeWatchEndpointSaver) { value.endpoint?.let { save(it) } }
|
||||
value.endpoint?.let { with(YouTubeWatchEndpointSaver) { save(it) } },
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ import it.vfsfitvnm.vimusic.Database
|
|||
import it.vfsfitvnm.vimusic.MainActivity
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.enums.ExoPlayerDiskCacheMaxSize
|
||||
import it.vfsfitvnm.vimusic.models.Event
|
||||
import it.vfsfitvnm.vimusic.models.QueuedMediaItem
|
||||
import it.vfsfitvnm.vimusic.query
|
||||
import it.vfsfitvnm.vimusic.utils.InvincibleService
|
||||
|
@ -285,11 +286,23 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene
|
|||
|
||||
val totalPlayTimeMs = playbackStats.totalPlayTimeMs
|
||||
|
||||
if (totalPlayTimeMs > 2000) {
|
||||
if (totalPlayTimeMs > 5000) {
|
||||
query {
|
||||
Database.incrementTotalPlayTimeMs(mediaItem.mediaId, totalPlayTimeMs)
|
||||
}
|
||||
}
|
||||
|
||||
if (totalPlayTimeMs > 30000) {
|
||||
query {
|
||||
Database.insert(
|
||||
Event(
|
||||
songId = mediaItem.mediaId,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
playTime = totalPlayTimeMs
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||
|
||||
import android.content.Intent
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.animation.AnimatedContentScope
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
|
@ -57,7 +58,6 @@ import it.vfsfitvnm.vimusic.utils.color
|
|||
import it.vfsfitvnm.vimusic.utils.enqueue
|
||||
import it.vfsfitvnm.vimusic.utils.forcePlay
|
||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||
import it.vfsfitvnm.vimusic.utils.shareAsYouTubeSong
|
||||
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
@ -241,7 +241,13 @@ fun BaseMediaItemMenu(
|
|||
onGoToAlbum = albumRoute::global,
|
||||
onGoToArtist = artistRoute::global,
|
||||
onShare = {
|
||||
context.shareAsYouTubeSong(mediaItem)
|
||||
val sendIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, "https://music.youtube.com/watch?v=${mediaItem.mediaId}")
|
||||
}
|
||||
|
||||
context.startActivity(Intent.createChooser(sendIntent, null))
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
|
|
|
@ -95,7 +95,7 @@ fun AlbumOverview(
|
|||
title = youtubeAlbum.title,
|
||||
thumbnailUrl = youtubeAlbum.thumbnail?.url,
|
||||
year = youtubeAlbum.year,
|
||||
authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
|
||||
authorsText = youtubeAlbum.authors?.joinToString("") { it.name ?: "" },
|
||||
shareUrl = youtubeAlbum.url,
|
||||
timestamp = System.currentTimeMillis()
|
||||
),
|
||||
|
|
|
@ -271,7 +271,7 @@ fun ArtistScreen2(browseId: String) {
|
|||
) { index, song ->
|
||||
SongItem(
|
||||
song = song,
|
||||
thumbnailSize = songThumbnailSizePx,
|
||||
thumbnailSizePx = songThumbnailSizePx,
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayAtIndex(
|
||||
|
@ -351,7 +351,7 @@ private suspend fun fetchArtist(browseId: String): Result<Artist>? {
|
|||
?.map { youtubeArtist ->
|
||||
Artist(
|
||||
id = browseId,
|
||||
name = youtubeArtist.name,
|
||||
name = youtubeArtist.name ?: "",
|
||||
thumbnailUrl = youtubeArtist.thumbnail?.url,
|
||||
info = youtubeArtist.description,
|
||||
shuffleVideoId = youtubeArtist.shuffleEndpoint?.videoId,
|
||||
|
|
|
@ -103,7 +103,7 @@ fun BuiltInPlaylistSongList(builtInPlaylist: BuiltInPlaylist) {
|
|||
) { index, song ->
|
||||
SongItem(
|
||||
song = song,
|
||||
thumbnailSize = thumbnailSize,
|
||||
thumbnailSizePx = thumbnailSize,
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayAtIndex(
|
||||
|
|
|
@ -58,8 +58,8 @@ import kotlinx.coroutines.flow.flowOn
|
|||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
fun HomePlaylistList(
|
||||
onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit,
|
||||
onPlaylistClicked: (Playlist) -> Unit,
|
||||
onBuiltInPlaylist: (BuiltInPlaylist) -> Unit,
|
||||
onPlaylistClick: (Playlist) -> Unit,
|
||||
) {
|
||||
val (colorPalette) = LocalAppearance.current
|
||||
|
||||
|
@ -186,7 +186,7 @@ fun HomePlaylistList(
|
|||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Favorites) }
|
||||
onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -200,7 +200,7 @@ fun HomePlaylistList(
|
|||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Offline) }
|
||||
onClick = { onBuiltInPlaylist(BuiltInPlaylist.Offline) }
|
||||
)
|
||||
.animateItemPlacement()
|
||||
)
|
||||
|
@ -216,7 +216,7 @@ fun HomePlaylistList(
|
|||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { onPlaylistClicked(playlistPreview.playlist) }
|
||||
onClick = { onPlaylistClick(playlistPreview.playlist) }
|
||||
)
|
||||
.animateItemPlacement()
|
||||
)
|
||||
|
|
|
@ -86,30 +86,27 @@ fun HomeScreen(onPlaylistUrl: (String) -> Unit) {
|
|||
tabIndex = tabIndex,
|
||||
onTabChanged = onTabChanged,
|
||||
tabColumnContent = { Item ->
|
||||
Item(0, "Songs", R.drawable.musical_notes)
|
||||
Item(1, "Playlists", R.drawable.playlist)
|
||||
Item(2, "Artists", R.drawable.person)
|
||||
Item(3, "Albums", R.drawable.disc)
|
||||
Item(0, "Quick picks", R.drawable.sparkles)
|
||||
Item(1, "Songs", R.drawable.musical_notes)
|
||||
Item(2, "Playlists", R.drawable.playlist)
|
||||
Item(3, "Artists", R.drawable.person)
|
||||
Item(4, "Albums", R.drawable.disc)
|
||||
},
|
||||
primaryIconButtonId = R.drawable.search,
|
||||
onPrimaryIconButtonClick = { searchRoute("") }
|
||||
) { currentTabIndex ->
|
||||
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||
when (currentTabIndex) {
|
||||
1 -> HomePlaylistList(
|
||||
onBuiltInPlaylistClicked = { builtInPlaylistRoute(it) },
|
||||
onPlaylistClicked = { localPlaylistRoute(it.id) }
|
||||
0 -> QuickPicks(
|
||||
onAlbumClick = { albumRoute(it) },
|
||||
)
|
||||
|
||||
2 -> HomeArtistList(
|
||||
onArtistClick = { artistRoute(it.id) }
|
||||
1 -> HomeSongList()
|
||||
2 -> HomePlaylistList(
|
||||
onBuiltInPlaylist = { builtInPlaylistRoute(it) },
|
||||
onPlaylistClick = { localPlaylistRoute(it.id) }
|
||||
)
|
||||
|
||||
3 -> HomeAlbumList(
|
||||
onAlbumClick = { albumRoute(it.id) }
|
||||
)
|
||||
|
||||
else -> HomeSongList()
|
||||
3 -> HomeArtistList(onArtistClick = { artistRoute(it.id) })
|
||||
4 -> HomeAlbumList(onAlbumClick = { albumRoute(it.id) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -162,7 +162,7 @@ fun HomeSongList() {
|
|||
) { index, song ->
|
||||
SongItem(
|
||||
song = song,
|
||||
thumbnailSize = thumbnailSize,
|
||||
thumbnailSizePx = thumbnailSize,
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayAtIndex(items.map(DetailedSong::asMediaItem), index)
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -165,11 +165,7 @@ fun LocalPlaylistSongList(
|
|||
transaction {
|
||||
runBlocking(Dispatchers.IO) {
|
||||
withContext(Dispatchers.IO) {
|
||||
YouTube.playlist(browseId)?.map {
|
||||
it.next()
|
||||
}?.map { playlist ->
|
||||
playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null })
|
||||
}
|
||||
YouTube.playlist(browseId)?.map { it.next() }
|
||||
}
|
||||
}?.getOrNull()?.let { remotePlaylist ->
|
||||
Database.clearPlaylist(playlistId)
|
||||
|
@ -222,7 +218,7 @@ fun LocalPlaylistSongList(
|
|||
) { index, song ->
|
||||
SongItem(
|
||||
song = song,
|
||||
thumbnailSize = thumbnailSize,
|
||||
thumbnailSizePx = thumbnailSize,
|
||||
onClick = {
|
||||
playlistWithSongs?.songs?.map(DetailedSong::asMediaItem)
|
||||
?.let { mediaItems ->
|
||||
|
|
|
@ -135,7 +135,7 @@ fun Lyrics(
|
|||
)?.map { it?.value }
|
||||
} else {
|
||||
YouTube.next(mediaId, null)
|
||||
?.map { nextResult -> nextResult.lyrics?.text()?.getOrNull() }
|
||||
?.map { nextResult -> nextResult.lyrics()?.getOrNull() }
|
||||
}?.map { newLyrics ->
|
||||
onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "")
|
||||
state = state.copy(isLoading = false)
|
||||
|
|
|
@ -149,7 +149,7 @@ fun PlayerBottomSheet(
|
|||
|
||||
SongItem(
|
||||
mediaItem = window.mediaItem,
|
||||
thumbnailSize = thumbnailSize,
|
||||
thumbnailSizePx = thumbnailSize,
|
||||
onClick = {
|
||||
if (isPlayingThisMediaItem) {
|
||||
if (shouldBePlaying) {
|
||||
|
|
|
@ -81,11 +81,7 @@ fun PlaylistSongList(
|
|||
stateSaver = resultSaver(YouTubePlaylistOrAlbumSaver),
|
||||
) {
|
||||
value = withContext(Dispatchers.IO) {
|
||||
YouTube.playlist(browseId)?.map {
|
||||
it.next()
|
||||
}?.map { playlist ->
|
||||
playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null })
|
||||
}
|
||||
YouTube.playlist(browseId)?.map { it.next() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -202,8 +198,8 @@ fun PlaylistSongList(
|
|||
|
||||
itemsIndexed(items = playlist.songs ?: emptyList()) { index, song ->
|
||||
SongItem(
|
||||
title = song.info.name,
|
||||
authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name },
|
||||
title = song.info?.name,
|
||||
authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name ?: "" },
|
||||
durationText = song.durationText,
|
||||
onClick = {
|
||||
playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
|
||||
|
|
|
@ -100,7 +100,7 @@ fun LocalSongSearch(
|
|||
) { song ->
|
||||
SongItem(
|
||||
song = song,
|
||||
thumbnailSize = thumbnailSize,
|
||||
thumbnailSizePx = thumbnailSize,
|
||||
onClick = {
|
||||
val mediaItem = song.asMediaItem
|
||||
binder?.stopRadio()
|
||||
|
|
|
@ -92,7 +92,7 @@ inline fun <T : YouTube.Item> SearchResult(
|
|||
|
||||
items(
|
||||
items = items,
|
||||
key = { it.key!! },
|
||||
key = YouTube.Item::key,
|
||||
itemContent = itemContent
|
||||
)
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlay(song.asMediaItem)
|
||||
binder?.setupRadio(song.info.endpoint)
|
||||
binder?.setupRadio(song.info?.endpoint)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
@ -130,7 +130,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { albumRoute(album.info.endpoint?.browseId) }
|
||||
onClick = { albumRoute(album.info?.endpoint?.browseId) }
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -159,7 +159,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { artistRoute(artist.info.endpoint?.browseId) }
|
||||
onClick = { artistRoute(artist.info?.endpoint?.browseId) }
|
||||
)
|
||||
)
|
||||
},
|
||||
|
@ -186,7 +186,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlay(video.asMediaItem)
|
||||
binder?.setupRadio(video.info.endpoint)
|
||||
binder?.setupRadio(video.info?.endpoint)
|
||||
}
|
||||
)
|
||||
},
|
||||
|
@ -217,7 +217,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { playlistRoute(playlist.info.endpoint?.browseId) }
|
||||
onClick = { playlistRoute(playlist.info?.endpoint?.browseId) }
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
|
@ -40,7 +40,7 @@ import it.vfsfitvnm.vimusic.utils.thumbnail
|
|||
@NonRestartableComposable
|
||||
fun SongItem(
|
||||
mediaItem: MediaItem,
|
||||
thumbnailSize: Int,
|
||||
thumbnailSizePx: Int,
|
||||
onClick: () -> Unit,
|
||||
menuContent: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -48,7 +48,7 @@ fun SongItem(
|
|||
trailingContent: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
SongItem(
|
||||
thumbnailModel = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSize),
|
||||
thumbnailModel = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx),
|
||||
title = mediaItem.mediaMetadata.title!!.toString(),
|
||||
authors = mediaItem.mediaMetadata.artist.toString(),
|
||||
durationText = mediaItem.mediaMetadata.extras?.getString("durationText") ?: "?",
|
||||
|
@ -65,7 +65,7 @@ fun SongItem(
|
|||
@NonRestartableComposable
|
||||
fun SongItem(
|
||||
song: DetailedSong,
|
||||
thumbnailSize: Int,
|
||||
thumbnailSizePx: Int,
|
||||
onClick: () -> Unit,
|
||||
menuContent: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -73,7 +73,7 @@ fun SongItem(
|
|||
trailingContent: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
SongItem(
|
||||
thumbnailModel = song.thumbnailUrl?.thumbnail(thumbnailSize),
|
||||
thumbnailModel = song.thumbnailUrl?.thumbnail(thumbnailSizePx),
|
||||
title = song.title,
|
||||
authors = song.artistsText ?: "",
|
||||
durationText = song.durationText,
|
||||
|
@ -90,8 +90,8 @@ fun SongItem(
|
|||
@NonRestartableComposable
|
||||
fun SongItem(
|
||||
thumbnailModel: Any?,
|
||||
title: String,
|
||||
authors: String,
|
||||
title: String?,
|
||||
authors: String?,
|
||||
durationText: String?,
|
||||
onClick: () -> Unit,
|
||||
menuContent: @Composable () -> Unit,
|
||||
|
@ -131,7 +131,7 @@ fun SongItem(
|
|||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun SongItem(
|
||||
title: String,
|
||||
title: String?,
|
||||
authors: String?,
|
||||
durationText: String?,
|
||||
onClick: () -> Unit,
|
||||
|
@ -167,7 +167,7 @@ fun SongItem(
|
|||
.weight(1f)
|
||||
) {
|
||||
BasicText(
|
||||
text = title,
|
||||
text = title ?: "",
|
||||
style = typography.xs.semiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
|
|
@ -79,8 +79,8 @@ fun SmallSongItem(
|
|||
) {
|
||||
SongItem(
|
||||
thumbnailModel = song.thumbnail?.size(thumbnailSizePx),
|
||||
title = song.info.name,
|
||||
authors = song.authors?.joinToString("") { it.name } ?: "",
|
||||
title = song.info?.name,
|
||||
authors = song.authors?.joinToString("") { it.name ?: "" },
|
||||
durationText = song.durationText,
|
||||
onClick = onClick,
|
||||
menuContent = {
|
||||
|
@ -148,14 +148,14 @@ fun VideoItem(
|
|||
|
||||
Column {
|
||||
BasicText(
|
||||
text = video.info.name,
|
||||
text = video.info?.name ?: "",
|
||||
style = typography.xs.semiBold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
BasicText(
|
||||
text = video.authors?.joinToString("") { it.name } ?: "",
|
||||
text = video.authors?.joinToString("") { it.name ?: "" } ?: "",
|
||||
style = typography.xs.semiBold.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
@ -252,7 +252,7 @@ fun PlaylistItem(
|
|||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
BasicText(
|
||||
text = playlist.info.name,
|
||||
text = playlist.info?.name ?: "",
|
||||
style = typography.xs.semiBold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
@ -322,14 +322,14 @@ fun AlbumItem(
|
|||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
BasicText(
|
||||
text = album.info.name,
|
||||
text = album.info?.name ?: "",
|
||||
style = typography.xs.semiBold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
BasicText(
|
||||
text = album.authors?.joinToString("") { it.name } ?: "",
|
||||
text = album.authors?.joinToString("") { it.name ?: "" } ?: "",
|
||||
style = typography.xs.semiBold.secondary,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
@ -406,7 +406,7 @@ fun ArtistItem(
|
|||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
BasicText(
|
||||
text = artist.info.name,
|
||||
text = artist.info?.name ?: "",
|
||||
style = typography.xs.semiBold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package it.vfsfitvnm.vimusic.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
|
@ -10,37 +8,23 @@ import androidx.media3.common.MediaMetadata
|
|||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||
import it.vfsfitvnm.youtubemusic.YouTube
|
||||
|
||||
fun Context.shareAsYouTubeSong(mediaItem: MediaItem) {
|
||||
val sendIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, "https://music.youtube.com/watch?v=${mediaItem.mediaId}")
|
||||
}
|
||||
|
||||
startActivity(Intent.createChooser(sendIntent, null))
|
||||
}
|
||||
|
||||
val YouTube.Item.Song.asMediaItem: MediaItem
|
||||
get() = MediaItem.Builder()
|
||||
.also {
|
||||
// println("$this")
|
||||
// println(info.endpoint?.videoId)
|
||||
}
|
||||
.setMediaId(info.endpoint!!.videoId!!)
|
||||
.setUri(info.endpoint!!.videoId)
|
||||
.setCustomCacheKey(info.endpoint!!.videoId)
|
||||
.setMediaId(key)
|
||||
.setUri(key)
|
||||
.setCustomCacheKey(key)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setTitle(info.name)
|
||||
.setArtist(authors?.joinToString("") { it.name })
|
||||
.setTitle(info?.name)
|
||||
.setArtist(authors?.joinToString("") { it.name ?: "" })
|
||||
.setAlbumTitle(album?.name)
|
||||
.setArtworkUri(thumbnail?.url?.toUri())
|
||||
.setExtras(
|
||||
bundleOf(
|
||||
"videoId" to info.endpoint!!.videoId,
|
||||
"videoId" to key,
|
||||
"albumId" to album?.endpoint?.browseId,
|
||||
"durationText" to durationText,
|
||||
"artistNames" to authors?.filter { it.endpoint != null }?.map { it.name },
|
||||
"artistNames" to authors?.filter { it.endpoint != null }?.mapNotNull { it.name },
|
||||
"artistIds" to authors?.mapNotNull { it.endpoint?.browseId },
|
||||
)
|
||||
)
|
||||
|
@ -50,19 +34,19 @@ val YouTube.Item.Song.asMediaItem: MediaItem
|
|||
|
||||
val YouTube.Item.Video.asMediaItem: MediaItem
|
||||
get() = MediaItem.Builder()
|
||||
.setMediaId(info.endpoint!!.videoId!!)
|
||||
.setUri(info.endpoint!!.videoId)
|
||||
.setCustomCacheKey(info.endpoint!!.videoId)
|
||||
.setMediaId(key)
|
||||
.setUri(key)
|
||||
.setCustomCacheKey(key)
|
||||
.setMediaMetadata(
|
||||
MediaMetadata.Builder()
|
||||
.setTitle(info.name)
|
||||
.setArtist(authors?.joinToString("") { it.name })
|
||||
.setTitle(info?.name)
|
||||
.setArtist(authors?.joinToString("") { it.name ?: "" })
|
||||
.setArtworkUri(thumbnail?.url?.toUri())
|
||||
.setExtras(
|
||||
bundleOf(
|
||||
"videoId" to info.endpoint!!.videoId,
|
||||
"videoId" to key,
|
||||
"durationText" to durationText,
|
||||
"artistNames" to if (isOfficialMusicVideo) authors?.filter { it.endpoint != null }?.map { it.name } else null,
|
||||
"artistNames" to if (isOfficialMusicVideo) authors?.filter { it.endpoint != null }?.mapNotNull { it.name } else null,
|
||||
"artistIds" to if (isOfficialMusicVideo) authors?.mapNotNull { it.endpoint?.browseId } else null,
|
||||
)
|
||||
)
|
||||
|
|
|
@ -20,13 +20,16 @@ import it.vfsfitvnm.youtubemusic.models.BrowseResponse
|
|||
import it.vfsfitvnm.youtubemusic.models.ContinuationResponse
|
||||
import it.vfsfitvnm.youtubemusic.models.GetQueueResponse
|
||||
import it.vfsfitvnm.youtubemusic.models.GetSearchSuggestionsResponse
|
||||
import it.vfsfitvnm.youtubemusic.models.MusicCarouselShelfRenderer
|
||||
import it.vfsfitvnm.youtubemusic.models.MusicResponsiveListItemRenderer
|
||||
import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer
|
||||
import it.vfsfitvnm.youtubemusic.models.MusicTwoRowItemRenderer
|
||||
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
|
||||
import it.vfsfitvnm.youtubemusic.models.NextResponse
|
||||
import it.vfsfitvnm.youtubemusic.models.PlayerResponse
|
||||
import it.vfsfitvnm.youtubemusic.models.Runs
|
||||
import it.vfsfitvnm.youtubemusic.models.SearchResponse
|
||||
import it.vfsfitvnm.youtubemusic.models.SectionListRenderer
|
||||
import it.vfsfitvnm.youtubemusic.models.ThumbnailRenderer
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
|
@ -36,7 +39,7 @@ import kotlinx.serialization.json.Json
|
|||
object YouTube {
|
||||
private const val Key = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
||||
|
||||
val client = HttpClient(OkHttp) {
|
||||
private val client = HttpClient(OkHttp) {
|
||||
BrowserUserAgent()
|
||||
|
||||
expectSuccess = true
|
||||
|
@ -162,37 +165,34 @@ object YouTube {
|
|||
}
|
||||
|
||||
data class Info<T : NavigationEndpoint.Endpoint>(
|
||||
val name: String,
|
||||
val name: String?,
|
||||
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 {
|
||||
abstract val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
||||
abstract val key: String?
|
||||
abstract val key: String
|
||||
|
||||
data class Song(
|
||||
val info: Info<NavigationEndpoint.Endpoint.Watch>,
|
||||
val info: Info<NavigationEndpoint.Endpoint.Watch>?,
|
||||
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
||||
val album: Info<NavigationEndpoint.Endpoint.Browse>?,
|
||||
val durationText: String?,
|
||||
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
||||
) : Item() {
|
||||
override val key: String?
|
||||
get() = info.endpoint?.videoId
|
||||
override val key: String
|
||||
get() = info!!.endpoint!!.videoId!!
|
||||
|
||||
companion object : FromMusicShelfRendererContent<Song> {
|
||||
companion object {
|
||||
val Filter = Filter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D")
|
||||
|
||||
override fun from(content: MusicShelfRenderer.Content): Song {
|
||||
fun from(content: MusicShelfRenderer.Content): Song? {
|
||||
val (mainRuns, otherRuns) = content.runs
|
||||
|
||||
// Possible configurations:
|
||||
|
@ -210,21 +210,22 @@ object YouTube {
|
|||
?.browseEndpoint
|
||||
?.type == "MUSIC_PAGE_TYPE_ALBUM"
|
||||
}
|
||||
?.let(Info.Companion::from)
|
||||
?.let(::Info)
|
||||
|
||||
return Song(
|
||||
info = Info.from(mainRuns.first()),
|
||||
info = mainRuns
|
||||
.firstOrNull()
|
||||
?.let(::Info),
|
||||
authors = otherRuns
|
||||
.getOrNull(otherRuns.lastIndex - if (album == null) 1 else 2)
|
||||
?.map(Info.Companion::from)
|
||||
?: emptyList(),
|
||||
?.map(::Info),
|
||||
album = album,
|
||||
durationText = otherRuns
|
||||
.lastOrNull()
|
||||
?.firstOrNull()?.text,
|
||||
thumbnail = content
|
||||
.thumbnail
|
||||
)
|
||||
).takeIf { it.info?.endpoint?.videoId != null }
|
||||
}
|
||||
|
||||
fun from(renderer: MusicResponsiveListItemRenderer): Song? {
|
||||
|
@ -236,15 +237,15 @@ object YouTube {
|
|||
?.text
|
||||
?.runs
|
||||
?.getOrNull(0)
|
||||
?.let { Info.from(it) } ?: return null,
|
||||
?.let(::Info),
|
||||
authors = renderer
|
||||
.flexColumns
|
||||
.getOrNull(1)
|
||||
?.musicResponsiveListItemFlexColumnRenderer
|
||||
?.text
|
||||
?.runs
|
||||
?.map { Info.from<NavigationEndpoint.Endpoint.Browse>(it) }
|
||||
?.takeIf { it.isNotEmpty() },
|
||||
?.map<Runs.Run, Info<NavigationEndpoint.Endpoint.Browse>>(::Info)
|
||||
?.takeIf(List<Any>::isNotEmpty),
|
||||
durationText = renderer
|
||||
.fixedColumns
|
||||
?.getOrNull(0)
|
||||
|
@ -260,53 +261,55 @@ object YouTube {
|
|||
?.text
|
||||
?.runs
|
||||
?.firstOrNull()
|
||||
?.let { Info.from(it) },
|
||||
?.let(::Info),
|
||||
thumbnail = renderer
|
||||
.thumbnail
|
||||
?.musicThumbnailRenderer
|
||||
?.thumbnail
|
||||
?.thumbnails
|
||||
?.firstOrNull()
|
||||
)
|
||||
).takeIf { it.info?.endpoint?.videoId != null }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Video(
|
||||
val info: Info<NavigationEndpoint.Endpoint.Watch>,
|
||||
val info: Info<NavigationEndpoint.Endpoint.Watch>?,
|
||||
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
||||
val viewsText: String?,
|
||||
val durationText: String?,
|
||||
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
||||
) : Item() {
|
||||
override val key: String?
|
||||
get() = info.endpoint?.videoId
|
||||
override val key: String
|
||||
get() = info!!.endpoint!!.videoId!!
|
||||
|
||||
val isOfficialMusicVideo: Boolean
|
||||
get() = info
|
||||
.endpoint
|
||||
?.endpoint
|
||||
?.watchEndpointMusicSupportedConfigs
|
||||
?.watchEndpointMusicConfig
|
||||
?.musicVideoType == "MUSIC_VIDEO_TYPE_OMV"
|
||||
|
||||
val isUserGeneratedContent: Boolean
|
||||
get() = info
|
||||
.endpoint
|
||||
?.endpoint
|
||||
?.watchEndpointMusicSupportedConfigs
|
||||
?.watchEndpointMusicConfig
|
||||
?.musicVideoType == "MUSIC_VIDEO_TYPE_UGC"
|
||||
|
||||
companion object : FromMusicShelfRendererContent<Video> {
|
||||
companion object {
|
||||
val Filter = Filter("EgWKAQIQAWoKEAkQChAFEAMQBA%3D%3D")
|
||||
|
||||
override fun from(content: MusicShelfRenderer.Content): Video {
|
||||
fun from(content: MusicShelfRenderer.Content): Video? {
|
||||
val (mainRuns, otherRuns) = content.runs
|
||||
|
||||
return Video(
|
||||
info = Info.from(mainRuns.first()),
|
||||
info = mainRuns
|
||||
.firstOrNull()
|
||||
?.let(::Info),
|
||||
authors = otherRuns
|
||||
.getOrNull(otherRuns.lastIndex - 2)
|
||||
?.map(Info.Companion::from),
|
||||
?.map(::Info),
|
||||
viewsText = otherRuns
|
||||
.getOrNull(otherRuns.lastIndex - 1)
|
||||
?.firstOrNull()
|
||||
|
@ -317,31 +320,31 @@ object YouTube {
|
|||
?.text,
|
||||
thumbnail = content
|
||||
.thumbnail
|
||||
)
|
||||
).takeIf { it.info?.endpoint?.videoId != null }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Album(
|
||||
val info: Info<NavigationEndpoint.Endpoint.Browse>,
|
||||
val info: Info<NavigationEndpoint.Endpoint.Browse>?,
|
||||
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
||||
val year: String?,
|
||||
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
||||
) : 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")
|
||||
|
||||
override fun from(content: MusicShelfRenderer.Content): Album {
|
||||
fun from(content: MusicShelfRenderer.Content): Album? {
|
||||
val (mainRuns, otherRuns) = content.runs
|
||||
|
||||
return Album(
|
||||
info = Info(
|
||||
name = mainRuns
|
||||
.first()
|
||||
.text,
|
||||
.firstOrNull()
|
||||
?.text,
|
||||
endpoint = content
|
||||
.musicResponsiveListItemRenderer
|
||||
.navigationEndpoint
|
||||
|
@ -349,37 +352,59 @@ object YouTube {
|
|||
),
|
||||
authors = otherRuns
|
||||
.getOrNull(otherRuns.lastIndex - 1)
|
||||
?.map(Info.Companion::from),
|
||||
?.map(::Info),
|
||||
year = otherRuns
|
||||
.getOrNull(otherRuns.lastIndex)
|
||||
?.firstOrNull()
|
||||
?.text,
|
||||
thumbnail = content
|
||||
.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(
|
||||
val info: Info<NavigationEndpoint.Endpoint.Browse>,
|
||||
val info: Info<NavigationEndpoint.Endpoint.Browse>?,
|
||||
val subscribersCountText: String?,
|
||||
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
||||
) : 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")
|
||||
|
||||
override fun from(content: MusicShelfRenderer.Content): Artist {
|
||||
fun from(content: MusicShelfRenderer.Content): Artist? {
|
||||
val (mainRuns, otherRuns) = content.runs
|
||||
|
||||
return Artist(
|
||||
info = Info(
|
||||
name = mainRuns
|
||||
.first()
|
||||
.text,
|
||||
.firstOrNull()
|
||||
?.text,
|
||||
endpoint = content
|
||||
.musicResponsiveListItemRenderer
|
||||
.navigationEndpoint
|
||||
|
@ -391,22 +416,43 @@ object YouTube {
|
|||
?.text,
|
||||
thumbnail = content
|
||||
.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(
|
||||
val info: Info<NavigationEndpoint.Endpoint.Browse>,
|
||||
val info: Info<NavigationEndpoint.Endpoint.Browse>?,
|
||||
val channel: Info<NavigationEndpoint.Endpoint.Browse>?,
|
||||
val songCount: Int?,
|
||||
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
|
||||
) : 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
|
||||
|
||||
return Playlist(
|
||||
|
@ -422,7 +468,7 @@ object YouTube {
|
|||
channel = otherRuns
|
||||
.firstOrNull()
|
||||
?.firstOrNull()
|
||||
?.let { Info.from(it) },
|
||||
?.let(::Info),
|
||||
songCount = otherRuns
|
||||
.lastOrNull()
|
||||
?.firstOrNull()
|
||||
|
@ -432,7 +478,36 @@ object YouTube {
|
|||
?.toIntOrNull(),
|
||||
thumbnail = content
|
||||
.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")
|
||||
}
|
||||
|
||||
interface FromMusicShelfRendererContent<out T : Item> {
|
||||
fun from(content: MusicShelfRenderer.Content): T
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
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(
|
||||
query: String,
|
||||
|
@ -495,7 +566,7 @@ object YouTube {
|
|||
SearchResult(
|
||||
items = musicShelfRenderer
|
||||
?.contents
|
||||
?.map(
|
||||
?.mapNotNull(
|
||||
when (filter) {
|
||||
Item.Song.Filter.value -> Item.Song.Companion::from
|
||||
Item.Album.Filter.value -> Item.Album.Companion::from
|
||||
|
@ -505,7 +576,7 @@ object YouTube {
|
|||
Item.FeaturedPlaylist.Filter.value -> Item.Playlist.Companion::from
|
||||
else -> error("Unknown filter: $filter")
|
||||
}
|
||||
) ?: emptyList(),
|
||||
),
|
||||
continuation = musicShelfRenderer
|
||||
?.continuations
|
||||
?.firstOrNull()
|
||||
|
@ -623,7 +694,7 @@ object YouTube {
|
|||
info = Info(
|
||||
name = renderer
|
||||
.title
|
||||
?.text ?: return@let null,
|
||||
?.text,
|
||||
endpoint = renderer
|
||||
.navigationEndpoint
|
||||
.watchEndpoint
|
||||
|
@ -632,14 +703,13 @@ object YouTube {
|
|||
.longBylineText
|
||||
?.splitBySeparator()
|
||||
?.getOrNull(0)
|
||||
?.map { Info.from(it) }
|
||||
?: emptyList(),
|
||||
?.map(::Info),
|
||||
album = renderer
|
||||
.longBylineText
|
||||
?.splitBySeparator()
|
||||
?.getOrNull(1)
|
||||
?.getOrNull(0)
|
||||
?.let { Info.from(it) },
|
||||
?.let(::Info),
|
||||
thumbnail = renderer
|
||||
.thumbnail
|
||||
.thumbnails
|
||||
|
@ -647,7 +717,7 @@ object YouTube {
|
|||
durationText = renderer
|
||||
.lengthText
|
||||
?.text
|
||||
)
|
||||
).takeIf { it.info?.endpoint?.videoId != null }
|
||||
}
|
||||
}
|
||||
}.recoverIfCancelled()
|
||||
|
@ -663,16 +733,6 @@ object YouTube {
|
|||
)?.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(
|
||||
videoId: String?,
|
||||
playlistId: String?,
|
||||
|
@ -759,7 +819,7 @@ object YouTube {
|
|||
info = Info(
|
||||
name = renderer
|
||||
.title
|
||||
?.text ?: return@mapNotNull null,
|
||||
?.text,
|
||||
endpoint = renderer
|
||||
.navigationEndpoint
|
||||
.watchEndpoint
|
||||
|
@ -768,14 +828,13 @@ object YouTube {
|
|||
.longBylineText
|
||||
?.splitBySeparator()
|
||||
?.getOrNull(0)
|
||||
?.map { run -> Info.from(run) }
|
||||
?: emptyList(),
|
||||
?.map(::Info),
|
||||
album = renderer
|
||||
.longBylineText
|
||||
?.splitBySeparator()
|
||||
?.getOrNull(1)
|
||||
?.getOrNull(0)
|
||||
?.let { run -> Info.from(run) },
|
||||
?.let(::Info),
|
||||
thumbnail = renderer
|
||||
.thumbnail
|
||||
.thumbnails
|
||||
|
@ -783,24 +842,14 @@ object YouTube {
|
|||
durationText = renderer
|
||||
.lengthText
|
||||
?.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()
|
||||
}
|
||||
|
@ -811,32 +860,23 @@ object YouTube {
|
|||
val params: String? = null,
|
||||
val playlistSetVideoId: String? = null,
|
||||
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>? {
|
||||
|
@ -875,12 +915,14 @@ object YouTube {
|
|||
parameter("continuation", continuation)
|
||||
}.body<ContinuationResponse>().let { continuationResponse ->
|
||||
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
|
||||
.continuationContents
|
||||
.musicShelfContinuation
|
||||
|
@ -897,7 +939,7 @@ object YouTube {
|
|||
|
||||
suspend fun album(browseId: String): Result<PlaylistOrAlbum>? {
|
||||
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 ->
|
||||
album.copy(songs = playlist.songs)
|
||||
}
|
||||
|
@ -950,7 +992,7 @@ object YouTube {
|
|||
?.subtitle
|
||||
?.splitBySeparator()
|
||||
?.getOrNull(1)
|
||||
?.map { Info.from(it) },
|
||||
?.map(::Info),
|
||||
year = body
|
||||
.header
|
||||
?.musicDetailHeaderRenderer
|
||||
|
@ -972,9 +1014,7 @@ object YouTube {
|
|||
?.musicShelfRenderer
|
||||
?.contents
|
||||
?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
||||
?.mapNotNull(Item.Song.Companion::from)
|
||||
// ?.filter { it.info.endpoint != null }
|
||||
,
|
||||
?.mapNotNull(Item.Song.Companion::from),
|
||||
url = body
|
||||
.microformat
|
||||
?.microformatDataRenderer
|
||||
|
@ -999,7 +1039,7 @@ object YouTube {
|
|||
}
|
||||
|
||||
data class Artist(
|
||||
val name: String,
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?,
|
||||
val shuffleEndpoint: NavigationEndpoint.Endpoint.Watch?,
|
||||
|
@ -1013,7 +1053,7 @@ object YouTube {
|
|||
.header
|
||||
?.musicImmersiveHeaderRenderer
|
||||
?.title
|
||||
?.text ?: "Unknown",
|
||||
?.text,
|
||||
description = body
|
||||
.header
|
||||
?.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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
package it.vfsfitvnm.youtubemusic.models
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class MusicCarouselShelfRenderer(
|
||||
val header: Header,
|
||||
|
@ -12,7 +10,8 @@ data class MusicCarouselShelfRenderer(
|
|||
@Serializable
|
||||
data class Content(
|
||||
val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?,
|
||||
val musicNavigationButtonRenderer: MusicNavigationButtonRenderer?
|
||||
val musicNavigationButtonRenderer: MusicNavigationButtonRenderer?,
|
||||
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -93,7 +93,7 @@ data class NavigationEndpoint(
|
|||
@Serializable
|
||||
data class Browse(
|
||||
val params: String?,
|
||||
val browseId: String,
|
||||
val browseId: String?,
|
||||
val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs?,
|
||||
) : Endpoint() {
|
||||
val type: String?
|
||||
|
|
Loading…
Add table
Reference in a new issue