Browse Source

Update manga metadata on library update with `sqldelight` (#7293)

Co-Authored-By: Ivan Iskandar <[email protected]>

Co-authored-by: Ivan Iskandar <[email protected]>
AntsyLich 2 years ago
parent
commit
5bb78eb77f

+ 32 - 3
app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt

@@ -1,7 +1,10 @@
 package eu.kanade.data.manga
 
 import eu.kanade.data.DatabaseHandler
+import eu.kanade.data.listOfStringsAdapter
+import eu.kanade.data.toLong
 import eu.kanade.domain.manga.model.Manga
+import eu.kanade.domain.manga.model.MangaUpdate
 import eu.kanade.domain.manga.repository.MangaRepository
 import eu.kanade.tachiyomi.util.system.logcat
 import kotlinx.coroutines.flow.Flow
@@ -11,6 +14,10 @@ class MangaRepositoryImpl(
     private val handler: DatabaseHandler,
 ) : MangaRepository {
 
+    override suspend fun getMangaById(id: Long): Manga {
+        return handler.awaitOne { mangasQueries.getMangaById(id, mangaMapper) }
+    }
+
     override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> {
         return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, mangaMapper) }
     }
@@ -25,11 +32,33 @@ class MangaRepositoryImpl(
         }
     }
 
-    override suspend fun updateLastUpdate(mangaId: Long, lastUpdate: Long) {
-        try {
-            handler.await { mangasQueries.updateLastUpdate(lastUpdate, mangaId) }
+    override suspend fun update(update: MangaUpdate): Boolean {
+        return try {
+            handler.await {
+                mangasQueries.update(
+                    source = update.source,
+                    url = update.url,
+                    artist = update.artist,
+                    author = update.author,
+                    description = update.description,
+                    genre = update.genre?.let(listOfStringsAdapter::encode),
+                    title = update.title,
+                    status = update.status,
+                    thumbnailUrl = update.thumbnailUrl,
+                    favorite = update.favorite?.toLong(),
+                    lastUpdate = update.lastUpdate,
+                    initialized = update.initialized?.toLong(),
+                    viewer = update.viewerFlags,
+                    chapterFlags = update.chapterFlags,
+                    coverLastModified = update.coverLastModified,
+                    dateAdded = update.dateAdded,
+                    mangaId = update.id,
+                )
+            }
+            true
         } catch (e: Exception) {
             logcat(LogPriority.ERROR, e)
+            false
         }
     }
 }

+ 4 - 2
app/src/main/java/eu/kanade/domain/DomainModule.kt

@@ -20,8 +20,9 @@ import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
 import eu.kanade.domain.history.interactor.UpsertHistory
 import eu.kanade.domain.history.repository.HistoryRepository
 import eu.kanade.domain.manga.interactor.GetFavoritesBySourceId
+import eu.kanade.domain.manga.interactor.GetMangaById
 import eu.kanade.domain.manga.interactor.ResetViewerFlags
-import eu.kanade.domain.manga.interactor.UpdateMangaLastUpdate
+import eu.kanade.domain.manga.interactor.UpdateManga
 import eu.kanade.domain.manga.repository.MangaRepository
 import eu.kanade.domain.source.interactor.GetEnabledSources
 import eu.kanade.domain.source.interactor.GetLanguagesWithSources
@@ -43,9 +44,10 @@ class DomainModule : InjektModule {
     override fun InjektRegistrar.registerInjectables() {
         addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
         addFactory { GetFavoritesBySourceId(get()) }
+        addFactory { GetMangaById(get()) }
         addFactory { GetNextChapter(get()) }
         addFactory { ResetViewerFlags(get()) }
-        addFactory { UpdateMangaLastUpdate(get()) }
+        addFactory { UpdateManga(get()) }
 
         addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
         addFactory { UpdateChapter(get()) }

+ 3 - 3
app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithSource.kt

@@ -5,7 +5,7 @@ import eu.kanade.domain.chapter.model.Chapter
 import eu.kanade.domain.chapter.model.toChapterUpdate
 import eu.kanade.domain.chapter.model.toDbChapter
 import eu.kanade.domain.chapter.repository.ChapterRepository
-import eu.kanade.domain.manga.interactor.UpdateMangaLastUpdate
+import eu.kanade.domain.manga.interactor.UpdateManga
 import eu.kanade.domain.manga.model.Manga
 import eu.kanade.domain.manga.model.toDbManga
 import eu.kanade.tachiyomi.data.download.DownloadManager
@@ -24,7 +24,7 @@ class SyncChaptersWithSource(
     private val downloadManager: DownloadManager = Injekt.get(),
     private val chapterRepository: ChapterRepository = Injekt.get(),
     private val shouldUpdateDbChapter: ShouldUpdateDbChapter = Injekt.get(),
-    private val updateMangaLastUpdate: UpdateMangaLastUpdate = Injekt.get(),
+    private val updateManga: UpdateManga = Injekt.get(),
 ) {
 
     suspend fun await(
@@ -171,7 +171,7 @@ class SyncChaptersWithSource(
 
         // Set this manga as updated since chapters were changed
         // Note that last_update actually represents last time the chapter list changed at all
-        updateMangaLastUpdate.await(manga.id, Date().time)
+        updateManga.awaitUpdateLastUpdate(manga.id)
 
         @Suppress("ConvertArgumentToSet") // See tachiyomiorg/tachiyomi#6372.
         return Pair(updatedToAdd.subtract(reAdded).toList(), toDelete.subtract(reAdded).toList())

+ 20 - 0
app/src/main/java/eu/kanade/domain/manga/interactor/GetMangaById.kt

@@ -0,0 +1,20 @@
+package eu.kanade.domain.manga.interactor
+
+import eu.kanade.domain.manga.model.Manga
+import eu.kanade.domain.manga.repository.MangaRepository
+import eu.kanade.tachiyomi.util.system.logcat
+import logcat.LogPriority
+
+class GetMangaById(
+    private val mangaRepository: MangaRepository,
+) {
+
+    suspend fun await(id: Long): Manga? {
+        return try {
+            mangaRepository.getMangaById(id)
+        } catch (e: Exception) {
+            logcat(LogPriority.ERROR, e)
+            null
+        }
+    }
+}

+ 61 - 0
app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt

@@ -0,0 +1,61 @@
+package eu.kanade.domain.manga.interactor
+
+import eu.kanade.domain.manga.model.Manga
+import eu.kanade.domain.manga.model.MangaUpdate
+import eu.kanade.domain.manga.model.hasCustomCover
+import eu.kanade.domain.manga.model.isLocal
+import eu.kanade.domain.manga.model.toDbManga
+import eu.kanade.domain.manga.repository.MangaRepository
+import eu.kanade.tachiyomi.data.cache.CoverCache
+import tachiyomi.source.model.MangaInfo
+import java.util.Date
+
+class UpdateManga(
+    private val mangaRepository: MangaRepository,
+) {
+
+    suspend fun awaitUpdateFromSource(
+        localManga: Manga,
+        remoteManga: MangaInfo,
+        manualFetch: Boolean,
+        coverCache: CoverCache,
+    ): Boolean {
+        // if the manga isn't a favorite, set its title from source and update in db
+        val title = if (!localManga.favorite) remoteManga.title else null
+
+        // Never refresh covers if the url is empty to avoid "losing" existing covers
+        val updateCover = remoteManga.cover.isNotEmpty() && (manualFetch || localManga.thumbnailUrl != remoteManga.cover)
+        val coverLastModified = if (updateCover) {
+            when {
+                localManga.isLocal() -> Date().time
+                localManga.hasCustomCover(coverCache) -> {
+                    coverCache.deleteFromCache(localManga.toDbManga(), false)
+                    null
+                }
+                else -> {
+                    coverCache.deleteFromCache(localManga.toDbManga(), false)
+                    Date().time
+                }
+            }
+        } else null
+
+        return mangaRepository.update(
+            MangaUpdate(
+                id = localManga.id,
+                title = title?.takeIf { it.isNotEmpty() },
+                coverLastModified = coverLastModified,
+                author = remoteManga.author,
+                artist = remoteManga.artist,
+                description = remoteManga.description,
+                genre = remoteManga.genres,
+                thumbnailUrl = remoteManga.cover.takeIf { it.isNotEmpty() },
+                status = remoteManga.status.toLong(),
+                initialized = true,
+            ),
+        )
+    }
+
+    suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
+        return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Date().time))
+    }
+}

+ 0 - 12
app/src/main/java/eu/kanade/domain/manga/interactor/UpdateMangaLastUpdate.kt

@@ -1,12 +0,0 @@
-package eu.kanade.domain.manga.interactor
-
-import eu.kanade.domain.manga.repository.MangaRepository
-
-class UpdateMangaLastUpdate(
-    private val mangaRepository: MangaRepository,
-) {
-
-    suspend fun await(mangaId: Long, lastUpdate: Long) {
-        mangaRepository.updateLastUpdate(mangaId, lastUpdate)
-    }
-}

+ 22 - 0
app/src/main/java/eu/kanade/domain/manga/model/Manga.kt

@@ -1,6 +1,11 @@
 package eu.kanade.domain.manga.model
 
+import eu.kanade.tachiyomi.data.cache.CoverCache
+import eu.kanade.tachiyomi.source.LocalSource
 import eu.kanade.tachiyomi.source.model.SManga
+import tachiyomi.source.model.MangaInfo
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
 import eu.kanade.tachiyomi.data.database.models.Manga as DbManga
 
 data class Manga(
@@ -62,3 +67,20 @@ fun Manga.toDbManga(): DbManga = DbManga.create(url, title, source).also {
     it.chapter_flags = chapterFlags.toInt()
     it.cover_last_modified = coverLastModified
 }
+
+fun Manga.toMangaInfo(): MangaInfo = MangaInfo(
+    artist = artist ?: "",
+    author = author ?: "",
+    cover = thumbnailUrl ?: "",
+    description = description ?: "",
+    genres = genre ?: emptyList(),
+    key = url,
+    status = status.toInt(),
+    title = title,
+)
+
+fun Manga.isLocal(): Boolean = source == LocalSource.ID
+
+fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
+    return coverCache.getCustomCoverFile(id).exists()
+}

+ 21 - 0
app/src/main/java/eu/kanade/domain/manga/model/MangaUpdate.kt

@@ -0,0 +1,21 @@
+package eu.kanade.domain.manga.model
+
+data class MangaUpdate(
+    val id: Long,
+    val source: Long? = null,
+    val favorite: Boolean? = null,
+    val lastUpdate: Long? = null,
+    val dateAdded: Long? = null,
+    val viewerFlags: Long? = null,
+    val chapterFlags: Long? = null,
+    val coverLastModified: Long? = null,
+    val url: String? = null,
+    val title: String? = null,
+    val artist: String? = null,
+    val author: String? = null,
+    val description: String? = null,
+    val genre: List<String>? = null,
+    val status: Long? = null,
+    val thumbnailUrl: String? = null,
+    val initialized: Boolean? = null,
+)

+ 4 - 1
app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt

@@ -1,13 +1,16 @@
 package eu.kanade.domain.manga.repository
 
 import eu.kanade.domain.manga.model.Manga
+import eu.kanade.domain.manga.model.MangaUpdate
 import kotlinx.coroutines.flow.Flow
 
 interface MangaRepository {
 
+    suspend fun getMangaById(id: Long): Manga
+
     fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>>
 
     suspend fun resetViewerFlags(): Boolean
 
-    suspend fun updateLastUpdate(mangaId: Long, lastUpdate: Long)
+    suspend fun update(update: MangaUpdate): Boolean
 }

+ 0 - 6
app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt

@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
 import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
 import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
 import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
-import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
 import eu.kanade.tachiyomi.data.database.tables.CategoryTable
 import eu.kanade.tachiyomi.data.database.tables.ChapterTable
 import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
@@ -101,11 +100,6 @@ interface MangaQueries : DbProvider {
         .withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
         .prepare()
 
-    fun updateLastUpdated(manga: Manga) = db.put()
-        .`object`(manga)
-        .withPutResolver(MangaLastUpdatedPutResolver())
-        .prepare()
-
     fun updateMangaFavorite(manga: Manga) = db.put()
         .`object`(manga)
         .withPutResolver(MangaFavoritePutResolver())

+ 0 - 32
app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaLastUpdatedPutResolver.kt

@@ -1,32 +0,0 @@
-package eu.kanade.tachiyomi.data.database.resolvers
-
-import androidx.core.content.contentValuesOf
-import com.pushtorefresh.storio.sqlite.StorIOSQLite
-import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
-import com.pushtorefresh.storio.sqlite.operations.put.PutResult
-import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
-import eu.kanade.tachiyomi.data.database.inTransactionReturn
-import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.tables.MangaTable
-
-class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
-
-    override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
-        val updateQuery = mapToUpdateQuery(manga)
-        val contentValues = mapToContentValues(manga)
-
-        val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
-        PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
-    }
-
-    fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
-        .table(MangaTable.TABLE)
-        .where("${MangaTable.COL_ID} = ?")
-        .whereArgs(manga.id)
-        .build()
-
-    fun mapToContentValues(manga: Manga) =
-        contentValuesOf(
-            MangaTable.COL_LAST_UPDATE to manga.last_update,
-        )
-}

+ 36 - 35
app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt

@@ -7,6 +7,11 @@ import android.os.IBinder
 import android.os.PowerManager
 import androidx.core.content.ContextCompat
 import eu.kanade.data.chapter.NoChaptersException
+import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
+import eu.kanade.domain.chapter.model.toDbChapter
+import eu.kanade.domain.manga.interactor.GetMangaById
+import eu.kanade.domain.manga.interactor.UpdateManga
+import eu.kanade.domain.manga.model.toMangaInfo
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.cache.CoverCache
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
@@ -14,6 +19,7 @@ import eu.kanade.tachiyomi.data.database.models.Category
 import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.database.models.LibraryManga
 import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.database.models.toDomainManga
 import eu.kanade.tachiyomi.data.database.models.toMangaInfo
 import eu.kanade.tachiyomi.data.download.DownloadManager
 import eu.kanade.tachiyomi.data.download.DownloadService
@@ -29,10 +35,8 @@ import eu.kanade.tachiyomi.data.track.TrackService
 import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.source.UnmeteredSource
 import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.model.toMangaInfo
 import eu.kanade.tachiyomi.source.model.toSChapter
 import eu.kanade.tachiyomi.source.model.toSManga
-import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
 import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
 import eu.kanade.tachiyomi.util.lang.withIOContext
 import eu.kanade.tachiyomi.util.prepUpdateCover
@@ -55,12 +59,15 @@ import kotlinx.coroutines.supervisorScope
 import kotlinx.coroutines.sync.Semaphore
 import kotlinx.coroutines.sync.withPermit
 import logcat.LogPriority
+import tachiyomi.source.model.MangaInfo
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 import java.io.File
 import java.util.concurrent.CopyOnWriteArrayList
 import java.util.concurrent.atomic.AtomicBoolean
 import java.util.concurrent.atomic.AtomicInteger
+import eu.kanade.domain.chapter.model.Chapter as DomainChapter
+import eu.kanade.domain.manga.model.Manga as DomainManga
 
 /**
  * This class will take care of updating the chapters of the manga from the library. It can be
@@ -77,6 +84,9 @@ class LibraryUpdateService(
     val downloadManager: DownloadManager = Injekt.get(),
     val trackManager: TrackManager = Injekt.get(),
     val coverCache: CoverCache = Injekt.get(),
+    private val getMangaById: GetMangaById = Injekt.get(),
+    private val updateManga: UpdateManga = Injekt.get(),
+    private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get(),
 ) : Service() {
 
     private lateinit var wakeLock: PowerManager.WakeLock
@@ -302,7 +312,7 @@ class LibraryUpdateService(
                                 }
 
                                 // Don't continue to update if manga not in library
-                                db.getManga(manga.id!!).executeAsBlocking() ?: return@forEach
+                                manga.id?.let { getMangaById.await(it) } ?: return@forEach
 
                                 withUpdateNotification(
                                     currentlyUpdatingManga,
@@ -322,19 +332,22 @@ class LibraryUpdateService(
 
                                             else -> {
                                                 // Convert to the manga that contains new chapters
-                                                val (newChapters, _) = updateManga(mangaWithNotif)
-
-                                                if (newChapters.isNotEmpty()) {
-                                                    if (mangaWithNotif.shouldDownloadNewChapters(db, preferences)) {
-                                                        downloadChapters(mangaWithNotif, newChapters)
-                                                        hasDownloads.set(true)
+                                                mangaWithNotif.toDomainManga()?.let { domainManga ->
+                                                    val (newChapters, _) = updateManga(domainManga)
+                                                    val newDbChapters = newChapters.map { it.toDbChapter() }
+
+                                                    if (newChapters.isNotEmpty()) {
+                                                        if (mangaWithNotif.shouldDownloadNewChapters(db, preferences)) {
+                                                            downloadChapters(mangaWithNotif, newDbChapters)
+                                                            hasDownloads.set(true)
+                                                        }
+
+                                                        // Convert to the manga that contains new chapters
+                                                        newUpdates.add(
+                                                            mangaWithNotif to newDbChapters.sortedByDescending { ch -> ch.source_order }
+                                                                .toTypedArray(),
+                                                        )
                                                     }
-
-                                                    // Convert to the manga that contains new chapters
-                                                    newUpdates.add(
-                                                        mangaWithNotif to newChapters.sortedByDescending { ch -> ch.source_order }
-                                                            .toTypedArray(),
-                                                    )
                                                 }
                                             }
                                         }
@@ -394,39 +407,27 @@ class LibraryUpdateService(
      * @param manga the manga to update.
      * @return a pair of the inserted and removed chapters.
      */
-    private suspend fun updateManga(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
+    private suspend fun updateManga(manga: DomainManga): Pair<List<DomainChapter>, List<DomainChapter>> {
         val source = sourceManager.getOrStub(manga.source)
 
-        var updatedManga: SManga = manga
+        val mangaInfo: MangaInfo = manga.toMangaInfo()
 
-        // Update manga details metadata
+        // Update manga metadata if needed
         if (preferences.autoUpdateMetadata()) {
-            val updatedMangaDetails = source.getMangaDetails(manga.toMangaInfo())
-            val sManga = updatedMangaDetails.toSManga()
-            // Avoid "losing" existing cover
-            if (!sManga.thumbnail_url.isNullOrEmpty()) {
-                manga.prepUpdateCover(coverCache, sManga, false)
-            } else {
-                sManga.thumbnail_url = manga.thumbnail_url
-            }
-
-            updatedManga = sManga
+            val updatedMangaInfo = source.getMangaDetails(manga.toMangaInfo())
+            updateManga.awaitUpdateFromSource(manga, updatedMangaInfo, manualFetch = false, coverCache)
         }
 
-        val chapters = source.getChapterList(updatedManga.toMangaInfo())
+        val chapters = source.getChapterList(mangaInfo)
             .map { it.toSChapter() }
 
         // Get manga from database to account for if it was removed during the update
-        val dbManga = db.getManga(manga.id!!).executeAsBlocking()
+        val dbManga = getMangaById.await(manga.id)
             ?: return Pair(emptyList(), emptyList())
 
-        // Copy into [dbManga] to retain favourite value
-        dbManga.copyFrom(updatedManga)
-        db.insertManga(dbManga).executeAsBlocking()
-
         // [dbmanga] was used so that manga data doesn't get overwritten
         // in case manga gets new chapter
-        return syncChaptersWithSource(chapters, dbManga, source)
+        return syncChaptersWithSource.await(chapters, dbManga, source)
     }
 
     private suspend fun updateCovers() {

+ 18 - 3
app/src/main/sqldelight/data/mangas.sq

@@ -58,7 +58,22 @@ deleteMangasNotInLibraryBySourceIds:
 DELETE FROM mangas
 WHERE favorite = 0 AND source IN :sourceIds;
 
-updateLastUpdate:
-UPDATE mangas
-SET last_update = :lastUpdate
+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),
+    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)
 WHERE _id = :mangaId;