diff --git a/app/schemas/com.subtracks.data.db.SubtracksDatabase/33.json b/app/schemas/com.subtracks.data.db.SubtracksDatabase/33.json new file mode 100644 index 00000000..2419ab48 --- /dev/null +++ b/app/schemas/com.subtracks.data.db.SubtracksDatabase/33.json @@ -0,0 +1,1900 @@ +{ + "formatVersion": 1, + "database": { + "version": 33, + "identityHash": "8804a58234c075cd84f4853b50fa5411", + "entities": [ + { + "tableName": "sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `creditsStale` INTEGER NOT NULL DEFAULT 1)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "creditsStale", + "columnName": "creditsStale", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "subsonic_sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` INTEGER NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `useTokenAuth` INTEGER NOT NULL, PRIMARY KEY(`sourceId`), FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "useTokenAuth", + "columnName": "useTokenAuth", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceId" + ] + }, + "indices": [ + { + "name": "index_subsonic_sources_sourceId", + "unique": false, + "columnNames": [ + "sourceId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_subsonic_sources_sourceId` ON `${TABLE_NAME}` (`sourceId`)" + } + ], + "foreignKeys": [ + { + "table": "sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "artists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` INTEGER NOT NULL, `id` TEXT NOT NULL, `name` TEXT NOT NULL COLLATE NOCASE, `albumCount` INTEGER NOT NULL, `starred` INTEGER, `coverArt` TEXT, `playCount` INTEGER NOT NULL DEFAULT 0, `played` INTEGER, `songCount` INTEGER NOT NULL DEFAULT 0, `sortName` TEXT NOT NULL COLLATE NOCASE DEFAULT '', `credited` INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(`sourceId`, `id`), FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumCount", + "columnName": "albumCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER" + }, + { + "fieldPath": "coverArt", + "columnName": "coverArt", + "affinity": "TEXT" + }, + { + "fieldPath": "playCount", + "columnName": "playCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "played", + "columnName": "played", + "affinity": "INTEGER" + }, + { + "fieldPath": "songCount", + "columnName": "songCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "sortName", + "columnName": "sortName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "credited", + "columnName": "credited", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceId", + "id" + ] + }, + "indices": [ + { + "name": "index_artists_sourceId", + "unique": false, + "columnNames": [ + "sourceId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_artists_sourceId` ON `${TABLE_NAME}` (`sourceId`)" + }, + { + "name": "index_artists_name", + "unique": false, + "columnNames": [ + "sourceId", + "sortName", + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_artists_name` ON `${TABLE_NAME}` (`sourceId`, `sortName`, `id`)" + }, + { + "name": "index_artists_albumCount", + "unique": false, + "columnNames": [ + "sourceId", + "albumCount", + "sortName", + "id" + ], + "orders": [ + "ASC", + "DESC", + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_artists_albumCount` ON `${TABLE_NAME}` (`sourceId` ASC, `albumCount` DESC, `sortName` ASC, `id` ASC)" + }, + { + "name": "index_artists_starred", + "unique": false, + "columnNames": [ + "sourceId", + "starred", + "sortName", + "id" + ], + "orders": [ + "ASC", + "DESC", + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_artists_starred` ON `${TABLE_NAME}` (`sourceId` ASC, `starred` DESC, `sortName` ASC, `id` ASC)" + }, + { + "name": "index_artists_frequent", + "unique": false, + "columnNames": [ + "sourceId", + "playCount", + "sortName", + "id" + ], + "orders": [ + "ASC", + "DESC", + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_artists_frequent` ON `${TABLE_NAME}` (`sourceId` ASC, `playCount` DESC, `sortName` ASC, `id` ASC)" + }, + { + "name": "index_artists_recent", + "unique": false, + "columnNames": [ + "sourceId", + "played", + "sortName", + "id" + ], + "orders": [ + "ASC", + "DESC", + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_artists_recent` ON `${TABLE_NAME}` (`sourceId` ASC, `played` DESC, `sortName` ASC, `id` ASC)" + } + ], + "foreignKeys": [ + { + "table": "sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "artist_search", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS5(`name`, `sortName`, tokenize=`trigram`, content=`artists`)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sortName", + "columnName": "sortName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "rowid" + ] + }, + "ftsVersion": "FTS5", + "ftsOptions": { + "tokenizer": "trigram", + "tokenizerArgs": [], + "contentTable": "artists", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC", + "contentRowId": "", + "columnSize": true, + "detail": "FULL" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_artist_search_BEFORE_UPDATE BEFORE UPDATE ON `artists` BEGIN DELETE FROM `artist_search` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_artist_search_BEFORE_DELETE BEFORE DELETE ON `artists` BEGIN DELETE FROM `artist_search` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_artist_search_AFTER_UPDATE AFTER UPDATE ON `artists` BEGIN INSERT INTO `artist_search`(`rowid`, `name`, `sortName`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`sortName`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_artist_search_AFTER_INSERT AFTER INSERT ON `artists` BEGIN INSERT INTO `artist_search`(`rowid`, `name`, `sortName`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`sortName`); END" + ] + }, + { + "tableName": "albums", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` INTEGER NOT NULL, `id` TEXT NOT NULL, `artistId` TEXT, `name` TEXT NOT NULL COLLATE NOCASE, `albumArtist` TEXT COLLATE NOCASE, `created` INTEGER NOT NULL, `coverArt` TEXT, `genre` TEXT, `year` INTEGER, `starred` INTEGER, `songCount` INTEGER NOT NULL, `playCount` INTEGER NOT NULL DEFAULT 0, `played` INTEGER, `artists` TEXT NOT NULL DEFAULT '', `sortName` TEXT NOT NULL COLLATE NOCASE DEFAULT '', `albumArtistSort` TEXT NOT NULL COLLATE NOCASE DEFAULT '', PRIMARY KEY(`sourceId`, `id`), FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumArtist", + "columnName": "albumArtist", + "affinity": "TEXT" + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "coverArt", + "columnName": "coverArt", + "affinity": "TEXT" + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT" + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "INTEGER" + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER" + }, + { + "fieldPath": "songCount", + "columnName": "songCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playCount", + "columnName": "playCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "played", + "columnName": "played", + "affinity": "INTEGER" + }, + { + "fieldPath": "artists", + "columnName": "artists", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "sortName", + "columnName": "sortName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "albumArtistSort", + "columnName": "albumArtistSort", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceId", + "id" + ] + }, + "indices": [ + { + "name": "index_albums_sourceId", + "unique": false, + "columnNames": [ + "sourceId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_albums_sourceId` ON `${TABLE_NAME}` (`sourceId`)" + }, + { + "name": "index_albums_sourceId_artistId", + "unique": false, + "columnNames": [ + "sourceId", + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_albums_sourceId_artistId` ON `${TABLE_NAME}` (`sourceId`, `artistId`)" + }, + { + "name": "index_albums_name", + "unique": false, + "columnNames": [ + "sourceId", + "sortName", + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_albums_name` ON `${TABLE_NAME}` (`sourceId`, `sortName`, `id`)" + }, + { + "name": "index_albums_artist", + "unique": false, + "columnNames": [ + "sourceId", + "albumArtistSort", + "year", + "sortName", + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_albums_artist` ON `${TABLE_NAME}` (`sourceId`, `albumArtistSort`, `year`, `sortName`, `id`)" + }, + { + "name": "index_albums_year", + "unique": false, + "columnNames": [ + "sourceId", + "year", + "sortName", + "id" + ], + "orders": [ + "ASC", + "DESC", + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_albums_year` ON `${TABLE_NAME}` (`sourceId` ASC, `year` DESC, `sortName` ASC, `id` ASC)" + }, + { + "name": "index_albums_added", + "unique": false, + "columnNames": [ + "sourceId", + "created", + "sortName", + "id" + ], + "orders": [ + "ASC", + "DESC", + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_albums_added` ON `${TABLE_NAME}` (`sourceId` ASC, `created` DESC, `sortName` ASC, `id` ASC)" + }, + { + "name": "index_albums_starred", + "unique": false, + "columnNames": [ + "sourceId", + "starred", + "sortName", + "id" + ], + "orders": [ + "ASC", + "DESC", + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_albums_starred` ON `${TABLE_NAME}` (`sourceId` ASC, `starred` DESC, `sortName` ASC, `id` ASC)" + }, + { + "name": "index_albums_frequent", + "unique": false, + "columnNames": [ + "sourceId", + "playCount", + "sortName", + "id" + ], + "orders": [ + "ASC", + "DESC", + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_albums_frequent` ON `${TABLE_NAME}` (`sourceId` ASC, `playCount` DESC, `sortName` ASC, `id` ASC)" + }, + { + "name": "index_albums_recent", + "unique": false, + "columnNames": [ + "sourceId", + "played", + "sortName", + "id" + ], + "orders": [ + "ASC", + "DESC", + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_albums_recent` ON `${TABLE_NAME}` (`sourceId` ASC, `played` DESC, `sortName` ASC, `id` ASC)" + } + ], + "foreignKeys": [ + { + "table": "sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "album_search", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS5(`name`, `albumArtist`, `sortName`, `albumArtistSort`, tokenize=`trigram`, content=`albums`)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumArtist", + "columnName": "albumArtist", + "affinity": "TEXT" + }, + { + "fieldPath": "sortName", + "columnName": "sortName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumArtistSort", + "columnName": "albumArtistSort", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "rowid" + ] + }, + "ftsVersion": "FTS5", + "ftsOptions": { + "tokenizer": "trigram", + "tokenizerArgs": [], + "contentTable": "albums", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC", + "contentRowId": "", + "columnSize": true, + "detail": "FULL" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_album_search_BEFORE_UPDATE BEFORE UPDATE ON `albums` BEGIN DELETE FROM `album_search` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_album_search_BEFORE_DELETE BEFORE DELETE ON `albums` BEGIN DELETE FROM `album_search` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_album_search_AFTER_UPDATE AFTER UPDATE ON `albums` BEGIN INSERT INTO `album_search`(`rowid`, `name`, `albumArtist`, `sortName`, `albumArtistSort`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`albumArtist`, NEW.`sortName`, NEW.`albumArtistSort`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_album_search_AFTER_INSERT AFTER INSERT ON `albums` BEGIN INSERT INTO `album_search`(`rowid`, `name`, `albumArtist`, `sortName`, `albumArtistSort`) VALUES (NEW.`rowid`, NEW.`name`, NEW.`albumArtist`, NEW.`sortName`, NEW.`albumArtistSort`); END" + ] + }, + { + "tableName": "discs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` INTEGER NOT NULL, `albumId` TEXT NOT NULL, `disc` INTEGER NOT NULL, `title` TEXT NOT NULL, PRIMARY KEY(`sourceId`, `albumId`, `disc`), FOREIGN KEY(`sourceId`, `albumId`) REFERENCES `albums`(`sourceId`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disc", + "columnName": "disc", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceId", + "albumId", + "disc" + ] + }, + "indices": [ + { + "name": "index_discs_sourceId_albumId", + "unique": false, + "columnNames": [ + "sourceId", + "albumId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_discs_sourceId_albumId` ON `${TABLE_NAME}` (`sourceId`, `albumId`)" + } + ], + "foreignKeys": [ + { + "table": "albums", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId", + "albumId" + ], + "referencedColumns": [ + "sourceId", + "id" + ] + } + ] + }, + { + "tableName": "album_artists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` INTEGER NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER NOT NULL, `artistId` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`sourceId`, `albumId`, `position`), FOREIGN KEY(`sourceId`, `albumId`) REFERENCES `albums`(`sourceId`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceId", + "albumId", + "position" + ] + }, + "indices": [ + { + "name": "index_album_artists_sourceId_artistId", + "unique": false, + "columnNames": [ + "sourceId", + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_album_artists_sourceId_artistId` ON `${TABLE_NAME}` (`sourceId`, `artistId`)" + } + ], + "foreignKeys": [ + { + "table": "albums", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId", + "albumId" + ], + "referencedColumns": [ + "sourceId", + "id" + ] + } + ] + }, + { + "tableName": "playlists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` INTEGER NOT NULL, `id` TEXT NOT NULL, `name` TEXT NOT NULL COLLATE NOCASE, `comment` TEXT, `coverArt` TEXT, `songCount` INTEGER NOT NULL, `created` INTEGER NOT NULL, `changed` INTEGER NOT NULL DEFAULT 0, `duration` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceId`, `id`), FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "comment", + "columnName": "comment", + "affinity": "TEXT" + }, + { + "fieldPath": "coverArt", + "columnName": "coverArt", + "affinity": "TEXT" + }, + { + "fieldPath": "songCount", + "columnName": "songCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "changed", + "columnName": "changed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceId", + "id" + ] + }, + "indices": [ + { + "name": "index_playlists_sourceId", + "unique": false, + "columnNames": [ + "sourceId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_sourceId` ON `${TABLE_NAME}` (`sourceId`)" + }, + { + "name": "index_playlists_name", + "unique": false, + "columnNames": [ + "sourceId", + "name", + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`sourceId`, `name`, `id`)" + }, + { + "name": "index_playlists_added", + "unique": false, + "columnNames": [ + "sourceId", + "created", + "name", + "id" + ], + "orders": [ + "ASC", + "DESC", + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_added` ON `${TABLE_NAME}` (`sourceId` ASC, `created` DESC, `name` ASC, `id` ASC)" + }, + { + "name": "index_playlists_updated", + "unique": false, + "columnNames": [ + "sourceId", + "changed", + "name", + "id" + ], + "orders": [ + "ASC", + "DESC", + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_updated` ON `${TABLE_NAME}` (`sourceId` ASC, `changed` DESC, `name` ASC, `id` ASC)" + } + ], + "foreignKeys": [ + { + "table": "sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "playlist_search", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS5(`name`, tokenize=`trigram`, content=`playlists`)", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "rowid" + ] + }, + "ftsVersion": "FTS5", + "ftsOptions": { + "tokenizer": "trigram", + "tokenizerArgs": [], + "contentTable": "playlists", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC", + "contentRowId": "", + "columnSize": true, + "detail": "FULL" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_playlist_search_BEFORE_UPDATE BEFORE UPDATE ON `playlists` BEGIN DELETE FROM `playlist_search` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_playlist_search_BEFORE_DELETE BEFORE DELETE ON `playlists` BEGIN DELETE FROM `playlist_search` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_playlist_search_AFTER_UPDATE AFTER UPDATE ON `playlists` BEGIN INSERT INTO `playlist_search`(`rowid`, `name`) VALUES (NEW.`rowid`, NEW.`name`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_playlist_search_AFTER_INSERT AFTER INSERT ON `playlists` BEGIN INSERT INTO `playlist_search`(`rowid`, `name`) VALUES (NEW.`rowid`, NEW.`name`); END" + ] + }, + { + "tableName": "playlist_songs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` INTEGER NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`sourceId`, `playlistId`, `position`), FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlistId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceId", + "playlistId", + "position" + ] + }, + "indices": [ + { + "name": "index_playlist_songs_sourceId_playlistId", + "unique": false, + "columnNames": [ + "sourceId", + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_sourceId_playlistId` ON `${TABLE_NAME}` (`sourceId`, `playlistId`)" + }, + { + "name": "index_playlist_songs_sourceId_songId", + "unique": false, + "columnNames": [ + "sourceId", + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_songs_sourceId_songId` ON `${TABLE_NAME}` (`sourceId`, `songId`)" + } + ], + "foreignKeys": [ + { + "table": "sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "songs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` INTEGER NOT NULL, `id` TEXT NOT NULL, `albumId` TEXT, `artistId` TEXT, `title` TEXT NOT NULL, `album` TEXT, `artist` TEXT, `duration` INTEGER, `track` INTEGER, `disc` INTEGER, `starred` INTEGER, `created` INTEGER NOT NULL DEFAULT 0, `playCount` INTEGER NOT NULL DEFAULT 0, `played` INTEGER, `trackGain` REAL, `albumGain` REAL, `trackPeak` REAL, `albumPeak` REAL, `artists` TEXT NOT NULL DEFAULT '', `sortName` TEXT NOT NULL COLLATE NOCASE DEFAULT '', `artistSort` TEXT NOT NULL COLLATE NOCASE DEFAULT '', PRIMARY KEY(`sourceId`, `id`), FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT" + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT" + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "album", + "columnName": "album", + "affinity": "TEXT" + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT" + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER" + }, + { + "fieldPath": "track", + "columnName": "track", + "affinity": "INTEGER" + }, + { + "fieldPath": "disc", + "columnName": "disc", + "affinity": "INTEGER" + }, + { + "fieldPath": "starred", + "columnName": "starred", + "affinity": "INTEGER" + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "playCount", + "columnName": "playCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "played", + "columnName": "played", + "affinity": "INTEGER" + }, + { + "fieldPath": "trackGain", + "columnName": "trackGain", + "affinity": "REAL" + }, + { + "fieldPath": "albumGain", + "columnName": "albumGain", + "affinity": "REAL" + }, + { + "fieldPath": "trackPeak", + "columnName": "trackPeak", + "affinity": "REAL" + }, + { + "fieldPath": "albumPeak", + "columnName": "albumPeak", + "affinity": "REAL" + }, + { + "fieldPath": "artists", + "columnName": "artists", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "sortName", + "columnName": "sortName", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "artistSort", + "columnName": "artistSort", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceId", + "id" + ] + }, + "indices": [ + { + "name": "index_songs_sourceId_artistId", + "unique": false, + "columnNames": [ + "sourceId", + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_songs_sourceId_artistId` ON `${TABLE_NAME}` (`sourceId`, `artistId`)" + }, + { + "name": "index_songs_sourceId_albumId_order", + "unique": false, + "columnNames": [ + "sourceId", + "albumId", + "disc", + "track", + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_songs_sourceId_albumId_order` ON `${TABLE_NAME}` (`sourceId`, `albumId`, `disc`, `track`, `id`)" + }, + { + "name": "index_songs_starred", + "unique": false, + "columnNames": [ + "sourceId", + "starred", + "sortName", + "id" + ], + "orders": [ + "ASC", + "DESC", + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_songs_starred` ON `${TABLE_NAME}` (`sourceId` ASC, `starred` DESC, `sortName` ASC, `id` ASC)" + } + ], + "foreignKeys": [ + { + "table": "sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "song_genres", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` INTEGER NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `genre` TEXT NOT NULL, PRIMARY KEY(`sourceId`, `songId`, `position`), FOREIGN KEY(`sourceId`, `songId`) REFERENCES `songs`(`sourceId`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "genre", + "columnName": "genre", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceId", + "songId", + "position" + ] + }, + "indices": [ + { + "name": "index_song_genres_sourceId_genre", + "unique": false, + "columnNames": [ + "sourceId", + "genre" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_genres_sourceId_genre` ON `${TABLE_NAME}` (`sourceId`, `genre`)" + } + ], + "foreignKeys": [ + { + "table": "songs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId", + "songId" + ], + "referencedColumns": [ + "sourceId", + "id" + ] + } + ] + }, + { + "tableName": "song_artists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` INTEGER NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `artistId` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`sourceId`, `songId`, `position`), FOREIGN KEY(`sourceId`, `songId`) REFERENCES `songs`(`sourceId`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceId", + "songId", + "position" + ] + }, + "indices": [ + { + "name": "index_song_artists_sourceId_artistId", + "unique": false, + "columnNames": [ + "sourceId", + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_artists_sourceId_artistId` ON `${TABLE_NAME}` (`sourceId`, `artistId`)" + } + ], + "foreignKeys": [ + { + "table": "songs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId", + "songId" + ], + "referencedColumns": [ + "sourceId", + "id" + ] + } + ] + }, + { + "tableName": "song_search", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS5(`title`, `artist`, `sortName`, `artistSort`, tokenize=`trigram`, content=`songs`)", + "fields": [ + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artist", + "columnName": "artist", + "affinity": "TEXT" + }, + { + "fieldPath": "sortName", + "columnName": "sortName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistSort", + "columnName": "artistSort", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "rowid" + ] + }, + "ftsVersion": "FTS5", + "ftsOptions": { + "tokenizer": "trigram", + "tokenizerArgs": [], + "contentTable": "songs", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC", + "contentRowId": "", + "columnSize": true, + "detail": "FULL" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_song_search_BEFORE_UPDATE BEFORE UPDATE ON `songs` BEGIN DELETE FROM `song_search` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_song_search_BEFORE_DELETE BEFORE DELETE ON `songs` BEGIN DELETE FROM `song_search` WHERE `rowid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_song_search_AFTER_UPDATE AFTER UPDATE ON `songs` BEGIN INSERT INTO `song_search`(`rowid`, `title`, `artist`, `sortName`, `artistSort`) VALUES (NEW.`rowid`, NEW.`title`, NEW.`artist`, NEW.`sortName`, NEW.`artistSort`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_song_search_AFTER_INSERT AFTER INSERT ON `songs` BEGIN INSERT INTO `song_search`(`rowid`, `title`, `artist`, `sortName`, `artistSort`) VALUES (NEW.`rowid`, NEW.`title`, NEW.`artist`, NEW.`sortName`, NEW.`artistSort`); END" + ] + }, + { + "tableName": "queue_entries", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `position` INTEGER NOT NULL, `sourceId` INTEGER NOT NULL, `kind` TEXT NOT NULL, `refId` TEXT NOT NULL, `rangeStart` INTEGER, `rangeEnd` INTEGER, FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "refId", + "columnName": "refId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rangeStart", + "columnName": "rangeStart", + "affinity": "INTEGER" + }, + { + "fieldPath": "rangeEnd", + "columnName": "rangeEnd", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_queue_entries_sourceId", + "unique": false, + "columnNames": [ + "sourceId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_queue_entries_sourceId` ON `${TABLE_NAME}` (`sourceId`)" + }, + { + "name": "index_queue_entries_position", + "unique": false, + "columnNames": [ + "position" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_queue_entries_position` ON `${TABLE_NAME}` (`position`)" + } + ], + "foreignKeys": [ + { + "table": "sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "up_next_entries", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `position` INTEGER NOT NULL, `sourceId` INTEGER NOT NULL, `kind` TEXT NOT NULL, `refId` TEXT NOT NULL, `rangeStart` INTEGER, `rangeEnd` INTEGER, FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "refId", + "columnName": "refId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rangeStart", + "columnName": "rangeStart", + "affinity": "INTEGER" + }, + { + "fieldPath": "rangeEnd", + "columnName": "rangeEnd", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_up_next_entries_sourceId", + "unique": false, + "columnNames": [ + "sourceId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_up_next_entries_sourceId` ON `${TABLE_NAME}` (`sourceId`)" + }, + { + "name": "index_up_next_entries_position", + "unique": false, + "columnNames": [ + "position" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_up_next_entries_position` ON `${TABLE_NAME}` (`position`)" + } + ], + "foreignKeys": [ + { + "table": "sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "playback_cursor", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `queuePosition` INTEGER NOT NULL, `shuffleEnabled` INTEGER NOT NULL DEFAULT 0, `repeatMode` INTEGER NOT NULL DEFAULT 0, `shuffleSeed` INTEGER NOT NULL DEFAULT 0, `shuffleSize` INTEGER NOT NULL DEFAULT 0, `positionMs` INTEGER NOT NULL DEFAULT 0, `upNextAnchor` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "queuePosition", + "columnName": "queuePosition", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "shuffleEnabled", + "columnName": "shuffleEnabled", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "repeatMode", + "columnName": "repeatMode", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "shuffleSeed", + "columnName": "shuffleSeed", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "shuffleSize", + "columnName": "shuffleSize", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "positionMs", + "columnName": "positionMs", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "upNextAnchor", + "columnName": "upNextAnchor", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "artwork_seeds", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cacheKey` TEXT NOT NULL, `primary` INTEGER NOT NULL, `secondary` INTEGER, `nameBusy` INTEGER, `sourceId` INTEGER NOT NULL, `storedAt` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`cacheKey`), FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "cacheKey", + "columnName": "cacheKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "secondary", + "columnName": "secondary", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameBusy", + "columnName": "nameBusy", + "affinity": "INTEGER" + }, + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "storedAt", + "columnName": "storedAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "cacheKey" + ] + }, + "indices": [ + { + "name": "index_artwork_seeds_sourceId", + "unique": false, + "columnNames": [ + "sourceId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_artwork_seeds_sourceId` ON `${TABLE_NAME}` (`sourceId`)" + } + ], + "foreignKeys": [ + { + "table": "sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "song_downloads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` INTEGER NOT NULL, `songId` TEXT NOT NULL, `status` TEXT NOT NULL, `engineId` INTEGER, `bytes` INTEGER NOT NULL DEFAULT 0, `total` INTEGER NOT NULL DEFAULT 0, `error` TEXT, `downloadedAt` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`sourceId`, `songId`), FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`sourceId`, `songId`) REFERENCES `songs`(`sourceId`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "engineId", + "columnName": "engineId", + "affinity": "INTEGER" + }, + { + "fieldPath": "bytes", + "columnName": "bytes", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "total", + "columnName": "total", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT" + }, + { + "fieldPath": "downloadedAt", + "columnName": "downloadedAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "sourceId", + "songId" + ] + }, + "indices": [ + { + "name": "index_song_downloads_sourceId", + "unique": false, + "columnNames": [ + "sourceId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_downloads_sourceId` ON `${TABLE_NAME}` (`sourceId`)" + }, + { + "name": "index_song_downloads_engineId", + "unique": false, + "columnNames": [ + "engineId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_song_downloads_engineId` ON `${TABLE_NAME}` (`engineId`)" + } + ], + "foreignKeys": [ + { + "table": "sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "songs", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId", + "songId" + ], + "referencedColumns": [ + "sourceId", + "id" + ] + } + ] + }, + { + "tableName": "pending_actions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `sourceId` INTEGER NOT NULL, `kind` TEXT NOT NULL, `targetId` TEXT NOT NULL, `starType` TEXT, `time` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sourceId", + "columnName": "sourceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "targetId", + "columnName": "targetId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starType", + "columnName": "starType", + "affinity": "TEXT" + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_pending_actions_sourceId", + "unique": false, + "columnNames": [ + "sourceId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pending_actions_sourceId` ON `${TABLE_NAME}` (`sourceId`)" + } + ], + "foreignKeys": [ + { + "table": "sources", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "sourceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "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, '8804a58234c075cd84f4853b50fa5411')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt b/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt index 77f28653..9707ab96 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt @@ -93,7 +93,8 @@ private const val DOWNLOADED_ARTIST_ALBUMS = "WHERE song_artists.sourceId = :sourceId AND song_artists.artistId = artists.id " + "AND sd.status = 'Completed' AND songs.albumId IS NOT NULL" -internal const val ARTIST_CREDITED = "artists.credited = 1" +internal const val ARTIST_CREDITED = + "(artists.credited = 1 OR COALESCE((SELECT creditsStale FROM sources WHERE id = artists.sourceId), 1) = 1)" internal const val ARTISTS_FILTER = "FROM artists WHERE sourceId = :sourceId " + @@ -212,24 +213,25 @@ interface LibraryDao { // The credited albums are the union of the lead id and the relation table, so a shared album // counts once per artist rather than twice. @Query( - "UPDATE artists SET " + - "playCount = COALESCE((SELECT SUM(a.playCount) FROM albums a " + - "WHERE a.sourceId = artists.sourceId AND a.id IN (" + - "SELECT albumId FROM album_artists WHERE sourceId = artists.sourceId AND artistId = artists.id " + - "UNION SELECT id FROM albums WHERE sourceId = artists.sourceId AND artistId = artists.id)), 0), " + - "played = (SELECT MAX(a.played) FROM albums a " + - "WHERE a.sourceId = artists.sourceId AND a.id IN (" + - "SELECT albumId FROM album_artists WHERE sourceId = artists.sourceId AND artistId = artists.id " + - "UNION SELECT id FROM albums WHERE sourceId = artists.sourceId AND artistId = artists.id)) " + - "WHERE sourceId = :sourceId AND (" + - "playCount IS NOT COALESCE((SELECT SUM(a.playCount) FROM albums a " + - "WHERE a.sourceId = artists.sourceId AND a.id IN (" + - "SELECT albumId FROM album_artists WHERE sourceId = artists.sourceId AND artistId = artists.id " + - "UNION SELECT id FROM albums WHERE sourceId = artists.sourceId AND artistId = artists.id)), 0) " + - "OR played IS NOT (SELECT MAX(a.played) FROM albums a " + - "WHERE a.sourceId = artists.sourceId AND a.id IN (" + - "SELECT albumId FROM album_artists WHERE sourceId = artists.sourceId AND artistId = artists.id " + - "UNION SELECT id FROM albums WHERE sourceId = artists.sourceId AND artistId = artists.id)))", + "WITH credited AS (" + + "SELECT albums.artistId AS artistId, albums.id AS albumId, " + + "albums.playCount AS playCount, albums.played AS played " + + "FROM albums WHERE albums.sourceId = :sourceId AND albums.artistId IS NOT NULL " + + "UNION " + + "SELECT album_artists.artistId, albums.id, albums.playCount, albums.played " + + "FROM album_artists JOIN albums " + + "ON albums.sourceId = album_artists.sourceId AND albums.id = album_artists.albumId " + + "WHERE album_artists.sourceId = :sourceId" + + "), totals AS (" + + "SELECT artistId AS id, SUM(playCount) AS playCount, MAX(played) AS played " + + "FROM credited GROUP BY artistId" + + ") " + + "UPDATE artists SET playCount = c.playCount, played = c.played " + + "FROM (SELECT artists.id AS id, COALESCE(totals.playCount, 0) AS playCount, totals.played AS played " + + "FROM artists LEFT JOIN totals ON totals.id = artists.id " + + "WHERE artists.sourceId = :sourceId) c " + + "WHERE artists.sourceId = :sourceId AND artists.id = c.id " + + "AND (artists.playCount != c.playCount OR artists.played IS NOT c.played)", ) suspend fun recomputeArtistPlayData(sourceId: Long) @@ -248,7 +250,7 @@ interface LibraryDao { "UPDATE artists SET songCount = c.songCount, credited = c.credited " + "FROM (SELECT artists.id AS id, COALESCE(counts.songCount, 0) AS songCount, " + "((NOT EXISTS (SELECT 1 FROM album_artists WHERE sourceId = artists.sourceId) " + - "AND NOT EXISTS (SELECT 1 FROM song_artists WHERE sourceId = artists.sourceId)) " + + "OR NOT EXISTS (SELECT 1 FROM song_artists WHERE sourceId = artists.sourceId)) " + "OR EXISTS (SELECT 1 FROM album_artists aa " + "WHERE aa.sourceId = artists.sourceId AND aa.artistId = artists.id) " + "OR EXISTS (SELECT 1 FROM song_artists sa " + diff --git a/app/src/main/kotlin/com/subtracks/data/db/Migrations.kt b/app/src/main/kotlin/com/subtracks/data/db/Migrations.kt index e1fe4e53..f571ab1e 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/Migrations.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/Migrations.kt @@ -557,8 +557,8 @@ val MIGRATION_31_32 = connection.execSQL("ALTER TABLE `artists` ADD COLUMN `credited` INTEGER NOT NULL DEFAULT 1") connection.execSQL( "UPDATE `artists` SET `credited` = 0 " + - "WHERE (EXISTS (SELECT 1 FROM `album_artists` WHERE `sourceId` = `artists`.`sourceId`) " + - "OR EXISTS (SELECT 1 FROM `song_artists` WHERE `sourceId` = `artists`.`sourceId`)) " + + "WHERE EXISTS (SELECT 1 FROM `album_artists` WHERE `sourceId` = `artists`.`sourceId`) " + + "AND EXISTS (SELECT 1 FROM `song_artists` WHERE `sourceId` = `artists`.`sourceId`) " + "AND NOT EXISTS (SELECT 1 FROM `album_artists` aa " + "WHERE aa.`sourceId` = `artists`.`sourceId` AND aa.`artistId` = `artists`.`id`) " + "AND NOT EXISTS (SELECT 1 FROM `song_artists` sa " + @@ -567,6 +567,18 @@ val MIGRATION_31_32 = } } +val MIGRATION_32_33 = + object : Migration(32, 33) { + override suspend fun migrate(connection: SQLiteConnection) { + connection.execSQL("ALTER TABLE `sources` ADD COLUMN `creditsStale` INTEGER NOT NULL DEFAULT 1") + connection.execSQL("DROP INDEX IF EXISTS `index_songs_starred`") + connection.execSQL( + "CREATE INDEX IF NOT EXISTS `index_songs_starred` ON `songs` " + + "(`sourceId` ASC, `starred` DESC, `sortName` ASC, `id` ASC)", + ) + } + } + // Mirrors the FTS5 table and content-sync triggers Room generates for the @Fts5 entities. private suspend fun createSearchIndex( connection: SQLiteConnection, @@ -632,4 +644,5 @@ val MIGRATIONS: Array = MIGRATION_29_30, MIGRATION_30_31, MIGRATION_31_32, + MIGRATION_32_33, ) diff --git a/app/src/main/kotlin/com/subtracks/data/db/QueueDao.kt b/app/src/main/kotlin/com/subtracks/data/db/QueueDao.kt index ec778442..69605597 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/QueueDao.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/QueueDao.kt @@ -34,7 +34,7 @@ private const val ARTIST_SONG_IDS = private const val ARTIST_SONGS_SELECT = "SELECT songs.*, albums.coverArt AS coverArt FROM songs " + - "JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " + + "LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " + "WHERE songs.sourceId = :sourceId AND songs.id IN $ARTIST_SONG_IDS" private const val ARTIST_SONGS_ORDER = @@ -95,7 +95,7 @@ interface QueueDao { @Query( "SELECT COUNT(*) FROM songs " + - "JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " + + "LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " + "WHERE songs.sourceId = :sourceId AND songs.id IN $ARTIST_SONG_IDS", ) suspend fun artistLength( @@ -203,7 +203,7 @@ interface QueueDao { @Query( "SELECT songs.id FROM songs " + - "JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " + + "LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " + "WHERE songs.sourceId = :sourceId AND songs.id IN $ARTIST_SONG_IDS" + ARTIST_SONGS_ORDER, ) diff --git a/app/src/main/kotlin/com/subtracks/data/db/SourcesDao.kt b/app/src/main/kotlin/com/subtracks/data/db/SourcesDao.kt index 2e8edc86..2b3adfeb 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/SourcesDao.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/SourcesDao.kt @@ -29,6 +29,15 @@ interface SourcesDao { @Query("SELECT id FROM sources WHERE isActive = 1 LIMIT 1") suspend fun activeSourceIdOnce(): Long? + @Query("SELECT creditsStale FROM sources WHERE id = :sourceId") + suspend fun creditsStale(sourceId: Long): Boolean? + + @Query("UPDATE sources SET creditsStale = :stale WHERE id = :sourceId") + suspend fun setCreditsStale( + sourceId: Long, + stale: Boolean, + ) + @Insert suspend fun insertSource(source: Source): Long diff --git a/app/src/main/kotlin/com/subtracks/data/db/SubtracksDatabase.kt b/app/src/main/kotlin/com/subtracks/data/db/SubtracksDatabase.kt index 7e4a1872..25a217d6 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/SubtracksDatabase.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/SubtracksDatabase.kt @@ -54,7 +54,7 @@ import com.subtracks.data.model.UpNextEntry SongDownload::class, PendingAction::class, ], - version = 32, + version = 33, exportSchema = true, ) @ColumnTypeConverters( diff --git a/app/src/main/kotlin/com/subtracks/data/model/Models.kt b/app/src/main/kotlin/com/subtracks/data/model/Models.kt index 72ea7595..80db8821 100644 --- a/app/src/main/kotlin/com/subtracks/data/model/Models.kt +++ b/app/src/main/kotlin/com/subtracks/data/model/Models.kt @@ -18,6 +18,7 @@ data class Source( val address: String, val isActive: Boolean = false, val createdAt: Long, + @ColumnInfo(defaultValue = "1") val creditsStale: Boolean = true, ) @Entity( @@ -263,8 +264,8 @@ data class PlaylistSong( ), Index( name = "index_songs_starred", - value = ["sourceId", "starred", "id"], - orders = [Index.Order.ASC, Index.Order.DESC, Index.Order.ASC], + value = ["sourceId", "starred", "sortName", "id"], + orders = [Index.Order.ASC, Index.Order.DESC, Index.Order.ASC, Index.Order.ASC], ), ], ) diff --git a/app/src/main/kotlin/com/subtracks/data/sync/SyncService.kt b/app/src/main/kotlin/com/subtracks/data/sync/SyncService.kt index 027295c8..fc253531 100644 --- a/app/src/main/kotlin/com/subtracks/data/sync/SyncService.kt +++ b/app/src/main/kotlin/com/subtracks/data/sync/SyncService.kt @@ -34,6 +34,8 @@ class SyncService( private var creditsChanged = false suspend fun sync(): SyncSummary { + val sources = db.sourcesDao() + val wasStale = sources.creditsStale(source.id) ?: true creditsChanged = false val artistsBefore = dbTotalChanges() syncArtists() @@ -41,7 +43,10 @@ class SyncService( syncAlbums() syncSongs() optimize() - if (creditsChanged || artistsChanged) recomputeArtistSongCounts() + if (creditsChanged || artistsChanged || wasStale) { + recomputeArtistSongCounts() + sources.setCreditsStale(source.id, false) + } aggregatePlayData() syncPlaylists() syncPlaylistSongs() @@ -83,7 +88,13 @@ class SyncService( albumsSynced += batch.size } val prunedBefore = pruned - pruned += pruneStaleIds(seenAlbums, { library.albumIdsAfter(source.id, it, PRUNE_PAGE) }, { library.deleteAlbums(source.id, it) }) + pruned += + pruneStaleIds( + seenAlbums, + { library.albumIdsAfter(source.id, it, PRUNE_PAGE) }, + { library.deleteAlbums(source.id, it) }, + markStale = { db.sourcesDao().setCreditsStale(source.id, true) }, + ) if (pruned > prunedBefore) creditsChanged = true pruned += pruneStaleDiscs(seenDiscs, { albumId, disc -> library.discKeysAfter(source.id, albumId, disc, PRUNE_PAGE) }) { @@ -101,7 +112,13 @@ class SyncService( songsSynced += batch.size } val prunedBefore = pruned - pruned += pruneStaleIds(seen, { library.songIdsAfter(source.id, it, PRUNE_PAGE) }, { library.deleteSongs(source.id, it) }) + pruned += + pruneStaleIds( + seen, + { library.songIdsAfter(source.id, it, PRUNE_PAGE) }, + { library.deleteSongs(source.id, it) }, + markStale = { db.sourcesDao().setCreditsStale(source.id, true) }, + ) if (pruned > prunedBefore) creditsChanged = true } @@ -117,7 +134,8 @@ class SyncService( } db.useWriterConnection { connection -> connection.immediateTransaction { - if (connection.albumIdsChanged(batch)) creditsChanged = true + val albumChanged = connection.albumIdsChanged(batch) + if (albumChanged) creditsChanged = true val resolved = connection.resolveSongSortNames(batch) connection.upsertChanged("songs", songColumns, primaryKey, resolved) { it.values() } connection.usePrepared("DELETE FROM song_genres WHERE sourceId = ? AND songId = ? AND position >= ?") { statement -> @@ -143,7 +161,11 @@ class SyncService( } } connection.upsertChanged("song_artists", songArtistColumns, songArtistKey, artists) { it.values() } - if (connection.totalChanges() != artistRowsBefore) creditsChanged = true + val artistRowsChanged = connection.totalChanges() != artistRowsBefore + if (albumChanged || artistRowsChanged) { + connection.markCreditsStale(batch.first().sourceId) + creditsChanged = true + } } } } @@ -167,6 +189,13 @@ class SyncService( private suspend fun dbTotalChanges(): Long = db.useWriterConnection { it.totalChanges() } + private suspend fun PooledConnection.markCreditsStale(sourceId: Long) { + usePrepared("UPDATE sources SET creditsStale = 1 WHERE id = ? AND creditsStale = 0") { statement -> + statement.bindLong(1, sourceId) + statement.step() + } + } + private suspend fun PooledConnection.albumIdsChanged(batch: List): Boolean { val expected = batch.associate { it.id to it.albumId }.toMutableMap() if (expected.isEmpty()) return false @@ -319,7 +348,10 @@ class SyncService( } } connection.upsertChanged("album_artists", albumArtistColumns, albumArtistKey, artists) { it.values() } - if (connection.totalChanges() != artistRowsBefore) creditsChanged = true + if (connection.totalChanges() != artistRowsBefore) { + connection.markCreditsStale(albums.first().sourceId) + creditsChanged = true + } } } } @@ -347,6 +379,7 @@ class SyncService( seen: HashedIds, pageOf: suspend (afterId: String) -> List, delete: suspend (Collection) -> Unit, + markStale: (suspend () -> Unit)? = null, ): Int { seen.freeze() var afterId = "" @@ -355,6 +388,7 @@ class SyncService( val ids = pageOf(afterId) if (ids.isEmpty()) return pruned val stale = ids.filterNot { seen.contains(idHash(it)) } + if (stale.isNotEmpty()) markStale?.invoke() stale.chunked(DELETE_CHUNK).forEach { delete(it) } pruned += stale.size if (ids.size < PRUNE_PAGE) return pruned diff --git a/app/src/test/kotlin/com/subtracks/data/db/MigrationsTest.kt b/app/src/test/kotlin/com/subtracks/data/db/MigrationsTest.kt index 682a2572..b4eff2ca 100644 --- a/app/src/test/kotlin/com/subtracks/data/db/MigrationsTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/db/MigrationsTest.kt @@ -64,6 +64,8 @@ class MigrationsTest { assertEquals(1, indexColumn(migrated, "index_albums_starred", "starred").descending) assertEquals(1, indexColumn(migrated, "index_albums_frequent", "playCount").descending) assertEquals(1, indexColumn(migrated, "index_albums_recent", "played").descending) + assertEquals("NOCASE", indexColumn(migrated, "index_songs_starred", "sortName").collation) + assertEquals(1, indexColumn(migrated, "index_songs_starred", "starred").descending) assertEquals("NOCASE", indexColumn(migrated, "index_artists_name", "sortName").collation) assertEquals(1, indexColumn(migrated, "index_artists_albumCount", "albumCount").descending) assertEquals(1, indexColumn(migrated, "index_artists_starred", "starred").descending) @@ -229,6 +231,13 @@ class MigrationsTest { "INSERT INTO album_artists (sourceId, albumId, position, artistId, name) " + "VALUES (1, 'al1', 0, 'a1', 'Alpha')", ) + migrated.execSQL( + "INSERT INTO songs (sourceId, id, albumId, artistId, title) VALUES (1, 's1', 'al1', 'a1', 'Song')", + ) + migrated.execSQL( + "INSERT INTO song_artists (sourceId, songId, position, artistId, name) " + + "VALUES (1, 's1', 0, 'a1', 'Alpha')", + ) MIGRATION_31_32.migrate(migrated) diff --git a/app/src/test/kotlin/com/subtracks/data/sync/SyncServiceTest.kt b/app/src/test/kotlin/com/subtracks/data/sync/SyncServiceTest.kt index a44d6eeb..e68dc3b5 100644 --- a/app/src/test/kotlin/com/subtracks/data/sync/SyncServiceTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/sync/SyncServiceTest.kt @@ -1167,6 +1167,33 @@ class SyncServiceTest { ) } + @Test + fun aStaleCreditMarkerForcesARecomputeOnTheNextSync() = + runTest { + insertSource() + val source = + FakeMusicSource( + artists = listOf(artist("a1", "A")), + albums = listOf(album("al1")), + songs = listOf(song("s1")), + ) + + SyncService(db, source).sync() + assertEquals(false, db.sourcesDao().creditsStale(1)) + + db.sourcesDao().setCreditsStale(1, true) + db.libraryDao().upsertArtists( + listOf(db.libraryDao().artistOnce(1, "a1")!!.copy(songCount = 0, credited = false)), + ) + + SyncService(db, source).sync() + + val artist = db.libraryDao().artistOnce(1, "a1")!! + assertEquals(1L, artist.songCount) + assertEquals(true, artist.credited) + assertEquals(false, db.sourcesDao().creditsStale(1)) + } + @Test fun everyAlbumArtistAndTrackArtistReachesTheSharedAlbum() = runTest { @@ -1251,6 +1278,9 @@ class SyncServiceTest { SyncService(db, source).sync() assertEquals(2L, db.libraryDao().artistOnce(1, "a1")!!.songCount) + assertEquals(listOf("s1", "s2"), db.queueDao().artistSongIds(1, "a1")) + assertEquals(2L, db.queueDao().artistLength(1, "a1")) + assertEquals(2, db.queueDao().artistSongs(1, "a1", 0, 10).size) db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed)) db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Completed))