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.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
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?,
|
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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
return if (value.size == 2) Info(id = value[0], name = value[1]) else null
|
||||||
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.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?>) =
|
||||||
|
|
|
@ -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) },
|
value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } },
|
||||||
with(YouTubeBrowseInfoListSaver) { value.authors?.let { 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)
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) },
|
value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } },
|
||||||
with(YouTubeBrowseInfoSaver) { value.channel?.let { 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)
|
||||||
|
|
|
@ -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?>> {
|
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) },
|
value.info?.let { with(YouTubeWatchInfoSaver) { save(it) } },
|
||||||
with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } },
|
value.authors?.let { with(YouTubeBrowseInfoListSaver) { save(it) } },
|
||||||
with(YouTubeBrowseInfoSaver) { value.album?.let { 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?>),
|
info = (value[0] as List<Any?>?)?.let(YouTubeWatchInfoSaver::restore),
|
||||||
authors = YouTubeBrowseInfoListSaver.restore(value[1] as List<List<Any?>>),
|
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)
|
||||||
|
|
|
@ -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) },
|
value.info?.let { with(YouTubeWatchInfoSaver) { save(it) } },
|
||||||
with(YouTubeBrowseInfoListSaver) { value.authors?.let { 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?>),
|
info = (value[0] as List<Any?>?)?.let(YouTubeWatchInfoSaver::restore),
|
||||||
authors = YouTubeBrowseInfoListSaver.restore(value[1] as List<List<Any?>>),
|
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)
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
),
|
),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -58,8 +58,8 @@ import kotlinx.coroutines.flow.flowOn
|
||||||
@ExperimentalFoundationApi
|
@ExperimentalFoundationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePlaylistList(
|
fun HomePlaylistList(
|
||||||
onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit,
|
onBuiltInPlaylist: (BuiltInPlaylist) -> Unit,
|
||||||
onPlaylistClicked: (Playlist) -> 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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -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(0, "Quick picks", R.drawable.sparkles)
|
||||||
Item(1, "Playlists", R.drawable.playlist)
|
Item(1, "Songs", R.drawable.musical_notes)
|
||||||
Item(2, "Artists", R.drawable.person)
|
Item(2, "Playlists", R.drawable.playlist)
|
||||||
Item(3, "Albums", R.drawable.disc)
|
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(
|
0 -> QuickPicks(
|
||||||
onBuiltInPlaylistClicked = { builtInPlaylistRoute(it) },
|
onAlbumClick = { albumRoute(it) },
|
||||||
onPlaylistClicked = { localPlaylistRoute(it.id) }
|
|
||||||
)
|
)
|
||||||
|
1 -> HomeSongList()
|
||||||
2 -> HomeArtistList(
|
2 -> HomePlaylistList(
|
||||||
onArtistClick = { artistRoute(it.id) }
|
onBuiltInPlaylist = { builtInPlaylistRoute(it) },
|
||||||
|
onPlaylistClick = { localPlaylistRoute(it.id) }
|
||||||
)
|
)
|
||||||
|
3 -> HomeArtistList(onArtistClick = { artistRoute(it.id) })
|
||||||
3 -> HomeAlbumList(
|
4 -> HomeAlbumList(onAlbumClick = { albumRoute(it.id) })
|
||||||
onAlbumClick = { albumRoute(it.id) }
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> HomeSongList()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
transaction {
|
||||||
runBlocking(Dispatchers.IO) {
|
runBlocking(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
YouTube.playlist(browseId)?.map {
|
YouTube.playlist(browseId)?.map { it.next() }
|
||||||
it.next()
|
|
||||||
}?.map { playlist ->
|
|
||||||
playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}?.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 ->
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -81,11 +81,7 @@ fun PlaylistSongList(
|
||||||
stateSaver = resultSaver(YouTubePlaylistOrAlbumSaver),
|
stateSaver = resultSaver(YouTubePlaylistOrAlbumSaver),
|
||||||
) {
|
) {
|
||||||
value = withContext(Dispatchers.IO) {
|
value = withContext(Dispatchers.IO) {
|
||||||
YouTube.playlist(browseId)?.map {
|
YouTube.playlist(browseId)?.map { it.next() }
|
||||||
it.next()
|
|
||||||
}?.map { playlist ->
|
|
||||||
playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
title = song.info?.name,
|
||||||
authors = (song.authors ?: playlist.authors)?.joinToString("") { it.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 ->
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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) }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
title: String?,
|
||||||
authors: 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,
|
||||||
|
|
|
@ -79,8 +79,8 @@ fun SmallSongItem(
|
||||||
) {
|
) {
|
||||||
SongItem(
|
SongItem(
|
||||||
thumbnailModel = song.thumbnail?.size(thumbnailSizePx),
|
thumbnailModel = song.thumbnail?.size(thumbnailSizePx),
|
||||||
title = song.info.name,
|
title = song.info?.name,
|
||||||
authors = song.authors?.joinToString("") { it.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
|
||||||
|
|
|
@ -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 {
|
.setMediaId(key)
|
||||||
// println("$this")
|
.setUri(key)
|
||||||
// println(info.endpoint?.videoId)
|
.setCustomCacheKey(key)
|
||||||
}
|
|
||||||
.setMediaId(info.endpoint!!.videoId!!)
|
|
||||||
.setUri(info.endpoint!!.videoId)
|
|
||||||
.setCustomCacheKey(info.endpoint!!.videoId)
|
|
||||||
.setMediaMetadata(
|
.setMediaMetadata(
|
||||||
MediaMetadata.Builder()
|
MediaMetadata.Builder()
|
||||||
.setTitle(info.name)
|
.setTitle(info?.name)
|
||||||
.setArtist(authors?.joinToString("") { it.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!!)
|
.setMediaId(key)
|
||||||
.setUri(info.endpoint!!.videoId)
|
.setUri(key)
|
||||||
.setCustomCacheKey(info.endpoint!!.videoId)
|
.setCustomCacheKey(key)
|
||||||
.setMediaMetadata(
|
.setMediaMetadata(
|
||||||
MediaMetadata.Builder()
|
MediaMetadata.Builder()
|
||||||
.setTitle(info.name)
|
.setTitle(info?.name)
|
||||||
.setArtist(authors?.joinToString("") { it.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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
@Suppress("UNCHECKED_CAST")
|
||||||
inline fun <reified T : NavigationEndpoint.Endpoint> from(run: Runs.Run): Info<T> {
|
constructor(run: Runs.Run) : this(
|
||||||
return Info(
|
|
||||||
name = run.text,
|
name = run.text,
|
||||||
endpoint = run.navigationEndpoint?.endpoint as T?
|
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?
|
override val key: String
|
||||||
get() = info.endpoint?.videoId
|
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)
|
?.map(::Info),
|
||||||
?: emptyList(),
|
|
||||||
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) }
|
?.map<Runs.Run, Info<NavigationEndpoint.Endpoint.Browse>>(::Info)
|
||||||
?.takeIf { it.isNotEmpty() },
|
?.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?
|
override val key: String
|
||||||
get() = info.endpoint?.videoId
|
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?
|
override val key: String
|
||||||
get() = info.endpoint?.browseId
|
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()
|
.firstOrNull()
|
||||||
.text,
|
?.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?
|
override val key: String
|
||||||
get() = info.endpoint?.browseId
|
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()
|
.firstOrNull()
|
||||||
.text,
|
?.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?
|
override val key: String
|
||||||
get() = info.endpoint?.browseId
|
get() = info!!.endpoint!!.browseId!!
|
||||||
|
|
||||||
companion object : FromMusicShelfRendererContent<Playlist> {
|
companion object {
|
||||||
override fun from(content: MusicShelfRenderer.Content): Playlist {
|
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) }
|
?.map(::Info),
|
||||||
?: emptyList(),
|
|
||||||
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) }
|
?.map(::Info),
|
||||||
?: emptyList(),
|
|
||||||
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(
|
lyricsBrowseId = tabs
|
||||||
browseId = tabs
|
|
||||||
.getOrNull(1)
|
.getOrNull(1)
|
||||||
?.tabRenderer
|
?.tabRenderer
|
||||||
?.endpoint
|
?.endpoint
|
||||||
?.browseEndpoint
|
?.browseEndpoint
|
||||||
?.browseId
|
?.browseId,
|
||||||
),
|
|
||||||
related = NextResult.Related(
|
|
||||||
browseId = tabs
|
|
||||||
.getOrNull(2)
|
|
||||||
?.tabRenderer
|
|
||||||
?.endpoint
|
|
||||||
?.browseEndpoint
|
|
||||||
?.browseId
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}.recoverIfCancelled()
|
}.recoverIfCancelled()
|
||||||
}
|
}
|
||||||
|
@ -811,17 +860,13 @@ 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 lyricsBrowseId: String?
|
||||||
val related: Related?,
|
|
||||||
) {
|
) {
|
||||||
class Lyrics(
|
suspend fun lyrics(): Result<String?>? {
|
||||||
val browseId: String?,
|
return if (lyricsBrowseId == null) {
|
||||||
) {
|
|
||||||
suspend fun text(): Result<String?>? {
|
|
||||||
return if (browseId == null) {
|
|
||||||
Result.success(null)
|
Result.success(null)
|
||||||
} else {
|
} else {
|
||||||
browse(browseId)?.map { body ->
|
browse(lyricsBrowseId)?.map { body ->
|
||||||
body.contents
|
body.contents
|
||||||
.sectionListRenderer
|
.sectionListRenderer
|
||||||
?.contents
|
?.contents
|
||||||
|
@ -834,11 +879,6 @@ object YouTube {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Related(
|
|
||||||
val browseId: String?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun browse(browseId: String): Result<BrowseResponse>? {
|
suspend fun browse(browseId: String): Result<BrowseResponse>? {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
client.post("/youtubei/v1/browse") {
|
client.post("/youtubei/v1/browse") {
|
||||||
|
@ -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
|
songs = songs?.plus(
|
||||||
|
continuationResponse
|
||||||
.continuationContents
|
.continuationContents
|
||||||
.musicShelfContinuation
|
.musicShelfContinuation
|
||||||
?.contents
|
?.contents
|
||||||
?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
||||||
?.mapNotNull(Item.Song.Companion::from) ?: emptyList()),
|
?.mapNotNull(Item.Song.Companion::from) ?: emptyList()
|
||||||
|
),
|
||||||
continuation = continuationResponse
|
continuation = continuationResponse
|
||||||
.continuationContents
|
.continuationContents
|
||||||
.musicShelfContinuation
|
.musicShelfContinuation
|
||||||
|
@ -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)
|
?.mapNotNull(Item.Song.Companion::from),
|
||||||
// ?.filter { it.info.endpoint != null }
|
|
||||||
,
|
|
||||||
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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?
|
||||||
|
|
Loading…
Add table
Reference in a new issue