mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	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:681003926a* 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:681003926a* 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.
This commit is contained in:
		@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,8 @@ interface Manga : SManga {
 | 
			
		||||
 | 
			
		||||
    var last_update: Long
 | 
			
		||||
 | 
			
		||||
    var next_update: Long
 | 
			
		||||
 | 
			
		||||
    var date_added: Long
 | 
			
		||||
 | 
			
		||||
    var viewer_flags: Int
 | 
			
		||||
 
 | 
			
		||||
@@ -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,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())
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -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]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user