Move SQLDelight to data module (#8954)
And use tachiyomi instead of eu.kanade.tachiyomi for package names in the module (cherry picked from commit 823749fc1ed16dbf52d43839888a70de089d65b1) # Conflicts: # app/build.gradle.kts # app/src/main/java/eu/kanade/data/history/HistoryRepositoryImpl.kt # app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt # app/src/main/java/eu/kanade/data/updates/UpdatesRepositoryImpl.kt # app/src/main/java/eu/kanade/tachiyomi/App.kt # app/src/main/java/eu/kanade/tachiyomi/AppModule.kt # app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupManager.kt # app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaExtensions.kt # data/src/main/java/tachiyomi/data/AndroidDatabaseHandler.kt # data/src/main/sqldelight/tachiyomi/data/eh.sq # data/src/main/sqldelight/tachiyomi/data/eh_favorites.sq # data/src/main/sqldelight/tachiyomi/data/feed_saved_search.sq # data/src/main/sqldelight/tachiyomi/data/merged.sq # data/src/main/sqldelight/tachiyomi/data/saved_search.sq # data/src/main/sqldelight/tachiyomi/data/search_metadata.sq # data/src/main/sqldelight/tachiyomi/data/search_tags.sq # data/src/main/sqldelight/tachiyomi/data/search_titles.sq # data/src/main/sqldelight/tachiyomi/migrations/16.sqm # data/src/main/sqldelight/tachiyomi/migrations/24.sqm
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,27 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
id("com.squareup.sqldelight")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "tachiyomi.data"
|
||||
|
||||
defaultConfig {
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
sqldelight {
|
||||
database("Database") {
|
||||
packageName = "tachiyomi.data"
|
||||
dialect = "sqlite:3.24"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":source-api"))
|
||||
api(libs.sqldelight.android.driver)
|
||||
api(libs.sqldelight.coroutines)
|
||||
api(libs.sqldelight.android.paging)
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,95 @@
|
||||
package tachiyomi.data
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import com.squareup.sqldelight.Query
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToOne
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AndroidDatabaseHandler(
|
||||
val db: Database,
|
||||
private val driver: SqlDriver,
|
||||
val queryDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
val transactionDispatcher: CoroutineDispatcher = queryDispatcher,
|
||||
) : DatabaseHandler {
|
||||
|
||||
val suspendingTransactionId = ThreadLocal<Int>()
|
||||
|
||||
override suspend fun <T> await(inTransaction: Boolean, block: suspend Database.() -> T): T {
|
||||
return dispatch(inTransaction, block)
|
||||
}
|
||||
|
||||
override suspend fun <T : Any> awaitList(
|
||||
inTransaction: Boolean,
|
||||
block: suspend Database.() -> Query<T>,
|
||||
): List<T> {
|
||||
return dispatch(inTransaction) { block(db).executeAsList() }
|
||||
}
|
||||
|
||||
override suspend fun <T : Any> awaitOne(
|
||||
inTransaction: Boolean,
|
||||
block: suspend Database.() -> Query<T>,
|
||||
): T {
|
||||
return dispatch(inTransaction) { block(db).executeAsOne() }
|
||||
}
|
||||
|
||||
override suspend fun <T : Any> awaitOneOrNull(
|
||||
inTransaction: Boolean,
|
||||
block: suspend Database.() -> Query<T>,
|
||||
): T? {
|
||||
return dispatch(inTransaction) { block(db).executeAsOneOrNull() }
|
||||
}
|
||||
|
||||
override fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>> {
|
||||
return block(db).asFlow().mapToList(queryDispatcher)
|
||||
}
|
||||
|
||||
override fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T> {
|
||||
return block(db).asFlow().mapToOne(queryDispatcher)
|
||||
}
|
||||
|
||||
override fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?> {
|
||||
return block(db).asFlow().mapToOneOrNull(queryDispatcher)
|
||||
}
|
||||
|
||||
override fun <T : Any> subscribeToPagingSource(
|
||||
countQuery: Database.() -> Query<Long>,
|
||||
queryProvider: Database.(Long, Long) -> Query<T>,
|
||||
): PagingSource<Long, T> {
|
||||
return QueryPagingSource(
|
||||
handler = this,
|
||||
countQuery = countQuery,
|
||||
queryProvider = { limit, offset ->
|
||||
queryProvider.invoke(db, limit, offset)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T> dispatch(inTransaction: Boolean, block: suspend Database.() -> T): T {
|
||||
// Create a transaction if needed and run the calling block inside it.
|
||||
if (inTransaction) {
|
||||
return withTransaction { block(db) }
|
||||
}
|
||||
|
||||
// If we're currently in the transaction thread, there's no need to dispatch our query.
|
||||
if (driver.currentTransaction() != null) {
|
||||
return block(db)
|
||||
}
|
||||
|
||||
// Get the current database context and run the calling block.
|
||||
val context = getCurrentDatabaseContext()
|
||||
return withContext(context) { block(db) }
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun getLibraryQuery() = LibraryQuery(driver)
|
||||
|
||||
fun getUpdatesQuery(after: Long) = UpdatesQuery(driver, after)
|
||||
// SY <--
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package tachiyomi.data
|
||||
|
||||
import com.squareup.sqldelight.ColumnAdapter
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import java.util.Date
|
||||
|
||||
val dateAdapter = object : ColumnAdapter<Date, Long> {
|
||||
override fun decode(databaseValue: Long): Date = Date(databaseValue)
|
||||
override fun encode(value: Date): Long = value.time
|
||||
}
|
||||
|
||||
private const val listOfStringsSeparator = ", "
|
||||
val listOfStringsAdapter = object : ColumnAdapter<List<String>, String> {
|
||||
override fun decode(databaseValue: String) =
|
||||
if (databaseValue.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
databaseValue.split(listOfStringsSeparator)
|
||||
}
|
||||
override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsSeparator)
|
||||
}
|
||||
|
||||
val updateStrategyAdapter = object : ColumnAdapter<UpdateStrategy, Long> {
|
||||
private val enumValues by lazy { UpdateStrategy.values() }
|
||||
|
||||
override fun decode(databaseValue: Long): UpdateStrategy =
|
||||
enumValues.getOrElse(databaseValue.toInt()) { UpdateStrategy.ALWAYS_UPDATE }
|
||||
|
||||
override fun encode(value: UpdateStrategy): Long = value.ordinal.toLong()
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private const val listOfStringsAndSeparator = " & "
|
||||
val listOfStringsAndAdapter = object : ColumnAdapter<List<String>, String> {
|
||||
override fun decode(databaseValue: String) =
|
||||
if (databaseValue.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
databaseValue.split(listOfStringsAndSeparator)
|
||||
}
|
||||
override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsAndSeparator)
|
||||
}
|
||||
|
||||
private const val listOfLongsSeparator = "/"
|
||||
val listOfLongsAdapter = object : ColumnAdapter<List<Long>, String> {
|
||||
override fun decode(databaseValue: String) =
|
||||
if (databaseValue.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
databaseValue.split(listOfLongsSeparator).mapNotNull { it.toLongOrNull() }
|
||||
}
|
||||
override fun encode(value: List<Long>) = value.joinToString(separator = listOfLongsSeparator)
|
||||
}
|
||||
// SY <--
|
||||
@@ -0,0 +1,36 @@
|
||||
package tachiyomi.data
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import com.squareup.sqldelight.Query
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface DatabaseHandler {
|
||||
|
||||
suspend fun <T> await(inTransaction: Boolean = false, block: suspend Database.() -> T): T
|
||||
|
||||
suspend fun <T : Any> awaitList(
|
||||
inTransaction: Boolean = false,
|
||||
block: suspend Database.() -> Query<T>,
|
||||
): List<T>
|
||||
|
||||
suspend fun <T : Any> awaitOne(
|
||||
inTransaction: Boolean = false,
|
||||
block: suspend Database.() -> Query<T>,
|
||||
): T
|
||||
|
||||
suspend fun <T : Any> awaitOneOrNull(
|
||||
inTransaction: Boolean = false,
|
||||
block: suspend Database.() -> Query<T>,
|
||||
): T?
|
||||
|
||||
fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>>
|
||||
|
||||
fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T>
|
||||
|
||||
fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?>
|
||||
|
||||
fun <T : Any> subscribeToPagingSource(
|
||||
countQuery: Database.() -> Query<Long>,
|
||||
queryProvider: Database.(Long, Long) -> Query<T>,
|
||||
): PagingSource<Long, T>
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package tachiyomi.data
|
||||
|
||||
import com.squareup.sqldelight.Query
|
||||
import com.squareup.sqldelight.db.SqlCursor
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import com.squareup.sqldelight.internal.copyOnWriteList
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import tachiyomi.view.LibraryView
|
||||
|
||||
private val mapper = { cursor: SqlCursor ->
|
||||
LibraryView(
|
||||
_id = cursor.getLong(0)!!,
|
||||
source = cursor.getLong(1)!!,
|
||||
url = cursor.getString(2)!!,
|
||||
artist = cursor.getString(3),
|
||||
author = cursor.getString(4),
|
||||
description = cursor.getString(5),
|
||||
genre = cursor.getString(6)?.let(listOfStringsAdapter::decode),
|
||||
title = cursor.getString(7)!!,
|
||||
status = cursor.getLong(8)!!,
|
||||
thumbnail_url = cursor.getString(9),
|
||||
favorite = cursor.getLong(10)!! == 1L,
|
||||
last_update = cursor.getLong(11) ?: 0,
|
||||
next_update = null,
|
||||
initialized = cursor.getLong(13)!! == 1L,
|
||||
viewer = cursor.getLong(14)!!,
|
||||
chapter_flags = cursor.getLong(15)!!,
|
||||
cover_last_modified = cursor.getLong(16)!!,
|
||||
date_added = cursor.getLong(17)!!,
|
||||
filtered_scanlators = cursor.getString(18)?.let(listOfStringsAndAdapter::decode),
|
||||
update_strategy = updateStrategyAdapter.decode(cursor.getLong(19)!!),
|
||||
totalCount = cursor.getLong(20)!!,
|
||||
readCount = cursor.getLong(21)!!,
|
||||
latestUpload = cursor.getLong(22)!!,
|
||||
chapterFetchedAt = cursor.getLong(23)!!,
|
||||
lastRead = cursor.getLong(24)!!,
|
||||
bookmarkCount = cursor.getLong(25)!!,
|
||||
category = cursor.getLong(26)!!,
|
||||
)
|
||||
}
|
||||
|
||||
class LibraryQuery(val driver: SqlDriver) : Query<LibraryView>(copyOnWriteList(), mapper) {
|
||||
override fun execute(): SqlCursor {
|
||||
return driver.executeQuery(
|
||||
null,
|
||||
"""
|
||||
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.category_id, 0) AS category
|
||||
FROM mangas M
|
||||
LEFT JOIN(
|
||||
SELECT
|
||||
chapters.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 history
|
||||
ON chapters._id = history.chapter_id
|
||||
GROUP BY chapters.manga_id
|
||||
) AS C
|
||||
ON M._id = C.manga_id
|
||||
LEFT JOIN mangas_categories AS MC
|
||||
ON MC.manga_id = M._id
|
||||
WHERE M.favorite = 1 AND M.source <> $MERGED_SOURCE_ID
|
||||
UNION
|
||||
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.category_id, 0) AS category
|
||||
FROM mangas M
|
||||
LEFT JOIN (
|
||||
SELECT merged.manga_id,merged.merge_id
|
||||
FROM merged
|
||||
GROUP BY merged.merge_id
|
||||
) as ME
|
||||
ON ME.merge_id = M._id
|
||||
LEFT JOIN(
|
||||
SELECT
|
||||
ME.merge_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 history
|
||||
ON chapters._id = history.chapter_id
|
||||
LEFT JOIN merged as ME
|
||||
ON ME.manga_id = chapters.manga_id
|
||||
GROUP BY ME.merge_id
|
||||
) AS C
|
||||
ON ME.merge_id = C.merge_id
|
||||
LEFT JOIN mangas_categories AS MC
|
||||
ON MC.manga_id = M._id
|
||||
WHERE M.favorite = 1 AND M.source = $MERGED_SOURCE_ID;
|
||||
""".trimIndent(),
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String = "LibraryQuery.sq:get"
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package tachiyomi.data
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.squareup.sqldelight.Query
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class QueryPagingSource<RowType : Any>(
|
||||
val handler: DatabaseHandler,
|
||||
val countQuery: Database.() -> Query<Long>,
|
||||
val queryProvider: Database.(Long, Long) -> Query<RowType>,
|
||||
) : PagingSource<Long, RowType>(), Query.Listener {
|
||||
|
||||
override val jumpingSupported: Boolean = true
|
||||
|
||||
private var currentQuery: Query<RowType>? by Delegates.observable(null) { _, old, new ->
|
||||
old?.removeListener(this)
|
||||
new?.addListener(this)
|
||||
}
|
||||
|
||||
init {
|
||||
registerInvalidatedCallback {
|
||||
currentQuery?.removeListener(this)
|
||||
currentQuery = null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, RowType> {
|
||||
try {
|
||||
val key = params.key ?: 0L
|
||||
val loadSize = params.loadSize
|
||||
val count = handler.awaitOne { countQuery() }
|
||||
|
||||
val (offset, limit) = when (params) {
|
||||
is LoadParams.Prepend -> key - loadSize to loadSize.toLong()
|
||||
else -> key to loadSize.toLong()
|
||||
}
|
||||
|
||||
val data = handler.awaitList {
|
||||
queryProvider(limit, offset)
|
||||
.also { currentQuery = it }
|
||||
}
|
||||
|
||||
val (prevKey, nextKey) = when (params) {
|
||||
is LoadParams.Append -> { offset - loadSize to offset + loadSize }
|
||||
else -> { offset to offset + loadSize }
|
||||
}
|
||||
|
||||
return LoadResult.Page(
|
||||
data = data,
|
||||
prevKey = if (offset <= 0L || prevKey < 0L) null else prevKey,
|
||||
nextKey = if (offset + loadSize >= count) null else nextKey,
|
||||
itemsBefore = maxOf(0L, offset).toInt(),
|
||||
itemsAfter = maxOf(0L, count - (offset + loadSize)).toInt(),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return LoadResult.Error(throwable = e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Long, RowType>): Long? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
val anchorPage = state.closestPageToPosition(anchorPosition)
|
||||
anchorPage?.prevKey ?: anchorPage?.nextKey
|
||||
}
|
||||
}
|
||||
|
||||
override fun queryResultsChanged() {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package tachiyomi.data
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asContextElement
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.RejectedExecutionException
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.coroutines.ContinuationInterceptor
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Returns the transaction dispatcher if we are on a transaction, or the database dispatchers.
|
||||
*/
|
||||
internal suspend fun AndroidDatabaseHandler.getCurrentDatabaseContext(): CoroutineContext {
|
||||
return coroutineContext[TransactionElement]?.transactionDispatcher ?: queryDispatcher
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the specified suspending [block] in a database transaction. The transaction will be
|
||||
* marked as successful unless an exception is thrown in the suspending [block] or the coroutine
|
||||
* is cancelled.
|
||||
*
|
||||
* SQLDelight will only perform at most one transaction at a time, additional transactions are queued
|
||||
* and executed on a first come, first serve order.
|
||||
*
|
||||
* Performing blocking database operations is not permitted in a coroutine scope other than the
|
||||
* one received by the suspending block. It is recommended that all [Dao] function invoked within
|
||||
* the [block] be suspending functions.
|
||||
*
|
||||
* The dispatcher used to execute the given [block] will utilize threads from SQLDelight's query executor.
|
||||
*/
|
||||
internal suspend fun <T> AndroidDatabaseHandler.withTransaction(block: suspend () -> T): T {
|
||||
// Use inherited transaction context if available, this allows nested suspending transactions.
|
||||
val transactionContext =
|
||||
coroutineContext[TransactionElement]?.transactionDispatcher ?: createTransactionContext()
|
||||
return withContext(transactionContext) {
|
||||
val transactionElement = coroutineContext[TransactionElement]!!
|
||||
transactionElement.acquire()
|
||||
try {
|
||||
db.transactionWithResult {
|
||||
runBlocking(transactionContext) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
transactionElement.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [CoroutineContext] for performing database operations within a coroutine transaction.
|
||||
*
|
||||
* The context is a combination of a dispatcher, a [TransactionElement] and a thread local element.
|
||||
*
|
||||
* * The dispatcher will dispatch coroutines to a single thread that is taken over from the SQLDelight
|
||||
* query executor. If the coroutine context is switched, suspending DAO functions will be able to
|
||||
* dispatch to the transaction thread.
|
||||
*
|
||||
* * The [TransactionElement] serves as an indicator for inherited context, meaning, if there is a
|
||||
* switch of context, suspending DAO methods will be able to use the indicator to dispatch the
|
||||
* database operation to the transaction thread.
|
||||
*
|
||||
* * The thread local element serves as a second indicator and marks threads that are used to
|
||||
* execute coroutines within the coroutine transaction, more specifically it allows us to identify
|
||||
* if a blocking DAO method is invoked within the transaction coroutine. Never assign meaning to
|
||||
* this value, for now all we care is if its present or not.
|
||||
*/
|
||||
private suspend fun AndroidDatabaseHandler.createTransactionContext(): CoroutineContext {
|
||||
val controlJob = Job()
|
||||
// make sure to tie the control job to this context to avoid blocking the transaction if
|
||||
// context get cancelled before we can even start using this job. Otherwise, the acquired
|
||||
// transaction thread will forever wait for the controlJob to be cancelled.
|
||||
// see b/148181325
|
||||
coroutineContext[Job]?.invokeOnCompletion {
|
||||
controlJob.cancel()
|
||||
}
|
||||
|
||||
val dispatcher = transactionDispatcher.acquireTransactionThread(controlJob)
|
||||
val transactionElement = TransactionElement(controlJob, dispatcher)
|
||||
val threadLocalElement =
|
||||
suspendingTransactionId.asContextElement(System.identityHashCode(controlJob))
|
||||
return dispatcher + transactionElement + threadLocalElement
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires a thread from the executor and returns a [ContinuationInterceptor] to dispatch
|
||||
* coroutines to the acquired thread. The [controlJob] is used to control the release of the
|
||||
* thread by cancelling the job.
|
||||
*/
|
||||
private suspend fun CoroutineDispatcher.acquireTransactionThread(
|
||||
controlJob: Job,
|
||||
): ContinuationInterceptor {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
// We got cancelled while waiting to acquire a thread, we can't stop our attempt to
|
||||
// acquire a thread, but we can cancel the controlling job so once it gets acquired it
|
||||
// is quickly released.
|
||||
controlJob.cancel()
|
||||
}
|
||||
try {
|
||||
dispatch(EmptyCoroutineContext) {
|
||||
runBlocking {
|
||||
// Thread acquired, resume coroutine
|
||||
continuation.resume(coroutineContext[ContinuationInterceptor]!!)
|
||||
controlJob.join()
|
||||
}
|
||||
}
|
||||
} catch (ex: RejectedExecutionException) {
|
||||
// Couldn't acquire a thread, cancel coroutine
|
||||
continuation.cancel(
|
||||
IllegalStateException(
|
||||
"Unable to acquire a thread to perform the database transaction",
|
||||
ex,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [CoroutineContext.Element] that indicates there is an on-going database transaction.
|
||||
*/
|
||||
private class TransactionElement(
|
||||
private val transactionThreadControlJob: Job,
|
||||
val transactionDispatcher: ContinuationInterceptor,
|
||||
) : CoroutineContext.Element {
|
||||
|
||||
companion object Key : CoroutineContext.Key<TransactionElement>
|
||||
|
||||
override val key: CoroutineContext.Key<TransactionElement>
|
||||
get() = TransactionElement
|
||||
|
||||
/**
|
||||
* Number of transactions (including nested ones) started with this element.
|
||||
* Call [acquire] to increase the count and [release] to decrease it. If the count reaches zero
|
||||
* when [release] is invoked then the transaction job is cancelled and the transaction thread
|
||||
* is released.
|
||||
*/
|
||||
private val referenceCount = AtomicInteger(0)
|
||||
|
||||
fun acquire() {
|
||||
referenceCount.incrementAndGet()
|
||||
}
|
||||
|
||||
fun release() {
|
||||
val count = referenceCount.decrementAndGet()
|
||||
if (count < 0) {
|
||||
throw IllegalStateException("Transaction was never started or was already released")
|
||||
} else if (count == 0) {
|
||||
// Cancel the job that controls the transaction thread, causing it to be released.
|
||||
transactionThreadControlJob.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package tachiyomi.data
|
||||
|
||||
import com.squareup.sqldelight.Query
|
||||
import com.squareup.sqldelight.db.SqlCursor
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import com.squareup.sqldelight.internal.copyOnWriteList
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import tachiyomi.view.UpdatesView
|
||||
|
||||
private val mapper = { cursor: SqlCursor ->
|
||||
UpdatesView(
|
||||
cursor.getLong(0)!!,
|
||||
cursor.getString(1)!!,
|
||||
cursor.getLong(2)!!,
|
||||
cursor.getString(3)!!,
|
||||
cursor.getString(4),
|
||||
cursor.getLong(5)!! == 1L,
|
||||
cursor.getLong(6)!! == 1L,
|
||||
cursor.getLong(7)!!,
|
||||
cursor.getLong(8)!!,
|
||||
cursor.getLong(9)!! == 1L,
|
||||
cursor.getString(10),
|
||||
cursor.getLong(11)!!,
|
||||
cursor.getLong(12)!!,
|
||||
cursor.getLong(13)!!,
|
||||
)
|
||||
}
|
||||
|
||||
class UpdatesQuery(val driver: SqlDriver, val after: Long) : Query<UpdatesView>(copyOnWriteList(), mapper) {
|
||||
override fun execute(): SqlCursor {
|
||||
return driver.executeQuery(
|
||||
null,
|
||||
"""
|
||||
SELECT
|
||||
mangas._id AS mangaId,
|
||||
mangas.title AS mangaTitle,
|
||||
chapters._id AS chapterId,
|
||||
chapters.name AS chapterName,
|
||||
chapters.scanlator,
|
||||
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
|
||||
FROM mangas JOIN chapters
|
||||
ON mangas._id = chapters.manga_id
|
||||
WHERE favorite = 1 AND source <> $MERGED_SOURCE_ID
|
||||
AND date_fetch > date_added
|
||||
AND dateUpload > :after
|
||||
UNION
|
||||
SELECT
|
||||
mangas._id AS mangaId,
|
||||
mangas.title AS mangaTitle,
|
||||
chapters._id AS chapterId,
|
||||
chapters.name AS chapterName,
|
||||
chapters.scanlator,
|
||||
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
|
||||
FROM mangas
|
||||
LEFT JOIN (
|
||||
SELECT merged.manga_id,merged.merge_id
|
||||
FROM merged
|
||||
GROUP BY merged.merge_id
|
||||
) as ME
|
||||
ON ME.merge_id = mangas._id
|
||||
JOIN chapters
|
||||
ON ME.manga_id = chapters.manga_id
|
||||
WHERE favorite = 1 AND source = $MERGED_SOURCE_ID
|
||||
AND date_fetch > date_added
|
||||
AND dateUpload > :after
|
||||
ORDER BY datefetch DESC;
|
||||
""".trimIndent(),
|
||||
1,
|
||||
binders = {
|
||||
bindLong(1, after)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String = "LibraryQuery.sq:get"
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import kotlin.collections.List;
|
||||
|
||||
CREATE TABLE categories(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
sort INTEGER NOT NULL,
|
||||
flags INTEGER NOT NULL,
|
||||
manga_order TEXT AS List<Long> NOT NULL
|
||||
);
|
||||
|
||||
-- Insert system category
|
||||
INSERT OR IGNORE INTO categories(_id, name, sort, flags, manga_order) 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 _id,name,sort,flags
|
||||
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, manga_order)
|
||||
VALUES (:name, :order, :flags, "");
|
||||
|
||||
delete:
|
||||
DELETE FROM categories
|
||||
WHERE _id = :categoryId;
|
||||
|
||||
update:
|
||||
UPDATE categories
|
||||
SET name = coalesce(:name, name),
|
||||
sort = coalesce(:order, sort),
|
||||
flags = coalesce(:flags, flags)
|
||||
WHERE _id = :categoryId;
|
||||
|
||||
updateAllFlags:
|
||||
UPDATE categories SET
|
||||
flags = coalesce(?, flags);
|
||||
|
||||
selectLastInsertedRowId:
|
||||
SELECT last_insert_rowid();
|
||||
@@ -0,0 +1,80 @@
|
||||
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 AS Float NOT NULL,
|
||||
source_order INTEGER NOT NULL,
|
||||
date_fetch INTEGER AS Long NOT NULL,
|
||||
date_upload INTEGER AS Long NOT NULL,
|
||||
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;
|
||||
|
||||
getChapterById:
|
||||
SELECT *
|
||||
FROM chapters
|
||||
WHERE _id = :id;
|
||||
|
||||
getChaptersByMangaId:
|
||||
SELECT *
|
||||
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;
|
||||
|
||||
getMergedChaptersByMangaId:
|
||||
SELECT chapters.*
|
||||
FROM (
|
||||
SELECT manga_id FROM merged WHERE merge_id = ?
|
||||
) AS M
|
||||
JOIN chapters
|
||||
ON chapters.manga_id = M.manga_id;
|
||||
|
||||
removeChaptersWithIds:
|
||||
DELETE FROM chapters
|
||||
WHERE _id IN :chapterIds;
|
||||
|
||||
insert:
|
||||
INSERT INTO chapters(manga_id,url,name,scanlator,read,bookmark,last_page_read,chapter_number,source_order,date_fetch,date_upload)
|
||||
VALUES (:mangaId,:url,:name,:scanlator,:read,:bookmark,:lastPageRead,:chapterNumber,:sourceOrder,:dateFetch,:dateUpload);
|
||||
|
||||
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)
|
||||
WHERE _id = :chapterId;
|
||||
|
||||
selectLastInsertedRowId:
|
||||
SELECT last_insert_rowid();
|
||||
@@ -0,0 +1,32 @@
|
||||
deleteBySyncId:
|
||||
DELETE FROM manga_sync WHERE sync_id = :syncId;
|
||||
|
||||
migrateSource:
|
||||
UPDATE mangas
|
||||
SET source = :newId
|
||||
WHERE source = :oldId;
|
||||
|
||||
getChaptersByMangaIds:
|
||||
SELECT * FROM chapters WHERE manga_id IN :mangaIds;
|
||||
|
||||
resetFilteredScanlatorsForAllManga:
|
||||
UPDATE mangas
|
||||
SET filtered_scanlators = NULL;
|
||||
|
||||
migrateAllNhentaiToOtherLang:
|
||||
UPDATE mangas
|
||||
SET source = :nh
|
||||
WHERE favorite = 1 AND source IN :sources;
|
||||
|
||||
resetReaderViewerForAllManga:
|
||||
UPDATE mangas
|
||||
SET viewer = 0;
|
||||
|
||||
fixReaderViewerBackupBug:
|
||||
UPDATE mangas
|
||||
SET viewer = 0
|
||||
WHERE viewer = -1;
|
||||
|
||||
addAllMangaInDatabaseToLibrary:
|
||||
UPDATE mangas
|
||||
SET favorite = 1;
|
||||
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE eh_favorites (
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
gid TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
category INTEGER NOT NULL
|
||||
);
|
||||
|
||||
selectAll:
|
||||
SELECT * FROM eh_favorites;
|
||||
|
||||
insertEhFavorites:
|
||||
INSERT INTO eh_favorites (_id, title, gid, token, category) VALUES (?, ?, ?, ?, ?);
|
||||
|
||||
deleteAll:
|
||||
DELETE FROM eh_favorites;
|
||||
@@ -0,0 +1,55 @@
|
||||
CREATE TABLE feed_saved_search (
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
source INTEGER NOT NULL,
|
||||
saved_search INTEGER,
|
||||
global INTEGER AS Boolean NOT NULL,
|
||||
FOREIGN KEY(saved_search) REFERENCES saved_search (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX feed_saved_search_saved_search_index ON feed_saved_search(saved_search);
|
||||
|
||||
selectAllGlobal:
|
||||
SELECT * FROM feed_saved_search WHERE global = 1;
|
||||
|
||||
countGlobal:
|
||||
SELECT count(*) FROM feed_saved_search WHERE global = 1;
|
||||
|
||||
selectBySource:
|
||||
SELECT * FROM feed_saved_search WHERE source = :sourceId AND global = 0;
|
||||
|
||||
insert:
|
||||
INSERT INTO feed_saved_search (source, saved_search, global) VALUES (:sourceId, :savedSearch, :global);
|
||||
|
||||
deleteById:
|
||||
DELETE FROM feed_saved_search WHERE _id = :id;
|
||||
|
||||
deleteAll:
|
||||
DELETE FROM feed_saved_search;
|
||||
|
||||
selectGlobalFeedSavedSearch:
|
||||
SELECT saved_search.*
|
||||
FROM (
|
||||
SELECT saved_search FROM feed_saved_search WHERE global = 1
|
||||
) AS M
|
||||
JOIN saved_search
|
||||
ON saved_search._id = M.saved_search;
|
||||
|
||||
selectSourceFeedSavedSearch:
|
||||
SELECT saved_search.*
|
||||
FROM (
|
||||
SELECT saved_search FROM feed_saved_search WHERE global = 0 AND source = :sourceId
|
||||
) AS M
|
||||
JOIN saved_search
|
||||
ON saved_search._id = M.saved_search;
|
||||
|
||||
countSourceFeedSavedSearch:
|
||||
SELECT count(*)
|
||||
FROM (
|
||||
SELECT saved_search FROM feed_saved_search WHERE global = 0 AND source = :sourceId
|
||||
) AS M
|
||||
JOIN saved_search
|
||||
ON saved_search._id = M.saved_search;
|
||||
|
||||
selectLastInsertedRowId:
|
||||
SELECT last_insert_rowid();
|
||||
@@ -0,0 +1,73 @@
|
||||
import java.util.Date;
|
||||
|
||||
CREATE TABLE history(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
chapter_id INTEGER NOT NULL UNIQUE,
|
||||
last_read INTEGER AS Date,
|
||||
time_read INTEGER NOT NULL,
|
||||
FOREIGN KEY(chapter_id) REFERENCES chapters (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX history_history_chapter_id_index ON history(chapter_id);
|
||||
|
||||
getHistoryByMangaId:
|
||||
SELECT
|
||||
H._id,
|
||||
H.chapter_id,
|
||||
H.last_read,
|
||||
H.time_read
|
||||
FROM history H
|
||||
JOIN chapters C
|
||||
ON H.chapter_id = C._id
|
||||
WHERE C.manga_id = :mangaId AND C._id = H.chapter_id;
|
||||
|
||||
getHistoryByChapterUrl:
|
||||
SELECT
|
||||
H._id,
|
||||
H.chapter_id,
|
||||
H.last_read,
|
||||
H.time_read
|
||||
FROM history H
|
||||
JOIN chapters C
|
||||
ON H.chapter_id = C._id
|
||||
WHERE C.url = :chapterUrl AND C._id = H.chapter_id;
|
||||
|
||||
resetHistoryById:
|
||||
UPDATE history
|
||||
SET last_read = 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
|
||||
INNER JOIN history H
|
||||
ON C._id = H.chapter_id
|
||||
WHERE M._id = :mangaId
|
||||
);
|
||||
|
||||
removeAllHistory:
|
||||
DELETE FROM history;
|
||||
|
||||
removeResettedHistory:
|
||||
DELETE FROM history
|
||||
WHERE last_read = 0;
|
||||
|
||||
upsert:
|
||||
INSERT INTO history(chapter_id, last_read, time_read)
|
||||
VALUES (:chapterId, :readAt, :time_read)
|
||||
ON CONFLICT(chapter_id)
|
||||
DO UPDATE
|
||||
SET
|
||||
last_read = :readAt,
|
||||
time_read = time_read + :time_read
|
||||
WHERE chapter_id = :chapterId;
|
||||
|
||||
getReadDuration:
|
||||
SELECT coalesce(sum(time_read), 0)
|
||||
FROM history;
|
||||
@@ -0,0 +1,60 @@
|
||||
CREATE TABLE manga_sync(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
sync_id INTEGER NOT NULL,
|
||||
remote_id INTEGER 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 AS Float NOT NULL,
|
||||
remote_url TEXT NOT NULL,
|
||||
start_date INTEGER AS Long NOT NULL,
|
||||
finish_date INTEGER AS Long NOT NULL,
|
||||
UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
delete:
|
||||
DELETE FROM manga_sync
|
||||
WHERE manga_id = :mangaId AND sync_id = :syncId;
|
||||
|
||||
getTracks:
|
||||
SELECT *
|
||||
FROM manga_sync;
|
||||
|
||||
getTrackById:
|
||||
SELECT *
|
||||
FROM manga_sync
|
||||
WHERE _id = :id;
|
||||
|
||||
getTracksByMangaIds:
|
||||
SELECT * FROM manga_sync WHERE manga_id IN :mangaIds;
|
||||
|
||||
getTracksByMangaId:
|
||||
SELECT *
|
||||
FROM manga_sync
|
||||
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)
|
||||
VALUES (:mangaId,:syncId,:remoteId,:libraryId,:title,:lastChapterRead,:totalChapters,:status,:score,:remoteUrl,:startDate,:finishDate);
|
||||
|
||||
update:
|
||||
UPDATE manga_sync
|
||||
SET
|
||||
manga_id = coalesce(:mangaId, manga_id),
|
||||
sync_id = coalesce(:syncId, sync_id),
|
||||
remote_id = coalesce(:mediaId, remote_id),
|
||||
library_id = coalesce(:libraryId, library_id),
|
||||
title = coalesce(:title, title),
|
||||
last_chapter_read = coalesce(:lastChapterRead, last_chapter_read),
|
||||
total_chapters = coalesce(:totalChapter, total_chapters),
|
||||
status = coalesce(:status, status),
|
||||
score = coalesce(:score, score),
|
||||
remote_url = coalesce(:trackingUrl, remote_url),
|
||||
start_date = coalesce(:startDate, start_date),
|
||||
finish_date = coalesce(:finishDate, finish_date)
|
||||
WHERE _id = :id;
|
||||
@@ -0,0 +1,158 @@
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy;
|
||||
import java.lang.String;
|
||||
import kotlin.collections.List;
|
||||
|
||||
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<String>,
|
||||
title TEXT NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
thumbnail_url TEXT,
|
||||
favorite INTEGER AS Boolean NOT NULL,
|
||||
last_update INTEGER AS Long,
|
||||
next_update INTEGER AS Long,
|
||||
initialized INTEGER AS Boolean NOT NULL,
|
||||
viewer INTEGER NOT NULL,
|
||||
chapter_flags INTEGER NOT NULL,
|
||||
cover_last_modified INTEGER AS Long NOT NULL,
|
||||
date_added INTEGER AS Long NOT NULL,
|
||||
filtered_scanlators TEXT AS List<String>,
|
||||
update_strategy INTEGER AS UpdateStrategy NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
|
||||
CREATE INDEX mangas_url_index ON mangas(url);
|
||||
|
||||
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 mangas.*
|
||||
FROM mangas
|
||||
WHERE favorite = 0 AND _id IN(
|
||||
SELECT chapters.manga_id FROM chapters WHERE read = 1 OR last_page_read != 0
|
||||
);
|
||||
|
||||
getSourceIdWithFavoriteCount:
|
||||
SELECT
|
||||
source,
|
||||
count(*)
|
||||
FROM mangas
|
||||
WHERE favorite = 1
|
||||
GROUP BY source;
|
||||
|
||||
getFavoriteBySourceId:
|
||||
SELECT *
|
||||
FROM mangas
|
||||
WHERE favorite = 1
|
||||
AND source = :sourceId;
|
||||
|
||||
getDuplicateLibraryManga:
|
||||
SELECT *
|
||||
FROM mangas
|
||||
WHERE favorite = 1
|
||||
AND LOWER(title) = :title
|
||||
LIMIT 1;
|
||||
|
||||
resetViewerFlags:
|
||||
UPDATE mangas
|
||||
SET viewer = 0;
|
||||
|
||||
getSourceIdsWithNonLibraryManga:
|
||||
SELECT source, COUNT(*) AS manga_count
|
||||
FROM mangas
|
||||
WHERE favorite = 0
|
||||
GROUP BY source;
|
||||
|
||||
deleteMangasNotInLibraryBySourceIds:
|
||||
DELETE FROM mangas
|
||||
WHERE favorite = 0 AND source IN :sourceIdsAND AND _id NOT IN (
|
||||
SELECT manga_id FROM merged WHERE manga_id != merge_id
|
||||
);
|
||||
|
||||
deleteMangasNotInLibraryAndNotReadBySourceIds:
|
||||
DELETE FROM mangas
|
||||
WHERE favorite = 0 AND source IN :sourceIdsAND AND _id NOT IN (
|
||||
SELECT manga_id FROM merged WHERE manga_id != merge_id
|
||||
) AND _id NOT IN (
|
||||
SELECT manga_id FROM chapters WHERE read = 1 OR last_page_read != 0
|
||||
);
|
||||
|
||||
insert:
|
||||
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,filtered_scanlators,update_strategy)
|
||||
VALUES (:source,:url,:artist,:author,:description,:genre,:title,:status,:thumbnailUrl,:favorite,:lastUpdate,:nextUpdate,:initialized,:viewerFlags,:chapterFlags,:coverLastModified,:dateAdded,:filteredScanlators,:updateStrategy);
|
||||
|
||||
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),
|
||||
filtered_scanlators = coalesce(:filteredScanlators, filtered_scanlators),
|
||||
update_strategy = coalesce(:updateStrategy, update_strategy)
|
||||
WHERE _id = :mangaId;
|
||||
|
||||
selectLastInsertedRowId:
|
||||
SELECT last_insert_rowid();
|
||||
|
||||
getEhMangaWithMetadata:
|
||||
SELECT mangas.* FROM mangas
|
||||
INNER JOIN search_metadata
|
||||
ON mangas._id = search_metadata.manga_id
|
||||
WHERE mangas.favorite = 1 AND (mangas.source = :eh OR mangas.source = :exh);
|
||||
|
||||
getIdsOfFavoriteMangaWithMetadata:
|
||||
SELECT mangas._id FROM mangas
|
||||
INNER JOIN search_metadata
|
||||
ON mangas._id = search_metadata.manga_id
|
||||
WHERE mangas.favorite = 1;
|
||||
|
||||
getBySource:
|
||||
SELECT * FROM mangas WHERE source = :sourceId;
|
||||
|
||||
getAll:
|
||||
SELECT * FROM mangas;
|
||||
|
||||
deleteById:
|
||||
DELETE FROM mangas WHERE _id = :id;
|
||||
|
||||
selectLastInsertRow:
|
||||
SELECT *
|
||||
FROM mangas
|
||||
WHERE _id = last_insert_rowid();
|
||||
|
||||
getIdByUrlAndSource:
|
||||
SELECT _id
|
||||
FROM mangas
|
||||
WHERE url = :url AND source = :source;
|
||||
@@ -0,0 +1,17 @@
|
||||
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
|
||||
);
|
||||
|
||||
insert:
|
||||
INSERT INTO mangas_categories(manga_id, category_id)
|
||||
VALUES (:mangaId, :categoryId);
|
||||
|
||||
deleteMangaCategoryByMangaId:
|
||||
DELETE FROM mangas_categories
|
||||
WHERE manga_id = :mangaId;
|
||||
@@ -0,0 +1,119 @@
|
||||
CREATE TABLE merged(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
info_manga INTEGER AS Boolean NOT NULL,
|
||||
get_chapter_updates INTEGER AS Boolean NOT NULL,
|
||||
chapter_sort_mode INTEGER NOT NULL,
|
||||
chapter_priority INTEGER NOT NULL,
|
||||
download_chapters INTEGER AS Boolean NOT NULL,
|
||||
merge_id INTEGER NOT NULL,
|
||||
merge_url TEXT NOT NULL,
|
||||
manga_id INTEGER,
|
||||
manga_url TEXT NOT NULL,
|
||||
manga_source INTEGER NOT NULL,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE SET NULL,
|
||||
FOREIGN KEY(merge_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX merged_merge_id_index ON merged(merge_id);
|
||||
|
||||
selectByMergeId:
|
||||
SELECT * FROM merged WHERE merge_id = ?;
|
||||
|
||||
selectByMergeUrl:
|
||||
SELECT * FROM merged WHERE merge_url = ?;
|
||||
|
||||
deleteByMergeId:
|
||||
DELETE FROM merged WHERE merge_id = ?;
|
||||
|
||||
selectMergedMangasById:
|
||||
SELECT mangas.*
|
||||
FROM (
|
||||
SELECT manga_id FROM merged WHERE merge_id = ?
|
||||
) AS M
|
||||
JOIN mangas
|
||||
ON mangas._id = M.manga_id;
|
||||
|
||||
selectMergedMangasForDownloadingById:
|
||||
SELECT mangas.*
|
||||
FROM (
|
||||
SELECT manga_id FROM merged WHERE merge_id = ? AND download_chapters = 1
|
||||
) AS M
|
||||
JOIN mangas
|
||||
ON mangas._id = M.manga_id;
|
||||
|
||||
selectMergedMangasByUrl:
|
||||
SELECT mangas.*
|
||||
FROM (
|
||||
SELECT manga_id FROM merged WHERE merge_url = ?
|
||||
) AS M
|
||||
JOIN mangas
|
||||
ON mangas._id = M.manga_id;
|
||||
|
||||
selectAllMergedMangas:
|
||||
SELECT mangas.*
|
||||
FROM (
|
||||
SELECT manga_id FROM merged
|
||||
) AS M
|
||||
JOIN mangas
|
||||
ON mangas._id = M.manga_id;
|
||||
|
||||
deleteByMergeUrl:
|
||||
DELETE FROM merged WHERE merge_url = ?;
|
||||
|
||||
selectAll:
|
||||
SELECT * FROM merged;
|
||||
|
||||
selectChaptersByMergedId:
|
||||
SELECT chapters.*
|
||||
FROM (
|
||||
SELECT manga_id FROM merged WHERE merge_id = ?
|
||||
) AS M
|
||||
JOIN chapters
|
||||
ON chapters.manga_id = M.manga_id;
|
||||
|
||||
insert:
|
||||
INSERT INTO merged(
|
||||
info_manga,
|
||||
get_chapter_updates,
|
||||
chapter_sort_mode,
|
||||
chapter_priority,
|
||||
download_chapters,
|
||||
merge_id,
|
||||
merge_url,
|
||||
manga_id,
|
||||
manga_url,
|
||||
manga_source
|
||||
)
|
||||
VALUES (
|
||||
:infoManga,
|
||||
:getChapterUpdates,
|
||||
:chapterSortMode,
|
||||
:chapterPriority,
|
||||
:downloadChapters,
|
||||
:mergeId,
|
||||
:mergeUrl,
|
||||
:mangaId,
|
||||
:mangaUrl,
|
||||
:mangaSource
|
||||
);
|
||||
|
||||
updateSettingsById:
|
||||
UPDATE merged
|
||||
SET
|
||||
get_chapter_updates = coalesce(:getChapterUpdates, get_chapter_updates),
|
||||
download_chapters = coalesce(:downloadChapters, download_chapters),
|
||||
info_manga = coalesce(:infoManga, info_manga),
|
||||
chapter_priority = coalesce(:chapterPriority, chapter_priority),
|
||||
chapter_sort_mode = coalesce(:chapterSortMode, chapter_sort_mode)
|
||||
WHERE _id = :id;
|
||||
|
||||
deleteById:
|
||||
DELETE FROM merged WHERE _id = ?;
|
||||
|
||||
deleteAll:
|
||||
DELETE FROM merged;
|
||||
|
||||
selectLastInsertedRowId:
|
||||
SELECT last_insert_rowid();
|
||||
@@ -0,0 +1,39 @@
|
||||
CREATE TABLE saved_search(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
source INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
query TEXT,
|
||||
filters_json TEXT
|
||||
);
|
||||
|
||||
selectBySource:
|
||||
SELECT * FROM saved_search WHERE source = :sourceId;
|
||||
|
||||
deleteBySource:
|
||||
DELETE FROM saved_search WHERE source = :sourceId;
|
||||
|
||||
selectAll:
|
||||
SELECT * FROM saved_search;
|
||||
|
||||
selectById:
|
||||
SELECT * FROM saved_search WHERE _id = :id;
|
||||
|
||||
selectByIds:
|
||||
SELECT * FROM saved_search WHERE _id IN :ids;
|
||||
|
||||
selectNamesAndSources:
|
||||
SELECT source, name
|
||||
FROM saved_search;
|
||||
|
||||
insert:
|
||||
INSERT INTO saved_search (source, name, query, filters_json)
|
||||
VALUES (:source, :name, :query, :filtersJson);
|
||||
|
||||
deleteById:
|
||||
DELETE FROM saved_search WHERE _id = :id;
|
||||
|
||||
deleteAll:
|
||||
DELETE FROM saved_search;
|
||||
|
||||
selectLastInsertedRowId:
|
||||
SELECT last_insert_rowid();
|
||||
@@ -0,0 +1,40 @@
|
||||
CREATE TABLE search_metadata (
|
||||
manga_id INTEGER NOT NULL PRIMARY KEY,
|
||||
uploader TEXT,
|
||||
extra TEXT NOT NULL,
|
||||
indexed_extra TEXT,
|
||||
extra_version INTEGER AS Int NOT NULL,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX search_metadata_uploader_index ON search_metadata(uploader);
|
||||
CREATE INDEX search_metadata_indexed_extra_index ON search_metadata(indexed_extra);
|
||||
|
||||
selectAll:
|
||||
SELECT * FROM search_metadata;
|
||||
|
||||
selectByMangaId:
|
||||
SELECT * FROM search_metadata WHERE manga_id = ?;
|
||||
|
||||
selectByIndexedExtra:
|
||||
SELECT * FROM search_metadata WHERE indexed_extra = ?;
|
||||
|
||||
upsert:
|
||||
INSERT INTO search_metadata(manga_id, uploader, extra, indexed_extra, extra_version)
|
||||
VALUES (:mangaId, :uploader, :extra, :indexedExtra, :extraVersion)
|
||||
ON CONFLICT(manga_id)
|
||||
DO UPDATE
|
||||
SET
|
||||
uploader = :uploader,
|
||||
extra = :extra,
|
||||
indexed_extra = :indexedExtra,
|
||||
extra_version = :extraVersion
|
||||
WHERE manga_id = :mangaId;
|
||||
|
||||
insert:
|
||||
INSERT INTO search_metadata (manga_id, uploader, extra, indexed_extra, extra_version)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
|
||||
deleteAll:
|
||||
DELETE FROM search_metadata;
|
||||
@@ -0,0 +1,31 @@
|
||||
CREATE TABLE search_tags (
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
namespace TEXT,
|
||||
name TEXT NOT NULL,
|
||||
type INTEGER AS Int NOT NULL,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX search_tags_manga_id_index ON search_tags(manga_id);
|
||||
CREATE INDEX search_tags_namespace_name_index ON search_tags(namespace, name);
|
||||
|
||||
|
||||
selectByMangaId:
|
||||
SELECT * FROM search_tags
|
||||
WHERE manga_id = ?;
|
||||
|
||||
deleteByManga:
|
||||
DELETE FROM search_tags WHERE manga_id = ?;
|
||||
|
||||
insert:
|
||||
INSERT INTO search_tags (manga_id, namespace, name, type)
|
||||
VALUES (?, ?, ?, ?);
|
||||
|
||||
insertItem:
|
||||
INSERT INTO search_tags (_id, manga_id, namespace, name, type)
|
||||
VALUES ?;
|
||||
|
||||
deleteAll:
|
||||
DELETE FROM search_titles;
|
||||
@@ -0,0 +1,27 @@
|
||||
CREATE TABLE search_titles (
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
type INTEGER AS Int NOT NULL,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX search_titles_manga_id_index ON search_titles(manga_id);
|
||||
CREATE INDEX search_titles_title_index ON search_titles(title);
|
||||
|
||||
selectByMangaId:
|
||||
SELECT * FROM search_titles
|
||||
WHERE manga_id = ?;
|
||||
|
||||
deleteByManga:
|
||||
DELETE FROM search_titles WHERE manga_id = ?;
|
||||
|
||||
insert:
|
||||
INSERT INTO search_titles (manga_id, title, type) VALUES (?, ?, ?);
|
||||
|
||||
insertItem:
|
||||
INSERT INTO search_titles (_id, manga_id, title, type) VALUES ?;
|
||||
|
||||
deleteAll:
|
||||
DELETE FROM search_titles;
|
||||
@@ -0,0 +1,24 @@
|
||||
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;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE mangas
|
||||
ADD COLUMN cover_last_modified INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE eh_favorites (
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
gid TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
category INTEGER NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS eh_favorites (
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
gid TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
category INTEGER NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE saved_search(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
source INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
query TEXT,
|
||||
filters_json TEXT
|
||||
);
|
||||
CREATE TABLE feed_saved_search (
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
source INTEGER NOT NULL,
|
||||
saved_search INTEGER,
|
||||
global INTEGER AS Boolean NOT NULL,
|
||||
FOREIGN KEY(saved_search) REFERENCES saved_search (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX feed_saved_search_saved_search_index ON feed_saved_search(saved_search);
|
||||
@@ -0,0 +1,275 @@
|
||||
DROP INDEX IF EXISTS chapters_manga_id_index;
|
||||
DROP INDEX IF EXISTS chapters_unread_by_manga_index;
|
||||
DROP INDEX IF EXISTS history_history_chapter_id_index;
|
||||
DROP INDEX IF EXISTS library_favorite_index;
|
||||
DROP INDEX IF EXISTS mangas_url_index;
|
||||
|
||||
DROP INDEX IF EXISTS search_metadata_uploader_index;
|
||||
DROP INDEX IF EXISTS search_metadata_indexed_extra_index;
|
||||
DROP INDEX IF EXISTS search_tags_manga_id_index;
|
||||
DROP INDEX IF EXISTS search_tags_namespace_name_index;
|
||||
DROP INDEX IF EXISTS search_titles_manga_id_index;
|
||||
DROP INDEX IF EXISTS search_titles_title_index;
|
||||
DROP INDEX IF EXISTS merged_merge_id_index;
|
||||
DROP INDEX IF EXISTS feed_saved_search_saved_search_index;
|
||||
|
||||
ALTER TABLE mangas RENAME TO manga_temp;
|
||||
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,
|
||||
title TEXT NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
thumbnail_url TEXT,
|
||||
favorite INTEGER NOT NULL,
|
||||
last_update INTEGER AS Long,
|
||||
next_update INTEGER AS Long,
|
||||
initialized INTEGER AS Boolean NOT NULL,
|
||||
viewer INTEGER NOT NULL,
|
||||
chapter_flags INTEGER NOT NULL,
|
||||
cover_last_modified INTEGER AS Long NOT NULL,
|
||||
date_added INTEGER AS Long NOT NULL,
|
||||
filtered_scanlators TEXT
|
||||
);
|
||||
INSERT INTO mangas
|
||||
SELECT _id,source,url,artist,author,description,genre,title,status,thumbnail_url,favorite,last_update,next_update,initialized,viewer,chapter_flags,cover_last_modified,date_added,filtered_scanlators
|
||||
FROM manga_temp;
|
||||
|
||||
ALTER TABLE categories RENAME TO categories_temp;
|
||||
CREATE TABLE categories(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
sort INTEGER NOT NULL,
|
||||
flags INTEGER NOT NULL,
|
||||
manga_order TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO categories
|
||||
SELECT _id,name,sort,flags,manga_order
|
||||
FROM categories_temp;
|
||||
|
||||
ALTER TABLE chapters RENAME TO chapters_temp;
|
||||
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 AS Float NOT NULL,
|
||||
source_order INTEGER NOT NULL,
|
||||
date_fetch INTEGER AS Long NOT NULL,
|
||||
date_upload INTEGER AS Long NOT NULL,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO chapters
|
||||
SELECT _id,manga_id,url,name,scanlator,read,bookmark,last_page_read,chapter_number,source_order,date_fetch,date_upload
|
||||
FROM chapters_temp;
|
||||
|
||||
ALTER TABLE history RENAME TO history_temp;
|
||||
CREATE TABLE history(
|
||||
history_id INTEGER NOT NULL PRIMARY KEY,
|
||||
history_chapter_id INTEGER NOT NULL UNIQUE,
|
||||
history_last_read INTEGER AS Long,
|
||||
history_time_read INTEGER AS Long,
|
||||
FOREIGN KEY(history_chapter_id) REFERENCES chapters (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO history
|
||||
SELECT history_id, history_chapter_id, history_last_read, history_time_read
|
||||
FROM history_temp;
|
||||
|
||||
ALTER TABLE mangas_categories RENAME TO mangas_categories_temp;
|
||||
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
|
||||
);
|
||||
INSERT INTO mangas_categories
|
||||
SELECT _id, manga_id, category_id
|
||||
FROM mangas_categories_temp;
|
||||
|
||||
ALTER TABLE manga_sync RENAME TO manga_sync_temp;
|
||||
CREATE TABLE manga_sync(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
sync_id INTEGER NOT NULL,
|
||||
remote_id INTEGER 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 AS Float NOT NULL,
|
||||
remote_url TEXT NOT NULL,
|
||||
start_date INTEGER AS Long NOT NULL,
|
||||
finish_date INTEGER AS Long NOT NULL,
|
||||
UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO manga_sync
|
||||
SELECT _id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date
|
||||
FROM manga_sync_temp;
|
||||
|
||||
ALTER TABLE eh_favorites RENAME TO eh_favorites_temp;
|
||||
CREATE TABLE eh_favorites (
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
gid TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
category INTEGER NOT NULL
|
||||
);
|
||||
INSERT INTO eh_favorites
|
||||
SELECT _id,title,gid,token,category
|
||||
FROM eh_favorites_temp;
|
||||
|
||||
ALTER TABLE saved_search RENAME TO saved_search_temp;
|
||||
CREATE TABLE saved_search(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
source INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
query TEXT,
|
||||
filters_json TEXT
|
||||
);
|
||||
INSERT INTO saved_search
|
||||
SELECT _id,source,name,query,filters_json
|
||||
FROM saved_search_temp;
|
||||
|
||||
ALTER TABLE feed_saved_search RENAME TO feed_saved_search_temp;
|
||||
CREATE TABLE feed_saved_search (
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
source INTEGER NOT NULL,
|
||||
saved_search INTEGER,
|
||||
global INTEGER AS Boolean NOT NULL,
|
||||
FOREIGN KEY(saved_search) REFERENCES saved_search (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO feed_saved_search
|
||||
SELECT _id, source, saved_search, global
|
||||
FROM feed_saved_search_temp;
|
||||
|
||||
ALTER TABLE search_metadata RENAME TO search_metadata_temp;
|
||||
CREATE TABLE search_metadata (
|
||||
manga_id INTEGER NOT NULL PRIMARY KEY,
|
||||
uploader TEXT,
|
||||
extra TEXT NOT NULL,
|
||||
indexed_extra TEXT,
|
||||
extra_version INTEGER AS Int NOT NULL,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO search_metadata
|
||||
SELECT manga_id, uploader, extra, indexed_extra, extra_version
|
||||
FROM search_metadata_temp;
|
||||
|
||||
ALTER TABLE search_tags RENAME TO search_tags_temp;
|
||||
CREATE TABLE search_tags (
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
namespace TEXT,
|
||||
name TEXT NOT NULL,
|
||||
type INTEGER AS Int NOT NULL,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO search_tags
|
||||
SELECT _id, manga_id, namespace, name, type
|
||||
FROM search_tags_temp;
|
||||
|
||||
ALTER TABLE search_titles RENAME TO search_titles_temp;
|
||||
CREATE TABLE search_titles (
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
type INTEGER AS Int NOT NULL,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO search_titles
|
||||
SELECT _id, manga_id, title, type
|
||||
FROM search_titles_temp;
|
||||
|
||||
ALTER TABLE merged RENAME TO merged_temp;
|
||||
CREATE TABLE merged(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
info_manga INTEGER AS Boolean NOT NULL,
|
||||
get_chapter_updates INTEGER AS Boolean NOT NULL,
|
||||
chapter_sort_mode INTEGER NOT NULL,
|
||||
chapter_priority INTEGER NOT NULL,
|
||||
download_chapters INTEGER AS Boolean NOT NULL,
|
||||
merge_id INTEGER NOT NULL,
|
||||
merge_url TEXT NOT NULL,
|
||||
manga_id INTEGER,
|
||||
manga_url TEXT NOT NULL,
|
||||
manga_source INTEGER NOT NULL,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE SET NULL,
|
||||
FOREIGN KEY(merge_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO merged
|
||||
SELECT _id,info_manga,get_chapter_updates,chapter_sort_mode,chapter_priority,download_chapters,merge_id,merge_url,manga_id,manga_url,manga_source
|
||||
FROM merged_temp;
|
||||
|
||||
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 history_history_chapter_id_index ON history(history_chapter_id);
|
||||
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
|
||||
CREATE INDEX mangas_url_index ON mangas(url);
|
||||
CREATE INDEX search_metadata_uploader_index ON search_metadata(uploader);
|
||||
CREATE INDEX search_metadata_indexed_extra_index ON search_metadata(indexed_extra);
|
||||
CREATE INDEX search_tags_manga_id_index ON search_tags(manga_id);
|
||||
CREATE INDEX search_tags_namespace_name_index ON search_tags(namespace, name);
|
||||
CREATE INDEX search_titles_manga_id_index ON search_titles(manga_id);
|
||||
CREATE INDEX search_titles_title_index ON search_titles(title);
|
||||
CREATE INDEX merged_merge_id_index ON merged(merge_id);
|
||||
CREATE INDEX feed_saved_search_saved_search_index ON feed_saved_search(saved_search);
|
||||
|
||||
CREATE VIEW IF NOT EXISTS historyView AS
|
||||
SELECT
|
||||
history.history_id AS id,
|
||||
mangas._id AS mangaId,
|
||||
chapters._id AS chapterId,
|
||||
mangas.title,
|
||||
mangas.thumbnail_url AS thumnailUrl,
|
||||
chapters.chapter_number AS chapterNumber,
|
||||
history.history_last_read AS readAt,
|
||||
max_last_read.history_last_read AS maxReadAt,
|
||||
max_last_read.history_chapter_id AS maxReadAtChapterId
|
||||
FROM mangas
|
||||
JOIN chapters
|
||||
ON mangas._id = chapters.manga_id
|
||||
JOIN history
|
||||
ON chapters._id = history.history_chapter_id
|
||||
JOIN (
|
||||
SELECT chapters.manga_id,chapters._id AS history_chapter_id, MAX(history.history_last_read) AS history_last_read
|
||||
FROM chapters JOIN history
|
||||
ON chapters._id = history.history_chapter_id
|
||||
GROUP BY chapters.manga_id
|
||||
) AS max_last_read
|
||||
ON chapters.manga_id = max_last_read.manga_id;
|
||||
|
||||
DROP TABLE IF EXISTS manga_sync_temp;
|
||||
DROP TABLE IF EXISTS mangas_categories_temp;
|
||||
DROP TABLE IF EXISTS history_temp;
|
||||
DROP TABLE IF EXISTS chapters_temp;
|
||||
DROP TABLE IF EXISTS categories_temp;
|
||||
DROP TABLE IF EXISTS eh_favorites_temp;
|
||||
DROP TABLE IF EXISTS saved_search_temp;
|
||||
DROP TABLE IF EXISTS feed_saved_search_temp;
|
||||
DROP TABLE IF EXISTS search_metadata_temp;
|
||||
DROP TABLE IF EXISTS search_tags_temp;
|
||||
DROP TABLE IF EXISTS search_titles_temp;
|
||||
DROP TABLE IF EXISTS merged_temp;
|
||||
DROP TABLE IF EXISTS manga_temp;
|
||||
@@ -0,0 +1,52 @@
|
||||
import java.util.Date;
|
||||
|
||||
DROP INDEX IF EXISTS history_history_chapter_id_index;
|
||||
DROP VIEW IF EXISTS historyView;
|
||||
|
||||
/**
|
||||
* [last_read] was made not-null
|
||||
* [time_read] was kept as long and made non-null
|
||||
* `history` prefix was removed from table name
|
||||
*/
|
||||
ALTER TABLE history RENAME TO history_temp;
|
||||
CREATE TABLE history(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
chapter_id INTEGER NOT NULL UNIQUE,
|
||||
last_read INTEGER AS Date NOT NULL,
|
||||
time_read INTEGER NOT NULL,
|
||||
FOREIGN KEY(chapter_id) REFERENCES chapters (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO history
|
||||
SELECT history_id, history_chapter_id, coalesce(history_last_read, 0), coalesce(history_time_read, 0)
|
||||
FROM history_temp;
|
||||
|
||||
/**
|
||||
* [history.time_read] was added as a column in [historyView]
|
||||
*/
|
||||
CREATE VIEW historyView AS
|
||||
SELECT
|
||||
history._id AS id,
|
||||
mangas._id AS mangaId,
|
||||
chapters._id AS chapterId,
|
||||
mangas.title,
|
||||
mangas.thumbnail_url AS thumbnailUrl,
|
||||
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
|
||||
JOIN history
|
||||
ON chapters._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;
|
||||
|
||||
CREATE INDEX history_history_chapter_id_index ON history(chapter_id);
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE sources(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
lang TEXT NOT NULL,
|
||||
name TEXT NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
DROP VIEW IF EXISTS historyView;
|
||||
|
||||
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
|
||||
JOIN history
|
||||
ON chapters._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;
|
||||
@@ -0,0 +1,17 @@
|
||||
UPDATE mangas
|
||||
SET chapter_flags = 0
|
||||
WHERE chapter_flags = -1;
|
||||
UPDATE mangas
|
||||
SET viewer = 0
|
||||
WHERE viewer = -1;
|
||||
UPDATE mangas
|
||||
SET date_added = 0
|
||||
WHERE date_added = -1;
|
||||
UPDATE mangas
|
||||
SET cover_last_modified = 0
|
||||
WHERE cover_last_modified = -1;
|
||||
UPDATE mangas
|
||||
SET last_update = 0
|
||||
WHERE last_update = -1;
|
||||
UPDATE categories
|
||||
SET manga_order = "";
|
||||
@@ -0,0 +1,20 @@
|
||||
CREATE VIEW updatesView AS
|
||||
SELECT
|
||||
mangas._id AS mangaId,
|
||||
mangas.title AS mangaTitle,
|
||||
chapters._id AS chapterId,
|
||||
chapters.name AS chapterName,
|
||||
chapters.scanlator,
|
||||
chapters.read,
|
||||
chapters.bookmark,
|
||||
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
|
||||
FROM mangas JOIN chapters
|
||||
ON mangas._id = chapters.manga_id
|
||||
WHERE favorite = 1
|
||||
AND date_fetch > date_added
|
||||
ORDER BY date_fetch DESC;
|
||||
@@ -0,0 +1,11 @@
|
||||
ALTER TABLE mangas
|
||||
ADD COLUMN date_added INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE mangas
|
||||
SET date_added = (
|
||||
SELECT MIN(date_fetch)
|
||||
FROM mangas M
|
||||
INNER JOIN chapters C
|
||||
ON M._id = C.manga_id
|
||||
GROUP BY M._id
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Insert Default category
|
||||
INSERT OR IGNORE INTO categories(_id, name, sort, flags, manga_order) 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;
|
||||
@@ -0,0 +1,3 @@
|
||||
UPDATE mangas
|
||||
SET filtered_scanlators = NULL
|
||||
WHERE filtered_scanlators = "";
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE mangas ADD COLUMN update_strategy INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,29 @@
|
||||
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.category_id, 0) AS category
|
||||
FROM mangas M
|
||||
LEFT JOIN(
|
||||
SELECT
|
||||
chapters.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 history
|
||||
ON chapters._id = history.chapter_id
|
||||
GROUP BY chapters.manga_id
|
||||
) AS C
|
||||
ON M._id = C.manga_id
|
||||
LEFT JOIN mangas_categories AS MC
|
||||
ON MC.manga_id = M._id
|
||||
WHERE M.favorite = 1;
|
||||
@@ -0,0 +1,23 @@
|
||||
DROP VIEW IF EXISTS updatesView;
|
||||
|
||||
CREATE VIEW updatesView AS
|
||||
SELECT
|
||||
mangas._id AS mangaId,
|
||||
mangas.title AS mangaTitle,
|
||||
chapters._id AS chapterId,
|
||||
chapters.name AS chapterName,
|
||||
chapters.scanlator,
|
||||
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
|
||||
FROM mangas JOIN chapters
|
||||
ON mangas._id = chapters.manga_id
|
||||
WHERE favorite = 1
|
||||
AND date_fetch > date_added
|
||||
ORDER BY date_fetch DESC;
|
||||
@@ -0,0 +1,19 @@
|
||||
DROP TABLE merged;
|
||||
CREATE TABLE merged(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
info_manga INTEGER AS Boolean NOT NULL,
|
||||
get_chapter_updates INTEGER AS Boolean NOT NULL,
|
||||
chapter_sort_mode INTEGER NOT NULL,
|
||||
chapter_priority INTEGER NOT NULL,
|
||||
download_chapters INTEGER AS Boolean NOT NULL,
|
||||
merge_id INTEGER NOT NULL,
|
||||
merge_url TEXT NOT NULL,
|
||||
manga_id INTEGER,
|
||||
manga_url TEXT NOT NULL,
|
||||
COL_MANGA_SOURCE INTEGER NOT NULL,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE SET NULL,
|
||||
FOREIGN KEY(merge_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX merged_merge_id_index ON merged(merge_id);
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE mangas
|
||||
ADD COLUMN filtered_scanlators TEXT;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS manga_related;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE mangas
|
||||
ADD COLUMN next_update INTEGER DEFAULT 0;
|
||||
@@ -0,0 +1,28 @@
|
||||
ALTER TABLE manga_sync
|
||||
RENAME TO manga_sync_tmp;
|
||||
|
||||
CREATE TABLE manga_sync(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
sync_id INTEGER NOT NULL,
|
||||
remote_id INTEGER 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 AS Float NOT NULL,
|
||||
remote_url TEXT NOT NULL,
|
||||
start_date INTEGER AS Long NOT NULL,
|
||||
finish_date INTEGER AS Long NOT NULL,
|
||||
UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO manga_sync(_id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date)
|
||||
SELECT _id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date
|
||||
FROM manga_sync_tmp;
|
||||
|
||||
|
||||
DROP TABLE manga_sync_tmp;
|
||||
@@ -0,0 +1,3 @@
|
||||
UPDATE chapters
|
||||
SET date_upload = date_fetch
|
||||
WHERE date_upload = 0;
|
||||
@@ -0,0 +1,64 @@
|
||||
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
|
||||
JOIN history
|
||||
ON chapters._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;
|
||||
|
||||
history:
|
||||
SELECT
|
||||
id,
|
||||
mangaId,
|
||||
chapterId,
|
||||
title,
|
||||
thumbnailUrl,
|
||||
source,
|
||||
favorite,
|
||||
cover_last_modified,
|
||||
chapterNumber,
|
||||
readAt,
|
||||
readDuration
|
||||
FROM historyView
|
||||
WHERE historyView.readAt > 0
|
||||
AND maxReadAtChapterId = historyView.chapterId
|
||||
AND lower(historyView.title) LIKE ('%' || :query || '%')
|
||||
ORDER BY readAt DESC;
|
||||
|
||||
getLatestHistory:
|
||||
SELECT
|
||||
id,
|
||||
mangaId,
|
||||
chapterId,
|
||||
title,
|
||||
thumbnailUrl,
|
||||
source,
|
||||
favorite,
|
||||
cover_last_modified,
|
||||
chapterNumber,
|
||||
readAt,
|
||||
readDuration
|
||||
FROM historyView
|
||||
WHERE historyView.readAt > 0
|
||||
ORDER BY readAt DESC
|
||||
LIMIT 1;
|
||||
@@ -0,0 +1,33 @@
|
||||
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.category_id, 0) AS category
|
||||
FROM mangas M
|
||||
LEFT JOIN(
|
||||
SELECT
|
||||
chapters.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 history
|
||||
ON chapters._id = history.chapter_id
|
||||
GROUP BY chapters.manga_id
|
||||
) AS C
|
||||
ON M._id = C.manga_id
|
||||
LEFT JOIN mangas_categories AS MC
|
||||
ON MC.manga_id = M._id
|
||||
WHERE M.favorite = 1;
|
||||
|
||||
library:
|
||||
SELECT *
|
||||
FROM libraryView;
|
||||
@@ -0,0 +1,26 @@
|
||||
CREATE VIEW updatesView AS
|
||||
SELECT
|
||||
mangas._id AS mangaId,
|
||||
mangas.title AS mangaTitle,
|
||||
chapters._id AS chapterId,
|
||||
chapters.name AS chapterName,
|
||||
chapters.scanlator,
|
||||
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
|
||||
FROM mangas JOIN chapters
|
||||
ON mangas._id = chapters.manga_id
|
||||
WHERE favorite = 1
|
||||
AND date_fetch > date_added
|
||||
ORDER BY date_fetch DESC;
|
||||
|
||||
updates:
|
||||
SELECT *
|
||||
FROM updatesView
|
||||
WHERE dateUpload > :after;
|
||||
Reference in New Issue
Block a user