mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-17 12:44:53 +00:00
Add local mutation timestamps for watchlist and watched state
Lays the groundwork for timestamp-based conflict resolution against Trakt/Simkl/Stremio (matching the pattern Nuvio already uses), by giving watchlist entries and watched episodes an updatedAt column and a soft-delete tombstone table so removals carry a comparable timestamp instead of vanishing on hard delete. Progress already had updatedAt; this brings watchlist/watched up to the same level.
This commit is contained in:
parent
757d19e8f4
commit
89c8c90669
5 changed files with 772 additions and 4 deletions
715
data/schemas/com.fluxa.app.data.local.AppDatabase/16.json
Normal file
715
data/schemas/com.fluxa.app.data.local.AppDatabase/16.json
Normal file
|
|
@ -0,0 +1,715 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 16,
|
||||
"identityHash": "e7e0e9a89764aed8cd18a0fcbd66abaf",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "content_items",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `contentId` TEXT NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `poster` TEXT, `background` TEXT, `logo` TEXT, `description` TEXT, `rating` TEXT, `releaseInfo` TEXT, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `contentId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "profileId",
|
||||
"columnName": "profileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentId",
|
||||
"columnName": "contentId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "poster",
|
||||
"columnName": "poster",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "background",
|
||||
"columnName": "background",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "logo",
|
||||
"columnName": "logo",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "rating",
|
||||
"columnName": "rating",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "releaseInfo",
|
||||
"columnName": "releaseInfo",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"contentId"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "watchlist_entries",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `contentId` TEXT NOT NULL, `addedAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `contentId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "profileId",
|
||||
"columnName": "profileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentId",
|
||||
"columnName": "contentId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "addedAt",
|
||||
"columnName": "addedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"contentId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_watchlist_entries_profile_addedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"addedAt"
|
||||
],
|
||||
"orders": [
|
||||
"ASC",
|
||||
"DESC"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_watchlist_entries_profile_addedAt` ON `${TABLE_NAME}` (`profileId` ASC, `addedAt` DESC)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "playback_progress",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `contentId` TEXT NOT NULL, `videoId` TEXT, `timeOffset` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `lastStreamIndex` INTEGER, `lastEpisodeName` TEXT, `lastStreamUrl` TEXT, `lastStreamTitle` TEXT, `lastBingeGroup` TEXT, `continueWatchingPoster` TEXT, `continueWatchingBackground` TEXT, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `contentId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "profileId",
|
||||
"columnName": "profileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentId",
|
||||
"columnName": "contentId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeOffset",
|
||||
"columnName": "timeOffset",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "duration",
|
||||
"columnName": "duration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStreamIndex",
|
||||
"columnName": "lastStreamIndex",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEpisodeName",
|
||||
"columnName": "lastEpisodeName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStreamUrl",
|
||||
"columnName": "lastStreamUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStreamTitle",
|
||||
"columnName": "lastStreamTitle",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastBingeGroup",
|
||||
"columnName": "lastBingeGroup",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "continueWatchingPoster",
|
||||
"columnName": "continueWatchingPoster",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "continueWatchingBackground",
|
||||
"columnName": "continueWatchingBackground",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"contentId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_playback_progress_profile_updatedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"updatedAt"
|
||||
],
|
||||
"orders": [
|
||||
"ASC",
|
||||
"DESC"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_playback_progress_profile_updatedAt` ON `${TABLE_NAME}` (`profileId` ASC, `updatedAt` DESC)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "user_feedback",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `contentId` TEXT NOT NULL, `isLiked` INTEGER, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `contentId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "profileId",
|
||||
"columnName": "profileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentId",
|
||||
"columnName": "contentId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLiked",
|
||||
"columnName": "isLiked",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"contentId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_user_feedback_profile_updatedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"updatedAt"
|
||||
],
|
||||
"orders": [
|
||||
"ASC",
|
||||
"DESC"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_user_feedback_profile_updatedAt` ON `${TABLE_NAME}` (`profileId` ASC, `updatedAt` DESC)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "track_preferences",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `contentId` TEXT NOT NULL, `lastAudioLanguage` TEXT, `lastSubtitleLanguage` TEXT, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `contentId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "profileId",
|
||||
"columnName": "profileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentId",
|
||||
"columnName": "contentId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastAudioLanguage",
|
||||
"columnName": "lastAudioLanguage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSubtitleLanguage",
|
||||
"columnName": "lastSubtitleLanguage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"contentId"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "watched_episodes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `seriesId` TEXT NOT NULL, `videoId` TEXT NOT NULL, `watchedAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `seriesId`, `videoId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "profileId",
|
||||
"columnName": "profileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "seriesId",
|
||||
"columnName": "seriesId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "watchedAt",
|
||||
"columnName": "watchedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updatedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"seriesId",
|
||||
"videoId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_watched_episodes_profile_series",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"seriesId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_watched_episodes_profile_series` ON `${TABLE_NAME}` (`profileId`, `seriesId`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "watched_content_durations",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `source` TEXT NOT NULL, `contentId` TEXT NOT NULL, `videoId` TEXT NOT NULL, `duration` INTEGER NOT NULL, `watchedAt` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `source`, `contentId`, `videoId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "profileId",
|
||||
"columnName": "profileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "source",
|
||||
"columnName": "source",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentId",
|
||||
"columnName": "contentId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "duration",
|
||||
"columnName": "duration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "watchedAt",
|
||||
"columnName": "watchedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"source",
|
||||
"contentId",
|
||||
"videoId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_watched_content_durations_profile",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profileId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_watched_content_durations_profile` ON `${TABLE_NAME}` (`profileId`)"
|
||||
},
|
||||
{
|
||||
"name": "index_watched_content_durations_profile_source",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"source"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_watched_content_durations_profile_source` ON `${TABLE_NAME}` (`profileId`, `source`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "external_playback_progress",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `provider` TEXT NOT NULL, `contentId` TEXT NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `poster` TEXT, `background` TEXT, `logo` TEXT, `description` TEXT, `rating` TEXT, `releaseInfo` TEXT, `videoId` TEXT, `timeOffset` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `lastEpisodeName` TEXT, `reason` TEXT, `continueWatchingPoster` TEXT, `continueWatchingBackground` TEXT, `syncedAt` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `provider`, `contentId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "profileId",
|
||||
"columnName": "profileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "provider",
|
||||
"columnName": "provider",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentId",
|
||||
"columnName": "contentId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "poster",
|
||||
"columnName": "poster",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "background",
|
||||
"columnName": "background",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "logo",
|
||||
"columnName": "logo",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "rating",
|
||||
"columnName": "rating",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "releaseInfo",
|
||||
"columnName": "releaseInfo",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeOffset",
|
||||
"columnName": "timeOffset",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "duration",
|
||||
"columnName": "duration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastEpisodeName",
|
||||
"columnName": "lastEpisodeName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "reason",
|
||||
"columnName": "reason",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "continueWatchingPoster",
|
||||
"columnName": "continueWatchingPoster",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "continueWatchingBackground",
|
||||
"columnName": "continueWatchingBackground",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "syncedAt",
|
||||
"columnName": "syncedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"provider",
|
||||
"contentId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_external_playback_progress_profile_syncedAt",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"syncedAt"
|
||||
],
|
||||
"orders": [
|
||||
"ASC",
|
||||
"DESC"
|
||||
],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_external_playback_progress_profile_syncedAt` ON `${TABLE_NAME}` (`profileId` ASC, `syncedAt` DESC)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "external_watched_episodes",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `provider` TEXT NOT NULL, `seriesId` TEXT NOT NULL, `videoId` TEXT NOT NULL, `syncedAt` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `provider`, `seriesId`, `videoId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "profileId",
|
||||
"columnName": "profileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "provider",
|
||||
"columnName": "provider",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "seriesId",
|
||||
"columnName": "seriesId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "syncedAt",
|
||||
"columnName": "syncedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"provider",
|
||||
"seriesId",
|
||||
"videoId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_external_watched_episodes_profile_provider_series",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"provider",
|
||||
"seriesId"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_external_watched_episodes_profile_provider_series` ON `${TABLE_NAME}` (`profileId`, `provider`, `seriesId`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "watchlist_removals",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `contentId` TEXT NOT NULL, `removedAt` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `contentId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "profileId",
|
||||
"columnName": "profileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentId",
|
||||
"columnName": "contentId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "removedAt",
|
||||
"columnName": "removedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"contentId"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "watched_episode_removals",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` TEXT NOT NULL, `seriesId` TEXT NOT NULL, `videoId` TEXT NOT NULL, `removedAt` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `seriesId`, `videoId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "profileId",
|
||||
"columnName": "profileId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "seriesId",
|
||||
"columnName": "seriesId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "removedAt",
|
||||
"columnName": "removedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"profileId",
|
||||
"seriesId",
|
||||
"videoId"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"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, 'e7e0e9a89764aed8cd18a0fcbd66abaf')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -242,6 +242,12 @@ interface WatchlistDao {
|
|||
@Query("SELECT contentId FROM watchlist_entries WHERE profileId = :profileId")
|
||||
fun observeWatchlistIds(profileId: String): Flow<List<String>>
|
||||
|
||||
@Query("SELECT * FROM watchlist_entries WHERE profileId = :profileId")
|
||||
suspend fun getWatchlistEntries(profileId: String): List<WatchlistEntryEntity>
|
||||
|
||||
@Query("SELECT * FROM watched_episodes WHERE profileId = :profileId AND seriesId = :seriesId")
|
||||
suspend fun getWatchedEpisodeEntries(profileId: String, seriesId: String): List<WatchedEpisodeEntity>
|
||||
|
||||
@Query("SELECT EXISTS(SELECT 1 FROM watchlist_entries WHERE profileId = :profileId AND contentId = :contentId)")
|
||||
suspend fun isInWatchlist(profileId: String, contentId: String): Boolean
|
||||
|
||||
|
|
@ -320,6 +326,15 @@ interface WatchlistDao {
|
|||
@Query("DELETE FROM watchlist_entries WHERE profileId = :profileId AND contentId = :contentId")
|
||||
suspend fun deleteWatchlistEntry(profileId: String, contentId: String)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsertWatchlistRemoval(removal: WatchlistRemovalEntity)
|
||||
|
||||
@Query("DELETE FROM watchlist_removals WHERE profileId = :profileId AND contentId = :contentId")
|
||||
suspend fun clearWatchlistRemoval(profileId: String, contentId: String)
|
||||
|
||||
@Query("SELECT * FROM watchlist_removals WHERE profileId = :profileId")
|
||||
suspend fun getWatchlistRemovals(profileId: String): List<WatchlistRemovalEntity>
|
||||
|
||||
@Query("DELETE FROM playback_progress WHERE profileId = :profileId AND contentId = :contentId")
|
||||
suspend fun deletePlaybackProgress(profileId: String, contentId: String)
|
||||
|
||||
|
|
@ -332,6 +347,15 @@ interface WatchlistDao {
|
|||
@Query("DELETE FROM watched_episodes WHERE profileId = :profileId AND seriesId = :seriesId AND videoId IN (:videoIds)")
|
||||
suspend fun deleteWatchedEpisodes(profileId: String, seriesId: String, videoIds: List<String>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsertWatchedEpisodeRemovals(removals: List<WatchedEpisodeRemovalEntity>)
|
||||
|
||||
@Query("DELETE FROM watched_episode_removals WHERE profileId = :profileId AND seriesId = :seriesId AND videoId IN (:videoIds)")
|
||||
suspend fun clearWatchedEpisodeRemovals(profileId: String, seriesId: String, videoIds: List<String>)
|
||||
|
||||
@Query("SELECT * FROM watched_episode_removals WHERE profileId = :profileId AND seriesId = :seriesId")
|
||||
suspend fun getWatchedEpisodeRemovals(profileId: String, seriesId: String): List<WatchedEpisodeRemovalEntity>
|
||||
|
||||
@Query("DELETE FROM watched_episodes WHERE profileId = :profileId")
|
||||
suspend fun deleteAllWatchedEpisodes(profileId: String)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ import androidx.room.RoomDatabase
|
|||
WatchedEpisodeEntity::class,
|
||||
WatchedContentDurationEntity::class,
|
||||
ExternalPlaybackProgressEntity::class,
|
||||
ExternalWatchedEpisodeEntity::class
|
||||
ExternalWatchedEpisodeEntity::class,
|
||||
WatchlistRemovalEntity::class,
|
||||
WatchedEpisodeRemovalEntity::class
|
||||
],
|
||||
version = 15,
|
||||
version = 16,
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,18 @@ data class ContentItemEntity(
|
|||
data class WatchlistEntryEntity(
|
||||
val profileId: String,
|
||||
val contentId: String,
|
||||
val addedAt: Long = System.currentTimeMillis()
|
||||
val addedAt: Long = System.currentTimeMillis(),
|
||||
val updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@Entity(
|
||||
tableName = "watchlist_removals",
|
||||
primaryKeys = ["profileId", "contentId"]
|
||||
)
|
||||
data class WatchlistRemovalEntity(
|
||||
val profileId: String,
|
||||
val contentId: String,
|
||||
val removedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@Entity(
|
||||
|
|
@ -90,7 +101,19 @@ data class WatchedEpisodeEntity(
|
|||
val profileId: String,
|
||||
val seriesId: String,
|
||||
val videoId: String,
|
||||
val watchedAt: Long = System.currentTimeMillis()
|
||||
val watchedAt: Long = System.currentTimeMillis(),
|
||||
val updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@Entity(
|
||||
tableName = "watched_episode_removals",
|
||||
primaryKeys = ["profileId", "seriesId", "videoId"]
|
||||
)
|
||||
data class WatchedEpisodeRemovalEntity(
|
||||
val profileId: String,
|
||||
val seriesId: String,
|
||||
val videoId: String,
|
||||
val removedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@Entity(
|
||||
|
|
|
|||
|
|
@ -94,7 +94,9 @@ class WatchlistManager @Inject constructor(
|
|||
dao.upsertContent(item.toContentItemEntity(profileId))
|
||||
if (dao.isInWatchlist(profileId, item.id)) {
|
||||
dao.deleteWatchlistEntry(profileId, item.id)
|
||||
dao.upsertWatchlistRemoval(WatchlistRemovalEntity(profileId, item.id))
|
||||
} else {
|
||||
dao.clearWatchlistRemoval(profileId, item.id)
|
||||
dao.upsertWatchlistEntry(WatchlistEntryEntity(profileId, item.id))
|
||||
}
|
||||
}
|
||||
|
|
@ -241,9 +243,11 @@ class WatchlistManager @Inject constructor(
|
|||
val cleanVideoIds = videoIds.filter { it.isNotBlank() }.distinct()
|
||||
if (cleanVideoIds.isEmpty()) return getLocalWatchedVideoIds(seriesId)
|
||||
if (watched) {
|
||||
dao.clearWatchedEpisodeRemovals(profileId, seriesId, cleanVideoIds)
|
||||
dao.upsertWatchedEpisodes(cleanVideoIds.map { WatchedEpisodeEntity(profileId, seriesId, it) })
|
||||
} else {
|
||||
dao.deleteWatchedEpisodes(profileId, seriesId, cleanVideoIds)
|
||||
dao.upsertWatchedEpisodeRemovals(cleanVideoIds.map { WatchedEpisodeRemovalEntity(profileId, seriesId, it) })
|
||||
}
|
||||
return getLocalWatchedVideoIds(seriesId)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue