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.

(cherry picked from commit 70ed49e4782579d6ce2e91ace75ef44f8460a64a)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt
This commit is contained in:
stinky-lizard 2021-07-01 18:11:21 -04:00 committed by Jobobby04
parent e59789f777
commit 0e636d68c4
11 changed files with 117 additions and 4 deletions

View File

@ -24,7 +24,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/** /**
* Version of the database. * Version of the database.
*/ */
const val DATABASE_VERSION = /* SY --> */ 7 /* SY <-- */ const val DATABASE_VERSION = /* SY --> */ 8 /* SY <-- */
} }
override fun onCreate(db: SupportSQLiteDatabase) = with(db) { override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@ -81,6 +81,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
if (oldVersion < 7) { if (oldVersion < 7) {
db.execSQL("DROP TABLE IF EXISTS manga_related") db.execSQL("DROP TABLE IF EXISTS manga_related")
} }
if (oldVersion < 8) {
db.execSQL(MangaTable.addNextUpdateCol)
}
} }
override fun onConfigure(db: SupportSQLiteDatabase) { override fun onConfigure(db: SupportSQLiteDatabase) {

View File

@ -23,6 +23,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_ID
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED 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_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_SOURCE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
@ -65,6 +66,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
COL_THUMBNAIL_URL to obj.thumbnail_url, COL_THUMBNAIL_URL to obj.thumbnail_url,
COL_FAVORITE to obj.favorite, COL_FAVORITE to obj.favorite,
COL_LAST_UPDATE to obj.last_update, COL_LAST_UPDATE to obj.last_update,
COL_NEXT_UPDATE to obj.next_update,
COL_INITIALIZED to obj.initialized, COL_INITIALIZED to obj.initialized,
COL_VIEWER to obj.viewer_flags, COL_VIEWER to obj.viewer_flags,
COL_CHAPTER_FLAGS to obj.chapter_flags, COL_CHAPTER_FLAGS to obj.chapter_flags,
@ -88,6 +90,7 @@ interface BaseMangaGetResolver {
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL)) thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1 favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE)) 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 initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER)) viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS)) chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaMigrationPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaNextUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaThumbnailPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
import eu.kanade.tachiyomi.data.database.tables.CategoryTable import eu.kanade.tachiyomi.data.database.tables.CategoryTable
@ -134,6 +135,11 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true)) .withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
.prepare() .prepare()
fun updateNextUpdated(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaNextUpdatedPutResolver())
.prepare()
fun updateLastUpdated(manga: Manga) = db.put() fun updateLastUpdated(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaLastUpdatedPutResolver()) .withPutResolver(MangaLastUpdatedPutResolver())

View File

@ -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)
}
}

View File

@ -28,6 +28,8 @@ object MangaTable {
const val COL_LAST_UPDATE = "last_update" const val COL_LAST_UPDATE = "last_update"
const val COL_NEXT_UPDATE = "next_update"
const val COL_DATE_ADDED = "date_added" const val COL_DATE_ADDED = "date_added"
const val COL_INITIALIZED = "initialized" const val COL_INITIALIZED = "initialized"
@ -63,6 +65,7 @@ object MangaTable {
$COL_THUMBNAIL_URL TEXT, $COL_THUMBNAIL_URL TEXT,
$COL_FAVORITE INTEGER NOT NULL, $COL_FAVORITE INTEGER NOT NULL,
$COL_LAST_UPDATE LONG, $COL_LAST_UPDATE LONG,
$COL_NEXT_UPDATE LONG,
$COL_INITIALIZED BOOLEAN NOT NULL, $COL_INITIALIZED BOOLEAN NOT NULL,
$COL_VIEWER INTEGER NOT NULL, $COL_VIEWER INTEGER NOT NULL,
$COL_CHAPTER_FLAGS INTEGER NOT NULL, $COL_CHAPTER_FLAGS INTEGER NOT NULL,
@ -94,6 +97,11 @@ object MangaTable {
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " + "ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
"GROUP BY $TABLE.$COL_ID)" "GROUP BY $TABLE.$COL_ID)"
val addNextUpdateCol: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_NEXT_UPDATE LONG DEFAULT 0"
// SY -->
val addFilteredScanlators: String val addFilteredScanlators: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FILTERED_SCANLATORS TEXT" get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FILTERED_SCANLATORS TEXT"
// SY <--
} }

View File

@ -1,6 +1,9 @@
package eu.kanade.tachiyomi.data.library package eu.kanade.tachiyomi.data.library
import eu.kanade.tachiyomi.data.database.models.Manga 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. * This class will provide various functions to rank manga to efficiently schedule manga to update.
@ -9,9 +12,26 @@ object LibraryUpdateRanker {
val rankingScheme = listOf( val rankingScheme = listOf(
(this::lexicographicRanking)(), (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. * Provides a total ordering over all the [Manga]s.
* *

View File

@ -293,7 +293,8 @@ class SettingsLibraryController : SettingsController() {
// ../../data/library/LibraryUpdateRanker.kt // ../../data/library/LibraryUpdateRanker.kt
val priorities = arrayOf( val priorities = arrayOf(
Pair("0", R.string.action_sort_alpha), 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] val defaultPriority = priorities[0]

View File

@ -97,6 +97,24 @@ fun syncChaptersWithSource(
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions. // Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) { 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()) return Pair(emptyList(), emptyList())
} }
@ -156,11 +174,29 @@ fun syncChaptersWithSource(
db.insertChapters(toChange).executeAsBlocking() 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. // Fix order in source.
db.fixChaptersSourceOrder(sourceChapters).executeAsBlocking() db.fixChaptersSourceOrder(sourceChapters).executeAsBlocking()
// Set this manga as updated since chapters were changed // Set this manga as updated since chapters were changed
val newestChapter = topChapters.getOrNull(0)
val dateFetch = newestChapter?.date_upload ?: manga.last_update
if (dateFetch == 0L) {
if (toAdd.isNotEmpty()) {
manga.last_update = Date().time manga.last_update = Date().time
}
} else manga.last_update = dateFetch
db.updateLastUpdated(manga).executeAsBlocking() db.updateLastUpdated(manga).executeAsBlocking()
} }

View File

@ -40,6 +40,7 @@
<string name="action_sort_total">Total chapters</string> <string name="action_sort_total">Total chapters</string>
<string name="action_sort_last_read">Last read</string> <string name="action_sort_last_read">Last read</string>
<string name="action_sort_last_checked">Last checked</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_latest_chapter">Latest chapter</string>
<string name="action_sort_chapter_fetch_date">Date fetched</string> <string name="action_sort_chapter_fetch_date">Date fetched</string>
<string name="action_sort_date_added">Date added</string> <string name="action_sort_date_added">Date added</string>