Add synchronized lyrics (#126)
This commit is contained in:
parent
4a16bc6960
commit
194864bcb4
16 changed files with 866 additions and 38 deletions
|
@ -93,6 +93,7 @@ dependencies {
|
||||||
kapt(libs.room.compiler)
|
kapt(libs.room.compiler)
|
||||||
|
|
||||||
implementation(projects.youtubeMusic)
|
implementation(projects.youtubeMusic)
|
||||||
|
implementation(projects.synchronizedLyrics)
|
||||||
|
|
||||||
coreLibraryDesugaring(libs.desugaring)
|
coreLibraryDesugaring(libs.desugaring)
|
||||||
}
|
}
|
||||||
|
|
592
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/16.json
Normal file
592
app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/16.json
Normal file
|
@ -0,0 +1,592 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 16,
|
||||||
|
"identityHash": "0cbca5b4016755ebf227461349581201",
|
||||||
|
"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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, 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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, 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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, '0cbca5b4016755ebf227461349581201')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -118,9 +118,15 @@ interface Database {
|
||||||
@Query("SELECT lyrics FROM Song WHERE id = :songId")
|
@Query("SELECT lyrics FROM Song WHERE id = :songId")
|
||||||
fun lyrics(songId: String): Flow<String?>
|
fun lyrics(songId: String): Flow<String?>
|
||||||
|
|
||||||
|
@Query("SELECT synchronizedLyrics FROM Song WHERE id = :songId")
|
||||||
|
fun synchronizedLyrics(songId: String): Flow<String?>
|
||||||
|
|
||||||
@Query("UPDATE Song SET lyrics = :lyrics WHERE id = :songId")
|
@Query("UPDATE Song SET lyrics = :lyrics WHERE id = :songId")
|
||||||
fun updateLyrics(songId: String, lyrics: String?): Int
|
fun updateLyrics(songId: String, lyrics: String?): Int
|
||||||
|
|
||||||
|
@Query("UPDATE Song SET synchronizedLyrics = :lyrics WHERE id = :songId")
|
||||||
|
fun updateSynchronizedLyrics(songId: String, lyrics: String?): Int
|
||||||
|
|
||||||
@Query("SELECT * FROM Artist WHERE id = :id")
|
@Query("SELECT * FROM Artist WHERE id = :id")
|
||||||
fun artist(id: String): Flow<Artist?>
|
fun artist(id: String): Flow<Artist?>
|
||||||
|
|
||||||
|
@ -344,7 +350,7 @@ interface Database {
|
||||||
views = [
|
views = [
|
||||||
SortedSongPlaylistMap::class
|
SortedSongPlaylistMap::class
|
||||||
],
|
],
|
||||||
version = 15,
|
version = 16,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 1, to = 2),
|
AutoMigration(from = 1, to = 2),
|
||||||
|
@ -358,6 +364,7 @@ interface Database {
|
||||||
AutoMigration(from = 11, to = 12, spec = DatabaseInitializer.From11To12Migration::class),
|
AutoMigration(from = 11, to = 12, spec = DatabaseInitializer.From11To12Migration::class),
|
||||||
AutoMigration(from = 12, to = 13),
|
AutoMigration(from = 12, to = 13),
|
||||||
AutoMigration(from = 13, to = 14),
|
AutoMigration(from = 13, to = 14),
|
||||||
|
AutoMigration(from = 15, to = 16),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
|
@ -11,6 +11,7 @@ data class Song(
|
||||||
val durationText: String,
|
val durationText: String,
|
||||||
val thumbnailUrl: String?,
|
val thumbnailUrl: String?,
|
||||||
val lyrics: String? = null,
|
val lyrics: String? = null,
|
||||||
|
val synchronizedLyrics: String? = null,
|
||||||
val likedAt: Long? = null,
|
val likedAt: Long? = null,
|
||||||
val totalPlayTimeMs: Long = 0
|
val totalPlayTimeMs: Long = 0
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -14,15 +14,19 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
@ -36,12 +40,15 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
import com.valentinilk.shimmer.shimmer
|
import com.valentinilk.shimmer.shimmer
|
||||||
|
import it.vfsfitvnm.synchronizedlyrics.LujjjhLyrics
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.query
|
import it.vfsfitvnm.vimusic.query
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||||
|
@ -52,14 +59,20 @@ import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.BlackColorPalette
|
import it.vfsfitvnm.vimusic.ui.styling.BlackColorPalette
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.DarkColorPalette
|
import it.vfsfitvnm.vimusic.ui.styling.DarkColorPalette
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.utils.SynchronizedLyrics
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
import it.vfsfitvnm.vimusic.utils.color
|
import it.vfsfitvnm.vimusic.utils.color
|
||||||
|
import it.vfsfitvnm.vimusic.utils.isShowingSynchronizedLyricsKey
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
import it.vfsfitvnm.vimusic.utils.verticalFadingEdge
|
import it.vfsfitvnm.vimusic.utils.verticalFadingEdge
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Lyrics(
|
fun Lyrics(
|
||||||
|
@ -68,7 +81,7 @@ fun Lyrics(
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
size: Dp,
|
size: Dp,
|
||||||
mediaMetadataProvider: () -> MediaMetadata,
|
mediaMetadataProvider: () -> MediaMetadata,
|
||||||
onLyricsUpdate: (String, String) -> Unit,
|
onLyricsUpdate: (Boolean, String, String) -> Unit,
|
||||||
nestedScrollConnectionProvider: () -> NestedScrollConnection,
|
nestedScrollConnectionProvider: () -> NestedScrollConnection,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
@ -80,32 +93,45 @@ fun Lyrics(
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
) {
|
) {
|
||||||
var isLoading by remember(mediaId) {
|
var isShowingSynchronizedLyrics by rememberPreference(isShowingSynchronizedLyricsKey, false)
|
||||||
|
|
||||||
|
var isLoading by remember(mediaId, isShowingSynchronizedLyrics) {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isEditingLyrics by remember(mediaId) {
|
var isEditingLyrics by remember(mediaId, isShowingSynchronizedLyrics) {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val lyrics by remember(mediaId) {
|
var lyrics by remember(mediaId, isShowingSynchronizedLyrics) {
|
||||||
Database.lyrics(mediaId).distinctUntilChanged().map flowMap@{ lyrics ->
|
mutableStateOf<String?>(".")
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(mediaId, isShowingSynchronizedLyrics) {
|
||||||
|
if (isShowingSynchronizedLyrics) {
|
||||||
|
Database.synchronizedLyrics(mediaId)
|
||||||
|
} else {
|
||||||
|
Database.lyrics(mediaId)
|
||||||
|
}.distinctUntilChanged().map flowMap@{ lyrics ->
|
||||||
if (lyrics != null) return@flowMap lyrics
|
if (lyrics != null) return@flowMap lyrics
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
|
||||||
YouTube.next(mediaId, null)?.map { nextResult ->
|
if (isShowingSynchronizedLyrics) {
|
||||||
nextResult.lyrics?.text()?.map { newLyrics ->
|
val mediaMetadata = mediaMetadataProvider()
|
||||||
onLyricsUpdate(mediaId, newLyrics ?: "")
|
LujjjhLyrics.forSong(mediaMetadata.artist?.toString() ?: "", mediaMetadata.title?.toString() ?: "")
|
||||||
isLoading = false
|
} else {
|
||||||
return@flowMap newLyrics ?: ""
|
YouTube.next(mediaId, null)?.map { nextResult -> nextResult.lyrics?.text()?.getOrNull() }
|
||||||
}
|
}?.map { newLyrics ->
|
||||||
|
onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "")
|
||||||
|
isLoading = false
|
||||||
|
return@flowMap newLyrics ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
null
|
null
|
||||||
}.distinctUntilChanged()
|
}.flowOn(Dispatchers.IO).collect { lyrics = it }
|
||||||
}.collectAsState(initial = ".", context = Dispatchers.IO)
|
}
|
||||||
|
|
||||||
if (isEditingLyrics) {
|
if (isEditingLyrics) {
|
||||||
TextFieldDialog(
|
TextFieldDialog(
|
||||||
|
@ -119,7 +145,12 @@ fun Lyrics(
|
||||||
},
|
},
|
||||||
onDone = {
|
onDone = {
|
||||||
query {
|
query {
|
||||||
Database.updateLyrics(mediaId, it)
|
if (isShowingSynchronizedLyrics) {
|
||||||
|
Database.updateSynchronizedLyrics(mediaId, it)
|
||||||
|
} else {
|
||||||
|
Database.updateLyrics(mediaId, it)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -146,7 +177,7 @@ fun Lyrics(
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
) {
|
) {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "An error has occurred while fetching the lyrics",
|
text = "An error has occurred while fetching the ${if (isShowingSynchronizedLyrics) "synchronized " else ""}lyrics",
|
||||||
style = typography.xs.center.medium.color(BlackColorPalette.text),
|
style = typography.xs.center.medium.color(BlackColorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(Color.Black.copy(0.4f))
|
.background(Color.Black.copy(0.4f))
|
||||||
|
@ -163,7 +194,7 @@ fun Lyrics(
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
) {
|
) {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "Lyrics are not available for this song",
|
text = "${if (isShowingSynchronizedLyrics) "Synchronized l" else "L"}yrics are not available for this song",
|
||||||
style = typography.xs.center.medium.color(BlackColorPalette.text),
|
style = typography.xs.center.medium.color(BlackColorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(Color.Black.copy(0.4f))
|
.background(Color.Black.copy(0.4f))
|
||||||
|
@ -187,16 +218,60 @@ fun Lyrics(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lyrics?.let { lyrics ->
|
lyrics?.let { lyrics ->
|
||||||
if (lyrics.isNotEmpty()) {
|
if (lyrics.isNotEmpty() && lyrics != ".") {
|
||||||
BasicText(
|
if (isShowingSynchronizedLyrics) {
|
||||||
text = lyrics,
|
val density = LocalDensity.current
|
||||||
style = typography.xs.center.medium.color(BlackColorPalette.text),
|
val player = LocalPlayerServiceBinder.current?.player ?: return@AnimatedVisibility
|
||||||
modifier = Modifier
|
|
||||||
.nestedScroll(remember { nestedScrollConnectionProvider() })
|
val synchronizedLyrics = remember(lyrics) {
|
||||||
.verticalFadingEdge()
|
SynchronizedLyrics(lyrics) {
|
||||||
.verticalScroll(rememberScrollState())
|
player.currentPosition
|
||||||
.padding(vertical = size / 4, horizontal = 32.dp)
|
}.also {
|
||||||
)
|
println("index: ${it.index}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val lazyListState = rememberLazyListState(synchronizedLyrics.index, with (density) { size.roundToPx() } / 6)
|
||||||
|
|
||||||
|
LaunchedEffect(synchronizedLyrics) {
|
||||||
|
while (isActive) {
|
||||||
|
delay(50)
|
||||||
|
if (synchronizedLyrics.update()) {
|
||||||
|
synchronizedLyrics.sentences.getOrNull(synchronizedLyrics.index)?.first?.let {
|
||||||
|
lazyListState.animateScrollToItem(synchronizedLyrics.index, with (density) { size.roundToPx() } / 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
|
userScrollEnabled = false,
|
||||||
|
contentPadding = PaddingValues(vertical = size / 2),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.verticalFadingEdge()
|
||||||
|
) {
|
||||||
|
itemsIndexed(items = synchronizedLyrics.sentences) { index, sentence ->
|
||||||
|
BasicText(
|
||||||
|
text = sentence.second,
|
||||||
|
style = typography.xs.center.medium.color(if (index == synchronizedLyrics.index) BlackColorPalette.text else BlackColorPalette.textDisabled),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 4.dp, horizontal = 32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
BasicText(
|
||||||
|
text = lyrics,
|
||||||
|
style = typography.xs.center.medium.color(BlackColorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.nestedScroll(remember { nestedScrollConnectionProvider() })
|
||||||
|
.verticalFadingEdge()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = size / 4, horizontal = 32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val menuState = LocalMenuState.current
|
val menuState = LocalMenuState.current
|
||||||
|
@ -210,6 +285,15 @@ fun Lyrics(
|
||||||
.clickable {
|
.clickable {
|
||||||
menuState.display {
|
menuState.display {
|
||||||
Menu {
|
Menu {
|
||||||
|
MenuEntry(
|
||||||
|
icon = R.drawable.time,
|
||||||
|
text = "Show ${if (isShowingSynchronizedLyrics) "static" else "synchronized"} lyrics",
|
||||||
|
onClick = {
|
||||||
|
menuState.hide()
|
||||||
|
isShowingSynchronizedLyrics = !isShowingSynchronizedLyrics
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
MenuEntry(
|
MenuEntry(
|
||||||
icon = R.drawable.pencil,
|
icon = R.drawable.pencil,
|
||||||
text = "Edit lyrics",
|
text = "Edit lyrics",
|
||||||
|
@ -237,11 +321,12 @@ fun Lyrics(
|
||||||
if (intent.resolveActivity(context.packageManager) != null) {
|
if (intent.resolveActivity(context.packageManager) != null) {
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Toast
|
||||||
context,
|
.makeText(
|
||||||
"No browser app found!",
|
context,
|
||||||
Toast.LENGTH_SHORT
|
"No browser app found!",
|
||||||
)
|
Toast.LENGTH_SHORT
|
||||||
|
)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,11 +104,21 @@ fun Thumbnail(
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
onShowLyrics(false)
|
onShowLyrics(false)
|
||||||
},
|
},
|
||||||
onLyricsUpdate = { mediaId, lyrics ->
|
onLyricsUpdate = { areSynchronized, mediaId, lyrics ->
|
||||||
if (Database.updateLyrics(mediaId, lyrics) == 0) {
|
if (areSynchronized) {
|
||||||
if (mediaId == mediaItem.mediaId) {
|
if (Database.updateSynchronizedLyrics(mediaId, lyrics) == 0) {
|
||||||
Database.insert(mediaItem) { song ->
|
if (mediaId == mediaItem.mediaId) {
|
||||||
song.copy(lyrics = lyrics)
|
Database.insert(mediaItem) { song ->
|
||||||
|
song.copy(synchronizedLyrics = lyrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Database.updateLyrics(mediaId, lyrics) == 0) {
|
||||||
|
if (mediaId == mediaItem.mediaId) {
|
||||||
|
Database.insert(mediaItem) { song ->
|
||||||
|
song.copy(lyrics = lyrics)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ const val repeatModeKey = "repeatMode"
|
||||||
const val skipSilenceKey = "skipSilence"
|
const val skipSilenceKey = "skipSilence"
|
||||||
const val volumeNormalizationKey = "volumeNormalization"
|
const val volumeNormalizationKey = "volumeNormalization"
|
||||||
const val persistentQueueKey = "persistentQueue"
|
const val persistentQueueKey = "persistentQueue"
|
||||||
|
const val isShowingSynchronizedLyricsKey = "isShowingSynchronizedLyrics"
|
||||||
|
|
||||||
inline fun <reified T : Enum<T>> SharedPreferences.getEnum(
|
inline fun <reified T : Enum<T>> SharedPreferences.getEnum(
|
||||||
key: String,
|
key: String,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.InternalComposeApi
|
import androidx.compose.runtime.InternalComposeApi
|
||||||
import androidx.compose.runtime.LaunchedEffectImpl
|
import androidx.compose.runtime.LaunchedEffectImpl
|
||||||
import androidx.compose.runtime.NonRestartableComposable
|
import androidx.compose.runtime.NonRestartableComposable
|
||||||
|
import androidx.compose.runtime.RememberObserver
|
||||||
import androidx.compose.runtime.currentComposer
|
import androidx.compose.runtime.currentComposer
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
@ -21,3 +22,13 @@ fun relaunchableEffect(
|
||||||
val launchedEffect = remember(key1) { LaunchedEffectImpl(applyContext, block) }
|
val launchedEffect = remember(key1) { LaunchedEffectImpl(applyContext, block) }
|
||||||
return launchedEffect::onRemembered
|
return launchedEffect::onRemembered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
fun relaunchableEffect2(
|
||||||
|
key1: Any?,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): RememberObserver {
|
||||||
|
val applyContext = currentComposer.applyCoroutineContext
|
||||||
|
return remember(key1) { LaunchedEffectImpl(applyContext, block) }
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package it.vfsfitvnm.vimusic.utils
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import it.vfsfitvnm.synchronizedlyrics.parseSentences
|
||||||
|
|
||||||
|
class SynchronizedLyrics(text: String, private val positionProvider: () -> Long) {
|
||||||
|
val sentences = parseSentences(text)
|
||||||
|
|
||||||
|
var index by mutableStateOf(currentIndex)
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val currentIndex: Int
|
||||||
|
get() {
|
||||||
|
var index = -1
|
||||||
|
for (item in sentences) {
|
||||||
|
if (item.first >= positionProvider()) break
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(): Boolean {
|
||||||
|
val newIndex = currentIndex
|
||||||
|
return if (newIndex != index) {
|
||||||
|
index = newIndex
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,8 @@ dependencyResolutionManagement {
|
||||||
version("kotlin", "1.7.0")
|
version("kotlin", "1.7.0")
|
||||||
plugin("kotlin-serialization","org.jetbrains.kotlin.plugin.serialization").versionRef("kotlin")
|
plugin("kotlin-serialization","org.jetbrains.kotlin.plugin.serialization").versionRef("kotlin")
|
||||||
|
|
||||||
|
library("kotlin-coroutines","org.jetbrains.kotlinx", "kotlinx-coroutines-core").version("1.6.4")
|
||||||
|
|
||||||
version("compose-compiler", "1.2.0")
|
version("compose-compiler", "1.2.0")
|
||||||
|
|
||||||
version("compose", "1.3.0-alpha02")
|
version("compose", "1.3.0-alpha02")
|
||||||
|
@ -62,3 +64,4 @@ include(":compose-routing")
|
||||||
include(":compose-reordering")
|
include(":compose-reordering")
|
||||||
include(":youtube-music")
|
include(":youtube-music")
|
||||||
include(":ktor-client-brotli")
|
include(":ktor-client-brotli")
|
||||||
|
include(":synchronized-lyrics")
|
||||||
|
|
1
synchronized-lyrics/.gitignore
vendored
Normal file
1
synchronized-lyrics/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
12
synchronized-lyrics/build.gradle.kts
Normal file
12
synchronized-lyrics/build.gradle.kts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets.all {
|
||||||
|
java.srcDir("src/$name/kotlin")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.kotlin.coroutines)
|
||||||
|
testImplementation(testLibs.junit)
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package it.vfsfitvnm.synchronizedlyrics
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
object LujjjhLyrics {
|
||||||
|
suspend fun forSong(artist: String, title: String): Result<String?>? {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
val artistParameter = URLEncoder.encode(artist, "UTF-8")
|
||||||
|
val titleParameter = URLEncoder.encode(title, "UTF-8")
|
||||||
|
|
||||||
|
URL("https://lyrics-api.lujjjh.com?artist=$artistParameter&name=$titleParameter")
|
||||||
|
.openConnection()
|
||||||
|
.getInputStream()
|
||||||
|
.bufferedReader()
|
||||||
|
.readText()
|
||||||
|
}.recoverIfCancelled()?.recoverCatching { throwable ->
|
||||||
|
if (throwable is FileNotFoundException) null else throw throwable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package it.vfsfitvnm.synchronizedlyrics
|
||||||
|
|
||||||
|
fun parseSentences(text: String): List<Pair<Long, String>> {
|
||||||
|
return mutableListOf(0L to "").apply {
|
||||||
|
for (line in text.trim().lines()) {
|
||||||
|
val sentence = line.substring(10)
|
||||||
|
|
||||||
|
if (sentence.startsWith(" 作词 : ") || sentence.startsWith(" 作曲 : ")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val position = line.take(10).run {
|
||||||
|
get(8).digitToInt() * 10L +
|
||||||
|
get(7).digitToInt() * 100 +
|
||||||
|
get(5).digitToInt() * 1000 +
|
||||||
|
get(4).digitToInt() * 10000 +
|
||||||
|
get(2).digitToInt() * 60 * 1000 +
|
||||||
|
get(1).digitToInt() * 600 * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
add(position to sentence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package it.vfsfitvnm.synchronizedlyrics
|
||||||
|
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
internal fun <T> Result<T>.recoverIfCancelled(): Result<T>? {
|
||||||
|
return when (exceptionOrNull()) {
|
||||||
|
is CancellationException -> null
|
||||||
|
else -> this
|
||||||
|
}
|
||||||
|
}
|
11
synchronized-lyrics/src/test/kotlin/Test.kt
Normal file
11
synchronized-lyrics/src/test/kotlin/Test.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class Test {
|
||||||
|
@Test
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun test() {
|
||||||
|
runBlocking {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue