diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/GetExcludedScanlators.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/GetExcludedScanlators.kt index f6dd5c758fa..f5d110cfc11 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/GetExcludedScanlators.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/GetExcludedScanlators.kt @@ -13,14 +13,14 @@ class GetExcludedScanlators( ) { suspend fun await(mangaId: Long): Set { - return database.excluded_scanlatorsQueries + return database.excluded_scanlatorQueries .getExcludedScanlatorsByMangaId(mangaId) .awaitAsList() .toSet() } fun subscribe(mangaId: Long): Flow> { - return database.excluded_scanlatorsQueries + return database.excluded_scanlatorQueries .getExcludedScanlatorsByMangaId(mangaId) .subscribeToList() .map { it.toSet() } diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/SetExcludedScanlators.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/SetExcludedScanlators.kt index 37b2778f278..e7c5d8ce346 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/SetExcludedScanlators.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/SetExcludedScanlators.kt @@ -11,16 +11,16 @@ class SetExcludedScanlators( suspend fun await(mangaId: Long, excludedScanlators: Set) { database.transaction { - val currentExcluded = database.excluded_scanlatorsQueries + val currentExcluded = database.excluded_scanlatorQueries .getExcludedScanlatorsByMangaId(mangaId) .awaitAsList() .toSet() val toAdd = excludedScanlators.minus(currentExcluded) for (scanlator in toAdd) { - database.excluded_scanlatorsQueries.insert(mangaId, scanlator) + database.excluded_scanlatorQueries.insert(mangaId, scanlator) } val toRemove = currentExcluded.minus(excludedScanlators) - database.excluded_scanlatorsQueries.remove(mangaId, toRemove) + database.excluded_scanlatorQueries.remove(mangaId, toRemove) } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt index 9e89672da41..ded7b2e11d1 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/advanced/ClearDatabaseScreen.kt @@ -253,7 +253,7 @@ class ClearDatabaseViewModel( suspend fun removeMangaBySourceId(keepReadManga: Boolean) = withNonCancellableContext { val state = state.value as? State.Ready ?: return@withNonCancellableContext - database.mangasQueries.deleteNonLibraryManga(state.selection, keepReadManga.toLong()) + database.mangaQueries.deleteNonLibraryManga(state.selection, keepReadManga.toLong()) database.historyQueries.removeResettedHistory() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt index aebb47a6e1a..a5942f9a465 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/MangaBackupCreator.kt @@ -33,13 +33,13 @@ class MangaBackupCreator( // Entry for this manga val mangaObject = manga.toBackupManga() - mangaObject.excludedScanlators = database.excluded_scanlatorsQueries + mangaObject.excludedScanlators = database.excluded_scanlatorQueries .getExcludedScanlatorsByMangaId(manga.id) .awaitAsList() if (options.chapters) { // Backup all the chapters - database.chaptersQueries + database.chapterQueries .getChaptersByMangaId( mangaId = manga.id, applyScanlatorFilter = 0, // false @@ -59,7 +59,7 @@ class MangaBackupCreator( } if (options.tracking) { - val tracks = database.manga_syncQueries + val tracks = database.manga_trackQueries .getTracksByMangaId(manga.id, backupTrackMapper) .awaitAsList() if (tracks.isNotEmpty()) { @@ -71,10 +71,10 @@ class MangaBackupCreator( val historyByMangaId = getHistory.await(manga.id) if (historyByMangaId.isNotEmpty()) { val history = historyByMangaId.map { history -> - val chapter = database.chaptersQueries + val chapter = database.chapterQueries .getChapterById(history.chapterId) .awaitAsOne() - BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration) + BackupHistory(chapter.remote_url, history.readAt?.time ?: 0L, history.readDuration) } if (history.isNotEmpty()) { mangaObject.history = history diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt index e1828a3020a..d4d591a5eb4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupChapter.kt @@ -52,17 +52,15 @@ val backupChapterMapper = { url: String, name: String, scanlator: String?, + chapterNumber: Double, + dateUpload: Long, + sourceOrder: Long, + memo: JsonObject, read: Boolean, bookmark: Boolean, lastPageRead: Long, - chapterNumber: Double, - sourceOrder: Long, dateFetch: Long, - dateUpload: Long, - lastModifiedAt: Long, - version: Long, - _: Long, - memo: JsonObject, + modifiedAt: Long, -> BackupChapter( url = url, @@ -75,8 +73,8 @@ val backupChapterMapper = { dateFetch = dateFetch, dateUpload = dateUpload, sourceOrder = sourceOrder, - lastModifiedAt = lastModifiedAt, - version = version, + lastModifiedAt = modifiedAt, + version = 0, memo = MemoColumnAdapter.encode(memo), ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt index 56108e68e8b..4d8e48eb0ef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt @@ -59,13 +59,13 @@ val backupTrackMapper = { _: Long, syncId: Long, mediaId: Long, + remoteUrl: String, libraryId: Long?, title: String, lastChapterRead: Double, totalChapters: Long, status: Long, score: Double, - remoteUrl: String, startDate: Long, finishDate: Long, private: Boolean, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt index 16d747e8b9b..2453ad886ba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesRestorer.kt @@ -26,7 +26,7 @@ class CategoriesRestorer( val dbCategory = dbCategoriesByName[it.name] if (dbCategory != null) return@map dbCategory val order = nextOrder++ - database.categoriesQueries + database.categoryQueries .insert(it.name, order, it.flags) .let { id -> it.toCategory(id).copy(order = order) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt index 2c12099c640..fd95fefcd10 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaRestorer.kt @@ -45,10 +45,10 @@ class MangaRestorer( private val currentFetchWindow = fetchInterval.getWindow(now.date, timeZone) suspend fun sortByNew(backupMangas: List): List { - val urlsBySource = database.mangasQueries + val urlsBySource = database.mangaQueries .getAllMangaSourceAndUrl() .awaitAsList() - .groupBy({ it.source }, { it.url }) + .groupBy({ it.source_id }, { it.remote_url }) return backupMangas .sortedWith( @@ -109,7 +109,7 @@ class MangaRestorer( } private suspend fun updateManga(manga: Manga): Manga { - database.mangasQueries.update( + database.mangaQueries.update( source = manga.source, url = manga.url, artist = manga.artist, @@ -119,7 +119,7 @@ class MangaRestorer( title = manga.title, status = manga.status, thumbnailUrl = manga.thumbnailUrl, - favorite = manga.favorite, + favoriteAt = manga.dateAdded.takeIf { manga.favorite }, lastUpdate = manga.lastUpdate, nextUpdate = null, calculateInterval = null, @@ -127,11 +127,8 @@ class MangaRestorer( viewer = manga.viewerFlags, chapterFlags = manga.chapterFlags, coverLastModified = manga.coverLastModified, - dateAdded = manga.dateAdded, mangaId = manga.id, updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode), - version = manga.version, - isSyncing = 1, notes = manga.notes, memo = manga.memo.let(MemoColumnAdapter::encode), ) @@ -194,7 +191,7 @@ class MangaRestorer( private suspend fun insertNewChapters(chapters: List) { database.transaction { chapters.forEach { chapter -> - database.chaptersQueries.insert( + database.chapterQueries.insert( chapter.mangaId, chapter.url, chapter.name, @@ -206,7 +203,6 @@ class MangaRestorer( chapter.sourceOrder, chapter.dateFetch, chapter.dateUpload, - chapter.version, chapter.memo, ) } @@ -216,7 +212,7 @@ class MangaRestorer( private suspend fun updateExistingChapters(chapters: List) { database.transaction { chapters.forEach { chapter -> - database.chaptersQueries.update( + database.chapterQueries.update( mangaId = null, url = null, name = null, @@ -229,8 +225,6 @@ class MangaRestorer( dateFetch = null, dateUpload = null, chapterId = chapter.id, - version = chapter.version, - isSyncing = 0, memo = chapter.memo.let(MemoColumnAdapter::encode), ) } @@ -243,7 +237,7 @@ class MangaRestorer( * @return id of [Manga], null if not found */ private suspend fun insertManga(manga: Manga): Long { - return database.mangasQueries.insertReturningId( + return database.mangaQueries.insertReturningId( source = manga.source, url = manga.url, artist = manga.artist, @@ -253,7 +247,7 @@ class MangaRestorer( title = manga.title, status = manga.status, thumbnailUrl = manga.thumbnailUrl, - favorite = manga.favorite, + favoriteAt = manga.dateAdded.takeIf { manga.favorite }, lastUpdate = manga.lastUpdate, nextUpdate = 0L, calculateInterval = 0L, @@ -261,9 +255,7 @@ class MangaRestorer( viewerFlags = manga.viewerFlags, chapterFlags = manga.chapterFlags, coverLastModified = manga.coverLastModified, - dateAdded = manga.dateAdded, updateStrategy = manga.updateStrategy, - version = manga.version, notes = manga.notes, memo = manga.memo, ) @@ -314,9 +306,9 @@ class MangaRestorer( if (mangaCategoriesToUpdate.isNotEmpty()) { database.transaction { - database.mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id) + database.manga_categoryQueries.deleteMangaCategoryByMangaId(manga.id) mangaCategoriesToUpdate.forEach { (mangaId, categoryId) -> - database.mangas_categoriesQueries.insert(mangaId, categoryId) + database.manga_categoryQueries.insert(mangaId, categoryId) } } } @@ -330,7 +322,7 @@ class MangaRestorer( val item = history.getHistoryImpl() if (dbHistory == null) { - val chapter = database.chaptersQueries + val chapter = database.chapterQueries .getChapterByUrlAndMangaId(history.url, manga.id) .awaitAsOneOrNull() return@mapNotNull if (chapter == null) { @@ -338,18 +330,18 @@ class MangaRestorer( null } else { // New history entry - item.copy(chapterId = chapter._id) + item.copy(chapterId = chapter.id) } } // Update history entry item.copy( - id = dbHistory._id, + id = dbHistory.id, chapterId = dbHistory.chapter_id, - readAt = max(item.readAt?.time ?: 0L, dbHistory.last_read?.time ?: 0L) + readAt = max(item.readAt?.time ?: 0L, dbHistory.read_at?.time ?: 0L) .takeIf { it > 0L } ?.let { Date(it) }, - readDuration = max(item.readDuration, dbHistory.time_read) - dbHistory.time_read, + readDuration = max(item.readDuration, dbHistory.read_duration) - dbHistory.read_duration, ) } @@ -399,7 +391,7 @@ class MangaRestorer( if (existingTracks.isEmpty()) return database.transaction { existingTracks.forEach { track -> - database.manga_syncQueries.update( + database.manga_trackQueries.update( track.mangaId, track.trackerId, track.remoteId, @@ -429,11 +421,11 @@ class MangaRestorer( */ private suspend fun restoreExcludedScanlators(manga: Manga, excludedScanlators: List) { if (excludedScanlators.isEmpty()) return - val existingExcludedScanlators = database.excluded_scanlatorsQueries + val existingExcludedScanlators = database.excluded_scanlatorQueries .getExcludedScanlatorsByMangaId(manga.id) .awaitAsList() val toInsert = excludedScanlators.filter { it !in existingExcludedScanlators } if (toInsert.isEmpty()) return - toInsert.forEach { database.excluded_scanlatorsQueries.insert(manga.id, it) } + toInsert.forEach { database.excluded_scanlatorQueries.insert(manga.id, it) } } } diff --git a/app/src/main/java/mihon/app/di/AppBindings.kt b/app/src/main/java/mihon/app/di/AppBindings.kt index 219bec558c7..76cb51877fe 100644 --- a/app/src/main/java/mihon/app/di/AppBindings.kt +++ b/app/src/main/java/mihon/app/di/AppBindings.kt @@ -16,11 +16,11 @@ import kotlinx.serialization.protobuf.ProtoBuf import nl.adaptivity.xmlutil.XmlDeclMode import nl.adaptivity.xmlutil.core.XmlVersion import nl.adaptivity.xmlutil.serialization.XML -import tachiyomi.data.Chapters +import tachiyomi.data.Chapter import tachiyomi.data.Database import tachiyomi.data.DateColumnAdapter import tachiyomi.data.History -import tachiyomi.data.Mangas +import tachiyomi.data.Manga import tachiyomi.data.MemoColumnAdapter import tachiyomi.data.StringListColumnAdapter import tachiyomi.data.UpdateStrategyColumnAdapter @@ -47,15 +47,15 @@ object AppBindings { return Database( driver = driver, historyAdapter = History.Adapter( - last_readAdapter = DateColumnAdapter, + read_atAdapter = DateColumnAdapter, ), - mangasAdapter = Mangas.Adapter( - genreAdapter = StringListColumnAdapter, - update_strategyAdapter = UpdateStrategyColumnAdapter, - memoAdapter = MemoColumnAdapter, + mangaAdapter = Manga.Adapter( + remote_genreAdapter = StringListColumnAdapter, + remote_update_strategyAdapter = UpdateStrategyColumnAdapter, + remote_memoAdapter = MemoColumnAdapter, ), - chaptersAdapter = Chapters.Adapter( - memoAdapter = MemoColumnAdapter, + chapterAdapter = Chapter.Adapter( + remote_memoAdapter = MemoColumnAdapter, ), ) } diff --git a/data/src/main/java/tachiyomi/data/category/CategoryRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/category/CategoryRepositoryImpl.kt index 90f2f75f043..94196d78035 100644 --- a/data/src/main/java/tachiyomi/data/category/CategoryRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/category/CategoryRepositoryImpl.kt @@ -20,37 +20,37 @@ class CategoryRepositoryImpl( ) : CategoryRepository { override suspend fun get(id: Long): Category? { - return database.categoriesQueries + return database.categoryQueries .getCategory(id, ::mapCategory) .awaitAsOneOrNull() } override suspend fun getAll(): List { - return database.categoriesQueries + return database.categoryQueries .getCategories(::mapCategory) .awaitAsList() } override fun getAllAsFlow(): Flow> { - return database.categoriesQueries + return database.categoryQueries .getCategories(::mapCategory) .subscribeToList() } override suspend fun getCategoriesByMangaId(mangaId: Long): List { - return database.categoriesQueries + return database.categoryQueries .getCategoriesByMangaId(mangaId, ::mapCategory) .awaitAsList() } override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow> { - return database.categoriesQueries + return database.categoryQueries .getCategoriesByMangaId(mangaId, ::mapCategory) .subscribeToList() } override suspend fun insert(category: Category) { - database.categoriesQueries.insert( + database.categoryQueries.insert( name = category.name, order = category.order, flags = category.flags, @@ -58,27 +58,27 @@ class CategoryRepositoryImpl( } override suspend fun updateName(categoryId: Long, name: String) { - database.categoriesQueries.updateName(name = name, categoryId = categoryId) + database.categoryQueries.updateName(name = name, categoryId = categoryId) } override suspend fun updateFlags(categoryId: Long, flags: Long) { - database.categoriesQueries.updateFlags(flags = flags, categoryId = categoryId) + database.categoryQueries.updateFlags(flags = flags, categoryId = categoryId) } override suspend fun updateAllFlags(flags: Long?) { - database.categoriesQueries.updateAllFlags(flags = flags) + database.categoryQueries.updateAllFlags(flags = flags) } override suspend fun updateAllOrders(orderedIds: List) { database.transaction { orderedIds.forEachIndexed { index, categoryId -> - database.categoriesQueries.updateOrder(order = index.toLong(), categoryId = categoryId) + database.categoryQueries.updateOrder(order = index.toLong(), categoryId = categoryId) } } } override suspend fun delete(categoryId: Long) { - database.categoriesQueries.delete(categoryId = categoryId) + database.categoryQueries.delete(categoryId = categoryId) } private fun mapCategory( diff --git a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt index 9ee0baec81d..34e732539f9 100644 --- a/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/chapter/ChapterRepositoryImpl.kt @@ -30,7 +30,7 @@ class ChapterRepositoryImpl( return try { database.transactionWithResult { chapters.map { chapter -> - val chapterId = database.chaptersQueries.insertReturningId( + val chapterId = database.chapterQueries.insertReturningId( chapter.mangaId, chapter.url, chapter.name, @@ -42,7 +42,6 @@ class ChapterRepositoryImpl( chapter.sourceOrder, chapter.dateFetch, chapter.dateUpload, - chapter.version, chapter.memo, ) .awaitAsOne() @@ -66,7 +65,7 @@ class ChapterRepositoryImpl( private suspend fun partialUpdate(vararg chapterUpdates: ChapterUpdate) { database.transaction { chapterUpdates.forEach { chapterUpdate -> - database.chaptersQueries.update( + database.chapterQueries.update( mangaId = chapterUpdate.mangaId, url = chapterUpdate.url, name = chapterUpdate.name, @@ -79,8 +78,6 @@ class ChapterRepositoryImpl( dateFetch = chapterUpdate.dateFetch, dateUpload = chapterUpdate.dateUpload, chapterId = chapterUpdate.id, - version = chapterUpdate.version, - isSyncing = 0, memo = chapterUpdate.memo?.let(MemoColumnAdapter::encode), ) } @@ -89,50 +86,50 @@ class ChapterRepositoryImpl( override suspend fun removeChaptersWithIds(chapterIds: List) { try { - database.chaptersQueries.removeChaptersWithIds(chapterIds) + database.chapterQueries.removeChaptersWithIds(chapterIds) } catch (e: Exception) { logcat(LogPriority.ERROR, e) } } override suspend fun getChapterByMangaId(mangaId: Long, applyScanlatorFilter: Boolean): List { - return database.chaptersQueries + return database.chapterQueries .getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter) .awaitAsList() } override suspend fun getScanlatorsByMangaId(mangaId: Long): List { - return database.chaptersQueries + return database.chapterQueries .getScanlatorsByMangaId(mangaId) { it.orEmpty() } .awaitAsList() } override fun getScanlatorsByMangaIdAsFlow(mangaId: Long): Flow> { - return database.chaptersQueries + return database.chapterQueries .getScanlatorsByMangaId(mangaId) { it.orEmpty() } .subscribeToList() } override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List { - return database.chaptersQueries + return database.chapterQueries .getBookmarkedChaptersByMangaId(mangaId, ::mapChapter) .awaitAsList() } override suspend fun getChapterById(id: Long): Chapter? { - return database.chaptersQueries + return database.chapterQueries .getChapterById(id, ::mapChapter) .awaitAsOneOrNull() } override suspend fun getChapterByMangaIdAsFlow(mangaId: Long, applyScanlatorFilter: Boolean): Flow> { - return database.chaptersQueries + return database.chapterQueries .getChaptersByMangaId(mangaId, applyScanlatorFilter.toLong(), ::mapChapter) .subscribeToList() } override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? { - return database.chaptersQueries + return database.chapterQueries .getChapterByUrlAndMangaId(url, mangaId, ::mapChapter) .awaitAsOneOrNull() } @@ -144,17 +141,15 @@ class ChapterRepositoryImpl( url: String, name: String, scanlator: String?, + chapterNumber: Double, + dateUpload: Long, + sourceOrder: Long, + memo: JsonObject, read: Boolean, bookmark: Boolean, lastPageRead: Long, - chapterNumber: Double, - sourceOrder: Long, dateFetch: Long, - dateUpload: Long, - lastModifiedAt: Long, - version: Long, - isSyncing: Long, - memo: JsonObject, + modifiedAt: Long, ): Chapter = Chapter( id = id, mangaId = mangaId, @@ -168,8 +163,8 @@ class ChapterRepositoryImpl( dateUpload = dateUpload, chapterNumber = chapterNumber, scanlator = scanlator, - lastModifiedAt = lastModifiedAt, - version = version, + lastModifiedAt = modifiedAt, + version = 0, memo = memo, ) } diff --git a/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt b/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt index 709c5800d85..f2aecd84c97 100644 --- a/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt +++ b/data/src/main/java/tachiyomi/data/history/HistoryMapper.kt @@ -25,7 +25,7 @@ object HistoryMapper { title: String, thumbnailUrl: String?, sourceId: Long, - isFavorite: Boolean, + favoriteAt: Long?, coverLastModified: Long, chapterNumber: Double, readAt: Date?, @@ -41,7 +41,7 @@ object HistoryMapper { coverData = MangaCover( mangaId = mangaId, sourceId = sourceId, - isMangaFavorite = isFavorite, + isMangaFavorite = favoriteAt != null, url = thumbnailUrl, lastModified = coverLastModified, ), diff --git a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt index bfa819a241d..479a817e78d 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaMapper.kt @@ -7,43 +7,38 @@ import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaWithChapterCount object MangaMapper { - @Suppress("UNUSED_PARAMETER") fun mapManga( id: Long, - source: Long, + sourceId: Long, url: String, - artist: String?, + title: String, author: String?, + artist: String?, description: String?, genre: List?, - title: String, status: Long, - thumbnailUrl: String?, - favorite: Boolean, - lastUpdate: Long?, - nextUpdate: Long?, - initialized: Boolean, - viewerFlags: Long, - chapterFlags: Long, - coverLastModified: Long, - dateAdded: Long, + cover: String?, updateStrategy: UpdateStrategy, - calculateInterval: Long, - lastModifiedAt: Long, - favoriteModifiedAt: Long?, - version: Long, - isSyncing: Long, - notes: String, memo: JsonObject, + favoriteAt: Long?, + notes: String, + readerFlags: Long, + chapterFlags: Long, + chapterLastUpdate: Long?, + chapterNextUpdate: Long?, + chapterFetchInterval: Long, + coverLastModified: Long, + initialized: Boolean, + modifiedAt: Long, ): Manga = Manga( id = id, - source = source, - favorite = favorite, - lastUpdate = lastUpdate ?: 0, - nextUpdate = nextUpdate ?: 0, - fetchInterval = calculateInterval.toInt(), - dateAdded = dateAdded, - viewerFlags = viewerFlags, + source = sourceId, + favorite = favoriteAt != null, + lastUpdate = chapterLastUpdate ?: 0, + nextUpdate = chapterNextUpdate ?: 0, + fetchInterval = chapterFetchInterval.toInt(), + dateAdded = favoriteAt ?: 0, + viewerFlags = readerFlags, chapterFlags = chapterFlags, coverLastModified = coverLastModified, url = url, @@ -53,43 +48,39 @@ object MangaMapper { description = description, genre = genre, status = status, - thumbnailUrl = thumbnailUrl, + thumbnailUrl = cover, updateStrategy = updateStrategy, initialized = initialized, - lastModifiedAt = lastModifiedAt, - favoriteModifiedAt = favoriteModifiedAt, - version = version, + lastModifiedAt = modifiedAt, + favoriteModifiedAt = favoriteAt, + version = 0, notes = notes, memo = memo, ) fun mapLibraryManga( id: Long, - source: Long, + sourceId: Long, url: String, - artist: String?, + title: String, author: String?, + artist: String?, description: String?, genre: List?, - title: String, status: Long, - thumbnailUrl: String?, - favorite: Boolean, - lastUpdate: Long?, - nextUpdate: Long?, - initialized: Boolean, - viewerFlags: Long, - chapterFlags: Long, - coverLastModified: Long, - dateAdded: Long, + cover: String?, updateStrategy: UpdateStrategy, - calculateInterval: Long, - lastModifiedAt: Long, - favoriteModifiedAt: Long?, - version: Long, - isSyncing: Long, - notes: String, memo: JsonObject, + favoriteAt: Long, + notes: String, + readerFlags: Long, + chapterFlags: Long, + chapterLastUpdate: Long?, + chapterNextUpdate: Long?, + chapterFetchInterval: Long, + coverLastModified: Long, + initialized: Boolean, + modifiedAt: Long, totalCount: Long, readCount: Double, latestUpload: Long, @@ -100,31 +91,27 @@ object MangaMapper { ): LibraryManga = LibraryManga( manga = mapManga( id, - source, + sourceId, url, - artist, + title, author, + artist, description, genre, - title, status, - thumbnailUrl, - favorite, - lastUpdate, - nextUpdate, - initialized, - viewerFlags, - chapterFlags, - coverLastModified, - dateAdded, + cover, updateStrategy, - calculateInterval, - lastModifiedAt, - favoriteModifiedAt, - version, - isSyncing, - notes, memo, + favoriteAt, + notes, + readerFlags, + chapterFlags, + chapterLastUpdate, + chapterNextUpdate, + chapterFetchInterval, + coverLastModified, + initialized, + modifiedAt, ), categories = categories.split(",").map { it.toLong() }, totalChapters = totalCount, @@ -137,60 +124,52 @@ object MangaMapper { fun mapMangaWithChapterCount( id: Long, - source: Long, + sourceId: Long, url: String, - artist: String?, + title: String, author: String?, + artist: String?, description: String?, genre: List?, - title: String, status: Long, - thumbnailUrl: String?, - favorite: Boolean, - lastUpdate: Long?, - nextUpdate: Long?, - initialized: Boolean, - viewerFlags: Long, - chapterFlags: Long, - coverLastModified: Long, - dateAdded: Long, + cover: String?, updateStrategy: UpdateStrategy, - calculateInterval: Long, - lastModifiedAt: Long, - favoriteModifiedAt: Long?, - version: Long, - isSyncing: Long, - notes: String, memo: JsonObject, + favoriteAt: Long?, + notes: String, + readerFlags: Long, + chapterFlags: Long, + chapterLastUpdate: Long?, + chapterNextUpdate: Long?, + chapterFetchInterval: Long, + coverLastModified: Long, + initialized: Boolean, + modifiedAt: Long, totalCount: Long, ): MangaWithChapterCount = MangaWithChapterCount( manga = mapManga( id, - source, + sourceId, url, - artist, + title, author, + artist, description, genre, - title, status, - thumbnailUrl, - favorite, - lastUpdate, - nextUpdate, - initialized, - viewerFlags, - chapterFlags, - coverLastModified, - dateAdded, + cover, updateStrategy, - calculateInterval, - lastModifiedAt, - favoriteModifiedAt, - version, - isSyncing, - notes, memo, + favoriteAt, + notes, + readerFlags, + chapterFlags, + chapterLastUpdate, + chapterNextUpdate, + chapterFetchInterval, + coverLastModified, + initialized, + modifiedAt, ), chapterCount = totalCount, ) diff --git a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt index f39bbf83c02..4455dd76596 100644 --- a/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/manga/MangaRepositoryImpl.kt @@ -35,37 +35,37 @@ class MangaRepositoryImpl( ) : MangaRepository { override suspend fun getMangaById(id: Long): Manga { - return database.mangasQueries + return database.mangaQueries .getMangaById(id, MangaMapper::mapManga) .awaitAsOne() } override fun getMangaByIdAsFlow(id: Long): Flow { - return database.mangasQueries + return database.mangaQueries .getMangaById(id, MangaMapper::mapManga) .subscribeToOne() } override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? { - return database.mangasQueries + return database.mangaQueries .getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga) .awaitAsOneOrNull() } override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow { - return database.mangasQueries + return database.mangaQueries .getMangaByUrlAndSource(url, sourceId, MangaMapper::mapManga) .subscribeToOneOrNull() } override suspend fun getFavorites(): List { - return database.mangasQueries + return database.mangaQueries .getFavorites(MangaMapper::mapManga) .awaitAsList() } override suspend fun getReadMangaNotInLibrary(): List { - return database.mangasQueries + return database.mangaQueries .getReadMangaNotInLibrary(MangaMapper::mapManga) .awaitAsList() } @@ -83,13 +83,13 @@ class MangaRepositoryImpl( } override fun getFavoritesBySourceId(sourceId: Long): Flow> { - return database.mangasQueries + return database.mangaQueries .getFavoriteBySourceId(sourceId, MangaMapper::mapManga) .subscribeToList() } override suspend fun getDuplicateLibraryManga(id: Long, title: String): List { - return database.mangasQueries + return database.mangaQueries .getDuplicateLibraryManga(id, title, MangaMapper::mapMangaWithChapterCount) .awaitAsList() } @@ -102,7 +102,7 @@ class MangaRepositoryImpl( val timeZone = TimeZone.currentSystemDefault() val epochMillis = Clock.System.now().toLocalDateTime(timeZone).date.atStartOfDayIn(timeZone).toEpochMilliseconds() - return database.mangasQueries + return database.mangaQueries .getUpcomingManga( startOfDay = epochMillis, statuses = statuses, @@ -117,7 +117,7 @@ class MangaRepositoryImpl( override suspend fun resetViewerFlags(): Boolean { return try { - database.mangasQueries.resetViewerFlags() + database.mangaQueries.resetViewerFlags() true } catch (e: Exception) { logcat(LogPriority.ERROR, e) @@ -127,9 +127,9 @@ class MangaRepositoryImpl( override suspend fun setMangaCategories(mangaId: Long, categoryIds: List) { database.transaction { - database.mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId) + database.manga_categoryQueries.deleteMangaCategoryByMangaId(mangaId) categoryIds.forEach { categoryId -> - database.mangas_categoriesQueries.insert(mangaId, categoryId) + database.manga_categoryQueries.insert(mangaId, categoryId) } } } @@ -157,7 +157,7 @@ class MangaRepositoryImpl( override suspend fun insertNetworkManga(manga: List): List { return database.transactionWithResult { manga.map { - database.mangasQueries.insertNetworkManga( + database.mangaQueries.insertNetworkManga( source = it.source, url = it.url, artist = it.artist, @@ -167,7 +167,7 @@ class MangaRepositoryImpl( title = it.title, status = it.status, thumbnailUrl = it.thumbnailUrl, - favorite = it.favorite, + favoriteAt = it.dateAdded.takeIf { _ -> it.favorite }, lastUpdate = it.lastUpdate, nextUpdate = it.nextUpdate, calculateInterval = it.fetchInterval.toLong(), @@ -175,9 +175,7 @@ class MangaRepositoryImpl( viewerFlags = it.viewerFlags, chapterFlags = it.chapterFlags, coverLastModified = it.coverLastModified, - dateAdded = it.dateAdded, updateStrategy = it.updateStrategy, - version = it.version, memo = it.memo, updateTitle = it.title.isNotBlank(), updateCover = !it.thumbnailUrl.isNullOrBlank(), @@ -192,7 +190,7 @@ class MangaRepositoryImpl( private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) { database.transaction { mangaUpdates.forEach { value -> - database.mangasQueries.update( + database.mangaQueries.update( source = value.source, url = value.url, artist = value.artist, @@ -202,7 +200,7 @@ class MangaRepositoryImpl( title = value.title, status = value.status, thumbnailUrl = value.thumbnailUrl, - favorite = value.favorite, + favoriteAt = value.dateAdded?.takeIf { it != 0L }, lastUpdate = value.lastUpdate, nextUpdate = value.nextUpdate, calculateInterval = value.fetchInterval?.toLong(), @@ -210,11 +208,8 @@ class MangaRepositoryImpl( viewer = value.viewerFlags, chapterFlags = value.chapterFlags, coverLastModified = value.coverLastModified, - dateAdded = value.dateAdded, mangaId = value.id, updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode), - version = value.version, - isSyncing = 0, notes = value.notes, memo = value.memo?.let(MemoColumnAdapter::encode), ) diff --git a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt index f6c88b2f0af..f746a806f35 100644 --- a/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/source/SourceRepositoryImpl.kt @@ -48,7 +48,7 @@ class SourceRepositoryImpl( } override fun getSourcesWithFavoriteCount(): Flow>> { - val sourceIdWithFavoriteCountFlow = database.mangasQueries + val sourceIdWithFavoriteCountFlow = database.mangaQueries .getSourceIdWithFavoriteCount() .subscribeToList() return combine(sourceIdWithFavoriteCountFlow, sourceManager.sources) { sourceIdWithFavoriteCount, _ -> @@ -66,7 +66,7 @@ class SourceRepositoryImpl( } override fun getSourcesWithNonLibraryManga(): Flow> { - return database.mangasQueries + return database.mangaQueries .getSourceIdsWithNonLibraryManga() .subscribeToList() .map { sourceId -> diff --git a/data/src/main/java/tachiyomi/data/source/StubSourceRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/source/StubSourceRepositoryImpl.kt index 8aee2e36fd8..aef9cdda294 100644 --- a/data/src/main/java/tachiyomi/data/source/StubSourceRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/source/StubSourceRepositoryImpl.kt @@ -19,19 +19,19 @@ class StubSourceRepositoryImpl( ) : StubSourceRepository { override fun subscribeAll(): Flow> { - return database.sourcesQueries + return database.sourceQueries .findAll(::mapStubSource) .subscribeToList() } override suspend fun getStubSource(id: Long): StubSource? { - return database.sourcesQueries + return database.sourceQueries .findOne(id, ::mapStubSource) .awaitAsOneOrNull() } override suspend fun upsertStubSource(id: Long, lang: String, name: String) { - database.sourcesQueries.upsert(id, lang, name) + database.sourceQueries.upsert(id, lang, name) } private fun mapStubSource( diff --git a/data/src/main/java/tachiyomi/data/track/TrackMapper.kt b/data/src/main/java/tachiyomi/data/track/TrackMapper.kt index 83a3f5b8660..d75b9ebe832 100644 --- a/data/src/main/java/tachiyomi/data/track/TrackMapper.kt +++ b/data/src/main/java/tachiyomi/data/track/TrackMapper.kt @@ -8,13 +8,13 @@ object TrackMapper { mangaId: Long, syncId: Long, remoteId: Long, + remoteUrl: String, libraryId: Long?, title: String, lastChapterRead: Double, totalChapters: Long, status: Long, score: Double, - remoteUrl: String, startDate: Long, finishDate: Long, private: Boolean, diff --git a/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt index befd963243a..4b3c1b6c769 100644 --- a/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt @@ -20,31 +20,31 @@ class TrackRepositoryImpl( ) : TrackRepository { override suspend fun getTrackById(id: Long): Track? { - return database.manga_syncQueries + return database.manga_trackQueries .getTrackById(id, TrackMapper::mapTrack) .awaitAsOneOrNull() } override suspend fun getTracksByMangaId(mangaId: Long): List { - return database.manga_syncQueries + return database.manga_trackQueries .getTracksByMangaId(mangaId, TrackMapper::mapTrack) .awaitAsList() } override fun getTracksAsFlow(): Flow> { - return database.manga_syncQueries + return database.manga_trackQueries .getTracks(TrackMapper::mapTrack) .subscribeToList() } override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow> { - return database.manga_syncQueries + return database.manga_trackQueries .getTracksByMangaId(mangaId, TrackMapper::mapTrack) .subscribeToList() } override suspend fun delete(mangaId: Long, trackerId: Long) { - database.manga_syncQueries.delete( + database.manga_trackQueries.delete( mangaId = mangaId, syncId = trackerId, ) @@ -61,7 +61,7 @@ class TrackRepositoryImpl( private suspend fun insertValues(vararg tracks: Track) { database.transaction { tracks.forEach { mangaTrack -> - database.manga_syncQueries.insert( + database.manga_trackQueries.insert( mangaId = mangaTrack.mangaId, syncId = mangaTrack.trackerId, remoteId = mangaTrack.remoteId, diff --git a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt index 651143628ea..d1327ada103 100644 --- a/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt +++ b/data/src/main/java/tachiyomi/data/updates/UpdatesRepositoryImpl.kt @@ -89,7 +89,7 @@ class UpdatesRepositoryImpl( bookmark: Boolean, lastPageRead: Long, sourceId: Long, - favorite: Boolean, + favoriteAt: Long, thumbnailUrl: String?, coverLastModified: Long, dateUpload: Long, @@ -110,7 +110,7 @@ class UpdatesRepositoryImpl( coverData = MangaCover( mangaId = mangaId, sourceId = sourceId, - isMangaFavorite = favorite, + isMangaFavorite = favoriteAt != 0L, url = thumbnailUrl, lastModified = coverLastModified, ), diff --git a/data/src/main/sqldelight/tachiyomi/data/categories.sq b/data/src/main/sqldelight/tachiyomi/data/categories.sq deleted file mode 100644 index a94f38f934f..00000000000 --- a/data/src/main/sqldelight/tachiyomi/data/categories.sq +++ /dev/null @@ -1,70 +0,0 @@ -CREATE TABLE categories( - _id INTEGER NOT NULL PRIMARY KEY, - name TEXT NOT NULL, - sort INTEGER NOT NULL, - flags INTEGER NOT NULL -); - --- Insert system category -INSERT OR IGNORE INTO categories(_id, name, sort, flags) VALUES (0, "", -1, 0); --- Disallow deletion of default category -CREATE TRIGGER IF NOT EXISTS system_category_delete_trigger BEFORE DELETE -ON categories -BEGIN SELECT CASE - WHEN old._id <= 0 THEN - RAISE(ABORT, "System category can't be deleted") - END; -END; - -getCategory: -SELECT * -FROM categories -WHERE _id = :id -LIMIT 1; - -getCategories: -SELECT -_id AS id, -name, -sort AS `order`, -flags -FROM categories -ORDER BY sort; - -getCategoriesByMangaId: -SELECT -C._id AS id, -C.name, -C.sort AS `order`, -C.flags -FROM categories C -JOIN mangas_categories MC -ON C._id = MC.category_id -WHERE MC.manga_id = :mangaId; - -insert: -INSERT INTO categories(name, sort, flags) -VALUES (:name, :order, :flags); - -delete: -DELETE FROM categories -WHERE _id = :categoryId; - -updateName: -UPDATE categories -SET name = :name -WHERE _id = :categoryId; - -updateOrder: -UPDATE categories -SET sort = :order -WHERE _id = :categoryId; - -updateFlags: -UPDATE categories -SET flags = :flags -WHERE _id = :categoryId; - -updateAllFlags: -UPDATE categories SET -flags = coalesce(:flags, flags); diff --git a/data/src/main/sqldelight/tachiyomi/data/category.sq b/data/src/main/sqldelight/tachiyomi/data/category.sq new file mode 100644 index 00000000000..e0926709ac9 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/data/category.sq @@ -0,0 +1,70 @@ +CREATE TABLE category( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + `order` INTEGER NOT NULL UNIQUE, + flags INTEGER NOT NULL +); + +-- Insert system category +INSERT OR IGNORE INTO category(id, name, `order`, flags) VALUES (0, "", -1, 0); +-- Disallow deletion of default category +CREATE TRIGGER IF NOT EXISTS system_category_delete_trigger BEFORE DELETE +ON category +BEGIN SELECT CASE + WHEN old.id <= 0 THEN + RAISE(ABORT, "System category can't be deleted") + END; +END; + +getCategory: +SELECT * +FROM category +WHERE id = :id +LIMIT 1; + +getCategories: +SELECT +id, +name, +`order`, +flags +FROM category +ORDER BY `order`; + +getCategoriesByMangaId: +SELECT +C.id, +C.name, +C.`order`, +C.flags +FROM category C +JOIN manga_category MC +ON C.id = MC.category_id +WHERE MC.manga_id = :mangaId; + +insert: +INSERT INTO category(name, `order`, flags) +VALUES (:name, :order, :flags); + +delete: +DELETE FROM category +WHERE id = :categoryId; + +updateName: +UPDATE category +SET name = :name +WHERE id = :categoryId; + +updateOrder: +UPDATE category +SET `order` = :order +WHERE id = :categoryId; + +updateFlags: +UPDATE category +SET flags = :flags +WHERE id = :categoryId; + +updateAllFlags: +UPDATE category SET +flags = coalesce(:flags, flags); diff --git a/data/src/main/sqldelight/tachiyomi/data/chapter.sq b/data/src/main/sqldelight/tachiyomi/data/chapter.sq new file mode 100644 index 00000000000..f9372d3bcfb --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/data/chapter.sq @@ -0,0 +1,100 @@ +import kotlin.Boolean; +import kotlinx.serialization.json.JsonObject; + +CREATE TABLE chapter( + id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + remote_url TEXT NOT NULL, + remote_name TEXT NOT NULL, + remote_scanlator TEXT, + remote_chapter_number REAL NOT NULL, + remote_date_upload INTEGER NOT NULL, + remote_order INTEGER NOT NULL, + remote_memo BLOB AS JsonObject NOT NULL, + user_read INTEGER AS Boolean NOT NULL, + user_bookmark INTEGER AS Boolean NOT NULL, + user_last_page_read INTEGER NOT NULL, + state_date_fetch INTEGER NOT NULL, + state_modified_at INTEGER NOT NULL, + UNIQUE(manga_id, remote_url), + FOREIGN KEY(manga_id) REFERENCES manga (id) + ON DELETE CASCADE +); + +CREATE TRIGGER update_modified_at_chapter +AFTER UPDATE ON chapter +FOR EACH ROW +BEGIN + UPDATE chapter + SET state_modified_at = strftime('%s', 'now') + WHERE id = new.id; +END; + +getChapterById: +SELECT * +FROM chapter +WHERE id = :id; + +getChaptersByMangaId: +SELECT C.* +FROM chapter C +LEFT JOIN excluded_scanlator ES +ON C.manga_id = ES.manga_id +AND C.remote_scanlator = ES.scanlator +WHERE C.manga_id = :mangaId +AND ( + :applyScanlatorFilter = 0 + OR ES.scanlator IS NULL +); + +getScanlatorsByMangaId: +SELECT remote_scanlator +FROM chapter +WHERE manga_id = :mangaId; + +getBookmarkedChaptersByMangaId: +SELECT * +FROM chapter +WHERE user_bookmark +AND manga_id = :mangaId; + +getChapterByUrl: +SELECT * +FROM chapter +WHERE remote_url = :chapterUrl; + +getChapterByUrlAndMangaId: +SELECT * +FROM chapter +WHERE remote_url = :chapterUrl +AND manga_id = :mangaId +LIMIT 1; + +removeChaptersWithIds: +DELETE FROM chapter +WHERE id IN :chapterIds; + +insert: +INSERT INTO chapter(manga_id, remote_url, remote_name, remote_scanlator, user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order, state_date_fetch, remote_date_upload, state_modified_at, remote_memo) +VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :memo); + +insertReturningId: +INSERT INTO chapter(manga_id, remote_url, remote_name, remote_scanlator, user_read, user_bookmark, user_last_page_read, remote_chapter_number, remote_order, state_date_fetch, remote_date_upload, state_modified_at, remote_memo) +VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :memo) +RETURNING id; + +update: +UPDATE chapter +SET manga_id = coalesce(:mangaId, manga_id), + remote_url = coalesce(:url, remote_url), + remote_name = coalesce(:name, remote_name), + remote_scanlator = coalesce(:scanlator, remote_scanlator), + user_read = coalesce(:read, user_read), + user_bookmark = coalesce(:bookmark, user_bookmark), + user_last_page_read = coalesce(:lastPageRead, user_last_page_read), + remote_chapter_number = coalesce(:chapterNumber, remote_chapter_number), + remote_order = coalesce(:sourceOrder, remote_order), + state_date_fetch = coalesce(:dateFetch, state_date_fetch), + remote_date_upload = coalesce(:dateUpload, remote_date_upload), + remote_memo = coalesce(:memo, remote_memo) +WHERE id = :chapterId; diff --git a/data/src/main/sqldelight/tachiyomi/data/chapters.sq b/data/src/main/sqldelight/tachiyomi/data/chapters.sq deleted file mode 100644 index b6280be75e8..00000000000 --- a/data/src/main/sqldelight/tachiyomi/data/chapters.sq +++ /dev/null @@ -1,128 +0,0 @@ -import kotlin.Boolean; -import kotlinx.serialization.json.JsonObject; - -CREATE TABLE chapters( - _id INTEGER NOT NULL PRIMARY KEY, - manga_id INTEGER NOT NULL, - url TEXT NOT NULL, - name TEXT NOT NULL, - scanlator TEXT, - read INTEGER AS Boolean NOT NULL, - bookmark INTEGER AS Boolean NOT NULL, - last_page_read INTEGER NOT NULL, - chapter_number REAL NOT NULL, - source_order INTEGER NOT NULL, - date_fetch INTEGER NOT NULL, - date_upload INTEGER NOT NULL, - last_modified_at INTEGER NOT NULL DEFAULT 0, - version INTEGER NOT NULL DEFAULT 0, - is_syncing INTEGER NOT NULL DEFAULT 0, - memo BLOB AS JsonObject NOT NULL DEFAULT '{}', - FOREIGN KEY(manga_id) REFERENCES mangas (_id) - ON DELETE CASCADE -); - -CREATE INDEX chapters_manga_id_index ON chapters(manga_id); -CREATE INDEX chapters_unread_by_manga_index ON chapters(manga_id, read) WHERE read = 0; -CREATE INDEX idx_chapters_url ON chapters(url); - -CREATE TRIGGER update_last_modified_at_chapters -AFTER UPDATE ON chapters -FOR EACH ROW -BEGIN - UPDATE chapters - SET last_modified_at = strftime('%s', 'now') - WHERE _id = new._id; -END; - -CREATE TRIGGER update_chapter_and_manga_version AFTER UPDATE ON chapters -WHEN new.is_syncing = 0 AND ( - new.read != old.read OR - new.bookmark != old.bookmark OR - new.last_page_read != old.last_page_read -) -BEGIN - -- Update the chapter version - UPDATE chapters SET version = version + 1 - WHERE _id = new._id; - - -- Update the manga version - UPDATE mangas SET version = version + 1 - WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0; -END; - -getChapterById: -SELECT * -FROM chapters -WHERE _id = :id; - -getChaptersByMangaId: -SELECT C.* -FROM chapters C -LEFT JOIN excluded_scanlators ES -ON C.manga_id = ES.manga_id -AND C.scanlator = ES.scanlator -WHERE C.manga_id = :mangaId -AND ( - :applyScanlatorFilter = 0 - OR ES.scanlator IS NULL -); - -getScanlatorsByMangaId: -SELECT scanlator -FROM chapters -WHERE manga_id = :mangaId; - -getBookmarkedChaptersByMangaId: -SELECT * -FROM chapters -WHERE bookmark -AND manga_id = :mangaId; - -getChapterByUrl: -SELECT * -FROM chapters -WHERE url = :chapterUrl; - -getChapterByUrlAndMangaId: -SELECT * -FROM chapters -WHERE url = :chapterUrl -AND manga_id = :mangaId -LIMIT 1; - -removeChaptersWithIds: -DELETE FROM chapters -WHERE _id IN :chapterIds; - -resetIsSyncing: -UPDATE chapters -SET is_syncing = 0 -WHERE is_syncing = 1; - -insert: -INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at, version, is_syncing, memo) -VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :version, 0, :memo); - -insertReturningId: -INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at, version, is_syncing, memo) -VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :version, 0, :memo) -RETURNING _id; - -update: -UPDATE chapters -SET manga_id = coalesce(:mangaId, manga_id), - url = coalesce(:url, url), - name = coalesce(:name, name), - scanlator = coalesce(:scanlator, scanlator), - read = coalesce(:read, read), - bookmark = coalesce(:bookmark, bookmark), - last_page_read = coalesce(:lastPageRead, last_page_read), - chapter_number = coalesce(:chapterNumber, chapter_number), - source_order = coalesce(:sourceOrder, source_order), - date_fetch = coalesce(:dateFetch, date_fetch), - date_upload = coalesce(:dateUpload, date_upload), - version = coalesce(:version, version), - is_syncing = coalesce(:isSyncing, is_syncing), - memo = coalesce(:memo, memo) -WHERE _id = :chapterId; diff --git a/data/src/main/sqldelight/tachiyomi/data/excluded_scanlator.sq b/data/src/main/sqldelight/tachiyomi/data/excluded_scanlator.sq new file mode 100644 index 00000000000..114a0f5c389 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/data/excluded_scanlator.sq @@ -0,0 +1,22 @@ +CREATE TABLE excluded_scanlator( + manga_id INTEGER NOT NULL, + scanlator TEXT NOT NULL, + UNIQUE(manga_id, scanlator), + FOREIGN KEY(manga_id) REFERENCES manga (id) + ON DELETE CASCADE +); + + +insert: +INSERT INTO excluded_scanlator(manga_id, scanlator) +VALUES (:mangaId, :scanlator); + +remove: +DELETE FROM excluded_scanlator +WHERE manga_id = :mangaId +AND scanlator IN :scanlators; + +getExcludedScanlatorsByMangaId: +SELECT scanlator +FROM excluded_scanlator +WHERE manga_id = :mangaId; diff --git a/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq b/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq deleted file mode 100644 index 37ed6489ec4..00000000000 --- a/data/src/main/sqldelight/tachiyomi/data/excluded_scanlators.sq +++ /dev/null @@ -1,23 +0,0 @@ -CREATE TABLE excluded_scanlators( - manga_id INTEGER NOT NULL, - scanlator TEXT NOT NULL, - FOREIGN KEY(manga_id) REFERENCES mangas (_id) - ON DELETE CASCADE -); - -CREATE INDEX excluded_scanlators_manga_id_index ON excluded_scanlators(manga_id); -CREATE INDEX idx_excluded_scanlators_scanlator ON excluded_scanlators(scanlator); - -insert: -INSERT INTO excluded_scanlators(manga_id, scanlator) -VALUES (:mangaId, :scanlator); - -remove: -DELETE FROM excluded_scanlators -WHERE manga_id = :mangaId -AND scanlator IN :scanlators; - -getExcludedScanlatorsByMangaId: -SELECT scanlator -FROM excluded_scanlators -WHERE manga_id = :mangaId; diff --git a/data/src/main/sqldelight/tachiyomi/data/history.sq b/data/src/main/sqldelight/tachiyomi/data/history.sq index 6cecd5784df..8daac320ea2 100644 --- a/data/src/main/sqldelight/tachiyomi/data/history.sq +++ b/data/src/main/sqldelight/tachiyomi/data/history.sq @@ -1,56 +1,59 @@ import java.util.Date; CREATE TABLE history( - _id INTEGER NOT NULL PRIMARY KEY, + id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, chapter_id INTEGER NOT NULL UNIQUE, - last_read INTEGER AS Date, - time_read INTEGER NOT NULL, - FOREIGN KEY(chapter_id) REFERENCES chapters (_id) + read_at INTEGER AS Date, + read_duration INTEGER NOT NULL, + FOREIGN KEY(manga_id) REFERENCES manga (id) + ON DELETE CASCADE, + FOREIGN KEY(chapter_id) REFERENCES chapter (id) ON DELETE CASCADE ); -CREATE INDEX history_history_chapter_id_index ON history(chapter_id); -CREATE INDEX idx_history_last_read ON history(last_read); +CREATE INDEX history_manga_id_index ON history(manga_id); +CREATE INDEX history_read_at_index ON history(read_at); getHistoryByMangaId: SELECT -H._id, +H.id, H.chapter_id, -H.last_read, -H.time_read +H.read_at, +H.read_duration FROM history H -JOIN chapters C -ON H.chapter_id = C._id -WHERE C.manga_id = :mangaId AND C._id = H.chapter_id; +JOIN chapter C +ON H.chapter_id = C.id +WHERE C.manga_id = :mangaId AND C.id = H.chapter_id; getHistoryByChapterUrlAndMangaId: SELECT -H._id, +H.id, H.chapter_id, -H.last_read, -H.time_read +H.read_at, +H.read_duration FROM history H -JOIN chapters C -ON H.chapter_id = C._id -WHERE C.url = :chapterUrl AND C.manga_id = :mangaId AND C._id = H.chapter_id +JOIN chapter C +ON H.chapter_id = C.id +WHERE C.remote_url = :chapterUrl AND C.manga_id = :mangaId AND C.id = H.chapter_id LIMIT 1; resetHistoryById: UPDATE history -SET last_read = 0 -WHERE _id = :historyId; +SET read_at = 0 +WHERE id = :historyId; resetHistoryByMangaId: UPDATE history -SET last_read = 0 -WHERE _id IN ( - SELECT H._id - FROM mangas M - INNER JOIN chapters C - ON M._id = C.manga_id +SET read_at = 0 +WHERE id IN ( + SELECT H.id + FROM manga M + INNER JOIN chapter C + ON M.id = C.manga_id INNER JOIN history H - ON C._id = H.chapter_id - WHERE M._id = :mangaId + ON C.id = H.chapter_id + WHERE M.id = :mangaId ); removeAllHistory: @@ -58,18 +61,20 @@ DELETE FROM history; removeResettedHistory: DELETE FROM history -WHERE last_read = 0; +WHERE read_at = 0; upsert: -INSERT INTO history(chapter_id, last_read, time_read) -VALUES (:chapterId, :readAt, :time_read) +INSERT INTO history(manga_id, chapter_id, read_at, read_duration) +SELECT C.manga_id, :chapterId, :readAt, :time_read +FROM chapter C +WHERE C.id = :chapterId ON CONFLICT(chapter_id) DO UPDATE SET - last_read = :readAt, - time_read = time_read + :time_read + read_at = :readAt, + read_duration = read_duration + :time_read WHERE chapter_id = :chapterId; getReadDuration: -SELECT coalesce(sum(time_read), 0) +SELECT coalesce(sum(read_duration), 0) FROM history; diff --git a/data/src/main/sqldelight/tachiyomi/data/manga.sq b/data/src/main/sqldelight/tachiyomi/data/manga.sq new file mode 100644 index 00000000000..2c397a921a2 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/data/manga.sq @@ -0,0 +1,262 @@ +import eu.kanade.tachiyomi.source.model.UpdateStrategy; +import kotlin.Boolean; +import kotlin.String; +import kotlin.collections.List; +import kotlinx.serialization.json.JsonObject; + +CREATE TABLE manga( + id INTEGER NOT NULL PRIMARY KEY, + source_id INTEGER NOT NULL, + remote_url TEXT NOT NULL, + remote_title TEXT NOT NULL, + remote_author TEXT, + remote_artist TEXT, + remote_description TEXT, + remote_genre TEXT AS List, + remote_status INTEGER NOT NULL, + remote_cover TEXT, + remote_update_strategy INTEGER AS UpdateStrategy NOT NULL, + remote_memo BLOB AS JsonObject NOT NULL, + user_favorite_at INTEGER, + user_notes TEXT NOT NULL, + user_reader_flags INTEGER NOT NULL, + user_chapter_flags INTEGER NOT NULL, + state_chapter_last_update INTEGER, + state_chapter_next_update INTEGER, + state_chapter_fetch_interval INTEGER NOT NULL, + state_cover_last_modified INTEGER NOT NULL, + state_initialized INTEGER AS Boolean NOT NULL, + state_modified_at INTEGER NOT NULL, + UNIQUE(source_id, remote_url) +); + +CREATE INDEX manga_user_favorite_at_index ON manga(user_favorite_at) WHERE user_favorite_at IS NOT NULL; + +CREATE TRIGGER update_modified_at_manga +AFTER UPDATE ON manga +FOR EACH ROW +BEGIN + UPDATE manga + SET state_modified_at = strftime('%s', 'now') + WHERE id = new.id; +END; + +getMangaById: +SELECT * +FROM manga +WHERE id = :id; + +-- TODO: this should ideally never really have more than 1 result +getMangaByUrlAndSource: +SELECT * +FROM manga +WHERE remote_url = :url +AND source_id = :source +LIMIT 1; + +getFavorites: +SELECT * +FROM manga +WHERE user_favorite_at IS NOT NULL; + +getReadMangaNotInLibrary: +SELECT * +FROM manga +WHERE user_favorite_at IS NULL AND id IN ( + SELECT DISTINCT chapter.manga_id + FROM chapter + WHERE user_read = 1 OR user_last_page_read != 0 +); + +getAllManga: +SELECT * +FROM manga; + +getAllMangaSourceAndUrl: +SELECT source_id, remote_url +FROM manga; + +getMangasWithFavoriteTimestamp: +SELECT * +FROM manga +WHERE user_favorite_at IS NOT NULL; + +getSourceIdWithFavoriteCount: +SELECT +source_id, +count(*) +FROM manga +WHERE user_favorite_at IS NOT NULL +GROUP BY source_id; + +getFavoriteBySourceId: +SELECT * +FROM manga +WHERE user_favorite_at IS NOT NULL +AND source_id = :sourceId; + +getDuplicateLibraryManga: +WITH +track_dupes AS ( + SELECT DISTINCT S2.manga_id + FROM manga_track S1 + INNER JOIN manga_track S2 + ON S1.tracker_id = S2.tracker_id + AND S1.remote_id = S2.remote_id + AND S1.manga_id != S2.manga_id + WHERE S1.manga_id = :id +), +duplicates AS ( + SELECT M.* + FROM manga M + LEFT JOIN track_dupes D + ON D.manga_id = id + WHERE user_favorite_at IS NOT NULL + AND id != :id + AND (lower(remote_title) LIKE '%' || lower(:title) || '%' OR D.manga_id IS NOT NULL) +), +chapter_counts AS ( + SELECT + M.id AS manga_id, + count(*) AS chapter_count + FROM duplicates M + JOIN chapter C + ON M.id = C.manga_id + LEFT JOIN excluded_scanlator ES + ON C.manga_id = ES.manga_id + AND C.remote_scanlator = ES.scanlator + WHERE ES.scanlator IS NULL + GROUP BY M.id +) +SELECT + M.*, + coalesce(CC.chapter_count, 0) AS chapter_count +FROM duplicates M +LEFT JOIN chapter_counts CC +ON M.id = CC.manga_id; + +getUpcomingManga: +SELECT * +FROM manga +WHERE state_chapter_next_update >= :startOfDay +AND user_favorite_at IS NOT NULL +AND remote_status IN :statuses +AND ( + -- includedEmpty being true expresses "don't care" state and bypasses the membership filter + :includedEmpty + OR EXISTS ( + SELECT 1 FROM manga_category + WHERE manga_category.manga_id = manga.id + AND COALESCE(manga_category.category_id, 0) IN :includedCategories + ) + OR (0 IN :includedCategories AND NOT EXISTS ( + SELECT 1 FROM manga_category WHERE manga_category.manga_id = manga.id) + ) +) +AND ( + -- excludedEmpty being true expresses "don't care" state and bypasses the membership filter + :excludedEmpty + OR ( + NOT EXISTS ( + SELECT 1 FROM manga_category + WHERE manga_category.manga_id = manga.id + AND COALESCE(manga_category.category_id, 0) IN :excludedCategories + ) + AND ( + 0 NOT IN :excludedCategories + OR EXISTS (SELECT 1 FROM manga_category WHERE manga_category.manga_id = manga.id) + ) + ) +) +ORDER BY state_chapter_next_update ASC; + +resetViewerFlags: +UPDATE manga +SET user_reader_flags = 0; + +getSourceIdsWithNonLibraryManga: +SELECT source_id, COUNT(*) AS manga_count +FROM manga +WHERE user_favorite_at IS NULL +GROUP BY source_id; + +deleteNonLibraryManga: +DELETE FROM manga +WHERE user_favorite_at IS NULL +AND source_id IN :sourceIds +AND ( + :keepReadManga = 0 + OR id NOT IN ( + SELECT DISTINCT manga_id + FROM chapter + WHERE user_read = 1 + OR user_last_page_read != 0 + ) +); + +insertReturningId: +INSERT INTO manga(source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title, remote_status, remote_cover, user_favorite_at, state_chapter_last_update, state_chapter_next_update, state_initialized, user_reader_flags, user_chapter_flags, state_cover_last_modified, remote_update_strategy, state_chapter_fetch_interval, state_modified_at, user_notes, remote_memo) +VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favoriteAt, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :updateStrategy, :calculateInterval, 0, :notes, :memo) +RETURNING id; + +update: +UPDATE manga SET + source_id = coalesce(:source, source_id), + remote_url = coalesce(:url, remote_url), + remote_artist = coalesce(:artist, remote_artist), + remote_author = coalesce(:author, remote_author), + remote_description = coalesce(:description, remote_description), + remote_genre = coalesce(:genre, remote_genre), + remote_title = coalesce(:title, remote_title), + remote_status = coalesce(:status, remote_status), + remote_cover = coalesce(:thumbnailUrl, remote_cover), + user_favorite_at = coalesce(:favoriteAt, user_favorite_at), + state_chapter_last_update = coalesce(:lastUpdate, state_chapter_last_update), + state_chapter_next_update = coalesce(:nextUpdate, state_chapter_next_update), + state_initialized = coalesce(:initialized, state_initialized), + user_reader_flags = coalesce(:viewer, user_reader_flags), + user_chapter_flags = coalesce(:chapterFlags, user_chapter_flags), + state_cover_last_modified = coalesce(:coverLastModified, state_cover_last_modified), + remote_update_strategy = coalesce(:updateStrategy, remote_update_strategy), + state_chapter_fetch_interval = coalesce(:calculateInterval, state_chapter_fetch_interval), + user_notes = coalesce(:notes, user_notes), + remote_memo = coalesce(:memo, remote_memo) +WHERE id = :mangaId; + +insertNetworkManga { + -- Insert the manga if it doesn't exist already + INSERT INTO manga( + source_id, remote_url, remote_artist, remote_author, remote_description, remote_genre, remote_title, remote_status, remote_cover, user_favorite_at, + state_chapter_last_update, state_chapter_next_update, state_initialized, user_reader_flags, user_chapter_flags, state_cover_last_modified, + remote_update_strategy, state_chapter_fetch_interval, state_modified_at, user_notes, remote_memo + ) + SELECT + :source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favoriteAt, + :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, + :updateStrategy, :calculateInterval, 0, '', :memo + WHERE NOT EXISTS(SELECT 0 FROM manga WHERE source_id = :source AND remote_url = :url); + + -- Update the relevant details if applicable and not favorite + UPDATE manga + SET + remote_title = CASE WHEN :updateTitle THEN :title ELSE remote_title END, + remote_cover = CASE WHEN :updateCover THEN :thumbnailUrl ELSE remote_cover END, + remote_author = CASE WHEN :updateDetails THEN :author ELSE remote_author END, + remote_artist = CASE WHEN :updateDetails THEN :artist ELSE remote_artist END, + remote_description = CASE WHEN :updateDetails THEN :description ELSE remote_description END, + remote_genre = CASE WHEN :updateDetails THEN :genre ELSE remote_genre END, + remote_status = CASE WHEN :updateDetails THEN :status ELSE remote_status END, + remote_update_strategy = CASE WHEN :updateDetails THEN :updateStrategy ELSE remote_update_strategy END, + state_initialized = :updateDetails, + remote_memo = :memo + WHERE source_id = :source + AND remote_url = :url + AND user_favorite_at IS NULL; + + -- Finally return the manga + SELECT * + FROM manga + WHERE source_id = :source + AND remote_url = :url + LIMIT 1; +} diff --git a/data/src/main/sqldelight/tachiyomi/data/manga_category.sq b/data/src/main/sqldelight/tachiyomi/data/manga_category.sq new file mode 100644 index 00000000000..4974db32d38 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/data/manga_category.sq @@ -0,0 +1,19 @@ +CREATE TABLE manga_category( + manga_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + UNIQUE(manga_id, category_id), + FOREIGN KEY(category_id) REFERENCES category (id) + ON DELETE CASCADE, + FOREIGN KEY(manga_id) REFERENCES manga (id) + ON DELETE CASCADE +); + +CREATE INDEX manga_category_category_id_index ON manga_category(category_id); + +insert: +INSERT INTO manga_category(manga_id, category_id) +VALUES (:mangaId, :categoryId); + +deleteMangaCategoryByMangaId: +DELETE FROM manga_category +WHERE manga_id = :mangaId; diff --git a/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq b/data/src/main/sqldelight/tachiyomi/data/manga_track.sq similarity index 64% rename from data/src/main/sqldelight/tachiyomi/data/manga_sync.sq rename to data/src/main/sqldelight/tachiyomi/data/manga_track.sq index f76b6bb7eb0..687142c96da 100644 --- a/data/src/main/sqldelight/tachiyomi/data/manga_sync.sq +++ b/data/src/main/sqldelight/tachiyomi/data/manga_track.sq @@ -1,54 +1,52 @@ import kotlin.Boolean; -CREATE TABLE manga_sync( - _id INTEGER NOT NULL PRIMARY KEY, +CREATE TABLE manga_track( + id INTEGER NOT NULL PRIMARY KEY, manga_id INTEGER NOT NULL, - sync_id INTEGER NOT NULL, + tracker_id INTEGER NOT NULL, remote_id INTEGER NOT NULL, + remote_url TEXT NOT NULL, library_id INTEGER, title TEXT NOT NULL, last_chapter_read REAL NOT NULL, total_chapters INTEGER NOT NULL, status INTEGER NOT NULL, score REAL NOT NULL, - remote_url TEXT NOT NULL, start_date INTEGER NOT NULL, finish_date INTEGER NOT NULL, - private INTEGER AS Boolean DEFAULT 0 NOT NULL, - UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE, - FOREIGN KEY(manga_id) REFERENCES mangas (_id) + private INTEGER AS Boolean NOT NULL DEFAULT 0, + UNIQUE (manga_id, tracker_id) ON CONFLICT REPLACE, + FOREIGN KEY(manga_id) REFERENCES manga (id) ON DELETE CASCADE ); -CREATE INDEX idx_manga_sync_manga_id ON manga_sync(manga_id); - delete: -DELETE FROM manga_sync -WHERE manga_id = :mangaId AND sync_id = :syncId; +DELETE FROM manga_track +WHERE manga_id = :mangaId AND tracker_id = :syncId; getTracks: SELECT * -FROM manga_sync; +FROM manga_track; getTrackById: SELECT * -FROM manga_sync -WHERE _id = :id; +FROM manga_track +WHERE id = :id; getTracksByMangaId: SELECT * -FROM manga_sync +FROM manga_track WHERE manga_id = :mangaId; insert: -INSERT INTO manga_sync(manga_id,sync_id,remote_id,library_id,title,last_chapter_read,total_chapters,status,score,remote_url,start_date,finish_date,private) +INSERT INTO manga_track(manga_id,tracker_id,remote_id,library_id,title,last_chapter_read,total_chapters,status,score,remote_url,start_date,finish_date,private) VALUES (:mangaId,:syncId,:remoteId,:libraryId,:title,:lastChapterRead,:totalChapters,:status,:score,:remoteUrl,:startDate,:finishDate,:private); update: -UPDATE manga_sync +UPDATE manga_track SET manga_id = coalesce(:mangaId, manga_id), - sync_id = coalesce(:syncId, sync_id), + tracker_id = coalesce(:syncId, tracker_id), remote_id = coalesce(:mediaId, remote_id), library_id = coalesce(:libraryId, library_id), title = coalesce(:title, title), @@ -60,4 +58,4 @@ SET start_date = coalesce(:startDate, start_date), finish_date = coalesce(:finishDate, finish_date), private = coalesce(:private, private) -WHERE _id = :id; +WHERE id = :id; diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas.sq b/data/src/main/sqldelight/tachiyomi/data/mangas.sq deleted file mode 100644 index b5d1909f759..00000000000 --- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq +++ /dev/null @@ -1,293 +0,0 @@ -import eu.kanade.tachiyomi.source.model.UpdateStrategy; -import kotlin.Boolean; -import kotlin.String; -import kotlin.collections.List; -import kotlinx.serialization.json.JsonObject; - -CREATE TABLE mangas( - _id INTEGER NOT NULL PRIMARY KEY, - source INTEGER NOT NULL, - url TEXT NOT NULL, - artist TEXT, - author TEXT, - description TEXT, - genre TEXT AS List, - title TEXT NOT NULL, - status INTEGER NOT NULL, - thumbnail_url TEXT, - favorite INTEGER AS Boolean NOT NULL, - last_update INTEGER, - next_update INTEGER, - initialized INTEGER AS Boolean NOT NULL, - viewer INTEGER NOT NULL, - chapter_flags INTEGER NOT NULL, - cover_last_modified INTEGER NOT NULL, - date_added INTEGER NOT NULL, - update_strategy INTEGER AS UpdateStrategy NOT NULL DEFAULT 0, - calculate_interval INTEGER DEFAULT 0 NOT NULL, - last_modified_at INTEGER NOT NULL DEFAULT 0, - favorite_modified_at INTEGER, - version INTEGER NOT NULL DEFAULT 0, - is_syncing INTEGER NOT NULL DEFAULT 0, - notes TEXT NOT NULL DEFAULT "", - memo BLOB AS JsonObject NOT NULL DEFAULT '{}' -); - -CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1; -CREATE INDEX mangas_url_index ON mangas(url); -CREATE INDEX idx_mangas_source ON mangas(source); - -CREATE TRIGGER update_last_favorited_at_mangas -AFTER UPDATE OF favorite ON mangas -BEGIN - UPDATE mangas - SET favorite_modified_at = strftime('%s', 'now') - WHERE _id = new._id; -END; - -CREATE TRIGGER update_last_modified_at_mangas -AFTER UPDATE ON mangas -FOR EACH ROW -BEGIN - UPDATE mangas - SET last_modified_at = strftime('%s', 'now') - WHERE _id = new._id; -END; - -CREATE TRIGGER update_manga_version AFTER UPDATE ON mangas -BEGIN - UPDATE mangas SET version = version + 1 - WHERE _id = new._id AND new.is_syncing = 0 AND ( - new.url != old.url OR - new.description != old.description OR - new.favorite != old.favorite - ); -END; - -getMangaById: -SELECT * -FROM mangas -WHERE _id = :id; - --- TODO: this should ideally never really have more than 1 result -getMangaByUrlAndSource: -SELECT * -FROM mangas -WHERE url = :url -AND source = :source -LIMIT 1; - -getFavorites: -SELECT * -FROM mangas -WHERE favorite = 1; - -getReadMangaNotInLibrary: -SELECT * -FROM mangas -WHERE favorite = 0 AND _id IN ( - SELECT DISTINCT chapters.manga_id - FROM chapters - WHERE read = 1 OR last_page_read != 0 -); - -getAllManga: -SELECT * -FROM mangas; - -getAllMangaSourceAndUrl: -SELECT source, url -FROM mangas; - -getMangasWithFavoriteTimestamp: -SELECT * -FROM mangas -WHERE favorite_modified_at IS NOT NULL; - -getSourceIdWithFavoriteCount: -SELECT -source, -count(*) -FROM mangas -WHERE favorite = 1 -GROUP BY source; - -getFavoriteBySourceId: -SELECT * -FROM mangas -WHERE favorite = 1 -AND source = :sourceId; - -getDuplicateLibraryManga: -WITH -track_dupes AS ( - SELECT DISTINCT S2.manga_id - FROM manga_sync S1 - INNER JOIN manga_sync S2 - ON S1.sync_id = S2.sync_id - AND S1.remote_id = S2.remote_id - AND S1.manga_id != S2.manga_id - WHERE S1.manga_id = :id -), -duplicates AS ( - SELECT M.* - FROM mangas M - LEFT JOIN track_dupes D - ON D.manga_id = _id - WHERE favorite = 1 - AND _id != :id - AND (lower(title) LIKE '%' || lower(:title) || '%' OR D.manga_id IS NOT NULL) -), -chapter_counts AS ( - SELECT - M._id AS manga_id, - count(*) AS chapter_count - FROM duplicates M - JOIN chapters C - ON M._id = C.manga_id - LEFT JOIN excluded_scanlators ES - ON C.manga_id = ES.manga_id - AND C.scanlator = ES.scanlator - WHERE ES.scanlator IS NULL - GROUP BY M._id -) -SELECT - M.*, - coalesce(CC.chapter_count, 0) AS chapter_count -FROM duplicates M -LEFT JOIN chapter_counts CC -ON M._id = CC.manga_id; - -getUpcomingManga: -SELECT * -FROM mangas -WHERE next_update >= :startOfDay -AND favorite = 1 -AND status IN :statuses -AND ( - -- includedEmpty being true expresses "don't care" state and bypasses the membership filter - :includedEmpty - OR EXISTS ( - SELECT 1 FROM mangas_categories - WHERE mangas_categories.manga_id = mangas._id - AND COALESCE(mangas_categories.category_id, 0) IN :includedCategories - ) - OR (0 IN :includedCategories AND NOT EXISTS ( - SELECT 1 FROM mangas_categories WHERE mangas_categories.manga_id = mangas._id) - ) -) -AND ( - -- excludedEmpty being true expresses "don't care" state and bypasses the membership filter - :excludedEmpty - OR ( - NOT EXISTS ( - SELECT 1 FROM mangas_categories - WHERE mangas_categories.manga_id = mangas._id - AND COALESCE(mangas_categories.category_id, 0) IN :excludedCategories - ) - AND ( - 0 NOT IN :excludedCategories - OR EXISTS (SELECT 1 FROM mangas_categories WHERE mangas_categories.manga_id = mangas._id) - ) - ) -) -ORDER BY next_update ASC; - -resetViewerFlags: -UPDATE mangas -SET viewer = 0; - -resetIsSyncing: -UPDATE mangas -SET is_syncing = 0 -WHERE is_syncing = 1; - -getSourceIdsWithNonLibraryManga: -SELECT source, COUNT(*) AS manga_count -FROM mangas -WHERE favorite = 0 -GROUP BY source; - -deleteNonLibraryManga: -DELETE FROM mangas -WHERE favorite = 0 -AND source IN :sourceIds -AND ( - :keepReadManga = 0 - OR _id NOT IN ( - SELECT DISTINCT manga_id - FROM chapters - WHERE read = 1 - OR last_page_read != 0 - ) -); - -insertReturningId: -INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at, version, notes, memo) -VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0, :version, :notes, :memo) -RETURNING _id; - -update: -UPDATE mangas SET - source = coalesce(:source, source), - url = coalesce(:url, url), - artist = coalesce(:artist, artist), - author = coalesce(:author, author), - description = coalesce(:description, description), - genre = coalesce(:genre, genre), - title = coalesce(:title, title), - status = coalesce(:status, status), - thumbnail_url = coalesce(:thumbnailUrl, thumbnail_url), - favorite = coalesce(:favorite, favorite), - last_update = coalesce(:lastUpdate, last_update), - next_update = coalesce(:nextUpdate, next_update), - initialized = coalesce(:initialized, initialized), - viewer = coalesce(:viewer, viewer), - chapter_flags = coalesce(:chapterFlags, chapter_flags), - cover_last_modified = coalesce(:coverLastModified, cover_last_modified), - date_added = coalesce(:dateAdded, date_added), - update_strategy = coalesce(:updateStrategy, update_strategy), - calculate_interval = coalesce(:calculateInterval, calculate_interval), - version = coalesce(:version, version), - is_syncing = coalesce(:isSyncing, is_syncing), - notes = coalesce(:notes, notes), - memo = coalesce(:memo, memo) -WHERE _id = :mangaId; - -insertNetworkManga { - -- Insert the manga if it doesn't exist already - INSERT INTO mangas( - source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, - last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, - update_strategy, calculate_interval, last_modified_at, version, memo - ) - SELECT - :source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, - :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, - :updateStrategy, :calculateInterval, 0, :version, :memo - WHERE NOT EXISTS(SELECT 0 FROM mangas WHERE source = :source AND url = :url); - - -- Update the relevant details if applicable and not favorite - UPDATE mangas - SET - title = CASE WHEN :updateTitle THEN :title ELSE title END, - thumbnail_url = CASE WHEN :updateCover THEN :thumbnailUrl ELSE thumbnail_url END, - author = CASE WHEN :updateDetails THEN :author ELSE author END, - artist = CASE WHEN :updateDetails THEN :artist ELSE artist END, - description = CASE WHEN :updateDetails THEN :description ELSE description END, - genre = CASE WHEN :updateDetails THEN :genre ELSE genre END, - status = CASE WHEN :updateDetails THEN :status ELSE status END, - update_strategy = CASE WHEN :updateDetails THEN :updateStrategy ELSE update_strategy END, - initialized = :updateDetails, - memo = :memo - WHERE source = :source - AND url = :url - AND favorite = 0; - - -- Finally return the manga - SELECT * - FROM mangas - WHERE source = :source - AND url = :url - LIMIT 1; -} diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq b/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq deleted file mode 100644 index 7500d797ab9..00000000000 --- a/data/src/main/sqldelight/tachiyomi/data/mangas_categories.sq +++ /dev/null @@ -1,27 +0,0 @@ -CREATE TABLE mangas_categories( - _id INTEGER NOT NULL PRIMARY KEY, - manga_id INTEGER NOT NULL, - category_id INTEGER NOT NULL, - FOREIGN KEY(category_id) REFERENCES categories (_id) - ON DELETE CASCADE, - FOREIGN KEY(manga_id) REFERENCES mangas (_id) - ON DELETE CASCADE -); - -CREATE INDEX idx_mangas_categories_manga_id ON mangas_categories(manga_id); -CREATE INDEX idx_mangas_categories_category_id ON mangas_categories(category_id); - -CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories -BEGIN - UPDATE mangas - SET version = version + 1 - WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0; -END; - -insert: -INSERT INTO mangas_categories(manga_id, category_id) -VALUES (:mangaId, :categoryId); - -deleteMangaCategoryByMangaId: -DELETE FROM mangas_categories -WHERE manga_id = :mangaId; \ No newline at end of file diff --git a/data/src/main/sqldelight/tachiyomi/data/source.sq b/data/src/main/sqldelight/tachiyomi/data/source.sq new file mode 100644 index 00000000000..e7ddd7ab6e8 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/data/source.sq @@ -0,0 +1,24 @@ +CREATE TABLE source( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + language TEXT NOT NULL +); + +findAll: +SELECT * +FROM source; + +findOne: +SELECT * +FROM source +WHERE id = :id; + +upsert: +INSERT INTO source(id, language, name) +VALUES (:id, :lang, :name) +ON CONFLICT(id) +DO UPDATE +SET + language = :lang, + name = :name +WHERE id = :id; diff --git a/data/src/main/sqldelight/tachiyomi/data/sources.sq b/data/src/main/sqldelight/tachiyomi/data/sources.sq deleted file mode 100644 index aac60bb7898..00000000000 --- a/data/src/main/sqldelight/tachiyomi/data/sources.sq +++ /dev/null @@ -1,24 +0,0 @@ -CREATE TABLE sources( - _id INTEGER NOT NULL PRIMARY KEY, - lang TEXT NOT NULL, - name TEXT NOT NULL -); - -findAll: -SELECT * -FROM sources; - -findOne: -SELECT * -FROM sources -WHERE _id = :id; - -upsert: -INSERT INTO sources(_id, lang, name) -VALUES (:id, :lang, :name) -ON CONFLICT(_id) -DO UPDATE -SET - lang = :lang, - name = :name -WHERE _id = :id; \ No newline at end of file diff --git a/data/src/main/sqldelight/tachiyomi/migrations/14.sqm b/data/src/main/sqldelight/tachiyomi/migrations/14.sqm new file mode 100644 index 00000000000..3c3d126e3e4 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/14.sqm @@ -0,0 +1,566 @@ +import eu.kanade.tachiyomi.source.model.UpdateStrategy; +import kotlin.Boolean; +import kotlin.String; +import kotlin.collections.List; +import kotlinx.serialization.json.JsonObject; + +-- --------------------------------------------------------------------------------------------------------- +-- table - manga +-- --------------------------------------------------------------------------------------------------------- + +-- Every view reads `mangas`, so they are dropped here and recreated against the new table below +DROP VIEW historyView; +DROP VIEW libraryView; +DROP VIEW updatesView; + +-- The sync scaffolding these maintain (version, is_syncing, favorite_modified_at) is gone +DROP TRIGGER update_last_favorited_at_mangas; +DROP TRIGGER update_last_modified_at_mangas; +DROP TRIGGER update_manga_version; +DROP TRIGGER update_chapter_and_manga_version; +DROP TRIGGER insert_manga_category_update_version; + +-- Entries sharing a source and url are merged into one, since the new table won't hold both. The one that +-- stays is a favorite if there is any, the oldest otherwise, and it takes over the details that it is +-- missing along with everything attached to the entries it absorbs. +CREATE TEMP TABLE duplicate_mangas AS +SELECT + _id, + first_value(_id) OVER (PARTITION BY source, url ORDER BY favorite DESC, _id ASC) AS keep_id +FROM mangas +WHERE (source, url) IN ( + SELECT source, url + FROM mangas + GROUP BY source, url + HAVING count(*) > 1 +); + +UPDATE mangas +SET favorite = merged.favorite, + initialized = merged.initialized, + artist = coalesce(mangas.artist, merged.artist), + author = coalesce(mangas.author, merged.author), + description = coalesce(mangas.description, merged.description), + genre = coalesce(mangas.genre, merged.genre), + thumbnail_url = coalesce(mangas.thumbnail_url, merged.thumbnail_url), + status = coalesce(nullif(mangas.status, 0), merged.status, 0), + viewer = coalesce(nullif(mangas.viewer, 0), merged.viewer, 0), + chapter_flags = coalesce(nullif(mangas.chapter_flags, 0), merged.chapter_flags, 0), + notes = coalesce(nullif(mangas.notes, ''), merged.notes, ''), + memo = coalesce(nullif(mangas.memo, '{}'), merged.memo, '{}'), + last_update = merged.last_update, + date_added = coalesce(merged.date_added, mangas.date_added), + favorite_modified_at = merged.favorite_modified_at, + cover_last_modified = merged.cover_last_modified +FROM ( + SELECT + D.keep_id, + max(M.favorite) AS favorite, + max(M.initialized) AS initialized, + max(M.artist) AS artist, + max(M.author) AS author, + max(M.description) AS description, + max(M.genre) AS genre, + max(M.thumbnail_url) AS thumbnail_url, + max(nullif(M.status, 0)) AS status, + max(nullif(M.viewer, 0)) AS viewer, + max(nullif(M.chapter_flags, 0)) AS chapter_flags, + max(nullif(M.notes, '')) AS notes, + max(nullif(M.memo, '{}')) AS memo, + max(M.last_update) AS last_update, + min(nullif(M.date_added, 0)) AS date_added, + min(M.favorite_modified_at) AS favorite_modified_at, + max(M.cover_last_modified) AS cover_last_modified + FROM duplicate_mangas D + JOIN mangas M + ON M._id = D._id + GROUP BY D.keep_id +) AS merged +WHERE mangas._id = merged.keep_id; + +-- Every child table is rebuilt further down and resolves the surviving entry there, so nothing +-- else has to be rewritten here first. +DELETE FROM mangas +WHERE _id IN (SELECT _id FROM duplicate_mangas WHERE _id != keep_id); + +CREATE TABLE manga( + id INTEGER NOT NULL PRIMARY KEY, + source_id INTEGER NOT NULL, + remote_url TEXT NOT NULL, + remote_title TEXT NOT NULL, + remote_author TEXT, + remote_artist TEXT, + remote_description TEXT, + remote_genre TEXT AS List, + remote_status INTEGER NOT NULL, + remote_cover TEXT, + remote_update_strategy INTEGER AS UpdateStrategy NOT NULL, + remote_memo BLOB AS JsonObject NOT NULL, + user_favorite_at INTEGER, + user_notes TEXT NOT NULL, + user_reader_flags INTEGER NOT NULL, + user_chapter_flags INTEGER NOT NULL, + state_chapter_last_update INTEGER, + state_chapter_next_update INTEGER, + state_chapter_fetch_interval INTEGER NOT NULL, + state_cover_last_modified INTEGER NOT NULL, + state_initialized INTEGER AS Boolean NOT NULL, + state_modified_at INTEGER NOT NULL, + UNIQUE(source_id, remote_url) +); + +-- `favorite`, `date_added` and `favorite_modified_at` all said the same thing: an entry is in the library +-- exactly when it has a timestamp, and that timestamp is when it was added. `date_added` is the +-- authoritative one, with the favorite timestamp covering entries added before it was written. That one +-- was written by a trigger in seconds, unlike every other timestamp here, so it is scaled up. +INSERT INTO manga( + id, source_id, remote_url, remote_title, remote_author, remote_artist, remote_description, remote_genre, + remote_status, remote_cover, remote_update_strategy, remote_memo, user_favorite_at, user_notes, + user_reader_flags, user_chapter_flags, state_chapter_last_update, state_chapter_next_update, state_chapter_fetch_interval, + state_cover_last_modified, state_initialized, state_modified_at +) +SELECT + _id, source, url, title, author, artist, description, genre, + status, thumbnail_url, update_strategy, memo, + CASE WHEN favorite = 1 THEN coalesce(nullif(date_added, 0), favorite_modified_at * 1000, 0) END, notes, + viewer, chapter_flags, last_update, next_update, calculate_interval, + cover_last_modified, initialized, last_modified_at +FROM mangas; + +DROP TABLE mangas; + +CREATE INDEX manga_user_favorite_at_index ON manga(user_favorite_at) WHERE user_favorite_at IS NOT NULL; + +CREATE TRIGGER update_modified_at_manga +AFTER UPDATE ON manga +FOR EACH ROW +BEGIN + UPDATE manga + SET state_modified_at = strftime('%s', 'now') + WHERE id = new.id; +END; + +-- --------------------------------------------------------------------------------------------------------- +-- table - chapter +-- --------------------------------------------------------------------------------------------------------- + +-- The entries that merged in the previous group brought their chapters with them +UPDATE chapters +SET manga_id = (SELECT keep_id FROM duplicate_mangas WHERE _id = chapters.manga_id) +WHERE manga_id IN (SELECT _id FROM duplicate_mangas WHERE _id != keep_id); + +-- Moving the chapters over brings rows for the same chapter together, and duplicates like those are +-- what issue #647 is about in the first place. The one with reading history is kept, the oldest +-- otherwise, and the read state and history of the others are merged into it. +CREATE TEMP TABLE duplicate_chapters AS +SELECT + _id, + first_value(_id) OVER (PARTITION BY manga_id, url ORDER BY has_history DESC, _id ASC) AS keep_id +FROM ( + SELECT + _id, + manga_id, + url, + EXISTS(SELECT 1 FROM history WHERE history.chapter_id = chapters._id) AS has_history + FROM chapters + WHERE (manga_id, url) IN ( + SELECT manga_id, url + FROM chapters + GROUP BY manga_id, url + HAVING count(*) > 1 + ) +); + +UPDATE chapters +SET read = merged.read, + bookmark = merged.bookmark, + last_page_read = merged.last_page_read, + date_fetch = merged.date_fetch, + date_upload = merged.date_upload +FROM ( + SELECT + D.keep_id, + max(C.read) AS read, + max(C.bookmark) AS bookmark, + max(C.last_page_read) AS last_page_read, + -- 0 means the fetch date was never recorded rather than "the beginning of time", so it only + -- wins when no copy recorded one. Updates only lists chapters fetched after the entry joined + -- the library, so letting a 0 through would hide the merged chapter from it. + coalesce(min(nullif(C.date_fetch, 0)), 0) AS date_fetch, + max(C.date_upload) AS date_upload + FROM duplicate_chapters D + JOIN chapters C + ON C._id = D._id + GROUP BY D.keep_id +) AS merged +WHERE chapters._id = merged.keep_id; + +-- The kept chapter has a history entry whenever any of its duplicates has one +UPDATE history +SET last_read = merged.last_read, + time_read = merged.time_read +FROM ( + SELECT + D.keep_id, + max(H.last_read) AS last_read, + -- Each duplicate accumulated its own reading time, so the durations add up; only the + -- last-read timestamp is a point in time and takes the latest. + sum(H.time_read) AS time_read + FROM duplicate_chapters D + JOIN history H + ON H.chapter_id = D._id + GROUP BY D.keep_id +) AS merged +WHERE history.chapter_id = merged.keep_id; + +-- Foreign keys are off during migrations, so the history of a discarded chapter goes explicitly +DELETE FROM history +WHERE chapter_id IN (SELECT _id FROM duplicate_chapters WHERE _id != keep_id); + +DELETE FROM chapters +WHERE _id IN (SELECT _id FROM duplicate_chapters WHERE _id != keep_id); + +DROP TABLE duplicate_chapters; + +CREATE TABLE chapter( + id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + remote_url TEXT NOT NULL, + remote_name TEXT NOT NULL, + remote_scanlator TEXT, + remote_chapter_number REAL NOT NULL, + remote_date_upload INTEGER NOT NULL, + remote_order INTEGER NOT NULL, + remote_memo BLOB NOT NULL, + user_read INTEGER NOT NULL, + user_bookmark INTEGER NOT NULL, + user_last_page_read INTEGER NOT NULL, + state_date_fetch INTEGER NOT NULL, + state_modified_at INTEGER NOT NULL, + UNIQUE(manga_id, remote_url), + FOREIGN KEY(manga_id) REFERENCES manga (id) + ON DELETE CASCADE +); + +INSERT INTO chapter( + id, manga_id, remote_url, remote_name, remote_scanlator, remote_chapter_number, remote_date_upload, + remote_order, remote_memo, user_read, user_bookmark, user_last_page_read, state_date_fetch, state_modified_at +) +SELECT + _id, manga_id, url, name, scanlator, chapter_number, date_upload, + source_order, memo, read, bookmark, last_page_read, date_fetch, last_modified_at +FROM chapters; + +DROP TABLE chapters; + +CREATE TRIGGER update_modified_at_chapter +AFTER UPDATE ON chapter +FOR EACH ROW +BEGIN + UPDATE chapter + SET state_modified_at = strftime('%s', 'now') + WHERE id = new.id; +END; + +-- --------------------------------------------------------------------------------------------------------- +-- table - category +-- --------------------------------------------------------------------------------------------------------- + +CREATE TABLE category( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + `order` INTEGER NOT NULL UNIQUE, + flags INTEGER NOT NULL +); + +-- `sort` was never unique, and reordering wrote colliding values often enough that it cannot simply be +-- copied under the new constraint. The rows keep the order they were displayed in and are renumbered from +-- 0, with ties broken by id. The system category is pinned to -1 rather than renumbered, because it is +-- identified by id 0 everywhere else and sorts ahead of the categories the user made. +INSERT INTO category(id, name, `order`, flags) +SELECT + _id, + name, + CASE + WHEN _id <= 0 THEN -1 + ELSE row_number() OVER (PARTITION BY (_id <= 0) ORDER BY sort, _id) - 1 + END, + flags +FROM categories; + +-- Dropping the table takes `system_category_delete_trigger` with it +DROP TABLE categories; + +CREATE TRIGGER IF NOT EXISTS system_category_delete_trigger BEFORE DELETE +ON category +BEGIN SELECT CASE + WHEN old.id <= 0 THEN + RAISE(ABORT, "System category can't be deleted") + END; +END; + +-- --------------------------------------------------------------------------------------------------------- +-- table - manga_category +-- --------------------------------------------------------------------------------------------------------- + +-- The surrogate key is gone: a manga belongs to a category once, and that pair is the identity of the row +CREATE TABLE manga_category( + manga_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + UNIQUE(manga_id, category_id), + FOREIGN KEY(category_id) REFERENCES category (id) + ON DELETE CASCADE, + FOREIGN KEY(manga_id) REFERENCES manga (id) + ON DELETE CASCADE +); + +-- Entries merged in the manga group brought their category rows with them, so the same pair can appear +-- twice by now; membership carries nothing else, so the copies simply collapse. +INSERT INTO manga_category(manga_id, category_id) +SELECT DISTINCT coalesce(D.keep_id, MC.manga_id), MC.category_id +FROM mangas_categories MC +LEFT JOIN duplicate_mangas D +ON D._id = MC.manga_id; + +DROP TABLE mangas_categories; + +CREATE INDEX manga_category_category_id_index ON manga_category(category_id); + +-- --------------------------------------------------------------------------------------------------------- +-- table - history +-- --------------------------------------------------------------------------------------------------------- + +-- `last_read` is when the chapter was last read and `time_read` is how long it has been read for, so +-- both are renamed to say so. The chapter it belongs to is already unique, which leaves +-- `history_history_chapter_id_index` a prefix of that constraint. +-- The table keeps its name, so the original is renamed aside and the new one built under +-- the real name; renaming a table into place makes SQLite rewrite its stored definition, +-- leaving it spelled differently from the same table on a fresh install. The views were +-- dropped at the top of this migration, so nothing refers to the name being moved. +ALTER TABLE history RENAME TO history_temp; + +CREATE TABLE history( + id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + chapter_id INTEGER NOT NULL UNIQUE, + read_at INTEGER, + read_duration INTEGER NOT NULL, + FOREIGN KEY(manga_id) REFERENCES manga (id) + ON DELETE CASCADE, + FOREIGN KEY(chapter_id) REFERENCES chapter (id) + ON DELETE CASCADE +); + +-- Reaching the entry a row belongs to meant going through its chapter; it names the entry +-- itself now, taken from that same chapter. A row whose chapter is already gone cannot name +-- one and is dropped, which is what the foreign key would do to it once it is enforced again. +INSERT INTO history(id, manga_id, chapter_id, read_at, read_duration) +SELECT H._id, C.manga_id, H.chapter_id, H.last_read, H.time_read +FROM history_temp H +JOIN chapter C +ON C.id = H.chapter_id; + +DROP TABLE history_temp; + +CREATE INDEX history_manga_id_index ON history(manga_id); +CREATE INDEX history_read_at_index ON history(read_at); + +-- --------------------------------------------------------------------------------------------------------- +-- table - excluded_scanlator +-- --------------------------------------------------------------------------------------------------------- + +CREATE TABLE excluded_scanlator( + manga_id INTEGER NOT NULL, + scanlator TEXT NOT NULL, + UNIQUE(manga_id, scanlator), + FOREIGN KEY(manga_id) REFERENCES manga (id) + ON DELETE CASCADE +); + +-- Excluding a scanlator twice excludes it once, and merged entries brought their exclusions along, so +-- the copies collapse. `excluded_scanlators_manga_id_index` is a prefix of the new constraint. +INSERT INTO excluded_scanlator(manga_id, scanlator) +SELECT DISTINCT coalesce(D.keep_id, ES.manga_id), ES.scanlator +FROM excluded_scanlators ES +LEFT JOIN duplicate_mangas D +ON D._id = ES.manga_id; + +DROP TABLE excluded_scanlators; + + +-- --------------------------------------------------------------------------------------------------------- +-- table - manga_track +-- --------------------------------------------------------------------------------------------------------- + +-- `manga_sync` is what tracking was called when it was still called syncing; `sync_id` is the tracker. +-- ON CONFLICT REPLACE stays: InsertTrack relies on it to upsert a tracker's entry for a manga. That +-- also means the copy below would silently drop a row when an entry that merged left two rows for the +-- same tracker pointing at the survivor, so the one to keep is chosen first: the one furthest read, +-- then the one already on the survivor, then the oldest. A track is taken whole rather than merged +-- field by field, because two rows on the same tracker can point at different remote entries. +CREATE TEMP TABLE duplicate_tracks AS +SELECT + T._id, + first_value(T._id) OVER ( + PARTITION BY D.keep_id, T.sync_id + ORDER BY T.last_chapter_read DESC, (T.manga_id = D.keep_id) DESC, T._id ASC + ) AS keep_track_id +FROM manga_sync T +JOIN duplicate_mangas D +ON D._id = T.manga_id; + +DELETE FROM manga_sync +WHERE _id IN (SELECT _id FROM duplicate_tracks WHERE _id != keep_track_id); + +DROP TABLE duplicate_tracks; + +CREATE TABLE manga_track( + id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + tracker_id INTEGER NOT NULL, + remote_id INTEGER NOT NULL, + remote_url TEXT NOT NULL, + library_id INTEGER, + title TEXT NOT NULL, + last_chapter_read REAL NOT NULL, + total_chapters INTEGER NOT NULL, + status INTEGER NOT NULL, + score REAL NOT NULL, + start_date INTEGER NOT NULL, + finish_date INTEGER NOT NULL, + private INTEGER NOT NULL DEFAULT 0, + UNIQUE (manga_id, tracker_id) ON CONFLICT REPLACE, + FOREIGN KEY(manga_id) REFERENCES manga (id) + ON DELETE CASCADE +); + +INSERT INTO manga_track( + id, manga_id, tracker_id, remote_id, remote_url, library_id, title, last_chapter_read, + total_chapters, status, score, start_date, finish_date, private +) +SELECT + T._id, coalesce(D.keep_id, T.manga_id), T.sync_id, T.remote_id, T.remote_url, T.library_id, + T.title, T.last_chapter_read, T.total_chapters, T.status, T.score, T.start_date, + T.finish_date, T.private +FROM manga_sync T +LEFT JOIN duplicate_mangas D +ON D._id = T.manga_id; + +DROP TABLE duplicate_mangas; + +-- `idx_manga_sync_manga_id` is a prefix of the unique constraint +DROP TABLE manga_sync; + +-- --------------------------------------------------------------------------------------------------------- +-- table - source +-- --------------------------------------------------------------------------------------------------------- + +-- The last of the plural table names, and `lang` spelled out. Nothing references this table, so it is a +-- straight copy; name comes before language to read the way the row does. +CREATE TABLE source( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + language TEXT NOT NULL +); + +INSERT INTO source(id, name, language) +SELECT _id, name, lang +FROM sources; + +DROP TABLE sources; + +-- The views come back reading the new table +CREATE VIEW historyView AS +SELECT + history.id AS id, + manga.id AS mangaId, + chapter.id AS chapterId, + manga.remote_title AS title, + manga.remote_cover AS thumbnailUrl, + manga.source_id AS sourceId, + manga.user_favorite_at AS favoriteAt, + manga.state_cover_last_modified AS coverLastModified, + chapter.remote_chapter_number AS chapterNumber, + history.read_at AS readAt, + history.read_duration AS readDuration, + max_read.read_at AS maxReadAt, + max_read.chapter_id AS maxReadAtChapterId +FROM manga +JOIN chapter +ON manga.id = chapter.manga_id +JOIN history +ON chapter.id = history.chapter_id +JOIN ( + SELECT chapter.manga_id,chapter.id AS chapter_id, MAX(history.read_at) AS read_at + FROM chapter JOIN history + ON chapter.id = history.chapter_id + GROUP BY chapter.manga_id +) AS max_read +ON chapter.manga_id = max_read.manga_id; + +CREATE VIEW libraryView AS +SELECT + M.*, + coalesce(C.total, 0) AS totalCount, + coalesce(C.readCount, 0) AS readCount, + coalesce(C.latestUpload, 0) AS latestUpload, + coalesce(C.fetchedAt, 0) AS chapterFetchedAt, + coalesce(C.lastRead, 0) AS lastRead, + coalesce(C.bookmarkCount, 0) AS bookmarkCount, + coalesce(MC.categories, '0') AS categories +FROM manga M +LEFT JOIN ( + SELECT + chapter.manga_id, + count(*) AS total, + sum(user_read) AS readCount, + coalesce(max(chapter.remote_date_upload), 0) AS latestUpload, + coalesce(max(history.read_at), 0) AS lastRead, + coalesce(max(chapter.state_date_fetch), 0) AS fetchedAt, + sum(chapter.user_bookmark) AS bookmarkCount + FROM chapter + LEFT JOIN excluded_scanlator + ON chapter.manga_id = excluded_scanlator.manga_id + AND chapter.remote_scanlator = excluded_scanlator.scanlator + LEFT JOIN history + ON chapter.id = history.chapter_id + WHERE excluded_scanlator.scanlator IS NULL + AND chapter.manga_id IN (SELECT id FROM manga WHERE user_favorite_at IS NOT NULL) + GROUP BY chapter.manga_id +) AS C +ON M.id = C.manga_id +LEFT JOIN ( + SELECT manga_id, group_concat(category_id) AS categories + FROM manga_category + WHERE manga_id IN (SELECT id FROM manga WHERE user_favorite_at IS NOT NULL) + GROUP BY manga_id +) AS MC +ON MC.manga_id = M.id +WHERE M.user_favorite_at IS NOT NULL; + +CREATE VIEW updatesView AS +SELECT + manga.id AS mangaId, + manga.remote_title AS mangaTitle, + chapter.id AS chapterId, + chapter.remote_name AS chapterName, + chapter.remote_scanlator AS scanlator, + chapter.remote_url AS chapterUrl, + chapter.user_read AS read, + chapter.user_bookmark AS bookmark, + chapter.user_last_page_read AS lastPageRead, + manga.source_id AS sourceId, + manga.user_favorite_at AS favoriteAt, + manga.remote_cover AS thumbnailUrl, + manga.state_cover_last_modified AS coverLastModified, + chapter.remote_date_upload AS dateUpload, + chapter.state_date_fetch AS dateFetch, + excluded_scanlator.scanlator AS excludedScanlator +FROM manga JOIN chapter +ON manga.id = chapter.manga_id +LEFT JOIN excluded_scanlator +ON manga.id = excluded_scanlator.manga_id +AND chapter.remote_scanlator = excluded_scanlator.scanlator +WHERE user_favorite_at IS NOT NULL +AND state_date_fetch > user_favorite_at +ORDER BY state_date_fetch DESC; diff --git a/data/src/main/sqldelight/tachiyomi/view/historyView.sq b/data/src/main/sqldelight/tachiyomi/view/historyView.sq index bdaeb52594e..5de9f624c0b 100644 --- a/data/src/main/sqldelight/tachiyomi/view/historyView.sq +++ b/data/src/main/sqldelight/tachiyomi/view/historyView.sq @@ -1,30 +1,30 @@ CREATE VIEW historyView AS SELECT - history._id AS id, - mangas._id AS mangaId, - chapters._id AS chapterId, - mangas.title, - mangas.thumbnail_url AS thumbnailUrl, - mangas.source, - mangas.favorite, - mangas.cover_last_modified, - chapters.chapter_number AS chapterNumber, - history.last_read AS readAt, - history.time_read AS readDuration, - max_last_read.last_read AS maxReadAt, - max_last_read.chapter_id AS maxReadAtChapterId -FROM mangas -JOIN chapters -ON mangas._id = chapters.manga_id + history.id AS id, + manga.id AS mangaId, + chapter.id AS chapterId, + manga.remote_title AS title, + manga.remote_cover AS thumbnailUrl, + manga.source_id AS sourceId, + manga.user_favorite_at AS favoriteAt, + manga.state_cover_last_modified AS coverLastModified, + chapter.remote_chapter_number AS chapterNumber, + history.read_at AS readAt, + history.read_duration AS readDuration, + max_read.read_at AS maxReadAt, + max_read.chapter_id AS maxReadAtChapterId +FROM manga +JOIN chapter +ON manga.id = chapter.manga_id JOIN history -ON chapters._id = history.chapter_id +ON chapter.id = history.chapter_id JOIN ( - SELECT chapters.manga_id,chapters._id AS chapter_id, MAX(history.last_read) AS last_read - FROM chapters JOIN history - ON chapters._id = history.chapter_id - GROUP BY chapters.manga_id -) AS max_last_read -ON chapters.manga_id = max_last_read.manga_id; + SELECT chapter.manga_id,chapter.id AS chapter_id, MAX(history.read_at) AS read_at + FROM chapter JOIN history + ON chapter.id = history.chapter_id + GROUP BY chapter.manga_id +) AS max_read +ON chapter.manga_id = max_read.manga_id; history: SELECT @@ -33,9 +33,9 @@ mangaId, chapterId, title, thumbnailUrl, -source, -favorite, -cover_last_modified, +sourceId, +favoriteAt, +coverLastModified, chapterNumber, readAt, readDuration @@ -52,9 +52,9 @@ mangaId, chapterId, title, thumbnailUrl, -source, -favorite, -cover_last_modified, +sourceId, +favoriteAt, +coverLastModified, chapterNumber, readAt, readDuration diff --git a/data/src/main/sqldelight/tachiyomi/view/libraryView.sq b/data/src/main/sqldelight/tachiyomi/view/libraryView.sq index 8ce19ae1de7..bf22a3bd847 100644 --- a/data/src/main/sqldelight/tachiyomi/view/libraryView.sq +++ b/data/src/main/sqldelight/tachiyomi/view/libraryView.sq @@ -8,33 +8,35 @@ SELECT coalesce(C.lastRead, 0) AS lastRead, coalesce(C.bookmarkCount, 0) AS bookmarkCount, coalesce(MC.categories, '0') AS categories -FROM mangas M +FROM manga M LEFT JOIN ( SELECT - chapters.manga_id, + chapter.manga_id, count(*) AS total, - sum(read) AS readCount, - coalesce(max(chapters.date_upload), 0) AS latestUpload, - coalesce(max(history.last_read), 0) AS lastRead, - coalesce(max(chapters.date_fetch), 0) AS fetchedAt, - sum(chapters.bookmark) AS bookmarkCount - FROM chapters - LEFT JOIN excluded_scanlators - ON chapters.manga_id = excluded_scanlators.manga_id - AND chapters.scanlator = excluded_scanlators.scanlator + sum(user_read) AS readCount, + coalesce(max(chapter.remote_date_upload), 0) AS latestUpload, + coalesce(max(history.read_at), 0) AS lastRead, + coalesce(max(chapter.state_date_fetch), 0) AS fetchedAt, + sum(chapter.user_bookmark) AS bookmarkCount + FROM chapter + LEFT JOIN excluded_scanlator + ON chapter.manga_id = excluded_scanlator.manga_id + AND chapter.remote_scanlator = excluded_scanlator.scanlator LEFT JOIN history - ON chapters._id = history.chapter_id - WHERE excluded_scanlators.scanlator IS NULL - GROUP BY chapters.manga_id + ON chapter.id = history.chapter_id + WHERE excluded_scanlator.scanlator IS NULL + AND chapter.manga_id IN (SELECT id FROM manga WHERE user_favorite_at IS NOT NULL) + GROUP BY chapter.manga_id ) AS C -ON M._id = C.manga_id +ON M.id = C.manga_id LEFT JOIN ( SELECT manga_id, group_concat(category_id) AS categories - FROM mangas_categories + FROM manga_category + WHERE manga_id IN (SELECT id FROM manga WHERE user_favorite_at IS NOT NULL) GROUP BY manga_id ) AS MC -ON MC.manga_id = M._id -WHERE M.favorite = 1; +ON MC.manga_id = M.id +WHERE M.user_favorite_at IS NOT NULL; library: SELECT * diff --git a/data/src/main/sqldelight/tachiyomi/view/updatesView.sq b/data/src/main/sqldelight/tachiyomi/view/updatesView.sq index 5995b1bedb1..a20bad97d31 100644 --- a/data/src/main/sqldelight/tachiyomi/view/updatesView.sq +++ b/data/src/main/sqldelight/tachiyomi/view/updatesView.sq @@ -1,29 +1,29 @@ CREATE VIEW updatesView AS SELECT - mangas._id AS mangaId, - mangas.title AS mangaTitle, - chapters._id AS chapterId, - chapters.name AS chapterName, - chapters.scanlator, - chapters.url AS chapterUrl, - chapters.read, - chapters.bookmark, - chapters.last_page_read, - mangas.source, - mangas.favorite, - mangas.thumbnail_url AS thumbnailUrl, - mangas.cover_last_modified AS coverLastModified, - chapters.date_upload AS dateUpload, - chapters.date_fetch AS datefetch, - excluded_scanlators.scanlator AS excludedScanlator -FROM mangas JOIN chapters -ON mangas._id = chapters.manga_id -LEFT JOIN excluded_scanlators -ON mangas._id = excluded_scanlators.manga_id -AND chapters.scanlator = excluded_scanlators.scanlator -WHERE favorite = 1 -AND date_fetch > date_added -ORDER BY date_fetch DESC; + manga.id AS mangaId, + manga.remote_title AS mangaTitle, + chapter.id AS chapterId, + chapter.remote_name AS chapterName, + chapter.remote_scanlator AS scanlator, + chapter.remote_url AS chapterUrl, + chapter.user_read AS read, + chapter.user_bookmark AS bookmark, + chapter.user_last_page_read AS lastPageRead, + manga.source_id AS sourceId, + manga.user_favorite_at AS favoriteAt, + manga.remote_cover AS thumbnailUrl, + manga.state_cover_last_modified AS coverLastModified, + chapter.remote_date_upload AS dateUpload, + chapter.state_date_fetch AS dateFetch, + excluded_scanlator.scanlator AS excludedScanlator +FROM manga JOIN chapter +ON manga.id = chapter.manga_id +LEFT JOIN excluded_scanlator +ON manga.id = excluded_scanlator.manga_id +AND chapter.remote_scanlator = excluded_scanlator.scanlator +WHERE user_favorite_at IS NOT NULL +AND state_date_fetch > user_favorite_at +ORDER BY state_date_fetch DESC; getRecentUpdates: SELECT * @@ -39,8 +39,8 @@ AND (:read IS NULL OR read = :read) -- Started means some progress but not finished, Read means finished chapter, thus: AND ( :started IS NULL - OR (:started = 1 AND last_page_read > 0 AND read = 0) - OR (:started = 0 AND last_page_read = 0 AND read = 0) + OR (:started = 1 AND lastPageRead > 0 AND read = 0) + OR (:started = 0 AND lastPageRead = 0 AND read = 0) ) AND (:bookmarked IS NULL OR bookmark = :bookmarked) AND ( @@ -50,12 +50,12 @@ AND ( -- includedEmpty being true expresses "don't care" state and bypasses the membership filter :includedEmpty OR EXISTS ( - SELECT 1 FROM mangas_categories - WHERE mangas_categories.manga_id = mangaId - AND COALESCE(mangas_categories.category_id, 0) IN :includedCategories + SELECT 1 FROM manga_category + WHERE manga_category.manga_id = mangaId + AND COALESCE(manga_category.category_id, 0) IN :includedCategories ) OR (0 IN :includedCategories AND NOT EXISTS ( - SELECT 1 FROM mangas_categories mc WHERE mc.manga_id = mangaId) + SELECT 1 FROM manga_category mc WHERE mc.manga_id = mangaId) ) ) AND ( @@ -63,13 +63,13 @@ AND ( :excludedEmpty OR ( NOT EXISTS ( - SELECT 1 FROM mangas_categories - WHERE mangas_categories.manga_id = mangaId - AND COALESCE(mangas_categories.category_id, 0) IN :excludedCategories + SELECT 1 FROM manga_category + WHERE manga_category.manga_id = mangaId + AND COALESCE(manga_category.category_id, 0) IN :excludedCategories ) AND ( 0 NOT IN :excludedCategories - OR EXISTS (SELECT 1 FROM mangas_categories WHERE mangas_categories.manga_id = mangaId) + OR EXISTS (SELECT 1 FROM manga_category WHERE manga_category.manga_id = mangaId) ) ) )