mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
drop the unused FTS5 search index
This commit is contained in:
@@ -39,7 +39,7 @@ Everything comes from the Nix flake devshell; do not install toolchains by hand.
|
||||
|
||||
- Prefer self-explanatory code; do not add comments unless the reason cannot be inferred from the code itself (docstrings for functions and exports are fine).
|
||||
- ktlint (`org.jlleitschuh.gradle.ktlint`) enforces style and flags unused imports; run `gradle :app:ktlintFormat` before committing. `.editorconfig` exempts `@Composable` functions from the lowerCamelCase function-naming rule.
|
||||
- Room 3 (`androidx.room3`) on AndroidX `sqlite-bundled` (`BundledSQLiteDriver`, SQLite 3.50+, FTS5). Do not reintroduce SQLDelight or the platform SQLite.
|
||||
- Room 3 (`androidx.room3`) on AndroidX `sqlite-bundled` (`BundledSQLiteDriver`, SQLite 3.50+). Do not reintroduce SQLDelight or the platform SQLite.
|
||||
- Networking uses OkHttp and DOM XML parsing; auth uses the Subsonic token scheme by default.
|
||||
- Use coroutines and `Flow`; library reads are exposed as `Flow` from Room. Large lists page with Paging 3 over Room `PagingSource` rather than loading the whole table.
|
||||
- Playback is Media3: `PlaybackService` is a `MediaSessionService` owning an `ExoPlayer`, and `PlaybackController` (app-scoped Koin singleton) is the only thing the UI talks to. The queue is a `queue_entries` list of references with optional ranges plus a `playback_cursor` row; keep only a bounded window in memory (never the whole queue, and never load a whole table to build one). Stream URLs are freshly salted per request, so attach artwork by `CoverArtRef` cache key, not by URL.
|
||||
|
||||
@@ -0,0 +1,775 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 10,
|
||||
"identityHash": "633620aafe3a9116bbc9d349082185e7",
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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, `albumCount` INTEGER NOT NULL, `starred` INTEGER, `coverArt` TEXT, 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"
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "sources",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"sourceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "albums",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` INTEGER NOT NULL, `id` TEXT NOT NULL, `artistId` TEXT, `name` TEXT NOT NULL, `albumArtist` TEXT, `created` INTEGER NOT NULL, `coverArt` TEXT, `genre` TEXT, `year` INTEGER, `starred` INTEGER, `songCount` INTEGER NOT NULL, `frequentRank` INTEGER, `recentRank` INTEGER, 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": "frequentRank",
|
||||
"columnName": "frequentRank",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "recentRank",
|
||||
"columnName": "recentRank",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "sources",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"sourceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "playlists",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` INTEGER NOT NULL, `id` TEXT NOT NULL, `name` TEXT NOT NULL, `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`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "sources",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"sourceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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, `genre` TEXT, `created` 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": "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": "genre",
|
||||
"columnName": "genre",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "created",
|
||||
"columnName": "created",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"sourceId",
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_songs_sourceId_albumId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"sourceId",
|
||||
"albumId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_songs_sourceId_albumId` ON `${TABLE_NAME}` (`sourceId`, `albumId`)"
|
||||
},
|
||||
{
|
||||
"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`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "sources",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"sourceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": "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, `positionMs` 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": "positionMs",
|
||||
"columnName": "positionMs",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "shuffle_order",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sequence` INTEGER NOT NULL, `flatPosition` INTEGER NOT NULL, PRIMARY KEY(`sequence`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "sequence",
|
||||
"columnName": "sequence",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "flatPosition",
|
||||
"columnName": "flatPosition",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"sequence"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "artwork_seeds",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cacheKey` TEXT NOT NULL, `primary` INTEGER NOT NULL, `secondary` INTEGER, PRIMARY KEY(`cacheKey`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "cacheKey",
|
||||
"columnName": "cacheKey",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "primary",
|
||||
"columnName": "primary",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "secondary",
|
||||
"columnName": "secondary",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"cacheKey"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"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, '633620aafe3a9116bbc9d349082185e7')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -86,5 +86,22 @@ val MIGRATION_8_9 =
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_9_10 =
|
||||
object : Migration(9, 10) {
|
||||
override suspend fun migrate(connection: SQLiteConnection) {
|
||||
connection.execSQL("DROP TABLE IF EXISTS `search_index`")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATIONS: Array<Migration> =
|
||||
arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
||||
arrayOf(
|
||||
MIGRATION_1_2,
|
||||
MIGRATION_2_3,
|
||||
MIGRATION_3_4,
|
||||
MIGRATION_4_5,
|
||||
MIGRATION_5_6,
|
||||
MIGRATION_6_7,
|
||||
MIGRATION_7_8,
|
||||
MIGRATION_8_9,
|
||||
MIGRATION_9_10,
|
||||
)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package com.subtracks.data.db
|
||||
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Insert
|
||||
import androidx.room3.Query
|
||||
import com.subtracks.data.model.SearchIndex
|
||||
|
||||
@Dao
|
||||
interface SearchDao {
|
||||
@Query("DELETE FROM search_index WHERE sourceId = :sourceId")
|
||||
suspend fun clear(sourceId: String)
|
||||
|
||||
@Insert
|
||||
suspend fun insert(items: List<SearchIndex>)
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM search_index
|
||||
WHERE search_index MATCH '"' || replace(:query, '"', '""') || '"'
|
||||
AND sourceId = :sourceId
|
||||
ORDER BY rank
|
||||
LIMIT :limit
|
||||
""",
|
||||
)
|
||||
suspend fun searchPhrase(
|
||||
sourceId: String,
|
||||
query: String,
|
||||
limit: Int,
|
||||
): List<SearchIndex>
|
||||
|
||||
suspend fun search(
|
||||
sourceId: String,
|
||||
query: String,
|
||||
limit: Int,
|
||||
): List<SearchIndex> {
|
||||
val phrase = query.trim()
|
||||
if (phrase.length < MIN_QUERY_LENGTH) return emptyList()
|
||||
return searchPhrase(sourceId, phrase, limit)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MIN_QUERY_LENGTH = 3
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package com.subtracks.data.db
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Insert
|
||||
import androidx.room3.Query
|
||||
import androidx.room3.Transaction
|
||||
import androidx.room3.Upsert
|
||||
import com.subtracks.data.model.Source
|
||||
import com.subtracks.data.model.SubsonicConfig
|
||||
@@ -42,14 +41,7 @@ interface SourcesDao {
|
||||
@Query("DELETE FROM sources WHERE id = :id")
|
||||
suspend fun deleteSourceRow(id: Long)
|
||||
|
||||
@Query("DELETE FROM search_index WHERE sourceId = :sourceId")
|
||||
suspend fun clearSearchIndex(sourceId: String)
|
||||
|
||||
@Transaction
|
||||
suspend fun deleteSource(id: Long) {
|
||||
deleteSourceRow(id)
|
||||
clearSearchIndex(id.toString())
|
||||
}
|
||||
suspend fun deleteSource(id: Long) = deleteSourceRow(id)
|
||||
|
||||
@Query("UPDATE sources SET isActive = (id = :sourceId)")
|
||||
suspend fun setActiveSource(sourceId: Long)
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.PlaylistSong
|
||||
import com.subtracks.data.model.QueueEntry
|
||||
import com.subtracks.data.model.QueueKindConverter
|
||||
import com.subtracks.data.model.SearchIndex
|
||||
import com.subtracks.data.model.ShuffleOrder
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.model.Source
|
||||
@@ -26,13 +25,12 @@ import com.subtracks.data.model.SubsonicSource
|
||||
Playlist::class,
|
||||
PlaylistSong::class,
|
||||
Song::class,
|
||||
SearchIndex::class,
|
||||
QueueEntry::class,
|
||||
PlaybackCursor::class,
|
||||
ShuffleOrder::class,
|
||||
ArtworkSeed::class,
|
||||
],
|
||||
version = 9,
|
||||
version = 10,
|
||||
exportSchema = true,
|
||||
)
|
||||
@ColumnTypeConverters(QueueKindConverter::class)
|
||||
@@ -41,8 +39,6 @@ abstract class SubtracksDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun sourcesDao(): SourcesDao
|
||||
|
||||
abstract fun searchDao(): SearchDao
|
||||
|
||||
abstract fun queueDao(): QueueDao
|
||||
|
||||
abstract fun artworkSeedDao(): ArtworkSeedDao
|
||||
|
||||
@@ -5,8 +5,6 @@ import androidx.room3.ColumnTypeConverter
|
||||
import androidx.room3.Embedded
|
||||
import androidx.room3.Entity
|
||||
import androidx.room3.ForeignKey
|
||||
import androidx.room3.Fts5
|
||||
import androidx.room3.FtsOptions
|
||||
import androidx.room3.Index
|
||||
import androidx.room3.PrimaryKey
|
||||
|
||||
@@ -168,16 +166,6 @@ data class SongListItem(
|
||||
val coverArt: String?,
|
||||
)
|
||||
|
||||
@Entity(tableName = "search_index")
|
||||
@Fts5(tokenizer = FtsOptions.TOKENIZER_TRIGRAM, notIndexed = ["sourceId", "type", "itemId"])
|
||||
data class SearchIndex(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "rowid") val rowId: Long = 0,
|
||||
val sourceId: String,
|
||||
val type: String,
|
||||
val itemId: String,
|
||||
val title: String,
|
||||
)
|
||||
|
||||
enum class QueueKind { Playlist, Album, Song, Songs }
|
||||
|
||||
@Entity(
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.PlaylistSong
|
||||
import com.subtracks.data.model.SearchIndex
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.source.MusicSource
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -53,18 +52,6 @@ class SyncService(
|
||||
library.deletePlaylistSongsFrom(source.id, playlist.id, (playlistSongCounts[playlist.id] ?: 0).toLong())
|
||||
}
|
||||
(existingPlaylistIds - playlists.map { it.id }).forEach { library.deletePlaylistSongsFrom(source.id, it, 0) }
|
||||
|
||||
val search = db.searchDao()
|
||||
val sourceKey = source.id.toString()
|
||||
search.clear(sourceKey)
|
||||
search.insert(
|
||||
buildList {
|
||||
artists.forEach { add(SearchIndex(sourceId = sourceKey, type = "artist", itemId = it.id, title = it.name)) }
|
||||
albums.forEach { add(SearchIndex(sourceId = sourceKey, type = "album", itemId = it.id, title = it.name)) }
|
||||
songs.forEach { add(SearchIndex(sourceId = sourceKey, type = "song", itemId = it.id, title = it.title)) }
|
||||
playlists.forEach { add(SearchIndex(sourceId = sourceKey, type = "playlist", itemId = it.id, title = it.name)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,35 +160,6 @@ class SyncServiceTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun searchIndexMatchesSubstrings() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
artists = listOf(artist("a1", name = "Radiohead")),
|
||||
songs = listOf(song("s1")),
|
||||
)
|
||||
|
||||
SyncService(db, source).sync()
|
||||
|
||||
val results = db.searchDao().search("1", "adio", 10)
|
||||
assertEquals(1, results.size)
|
||||
assertEquals("a1", results.single().itemId)
|
||||
assertEquals("artist", results.single().type)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun searchIgnoresTooShortQueries() =
|
||||
runTest {
|
||||
insertSource()
|
||||
SyncService(db, FakeMusicSource(artists = listOf(artist("a1", name = "Radiohead")))).sync()
|
||||
|
||||
assertEquals(0, db.searchDao().search("1", "ad", 10).size)
|
||||
assertEquals(0, db.searchDao().search("1", "", 10).size)
|
||||
assertEquals(1, db.searchDao().search("1", "adi", 10).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncUpdatesExistingRows() =
|
||||
runTest {
|
||||
|
||||
Reference in New Issue
Block a user