瀏覽代碼

Imported implementation for updating library by next expected update from Neko (#5436)

* Imported implementation for updating library by next expected update from Neko. This sort uses the last 4 updates for a manga to compute an average time between updates and then extrapolates when the next update should occur.

Currently seems to work perfectly. However, I may have silently messed something up along the way.

All code and algorithms are credited to kyjibo on GitHub. The original commit adding this functionality is here: https://github.com/CarlosEsco/Neko/commit/681003926ae1e07b925155d4e1f43972bbe2b843

* Imported implementation for updating library by next expected update from Neko. This sort uses the last 4 updates for a manga to compute an average time between updates and then extrapolates when the next update should occur.

Currently seems to work perfectly. However, I may have silently messed something up along the way.

All code and algorithms are credited to kyjibo on GitHub. The original commit adding this functionality is here: https://github.com/CarlosEsco/Neko/commit/681003926ae1e07b925155d4e1f43972bbe2b843

* Remove commented-out line from LibraryUpdateRanker

I missed removing this when first committing. The removed line is a holdover from Neko, which requires 7+, but I removed the function that requires this.
stinky-lizard 3 年之前
父節點
當前提交
70ed49e478

+ 4 - 1
app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt

@@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
         /**
          * Version of the database.
          */
-        const val DATABASE_VERSION = 11
+        const val DATABASE_VERSION = 12
     }
 
     override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@@ -82,6 +82,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
             db.execSQL(MangaTable.addDateAdded)
             db.execSQL(MangaTable.backfillDateAdded)
         }
+        if (oldVersion < 12) {
+            db.execSQL(MangaTable.addNextUpdateCol)
+        }
     }
 
     override fun onConfigure(db: SupportSQLiteDatabase) {

+ 3 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt

@@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
 import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
 import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
 import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
+import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_NEXT_UPDATE
 import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
 import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
 import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
@@ -62,6 +63,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
             COL_THUMBNAIL_URL to obj.thumbnail_url,
             COL_FAVORITE to obj.favorite,
             COL_LAST_UPDATE to obj.last_update,
+            COL_NEXT_UPDATE to obj.next_update,
             COL_INITIALIZED to obj.initialized,
             COL_VIEWER to obj.viewer_flags,
             COL_CHAPTER_FLAGS to obj.chapter_flags,
@@ -84,6 +86,7 @@ interface BaseMangaGetResolver {
         thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
         favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
         last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
+        next_update = cursor.getLong(cursor.getColumnIndex(COL_NEXT_UPDATE))
         initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
         viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
         chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))

+ 2 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt

@@ -15,6 +15,8 @@ interface Manga : SManga {
 
     var last_update: Long
 
+    var next_update: Long
+
     var date_added: Long
 
     var viewer_flags: Int

+ 2 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt

@@ -26,6 +26,8 @@ open class MangaImpl : Manga {
 
     override var last_update: Long = 0
 
+    override var next_update: Long = 0
+
     override var date_added: Long = 0
 
     override var initialized: Boolean = false

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

@@ -6,12 +6,7 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery
 import eu.kanade.tachiyomi.data.database.DbProvider
 import eu.kanade.tachiyomi.data.database.models.LibraryManga
 import eu.kanade.tachiyomi.data.database.models.Manga
-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.resolvers.MangaTitlePutResolver
+import eu.kanade.tachiyomi.data.database.resolvers.*
 import eu.kanade.tachiyomi.data.database.tables.CategoryTable
 import eu.kanade.tachiyomi.data.database.tables.ChapterTable
 import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
@@ -97,6 +92,11 @@ interface MangaQueries : DbProvider {
         .withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
         .prepare()
 
+    fun updateNextUpdated(manga: Manga) = db.put()
+        .`object`(manga)
+        .withPutResolver(MangaNextUpdatedPutResolver())
+        .prepare()
+
     fun updateLastUpdated(manga: Manga) = db.put()
         .`object`(manga)
         .withPutResolver(MangaLastUpdatedPutResolver())

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

@@ -0,0 +1,31 @@
+package eu.kanade.tachiyomi.data.database.resolvers
+
+import android.content.ContentValues
+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 MangaNextUpdatedPutResolver : 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) = ContentValues(1).apply {
+        put(MangaTable.COL_NEXT_UPDATE, manga.next_update)
+    }
+}

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

@@ -28,6 +28,8 @@ object MangaTable {
 
     const val COL_LAST_UPDATE = "last_update"
 
+    const val COL_NEXT_UPDATE = "next_update"
+
     const val COL_DATE_ADDED = "date_added"
 
     const val COL_INITIALIZED = "initialized"
@@ -57,6 +59,7 @@ object MangaTable {
             $COL_THUMBNAIL_URL TEXT,
             $COL_FAVORITE INTEGER NOT NULL,
             $COL_LAST_UPDATE LONG,
+            $COL_NEXT_UPDATE LONG,
             $COL_INITIALIZED BOOLEAN NOT NULL,
             $COL_VIEWER INTEGER NOT NULL,
             $COL_CHAPTER_FLAGS INTEGER NOT NULL,
@@ -86,4 +89,7 @@ object MangaTable {
             "FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
             "ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
             "GROUP BY $TABLE.$COL_ID)"
+
+    val addNextUpdateCol: String
+        get() = "ALTER TABLE $TABLE ADD COLUMN $COL_NEXT_UPDATE LONG DEFAULT 0"
 }

+ 21 - 1
app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateRanker.kt

@@ -1,6 +1,9 @@
 package eu.kanade.tachiyomi.data.library
 
 import eu.kanade.tachiyomi.data.database.models.Manga
+import java.util.Collections
+import kotlin.Comparator
+import kotlin.math.abs
 
 /**
  * This class will provide various functions to rank manga to efficiently schedule manga to update.
@@ -9,9 +12,26 @@ object LibraryUpdateRanker {
 
     val rankingScheme = listOf(
         (this::lexicographicRanking)(),
-        (this::latestFirstRanking)()
+        (this::latestFirstRanking)(),
+        (this::nextFirstRanking)()
     )
 
+    /**
+     * Provides a total ordering over all the Mangas.
+     *
+     * Orders the manga based on the distance between the next expected update and now.
+     * The comparator is reversed, placing the smallest (and thus closest to updating now) first.
+     */
+    fun nextFirstRanking(): Comparator<Manga> {
+        val time = System.currentTimeMillis()
+        return Collections.reverseOrder(
+            Comparator { mangaFirst: Manga,
+                mangaSecond: Manga ->
+                compareValues(abs(mangaSecond.next_update - time), abs(mangaFirst.next_update - time))
+            }
+        )
+    }
+
     /**
      * Provides a total ordering over all the [Manga]s.
      *

+ 2 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt

@@ -251,7 +251,8 @@ class SettingsLibraryController : SettingsController() {
                 // ../../data/library/LibraryUpdateRanker.kt
                 val priorities = arrayOf(
                     Pair("0", R.string.action_sort_alpha),
-                    Pair("1", R.string.action_sort_last_checked)
+                    Pair("1", R.string.action_sort_last_checked),
+                    Pair("2", R.string.action_sort_next_updated)
                 )
                 val defaultPriority = priorities[0]
 

+ 37 - 1
app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt

@@ -96,6 +96,24 @@ fun syncChaptersWithSource(
 
     // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
     if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
+        val topChapters = dbChapters.sortedByDescending { it.date_upload }.take(4)
+        val newestDate = topChapters.getOrNull(0)?.date_upload ?: 0L
+
+        // Recalculate update rate if unset and enough chapters are present
+        if (manga.next_update == 0L && topChapters.size > 1) {
+            var delta = 0L
+            for (i in 0 until topChapters.size - 1) {
+                delta += (topChapters[i].date_upload - topChapters[i + 1].date_upload)
+            }
+            delta /= topChapters.size - 1
+            manga.next_update = newestDate + delta
+            db.updateNextUpdated(manga).executeAsBlocking()
+        }
+
+        if (newestDate != 0L && newestDate != manga.last_update) {
+            manga.last_update = newestDate
+            db.updateLastUpdated(manga).executeAsBlocking()
+        }
         return Pair(emptyList(), emptyList())
     }
 
@@ -140,11 +158,29 @@ fun syncChaptersWithSource(
             db.insertChapters(toChange).executeAsBlocking()
         }
 
+        val topChapters = db.getChapters(manga).executeAsBlocking().sortedByDescending { it.date_upload }.take(4)
+        // Recalculate next update since chapters were changed
+        if (topChapters.size > 1) {
+            var delta = 0L
+            for (i in 0 until topChapters.size - 1) {
+                delta += (topChapters[i].date_upload - topChapters[i + 1].date_upload)
+            }
+            delta /= topChapters.size - 1
+            manga.next_update = topChapters[0].date_upload + delta
+            db.updateNextUpdated(manga).executeAsBlocking()
+        }
+
         // Fix order in source.
         db.fixChaptersSourceOrder(sourceChapters).executeAsBlocking()
 
         // Set this manga as updated since chapters were changed
-        manga.last_update = Date().time
+        val newestChapter = topChapters.getOrNull(0)
+        val dateFetch = newestChapter?.date_upload ?: manga.last_update
+        if (dateFetch == 0L) {
+            if (toAdd.isNotEmpty()) {
+                manga.last_update = Date().time
+            }
+        } else manga.last_update = dateFetch
         db.updateLastUpdated(manga).executeAsBlocking()
     }
 

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -40,6 +40,7 @@
     <string name="action_sort_total">Total chapters</string>
     <string name="action_sort_last_read">Last read</string>
     <string name="action_sort_last_checked">Last checked</string>
+    <string name="action_sort_next_updated">Next expected update</string>
     <string name="action_sort_latest_chapter">Latest chapter</string>
     <string name="action_sort_chapter_fetch_date">Date fetched</string>
     <string name="action_sort_date_added">Date added</string>