mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-05 03:44:36 -05:00
Non-Extension Index changes for 1.6
This commit is contained in:
@@ -70,6 +70,7 @@ exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exp
|
|||||||
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
|
||||||
exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
|
exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
|
||||||
exposed-kotlintime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }
|
exposed-kotlintime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }
|
||||||
|
exposed-json = { module = "org.jetbrains.exposed:exposed-json ", version.ref = "exposed" }
|
||||||
postgres = "org.postgresql:postgresql:42.7.11"
|
postgres = "org.postgresql:postgresql:42.7.11"
|
||||||
h2 = "com.h2database:h2:2.4.240"
|
h2 = "com.h2database:h2:2.4.240"
|
||||||
hikaricp = "com.zaxxer:HikariCP:7.1.0"
|
hikaricp = "com.zaxxer:HikariCP:7.1.0"
|
||||||
@@ -245,6 +246,7 @@ exposed = [
|
|||||||
"exposed-jdbc",
|
"exposed-jdbc",
|
||||||
"exposed-javatime",
|
"exposed-javatime",
|
||||||
"exposed-kotlintime",
|
"exposed-kotlintime",
|
||||||
|
"exposed-json",
|
||||||
]
|
]
|
||||||
systemtray = [
|
systemtray = [
|
||||||
"systemtray-core",
|
"systemtray-core",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception that handles HTTP codes considered not successful by OkHttp.
|
||||||
|
* Use it to have a standardized error message in the app across the extensions.
|
||||||
|
*
|
||||||
|
* @see Response.isSuccessful
|
||||||
|
* @since tachiyomix 1.6
|
||||||
|
* @param code [Int] the HTTP status code
|
||||||
|
*/
|
||||||
|
class HttpException(
|
||||||
|
val code: Int,
|
||||||
|
) : IllegalStateException("HTTP error $code")
|
||||||
@@ -2,26 +2,25 @@ package eu.kanade.tachiyomi.network
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.cash.quickjs.QuickJs
|
import app.cash.quickjs.QuickJs
|
||||||
import kotlinx.coroutines.Dispatchers
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Util for evaluating JavaScript in sources.
|
* Util for evaluating JavaScript in sources.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("UNUSED", "UNCHECKED_CAST")
|
||||||
class JavaScriptEngine(
|
class JavaScriptEngine(
|
||||||
@Suppress("UNUSED_PARAMETER") context: Context,
|
context: Context,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Evaluate arbitrary JavaScript code and get the result as a primitive type
|
* Evaluate arbitrary JavaScript code and get the result as a primitive type
|
||||||
* (e.g., String, Int).
|
* (e.g., String, Int).
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.4
|
* @since tachiyomix 1.4
|
||||||
* @param script JavaScript to execute.
|
* @param script JavaScript to execute.
|
||||||
* @return Result of JavaScript code as a primitive type.
|
* @return Result of JavaScript code as a primitive type.
|
||||||
*/
|
*/
|
||||||
@Suppress("UNUSED", "UNCHECKED_CAST")
|
|
||||||
suspend fun <T> evaluate(script: String): T =
|
suspend fun <T> evaluate(script: String): T =
|
||||||
withContext(Dispatchers.IO) {
|
withIOContext {
|
||||||
QuickJs.create().use {
|
QuickJs.create().use {
|
||||||
it.evaluate(script) as T
|
it.evaluate(script) as T
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,5 +128,7 @@ class NetworkHelper(
|
|||||||
// val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
|
// val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
|
||||||
val client by lazy { baseClientBuilder.build() }
|
val client by lazy { baseClientBuilder.build() }
|
||||||
|
|
||||||
|
@Deprecated("The regular client handles Cloudflare by default")
|
||||||
|
@Suppress("UNUSED")
|
||||||
val cloudflareClient by lazy { client }
|
val cloudflareClient by lazy { client }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,14 @@ import rx.Observable
|
|||||||
import rx.Producer
|
import rx.Producer
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import kotlin.concurrent.atomics.AtomicBoolean
|
||||||
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
val jsonMime = "application/json; charset=utf-8".toMediaType()
|
val jsonMime = "application/json; charset=utf-8".toMediaType()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAtomicApi::class)
|
||||||
|
@Deprecated("Use suspend APIs instead")
|
||||||
fun Call.asObservable(): Observable<Response> {
|
fun Call.asObservable(): Observable<Response> {
|
||||||
return Observable.unsafeCreate { subscriber ->
|
return Observable.unsafeCreate { subscriber ->
|
||||||
// Since Call is a one-shot type, clone it for each new subscriber.
|
// Since Call is a one-shot type, clone it for each new subscriber.
|
||||||
@@ -27,9 +30,11 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
|
|
||||||
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
// Wrap the call in a helper which handles both unsubscription and backpressure.
|
||||||
val requestArbiter =
|
val requestArbiter =
|
||||||
object : AtomicBoolean(), Producer, Subscription {
|
object : Producer, Subscription {
|
||||||
|
val boolean = AtomicBoolean(false)
|
||||||
|
|
||||||
override fun request(n: Long) {
|
override fun request(n: Long) {
|
||||||
if (n == 0L || !compareAndSet(false, true)) return
|
if (n == 0L || !boolean.compareAndSet(expectedValue = false, newValue = true)) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val response = call.execute()
|
val response = call.execute()
|
||||||
@@ -37,15 +42,15 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
subscriber.onNext(response)
|
subscriber.onNext(response)
|
||||||
subscriber.onCompleted()
|
subscriber.onCompleted()
|
||||||
}
|
}
|
||||||
} catch (error: Exception) {
|
} catch (e: Exception) {
|
||||||
if (!subscriber.isUnsubscribed) {
|
if (!subscriber.isUnsubscribed) {
|
||||||
subscriber.onError(error)
|
subscriber.onError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unsubscribe() {
|
override fun unsubscribe() {
|
||||||
// call.cancel()
|
call.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isUnsubscribed(): Boolean = call.isCanceled()
|
override fun isUnsubscribed(): Boolean = call.isCanceled()
|
||||||
@@ -56,50 +61,50 @@ fun Call.asObservable(): Observable<Response> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Call.asObservableSuccess(): Observable<Response> =
|
@Deprecated("Use suspend APIs instead")
|
||||||
asObservable()
|
fun Call.asObservableSuccess(): Observable<Response> {
|
||||||
.doOnNext { response ->
|
@Suppress("DEPRECATION")
|
||||||
if (!response.isSuccessful) {
|
return asObservable().doOnNext { response ->
|
||||||
response.close()
|
if (!response.isSuccessful) {
|
||||||
throw HttpException(response.code)
|
response.close()
|
||||||
|
throw HttpException(response.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on https://github.com/square/okhttp/blob/master/okhttp-coroutines/src/main/kotlin/okhttp3/coroutines/ExecuteAsync.kt
|
||||||
|
// and https://github.com/gildor/kotlin-coroutines-okhttp
|
||||||
|
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
try {
|
||||||
|
this.cancel()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
this.enqueue(
|
||||||
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
|
||||||
return suspendCancellableCoroutine { continuation ->
|
|
||||||
val callback =
|
|
||||||
object : Callback {
|
object : Callback {
|
||||||
override fun onResponse(
|
|
||||||
call: Call,
|
|
||||||
response: Response,
|
|
||||||
) {
|
|
||||||
continuation.resume(response) { _, resourceToClose, _ ->
|
|
||||||
response.body.close()
|
|
||||||
resourceToClose.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(
|
override fun onFailure(
|
||||||
call: Call,
|
call: Call,
|
||||||
e: IOException,
|
e: IOException,
|
||||||
) {
|
) {
|
||||||
// Don't bother with resuming the continuation if it is already cancelled.
|
|
||||||
if (continuation.isCancelled) return
|
if (continuation.isCancelled) return
|
||||||
val exception = IOException(e.message, e).apply { stackTrace = callStack }
|
val exception = IOException(e.message, e).apply { stackTrace = callStack }
|
||||||
continuation.resumeWithException(exception)
|
continuation.resumeWithException(exception)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
enqueue(callback)
|
override fun onResponse(
|
||||||
|
call: Call,
|
||||||
continuation.invokeOnCancellation {
|
response: Response,
|
||||||
try {
|
) {
|
||||||
cancel()
|
continuation.resume(response) { _, value, _ ->
|
||||||
} catch (ex: Throwable) {
|
value.close()
|
||||||
// Ignore cancel exception
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +114,7 @@ suspend fun Call.await(): Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since extensions-lib 1.5
|
* Similar to [await] but throws [HttpException] if [Response.isSuccessful] returns false
|
||||||
*/
|
*/
|
||||||
suspend fun Call.awaitSuccess(): Response {
|
suspend fun Call.awaitSuccess(): Response {
|
||||||
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
|
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
|
||||||
@@ -150,7 +155,3 @@ fun <T> decodeFromJsonResponse(
|
|||||||
response.body.source().use {
|
response.body.source().use {
|
||||||
json.decodeFromBufferedSource(deserializer, it)
|
json.decodeFromBufferedSource(deserializer, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
class HttpException(
|
|
||||||
val code: Int,
|
|
||||||
) : IllegalStateException("HTTP error $code")
|
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ class ProgressResponseBody(
|
|||||||
val bytesRead = super.read(sink, byteCount)
|
val bytesRead = super.read(sink, byteCount)
|
||||||
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
||||||
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
||||||
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
progressListener.update(
|
||||||
|
totalBytesRead,
|
||||||
|
responseBody.contentLength(),
|
||||||
|
bytesRead == -1L,
|
||||||
|
)
|
||||||
return bytesRead
|
return bytesRead
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import okhttp3.CacheControl
|
|||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import java.util.concurrent.TimeUnit.MINUTES
|
import java.util.concurrent.TimeUnit.MINUTES
|
||||||
@@ -18,13 +19,7 @@ fun GET(
|
|||||||
url: String,
|
url: String,
|
||||||
headers: Headers = DEFAULT_HEADERS,
|
headers: Headers = DEFAULT_HEADERS,
|
||||||
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
cache: CacheControl = DEFAULT_CACHE_CONTROL,
|
||||||
): Request =
|
): Request = GET(url.toHttpUrl(), headers, cache)
|
||||||
Request
|
|
||||||
.Builder()
|
|
||||||
.url(url)
|
|
||||||
.headers(headers)
|
|
||||||
.cacheControl(cache)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since extensions-lib 1.4
|
* @since extensions-lib 1.4
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ package eu.kanade.tachiyomi.source
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.model.SMangaUpdate
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||||
|
|
||||||
@@ -11,68 +17,62 @@ interface CatalogueSource : Source {
|
|||||||
*/
|
*/
|
||||||
override val lang: String
|
override val lang: String
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the source has support for latest updates.
|
|
||||||
*/
|
|
||||||
val supportsLatest: Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a page with a list of manga.
|
|
||||||
*
|
|
||||||
* @since extensions-lib 1.5
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
suspend fun getPopularManga(page: Int): MangasPage = fetchPopularManga(page).awaitSingle()
|
override suspend fun getPopularManga(page: Int): MangasPage = fetchPopularManga(page).awaitSingle()
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a page with a list of manga.
|
|
||||||
*
|
|
||||||
* @since extensions-lib 1.5
|
|
||||||
* @param page the page number to retrieve.
|
|
||||||
* @param query the search query.
|
|
||||||
* @param filters the list of filters to apply.
|
|
||||||
*/
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
suspend fun getSearchManga(
|
override suspend fun getLatestUpdates(page: Int): MangasPage = fetchLatestUpdates(page).awaitSingle()
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override suspend fun getSearchManga(
|
||||||
page: Int,
|
page: Int,
|
||||||
query: String,
|
query: String,
|
||||||
filters: FilterList,
|
filters: FilterList,
|
||||||
): MangasPage = fetchSearchManga(page, query, filters).awaitSingle()
|
): MangasPage = fetchSearchManga(page, query, filters).awaitSingle()
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override suspend fun getMangaUpdate(
|
||||||
|
manga: SManga,
|
||||||
|
chapters: List<SChapter>,
|
||||||
|
fetchDetails: Boolean,
|
||||||
|
fetchChapters: Boolean,
|
||||||
|
): SMangaUpdate =
|
||||||
|
supervisorScope {
|
||||||
|
val asyncManga = if (fetchDetails) async { fetchMangaDetails(manga).awaitSingle() } else null
|
||||||
|
val asyncChapters = if (fetchChapters) async { fetchChapterList(manga).awaitSingle() } else null
|
||||||
|
SMangaUpdate(asyncManga?.await() ?: manga, asyncChapters?.await() ?: chapters)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a page with a list of latest manga updates.
|
* Returns an observable containing a page with a list of manga.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.5
|
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Deprecated("Use the suspend API instead", ReplaceWith("getPopularManga"))
|
||||||
suspend fun getLatestUpdates(page: Int): MangasPage = fetchLatestUpdates(page).awaitSingle()
|
fun fetchPopularManga(page: Int): Observable<MangasPage> = throw UnsupportedOperationException()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of filters for the source.
|
* Returns an observable containing a page with a list of manga.
|
||||||
|
*
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
* @param query the search query.
|
||||||
|
* @param filters the list of filters to apply.
|
||||||
*/
|
*/
|
||||||
fun getFilterList(): FilterList
|
@Deprecated("Use the suspend API instead", ReplaceWith("getSearchManga"))
|
||||||
|
|
||||||
@Deprecated(
|
|
||||||
"Use the non-RxJava API instead",
|
|
||||||
ReplaceWith("getPopularManga"),
|
|
||||||
)
|
|
||||||
fun fetchPopularManga(page: Int): Observable<MangasPage> = throw IllegalStateException("Not used")
|
|
||||||
|
|
||||||
@Deprecated(
|
|
||||||
"Use the non-RxJava API instead",
|
|
||||||
ReplaceWith("getSearchManga"),
|
|
||||||
)
|
|
||||||
fun fetchSearchManga(
|
fun fetchSearchManga(
|
||||||
page: Int,
|
page: Int,
|
||||||
query: String,
|
query: String,
|
||||||
filters: FilterList,
|
filters: FilterList,
|
||||||
): Observable<MangasPage> = throw IllegalStateException("Not used")
|
): Observable<MangasPage> = throw UnsupportedOperationException()
|
||||||
|
|
||||||
@Deprecated(
|
/**
|
||||||
"Use the non-RxJava API instead",
|
* Returns an observable containing a page with a list of latest manga updates.
|
||||||
ReplaceWith("getLatestUpdates"),
|
*
|
||||||
)
|
* @param page the page number to retrieve.
|
||||||
fun fetchLatestUpdates(page: Int): Observable<MangasPage> = throw IllegalStateException("Not used")
|
*/
|
||||||
|
@Deprecated("Use the suspend API instead", ReplaceWith("getLatestUpdates"))
|
||||||
|
fun fetchLatestUpdates(page: Int): Observable<MangasPage> = throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
typealias PreferenceScreen = androidx.preference.PreferenceScreen
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.source
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.model.SMangaUpdate
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A basic interface for creating a source. It could be an online source, a local source, etc.
|
* A basic interface for creating a source. It could be an online source, a local source, etc.
|
||||||
@@ -24,53 +26,86 @@ interface Source {
|
|||||||
get() = ""
|
get() = ""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the updated details for a manga.
|
* Whether the source has support for latest updates.
|
||||||
*
|
|
||||||
* @since extensions-lib 1.5
|
|
||||||
* @param manga the manga to update.
|
|
||||||
* @return the updated manga.
|
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
val supportsLatest: Boolean
|
||||||
suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the available chapters for a manga.
|
* Returns the list of filters for the source.
|
||||||
*
|
|
||||||
* @since extensions-lib 1.5
|
|
||||||
* @param manga the manga to update.
|
|
||||||
* @return the chapters for the manga.
|
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
fun getFilterList(): FilterList = FilterList()
|
||||||
suspend fun getChapterList(manga: SManga): List<SChapter> = fetchChapterList(manga).awaitSingle()
|
|
||||||
|
/**
|
||||||
|
* Get a page with a list of manga.
|
||||||
|
*
|
||||||
|
* @since tachiyomix 1.6
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
suspend fun getPopularManga(page: Int): MangasPage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a page with a list of latest manga updates.
|
||||||
|
*
|
||||||
|
* @since tachiyomix 1.6
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
*/
|
||||||
|
suspend fun getLatestUpdates(page: Int): MangasPage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a page with a list of manga.
|
||||||
|
*
|
||||||
|
* @since tachiyomix 1.6
|
||||||
|
* @param page the page number to retrieve.
|
||||||
|
* @param query the search query.
|
||||||
|
* @param filters the list of filters to apply.
|
||||||
|
*/
|
||||||
|
suspend fun getSearchManga(
|
||||||
|
page: Int,
|
||||||
|
query: String,
|
||||||
|
filters: FilterList,
|
||||||
|
): MangasPage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches updated information for a manga.
|
||||||
|
*
|
||||||
|
* Depending on the provided flags or source availability, this may include
|
||||||
|
* updated manga metadata, available chapters, or both.
|
||||||
|
*
|
||||||
|
* If a value is not requested, the existing provided value can be returned as-is.
|
||||||
|
* The host app may apply any returned updates regardless of the flags,
|
||||||
|
* so care should be taken to only return accurate and intentional changes.
|
||||||
|
*
|
||||||
|
* @since tachiyomix 1.6
|
||||||
|
* @param manga The manga to fetch updates for.
|
||||||
|
* @param chapters Existing chapters of the manga
|
||||||
|
* @param fetchDetails Whether to fetch updated manga details.
|
||||||
|
* @param fetchChapters Whether to fetch available chapters.
|
||||||
|
*/
|
||||||
|
suspend fun getMangaUpdate(
|
||||||
|
manga: SManga,
|
||||||
|
chapters: List<SChapter>,
|
||||||
|
fetchDetails: Boolean,
|
||||||
|
fetchChapters: Boolean,
|
||||||
|
): SMangaUpdate
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the list of pages a chapter has. Pages should be returned
|
* Get the list of pages a chapter has. Pages should be returned
|
||||||
* in the expected order; the index is ignored.
|
* in the expected order; the index is ignored.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.5
|
* @since tachiyomix 1.6
|
||||||
* @param chapter the chapter.
|
* @param chapter the chapter.
|
||||||
* @return the pages for the chapter.
|
* @return the pages for the chapter.
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
suspend fun getPageList(chapter: SChapter): List<Page>
|
||||||
suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
|
|
||||||
|
|
||||||
@Deprecated(
|
@Deprecated("Use the combined suspend API instead", ReplaceWith("getMangaUpdate"))
|
||||||
"Use the non-RxJava API instead",
|
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw UnsupportedOperationException()
|
||||||
ReplaceWith("getMangaDetails"),
|
|
||||||
)
|
|
||||||
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used")
|
|
||||||
|
|
||||||
@Deprecated(
|
@Deprecated("Use the combined suspend API instead", ReplaceWith("getMangaUpdate"))
|
||||||
"Use the non-RxJava API instead",
|
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw UnsupportedOperationException()
|
||||||
ReplaceWith("getChapterList"),
|
|
||||||
)
|
|
||||||
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used")
|
|
||||||
|
|
||||||
@Deprecated(
|
@Deprecated("Use the suspend API instead", ReplaceWith("getPageList"))
|
||||||
"Use the non-RxJava API instead",
|
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = throw UnsupportedOperationException()
|
||||||
ReplaceWith("getPageList"),
|
|
||||||
)
|
|
||||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = throw IllegalStateException("Not used")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
// fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ import eu.kanade.tachiyomi.source.model.MangasPage
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.model.SMangaUpdate
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||||
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
|
||||||
import eu.kanade.tachiyomi.util.storage.EpubFile
|
import eu.kanade.tachiyomi.util.storage.EpubFile
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
@@ -167,8 +170,19 @@ class LocalSource(
|
|||||||
return MangasPage(mangas.toList(), false)
|
return MangasPage(mangas.toList(), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getMangaUpdate(
|
||||||
|
manga: SManga,
|
||||||
|
chapters: List<SChapter>,
|
||||||
|
fetchDetails: Boolean,
|
||||||
|
fetchChapters: Boolean,
|
||||||
|
): SMangaUpdate = supervisorScope {
|
||||||
|
val asyncManga = if (fetchDetails) async { getMangaDetails(manga) } else null
|
||||||
|
val asyncChapters = if (fetchChapters) async { getChapterList(manga) } else null
|
||||||
|
SMangaUpdate(asyncManga?.await() ?: manga, asyncChapters?.await() ?: chapters)
|
||||||
|
}
|
||||||
|
|
||||||
// Manga details related
|
// Manga details related
|
||||||
override suspend fun getMangaDetails(manga: SManga): SManga =
|
private suspend fun getMangaDetails(manga: SManga): SManga =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
coverManager.find(manga.url)?.let {
|
coverManager.find(manga.url)?.let {
|
||||||
manga.thumbnail_url = it.absolutePath
|
manga.thumbnail_url = it.absolutePath
|
||||||
@@ -289,7 +303,7 @@ class LocalSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Chapters
|
// Chapters
|
||||||
override suspend fun getChapterList(manga: SManga): List<SChapter> =
|
private suspend fun getChapterList(manga: SManga): List<SChapter> =
|
||||||
fileSystem
|
fileSystem
|
||||||
.getFilesInMangaDirectory(manga.url)
|
.getFilesInMangaDirectory(manga.url)
|
||||||
// Only keep supported formats
|
// Only keep supported formats
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
data class MangasPage(
|
class MangasPage(
|
||||||
val mangas: List<SManga>,
|
val mangas: List<SManga>,
|
||||||
val hasNextPage: Boolean,
|
val hasNextPage: Boolean,
|
||||||
)
|
) {
|
||||||
|
@Deprecated("MangasPage is now a regular class")
|
||||||
|
operator fun component1(): List<SManga> = mangas
|
||||||
|
|
||||||
|
@Deprecated("MangasPage is now a regular class")
|
||||||
|
operator fun component2(): Boolean = hasNextPage
|
||||||
|
|
||||||
|
@Deprecated("MangasPage is now a regular class")
|
||||||
|
fun copy(
|
||||||
|
mangas: List<SManga> = this.mangas,
|
||||||
|
hasNextPage: Boolean = this.hasNextPage,
|
||||||
|
): MangasPage =
|
||||||
|
MangasPage(
|
||||||
|
mangas = mangas,
|
||||||
|
hasNextPage = hasNextPage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,12 +27,4 @@ open class Page(
|
|||||||
-1
|
-1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val QUEUE = 0
|
|
||||||
const val LOAD_PAGE = 1
|
|
||||||
const val DOWNLOAD_IMAGE = 2
|
|
||||||
const val READY = 3
|
|
||||||
const val ERROR = 4
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface SChapter : Serializable {
|
interface SChapter : Serializable {
|
||||||
@@ -9,12 +10,25 @@ interface SChapter : Serializable {
|
|||||||
|
|
||||||
var name: String
|
var name: String
|
||||||
|
|
||||||
var date_upload: Long
|
|
||||||
|
|
||||||
var chapter_number: Float
|
var chapter_number: Float
|
||||||
|
|
||||||
var scanlator: String?
|
var scanlator: String?
|
||||||
|
|
||||||
|
var date_upload: Long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra metadata associated with the chapter.
|
||||||
|
*
|
||||||
|
* The JSON object is not visible to users and intended for internal or source-specific
|
||||||
|
* purposes. Apps may define their own namespaced keys (e.g., `"mihon.*"`) for sources to populate.
|
||||||
|
*
|
||||||
|
* This allows apps to attach and ask for custom information without affecting the visible
|
||||||
|
* chapter data.
|
||||||
|
*
|
||||||
|
* @since tachiyomix 1.6
|
||||||
|
*/
|
||||||
|
var memo: JsonObject
|
||||||
|
|
||||||
fun copyFrom(other: SChapter) {
|
fun copyFrom(other: SChapter) {
|
||||||
name = other.name
|
name = other.name
|
||||||
url = other.url
|
url = other.url
|
||||||
|
|||||||
@@ -2,14 +2,18 @@
|
|||||||
|
|
||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
|
||||||
class SChapterImpl : SChapter {
|
class SChapterImpl : SChapter {
|
||||||
override lateinit var url: String
|
override lateinit var url: String
|
||||||
|
|
||||||
override lateinit var name: String
|
override lateinit var name: String
|
||||||
|
|
||||||
override var date_upload: Long = 0
|
|
||||||
|
|
||||||
override var chapter_number: Float = -1f
|
override var chapter_number: Float = -1f
|
||||||
|
|
||||||
override var scanlator: String? = null
|
override var scanlator: String? = null
|
||||||
|
|
||||||
|
override var date_upload: Long = 0
|
||||||
|
|
||||||
|
override var memo: JsonObject = JsonObject(emptyMap())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface SManga : Serializable {
|
interface SManga : Serializable {
|
||||||
@@ -9,22 +10,58 @@ interface SManga : Serializable {
|
|||||||
|
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
|
var thumbnail_url: String?
|
||||||
|
|
||||||
var artist: String?
|
var artist: String?
|
||||||
|
|
||||||
var author: String?
|
var author: String?
|
||||||
|
|
||||||
|
var status: Int
|
||||||
|
|
||||||
var description: String?
|
var description: String?
|
||||||
|
|
||||||
var genre: String?
|
var genre: String?
|
||||||
|
|
||||||
var status: Int
|
|
||||||
|
|
||||||
var thumbnail_url: String?
|
|
||||||
|
|
||||||
var update_strategy: UpdateStrategy
|
var update_strategy: UpdateStrategy
|
||||||
|
|
||||||
var initialized: Boolean
|
var initialized: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra metadata associated with the manga.
|
||||||
|
*
|
||||||
|
* The JSON object is not visible to users and intended for internal or source-specific
|
||||||
|
* purposes. Apps may define their own namespaced keys (e.g., `"mihon.*"`) for sources to populate.
|
||||||
|
*
|
||||||
|
* This allows apps to attach and ask for custom information without affecting the visible
|
||||||
|
* manga data.
|
||||||
|
*
|
||||||
|
* @since tachiyomix 1.6
|
||||||
|
*/
|
||||||
|
var memo: JsonObject
|
||||||
|
|
||||||
|
fun getGenres(): List<String>? {
|
||||||
|
if (genre.isNullOrBlank()) return null
|
||||||
|
return genre
|
||||||
|
?.split(", ")
|
||||||
|
?.map { it.trim() }
|
||||||
|
?.filterNot { it.isBlank() }
|
||||||
|
?.distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copy() =
|
||||||
|
create().also {
|
||||||
|
it.url = url
|
||||||
|
it.title = title
|
||||||
|
it.artist = artist
|
||||||
|
it.author = author
|
||||||
|
it.description = description
|
||||||
|
it.genre = genre
|
||||||
|
it.status = status
|
||||||
|
it.thumbnail_url = thumbnail_url
|
||||||
|
it.update_strategy = update_strategy
|
||||||
|
it.initialized = initialized
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val UNKNOWN = 0
|
const val UNKNOWN = 0
|
||||||
const val ONGOING = 1
|
const val ONGOING = 1
|
||||||
@@ -37,30 +74,3 @@ interface SManga : Serializable {
|
|||||||
fun create(): SManga = SMangaImpl()
|
fun create(): SManga = SMangaImpl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun SManga.toMangaInfo(): MangaInfo {
|
|
||||||
// return MangaInfo(
|
|
||||||
// key = this.url,
|
|
||||||
// title = this.title,
|
|
||||||
// artist = this.artist ?: "",
|
|
||||||
// author = this.author ?: "",
|
|
||||||
// description = this.description ?: "",
|
|
||||||
// genres = this.genre?.split(", ") ?: emptyList(),
|
|
||||||
// status = this.status,
|
|
||||||
// cover = this.thumbnail_url ?: ""
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// fun MangaInfo.toSManga(): SManga {
|
|
||||||
// val mangaInfo = this
|
|
||||||
// return SManga.create().apply {
|
|
||||||
// url = mangaInfo.key
|
|
||||||
// title = mangaInfo.title
|
|
||||||
// artist = mangaInfo.artist
|
|
||||||
// author = mangaInfo.author
|
|
||||||
// description = mangaInfo.description
|
|
||||||
// genre = mangaInfo.genres.joinToString(", ")
|
|
||||||
// status = mangaInfo.status
|
|
||||||
// thumbnail_url = mangaInfo.cover
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -2,24 +2,28 @@
|
|||||||
|
|
||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
|
||||||
class SMangaImpl : SManga {
|
class SMangaImpl : SManga {
|
||||||
override lateinit var url: String
|
override lateinit var url: String
|
||||||
|
|
||||||
override lateinit var title: String
|
override lateinit var title: String
|
||||||
|
|
||||||
|
override var thumbnail_url: String? = null
|
||||||
|
|
||||||
override var artist: String? = null
|
override var artist: String? = null
|
||||||
|
|
||||||
override var author: String? = null
|
override var author: String? = null
|
||||||
|
|
||||||
|
override var status: Int = 0
|
||||||
|
|
||||||
override var description: String? = null
|
override var description: String? = null
|
||||||
|
|
||||||
override var genre: String? = null
|
override var genre: String? = null
|
||||||
|
|
||||||
override var status: Int = 0
|
|
||||||
|
|
||||||
override var thumbnail_url: String? = null
|
|
||||||
|
|
||||||
override var update_strategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE
|
override var update_strategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE
|
||||||
|
|
||||||
override var initialized: Boolean = false
|
override var initialized: Boolean = false
|
||||||
|
|
||||||
|
override var memo: JsonObject = JsonObject(emptyMap())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
@Suppress("UNUSED")
|
||||||
|
class SMangaUpdate(
|
||||||
|
val manga: SManga,
|
||||||
|
val chapters: List<SChapter>,
|
||||||
|
)
|
||||||
@@ -1,6 +1,22 @@
|
|||||||
package eu.kanade.tachiyomi.source.model
|
package eu.kanade.tachiyomi.source.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the update strategy for a single [SManga].
|
||||||
|
* The strategy used will only take effect on the library update.
|
||||||
|
*
|
||||||
|
* @since extensions-lib 1.4
|
||||||
|
*/
|
||||||
enum class UpdateStrategy {
|
enum class UpdateStrategy {
|
||||||
|
/**
|
||||||
|
* Series marked as always update will be included in the library
|
||||||
|
* update if they aren't excluded by additional restrictions.
|
||||||
|
*/
|
||||||
ALWAYS_UPDATE,
|
ALWAYS_UPDATE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Series marked as only fetch once will be automatically skipped
|
||||||
|
* during library updates. Useful for cases where the series is previously
|
||||||
|
* known to be finished and have only a single chapter, for example.
|
||||||
|
*/
|
||||||
ONLY_FETCH_ONCE,
|
ONLY_FETCH_ONCE,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import java.security.MessageDigest
|
|||||||
/**
|
/**
|
||||||
* A simple implementation for sources from a website.
|
* A simple implementation for sources from a website.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
|
||||||
abstract class HttpSource : CatalogueSource {
|
abstract class HttpSource : CatalogueSource {
|
||||||
/**
|
/**
|
||||||
* Network service.
|
* Network service.
|
||||||
@@ -37,11 +36,24 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*/
|
*/
|
||||||
abstract val baseUrl: String
|
abstract val baseUrl: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the base (home) URL of the website as a string.
|
||||||
|
*
|
||||||
|
* This is typically the root address that serves as the main entry point
|
||||||
|
* to the site's content, such as "https://mihon.tech".
|
||||||
|
*
|
||||||
|
* This method is used in the browse screen to determine the URL
|
||||||
|
* opened when tapping "Open in WebView".
|
||||||
|
*
|
||||||
|
* @return The website’s home page URL. Defaults to [baseUrl].
|
||||||
|
*/
|
||||||
|
open fun getHomeUrl(): String = baseUrl
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version id used to generate the source id. If the site completely changes and urls are
|
* Version id used to generate the source id. If the site completely changes and urls are
|
||||||
* incompatible, you may increase this value and it'll be considered as a new source.
|
* incompatible, you may increase this value and it'll be considered as a new source.
|
||||||
*/
|
*/
|
||||||
open val versionId = 1
|
open val versionId: Int = 1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ID of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
* ID of the source. By default it uses a generated id using the first 16 characters (64 bits)
|
||||||
@@ -53,7 +65,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* Note: the generated ID sets the sign bit to `0`.
|
* Note: the generated ID sets the sign bit to `0`.
|
||||||
*/
|
*/
|
||||||
override val id by lazy { generateId() }
|
override val id: Long by lazy { generateId(name, lang, versionId) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Headers used for requests.
|
* Headers used for requests.
|
||||||
@@ -63,10 +75,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Default network client for doing requests.
|
* Default network client for doing requests.
|
||||||
*/
|
*/
|
||||||
open val client: OkHttpClient
|
open val client: OkHttpClient get() = network.client
|
||||||
get() = network.client
|
|
||||||
|
|
||||||
private fun generateId(): Long = generateId("${name.lowercase()}/$lang/$versionId")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a unique ID for the source based on the provided [name], [lang] and
|
* Generates a unique ID for the source based on the provided [name], [lang] and
|
||||||
@@ -91,10 +100,6 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
versionId: Int,
|
versionId: Int,
|
||||||
): Long {
|
): Long {
|
||||||
val key = "${name.lowercase()}/$lang/$versionId"
|
val key = "${name.lowercase()}/$lang/$versionId"
|
||||||
return generateId(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun generateId(key: String): Long {
|
|
||||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
|
||||||
}
|
}
|
||||||
@@ -102,7 +107,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Headers builder for requests. Implementations can override this method for custom headers.
|
* Headers builder for requests. Implementations can override this method for custom headers.
|
||||||
*/
|
*/
|
||||||
protected open fun headersBuilder() =
|
protected open fun headersBuilder(): Headers.Builder =
|
||||||
Headers.Builder().apply {
|
Headers.Builder().apply {
|
||||||
add("User-Agent", network.defaultUserAgentProvider())
|
add("User-Agent", network.defaultUserAgentProvider())
|
||||||
}
|
}
|
||||||
@@ -110,7 +115,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
/**
|
/**
|
||||||
* Visible name of the source.
|
* Visible name of the source.
|
||||||
*/
|
*/
|
||||||
override fun toString() = "$name (${lang.uppercase()})"
|
override fun toString(): String = "$name (${lang.uppercase()})"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||||
@@ -118,7 +123,8 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("Use the suspend API instead", ReplaceWith("getPopularManga"))
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> =
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> =
|
||||||
client
|
client
|
||||||
.newCall(popularMangaRequest(page))
|
.newCall(popularMangaRequest(page))
|
||||||
@@ -132,14 +138,24 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
protected abstract fun popularMangaRequest(page: Int): Request
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
|
protected open fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
protected abstract fun popularMangaParse(response: Response): MangasPage
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
|
protected open fun popularMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
* Returns an observable containing a page with a list of manga. Normally it's not needed to
|
||||||
@@ -149,22 +165,17 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @param filters the list of filters to apply.
|
||||||
*/
|
*/
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("Use the suspend API instead", ReplaceWith("getSearchManga"))
|
||||||
override fun fetchSearchManga(
|
override fun fetchSearchManga(
|
||||||
page: Int,
|
page: Int,
|
||||||
query: String,
|
query: String,
|
||||||
filters: FilterList,
|
filters: FilterList,
|
||||||
): Observable<MangasPage> =
|
): Observable<MangasPage> =
|
||||||
Observable
|
client
|
||||||
.defer {
|
.newCall(searchMangaRequest(page, query, filters))
|
||||||
try {
|
.asObservableSuccess()
|
||||||
client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess()
|
.map { response ->
|
||||||
} catch (e: NoClassDefFoundError) {
|
|
||||||
// RxJava doesn't handle Errors, which tends to happen during global searches
|
|
||||||
// if an old extension using non-existent classes is still around
|
|
||||||
throw RuntimeException(e)
|
|
||||||
}
|
|
||||||
}.map { response ->
|
|
||||||
searchMangaParse(response)
|
searchMangaParse(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,25 +186,36 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @param filters the list of filters to apply.
|
||||||
*/
|
*/
|
||||||
protected abstract fun searchMangaRequest(
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
|
protected open fun searchMangaRequest(
|
||||||
page: Int,
|
page: Int,
|
||||||
query: String,
|
query: String,
|
||||||
filters: FilterList,
|
filters: FilterList,
|
||||||
): Request
|
): Request = throw UnsupportedOperationException()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
protected abstract fun searchMangaParse(response: Response): MangasPage
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
|
protected open fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable containing a page with a list of latest manga updates.
|
* Returns an observable containing a page with a list of latest manga updates.
|
||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated("Use the suspend API instead", ReplaceWith("getLatestUpdates"))
|
||||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
|
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
|
||||||
client
|
client
|
||||||
.newCall(latestUpdatesRequest(page))
|
.newCall(latestUpdatesRequest(page))
|
||||||
@@ -207,26 +229,33 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param page the page number to retrieve.
|
* @param page the page number to retrieve.
|
||||||
*/
|
*/
|
||||||
protected abstract fun latestUpdatesRequest(page: Int): Request
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
|
protected open fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
protected abstract fun latestUpdatesParse(response: Response): MangasPage
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
|
protected open fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the updated details for a manga.
|
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||||
* Normally it's not needed to override this method.
|
* override this method.
|
||||||
*
|
*
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to be updated.
|
||||||
* @return the updated manga.
|
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
override suspend fun getMangaDetails(manga: SManga): SManga = fetchMangaDetails(manga).awaitSingle()
|
@Deprecated("Use the combined suspend API instead", replaceWith = ReplaceWith("getMangaUpdate"))
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||||
client
|
client
|
||||||
.newCall(mangaDetailsRequest(manga))
|
.newCall(mangaDetailsRequest(manga))
|
||||||
@@ -241,6 +270,11 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param manga the manga to be updated.
|
* @param manga the manga to be updated.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
open fun mangaDetailsRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
open fun mangaDetailsRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -248,37 +282,28 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
protected abstract fun mangaDetailsParse(response: Response): SManga
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
|
protected open fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all the available chapters for a manga.
|
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
||||||
* Normally it's not needed to override this method.
|
* override this method.
|
||||||
*
|
*
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to look for chapters.
|
||||||
* @return the chapters for the manga.
|
|
||||||
* @throws LicensedMangaChaptersException if a manga is licensed and therefore no chapters are available.
|
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
@Deprecated("Use the combined suspend API instead", replaceWith = ReplaceWith("getMangaUpdate"))
|
||||||
if (manga.status == SManga.LICENSED) {
|
|
||||||
throw LicensedMangaChaptersException()
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetchChapterList(manga).awaitSingle()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
|
||||||
if (manga.status != SManga.LICENSED) {
|
client
|
||||||
client
|
.newCall(chapterListRequest(manga))
|
||||||
.newCall(chapterListRequest(manga))
|
.asObservableSuccess()
|
||||||
.asObservableSuccess()
|
.map { response ->
|
||||||
.map { response ->
|
chapterListParse(response)
|
||||||
chapterListParse(response)
|
}
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Observable.error(LicensedMangaChaptersException())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the request for updating the chapter list. Override only if it's needed to override
|
* Returns the request for updating the chapter list. Override only if it's needed to override
|
||||||
@@ -286,6 +311,11 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param manga the manga to look for chapters.
|
* @param manga the manga to look for chapters.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
protected open fun chapterListRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
protected open fun chapterListRequest(manga: SManga): Request = GET(baseUrl + manga.url, headers)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -293,19 +323,20 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
protected abstract fun chapterListParse(response: Response): List<SChapter>
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
|
protected open fun chapterListParse(response: Response): List<SChapter> = throw UnsupportedOperationException()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the list of pages a chapter has. Pages should be returned
|
* Returns an observable with the page list for a chapter.
|
||||||
* in the expected order; the index is ignored.
|
|
||||||
*
|
*
|
||||||
* @param chapter the chapter.
|
* @param chapter the chapter whose page list has to be fetched.
|
||||||
* @return the pages for the chapter.
|
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
override suspend fun getPageList(chapter: SChapter): List<Page> = fetchPageList(chapter).awaitSingle()
|
@Deprecated("Use the suspend API instead", ReplaceWith("getPageList"))
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
|
||||||
client
|
client
|
||||||
.newCall(pageListRequest(chapter))
|
.newCall(pageListRequest(chapter))
|
||||||
@@ -320,6 +351,11 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param chapter the chapter whose page list has to be fetched.
|
* @param chapter the chapter whose page list has to be fetched.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
protected open fun pageListRequest(chapter: SChapter): Request = GET(baseUrl + chapter.url, headers)
|
protected open fun pageListRequest(chapter: SChapter): Request = GET(baseUrl + chapter.url, headers)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -327,31 +363,47 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
protected abstract fun pageListParse(response: Response): List<Page>
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
|
protected open fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the page containing the source url of the image. If there's any
|
* Returns an observable with the page containing the source url of the image. If there's any
|
||||||
* error, it will return null instead of throwing an exception.
|
* error, it will return null instead of throwing an exception.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.5
|
|
||||||
* @param page the page whose source image has to be fetched.
|
* @param page the page whose source image has to be fetched.
|
||||||
*/
|
*/
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
open suspend fun getImageUrl(page: Page): String = fetchImageUrl(page).awaitSingle()
|
@Deprecated("Use the suspend API instead", ReplaceWith("getImageUrl"))
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
|
|
||||||
open fun fetchImageUrl(page: Page): Observable<String> =
|
open fun fetchImageUrl(page: Page): Observable<String> =
|
||||||
client
|
client
|
||||||
.newCall(imageUrlRequest(page))
|
.newCall(imageUrlRequest(page))
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
.map { imageUrlParse(it) }
|
.map { imageUrlParse(it) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the image url for the provided [page]. The function is only called if [Page.imageUrl] is null.
|
||||||
|
*
|
||||||
|
* @since tachiyomix 1.6
|
||||||
|
* @param page the page whose source image has to be fetched.
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
open suspend fun getImageUrl(page: Page): String = fetchImageUrl(page).awaitSingle()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the request for getting the url to the source image. Override only if it's needed to
|
* Returns the request for getting the url to the source image. Override only if it's needed to
|
||||||
* override the url, send different headers or request method like POST.
|
* override the url, send different headers or request method like POST.
|
||||||
*
|
*
|
||||||
* @param page the chapter whose page list has to be fetched
|
* @param page the chapter whose page list has to be fetched
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
protected open fun imageUrlRequest(page: Page): Request = GET(page.url, headers)
|
protected open fun imageUrlRequest(page: Page): Request = GET(page.url, headers)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -359,16 +411,14 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
protected abstract fun imageUrlParse(response: Response): String
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
|
protected open fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
/**
|
suspend fun getImage(page: Page): Response =
|
||||||
* Returns the response of the source image.
|
|
||||||
* Typically does not need to be overridden.
|
|
||||||
*
|
|
||||||
* @since extensions-lib 1.5
|
|
||||||
* @param page the page whose source image has to be downloaded.
|
|
||||||
*/
|
|
||||||
open suspend fun getImage(page: Page): Response =
|
|
||||||
client
|
client
|
||||||
.newCachelessCallWithProgress(imageRequest(page), page)
|
.newCachelessCallWithProgress(imageRequest(page), page)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
@@ -387,6 +437,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param url the full url to the chapter.
|
* @param url the full url to the chapter.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("Unused")
|
||||||
fun SChapter.setUrlWithoutDomain(url: String) {
|
fun SChapter.setUrlWithoutDomain(url: String) {
|
||||||
this.url = getUrlWithoutDomain(url)
|
this.url = getUrlWithoutDomain(url)
|
||||||
}
|
}
|
||||||
@@ -397,6 +448,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
*
|
*
|
||||||
* @param url the full url to the manga.
|
* @param url the full url to the manga.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("Unused")
|
||||||
fun SManga.setUrlWithoutDomain(url: String) {
|
fun SManga.setUrlWithoutDomain(url: String) {
|
||||||
this.url = getUrlWithoutDomain(url)
|
this.url = getUrlWithoutDomain(url)
|
||||||
}
|
}
|
||||||
@@ -417,7 +469,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
out += "#" + uri.fragment
|
out += "#" + uri.fragment
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
} catch (e: URISyntaxException) {
|
} catch (_: URISyntaxException) {
|
||||||
orig
|
orig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,6 +480,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* @param manga the manga
|
* @param manga the manga
|
||||||
* @return url of the manga
|
* @return url of the manga
|
||||||
*/
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
open fun getMangaUrl(manga: SManga): String = mangaDetailsRequest(manga).url.toString()
|
open fun getMangaUrl(manga: SManga): String = mangaDetailsRequest(manga).url.toString()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -437,6 +490,7 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* @param chapter the chapter
|
* @param chapter the chapter
|
||||||
* @return url of the chapter
|
* @return url of the chapter
|
||||||
*/
|
*/
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
open fun getChapterUrl(chapter: SChapter): String = pageListRequest(chapter).url.toString()
|
open fun getChapterUrl(chapter: SChapter): String = pageListRequest(chapter).url.toString()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -446,15 +500,9 @@ abstract class HttpSource : CatalogueSource {
|
|||||||
* @param chapter the chapter to be added.
|
* @param chapter the chapter to be added.
|
||||||
* @param manga the manga of the chapter.
|
* @param manga the manga of the chapter.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated("All modifications should be done when constructing the chapter")
|
||||||
open fun prepareNewChapter(
|
open fun prepareNewChapter(
|
||||||
chapter: SChapter,
|
chapter: SChapter,
|
||||||
manga: SManga,
|
manga: SManga,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the list of filters for the source.
|
|
||||||
*/
|
|
||||||
override fun getFilterList() = FilterList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class LicensedMangaChaptersException : Exception("Licensed - No chapters to show")
|
|
||||||
|
|||||||
@@ -12,12 +12,20 @@ import org.jsoup.nodes.Element
|
|||||||
/**
|
/**
|
||||||
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
* A simple implementation for sources from a website using Jsoup, an HTML parser.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
message =
|
||||||
|
"In most cases sources only require a subset of the methods from this class. " +
|
||||||
|
"Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
abstract class ParsedHttpSource : HttpSource() {
|
abstract class ParsedHttpSource : HttpSource() {
|
||||||
/**
|
/**
|
||||||
* Parses the response from the site and returns a [MangasPage] object.
|
* Parses the response from the site and returns a [MangasPage] object.
|
||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
override fun popularMangaParse(response: Response): MangasPage {
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
@@ -58,6 +66,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
@@ -98,6 +109,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
|
|
||||||
@@ -138,6 +152,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
override fun mangaDetailsParse(response: Response): SManga = mangaDetailsParse(response.asJsoup())
|
override fun mangaDetailsParse(response: Response): SManga = mangaDetailsParse(response.asJsoup())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -152,6 +169,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
val document = response.asJsoup()
|
val document = response.asJsoup()
|
||||||
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
return document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||||
@@ -174,6 +194,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
override fun pageListParse(response: Response): List<Page> = pageListParse(response.asJsoup())
|
override fun pageListParse(response: Response): List<Page> = pageListParse(response.asJsoup())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,6 +211,9 @@ abstract class ParsedHttpSource : HttpSource() {
|
|||||||
*
|
*
|
||||||
* @param response the response from the site.
|
* @param response the response from the site.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
"The helper functions are inherently limiting and hides the underlying implementation. Source developers should make their own implementation according to their needs.",
|
||||||
|
)
|
||||||
override fun imageUrlParse(response: Response): String = imageUrlParse(response.asJsoup())
|
override fun imageUrlParse(response: Response): String = imageUrlParse(response.asJsoup())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,26 +1,44 @@
|
|||||||
package eu.kanade.tachiyomi.source.online
|
package eu.kanade.tachiyomi.source.online
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A source that may handle opening an SManga for a given URI.
|
* A source that may handle opening an SManga or SChapter for a given URI.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.5
|
* @since extensions-lib 1.5
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
|
||||||
interface ResolvableSource : Source {
|
interface ResolvableSource : Source {
|
||||||
/**
|
/**
|
||||||
* Whether this source may potentially handle the given URI.
|
* Returns what the given URI may open.
|
||||||
|
* Returns [UriType.Unknown] if the source is not able to resolve the URI.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.5
|
* @since extensions-lib 1.5
|
||||||
*/
|
*/
|
||||||
fun canResolveUri(uri: String): Boolean
|
fun getUriType(uri: String): UriType
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called if canHandleUri is true. Returns the corresponding SManga, if possible.
|
* Called if [getUriType] is [UriType.Manga].
|
||||||
|
* Returns the corresponding SManga, if possible.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.5
|
* @since extensions-lib 1.5
|
||||||
*/
|
*/
|
||||||
suspend fun getManga(uri: String): SManga?
|
suspend fun getManga(uri: String): SManga?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called if [getUriType] is [UriType.Chapter].
|
||||||
|
* Returns the corresponding SChapter, if possible.
|
||||||
|
*
|
||||||
|
* @since extensions-lib 1.5
|
||||||
|
*/
|
||||||
|
suspend fun getChapter(uri: String): SChapter?
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface UriType {
|
||||||
|
data object Manga : UriType
|
||||||
|
|
||||||
|
data object Chapter : UriType
|
||||||
|
|
||||||
|
data object Unknown : UriType
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
|
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.exposed.v1.core.LikePattern
|
import org.jetbrains.exposed.v1.core.LikePattern
|
||||||
@@ -167,6 +168,7 @@ class ChapterMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
|
@GraphQLDeprecated("Deprecated in Tachiyomix 1.6", ReplaceWith("fetchMangaAndChapters"))
|
||||||
fun fetchChapters(input: FetchChaptersInput): CompletableFuture<FetchChaptersPayload?> {
|
fun fetchChapters(input: FetchChaptersInput): CompletableFuture<FetchChaptersPayload?> {
|
||||||
val (clientMutationId, mangaId) = input
|
val (clientMutationId, mangaId) = input
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package suwayomi.tachidesk.graphql.mutations
|
package suwayomi.tachidesk.graphql.mutations
|
||||||
|
|
||||||
|
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||||
import org.jetbrains.exposed.v1.core.LikePattern
|
import org.jetbrains.exposed.v1.core.LikePattern
|
||||||
import org.jetbrains.exposed.v1.core.Op
|
import org.jetbrains.exposed.v1.core.Op
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
@@ -14,12 +15,16 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
|||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||||
|
import suwayomi.tachidesk.graphql.types.ChapterType
|
||||||
import suwayomi.tachidesk.graphql.types.MangaMetaType
|
import suwayomi.tachidesk.graphql.types.MangaMetaType
|
||||||
import suwayomi.tachidesk.graphql.types.MangaType
|
import suwayomi.tachidesk.graphql.types.MangaType
|
||||||
import suwayomi.tachidesk.graphql.types.MetaInput
|
import suwayomi.tachidesk.graphql.types.MetaInput
|
||||||
|
import suwayomi.tachidesk.manga.impl.Chapter
|
||||||
import suwayomi.tachidesk.manga.impl.Library
|
import suwayomi.tachidesk.manga.impl.Library
|
||||||
import suwayomi.tachidesk.manga.impl.Manga
|
import suwayomi.tachidesk.manga.impl.Manga
|
||||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||||
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||||
@@ -146,6 +151,7 @@ class MangaMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@RequireAuth
|
@RequireAuth
|
||||||
|
@GraphQLDeprecated("Deprecated in Tachiyomix 1.6", ReplaceWith("fetchMangaAndChapters"))
|
||||||
fun fetchManga(input: FetchMangaInput): CompletableFuture<FetchMangaPayload?> {
|
fun fetchManga(input: FetchMangaInput): CompletableFuture<FetchMangaPayload?> {
|
||||||
val (clientMutationId, id) = input
|
val (clientMutationId, id) = input
|
||||||
|
|
||||||
@@ -163,6 +169,58 @@ class MangaMutation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class FetchMangaAndChaptersInput(
|
||||||
|
val clientMutationId: String? = null,
|
||||||
|
val id: Int,
|
||||||
|
val fetchManga: Boolean,
|
||||||
|
val fetchChapters: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FetchMangaAndChaptersPayload(
|
||||||
|
val clientMutationId: String?,
|
||||||
|
val manga: MangaType,
|
||||||
|
val chapters: List<ChapterType>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@RequireAuth
|
||||||
|
fun fetchMangaAndChapters(input: FetchMangaAndChaptersInput): CompletableFuture<FetchMangaAndChaptersPayload?> {
|
||||||
|
val (clientMutationId, id, fetchManga, fetchChapters) = input
|
||||||
|
|
||||||
|
return future {
|
||||||
|
var mangaEntry =
|
||||||
|
transaction { MangaTable.selectAll().where { MangaTable.id eq id }.first() }
|
||||||
|
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
|
val sMangaUpdate = Manga.fetchMangaAndChapters(
|
||||||
|
mangaEntry = mangaEntry,
|
||||||
|
source = source,
|
||||||
|
fetchDetails = fetchManga,
|
||||||
|
fetchChapters = fetchChapters
|
||||||
|
)
|
||||||
|
|
||||||
|
Manga.updateMangaDatabase(mangaEntry, source, sMangaUpdate.manga)
|
||||||
|
mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq id }.first() }
|
||||||
|
Chapter.updateChapterListDatabase(mangaEntry, sMangaUpdate.chapters, source)
|
||||||
|
|
||||||
|
val (manga, chapters) =
|
||||||
|
transaction {
|
||||||
|
Pair(
|
||||||
|
MangaTable.selectAll().where { MangaTable.id eq id }.first(),
|
||||||
|
ChapterTable
|
||||||
|
.selectAll()
|
||||||
|
.where { ChapterTable.manga eq id }
|
||||||
|
.orderBy(ChapterTable.sourceOrder)
|
||||||
|
.map { ChapterType(it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
FetchMangaAndChaptersPayload(
|
||||||
|
clientMutationId = clientMutationId,
|
||||||
|
manga = MangaType(manga),
|
||||||
|
chapters = chapters,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class SetMangaMetaInput(
|
data class SetMangaMetaInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
val meta: MangaMetaType,
|
val meta: MangaMetaType,
|
||||||
|
|||||||
@@ -7,16 +7,18 @@ package suwayomi.tachidesk.manga.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterSanitizer.sanitize
|
import eu.kanade.tachiyomi.util.chapter.ChapterSanitizer.sanitize
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.github.reactivecircus.cache4k.Cache
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
import org.jetbrains.exposed.v1.core.SortOrder
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
import org.jetbrains.exposed.v1.core.and
|
import org.jetbrains.exposed.v1.core.and
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||||
@@ -32,7 +34,6 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
|||||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import suwayomi.tachidesk.manga.impl.Manga.getManga
|
|
||||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
||||||
import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput
|
import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput
|
||||||
import suwayomi.tachidesk.manga.impl.track.Track
|
import suwayomi.tachidesk.manga.impl.track.Track
|
||||||
@@ -50,7 +51,6 @@ import suwayomi.tachidesk.server.serverConfig
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.time.Duration.Companion.minutes
|
|
||||||
|
|
||||||
private fun List<ChapterDataClass>.removeDuplicates(currentChapter: ChapterDataClass): List<ChapterDataClass> =
|
private fun List<ChapterDataClass>.removeDuplicates(currentChapter: ChapterDataClass): List<ChapterDataClass> =
|
||||||
groupBy { it.chapterNumber }
|
groupBy { it.chapterNumber }
|
||||||
@@ -104,267 +104,268 @@ object Chapter {
|
|||||||
.associateBy({ it[ChapterTable.url] }, { it })
|
.associateBy({ it[ChapterTable.url] }, { it })
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapterList.mapIndexed { index, it ->
|
return chapterList.map {
|
||||||
|
|
||||||
val dbChapter = dbChapterMap.getValue(it.url)
|
val dbChapter = dbChapterMap.getValue(it.url)
|
||||||
|
ChapterTable.toDataClass(dbChapter)
|
||||||
ChapterDataClass(
|
|
||||||
id = dbChapter[ChapterTable.id].value,
|
|
||||||
url = it.url,
|
|
||||||
name = it.name,
|
|
||||||
uploadDate = it.date_upload,
|
|
||||||
chapterNumber = it.chapter_number,
|
|
||||||
scanlator = it.scanlator,
|
|
||||||
mangaId = mangaId,
|
|
||||||
read = dbChapter[ChapterTable.isRead],
|
|
||||||
bookmarked = dbChapter[ChapterTable.isBookmarked],
|
|
||||||
lastPageRead = dbChapter[ChapterTable.lastPageRead],
|
|
||||||
lastReadAt = dbChapter[ChapterTable.lastReadAt],
|
|
||||||
index = chapterList.size - index,
|
|
||||||
fetchedAt = dbChapter[ChapterTable.fetchedAt],
|
|
||||||
realUrl = dbChapter[ChapterTable.realUrl],
|
|
||||||
downloaded = dbChapter[ChapterTable.isDownloaded],
|
|
||||||
pageCount = dbChapter[ChapterTable.pageCount],
|
|
||||||
lastModifiedAt = dbChapter[ChapterTable.lastModifiedAt],
|
|
||||||
version = dbChapter[ChapterTable.version],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val map: Cache<Int, Mutex> =
|
|
||||||
Cache
|
|
||||||
.Builder<Int, Mutex>()
|
|
||||||
.expireAfterAccess(10.minutes)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
suspend fun fetchChapterList(mangaId: Int): List<SChapter> {
|
suspend fun fetchChapterList(mangaId: Int): List<SChapter> {
|
||||||
val mutex = map.get(mangaId) { Mutex() }
|
val mutex = Manga.mangaInfoMutex.get(mangaId) { Mutex() }
|
||||||
val chapterList =
|
val chapterList =
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val manga = getManga(mangaId)
|
val mangaEntry = transaction {
|
||||||
val source = getCatalogueSourceOrStub(manga.sourceId.toLong())
|
MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||||
|
|
||||||
val sManga =
|
|
||||||
SManga.create().apply {
|
|
||||||
title = manga.title
|
|
||||||
url = manga.url
|
|
||||||
description = manga.description
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentLatestChapterNumber = Manga.getLatestChapter(mangaId)?.chapterNumber ?: 0f
|
|
||||||
val numberOfCurrentChapters = getCountOfMangaChapters(mangaId)
|
|
||||||
|
|
||||||
val chapters = source.getChapterList(sManga)
|
|
||||||
// it's possible that the source returns a list containing chapters with the same url
|
|
||||||
// once such duplicated chapters have been added, they aren't being removed anymore as long as there is
|
|
||||||
// a chapter with the same url in the fetched chapter list, even if the duplicated chapter itself
|
|
||||||
// does not exist anymore on the source
|
|
||||||
val uniqueChapters = chapters.distinctBy { it.url }
|
|
||||||
|
|
||||||
if (uniqueChapters.isEmpty()) {
|
|
||||||
throw Exception("No chapters found")
|
|
||||||
}
|
}
|
||||||
|
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
// Recognize number for new chapters.
|
val chapters = Manga.fetchMangaAndChapters(
|
||||||
uniqueChapters.forEach { chapter ->
|
mangaEntry = mangaEntry,
|
||||||
(source as? HttpSource)?.prepareNewChapter(chapter, sManga)
|
source = source,
|
||||||
val chapterNumber = ChapterRecognition.parseChapterNumber(manga.title, chapter.name, chapter.chapter_number.toDouble())
|
fetchDetails = false,
|
||||||
chapter.chapter_number = chapterNumber.toFloat()
|
fetchChapters = true,
|
||||||
chapter.name = chapter.name.sanitize(manga.title)
|
).chapters
|
||||||
chapter.scanlator = chapter.scanlator?.ifBlank { null }?.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
val now = Instant.now().epochSecond
|
updateChapterListDatabase(mangaEntry, chapters, source)
|
||||||
// Used to not set upload date of older chapters
|
|
||||||
// to a higher value than newer chapters
|
|
||||||
var maxSeenUploadDate = 0L
|
|
||||||
|
|
||||||
val chaptersInDb =
|
|
||||||
transaction {
|
|
||||||
ChapterTable
|
|
||||||
.selectAll()
|
|
||||||
.where { ChapterTable.manga eq mangaId }
|
|
||||||
.map { ChapterTable.toDataClass(it) }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// new chapters after they have been added to the database for auto downloads
|
|
||||||
val insertedChapterIds = mutableListOf<Int>()
|
|
||||||
|
|
||||||
val chaptersToInsert = mutableListOf<ChapterDataClass>() // do not yet have an ID from the database
|
|
||||||
val chaptersToUpdate = mutableListOf<ChapterDataClass>()
|
|
||||||
|
|
||||||
uniqueChapters.reversed().forEachIndexed { index, fetchedChapter ->
|
|
||||||
val chapterEntry = chaptersInDb.find { it.url == fetchedChapter.url }
|
|
||||||
|
|
||||||
val chapterData =
|
|
||||||
ChapterDataClass.fromSChapter(
|
|
||||||
fetchedChapter,
|
|
||||||
chapterEntry?.id ?: 0,
|
|
||||||
index + 1,
|
|
||||||
now,
|
|
||||||
mangaId,
|
|
||||||
runCatching {
|
|
||||||
(source as? HttpSource)?.getChapterUrl(fetchedChapter)
|
|
||||||
}.getOrNull(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (chapterEntry == null) {
|
|
||||||
val newChapterData =
|
|
||||||
if (chapterData.uploadDate == 0L) {
|
|
||||||
val altDateUpload = if (maxSeenUploadDate == 0L) now else maxSeenUploadDate
|
|
||||||
chapterData.copy(uploadDate = altDateUpload)
|
|
||||||
} else {
|
|
||||||
maxSeenUploadDate = max(maxSeenUploadDate, chapterData.uploadDate)
|
|
||||||
chapterData
|
|
||||||
}
|
|
||||||
chaptersToInsert.add(newChapterData)
|
|
||||||
} else {
|
|
||||||
val newChapterData =
|
|
||||||
if (chapterData.uploadDate == 0L) {
|
|
||||||
chapterData.copy(uploadDate = chapterEntry.uploadDate)
|
|
||||||
} else {
|
|
||||||
chapterData
|
|
||||||
}
|
|
||||||
chaptersToUpdate.add(newChapterData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val deletedChapterNumbers = TreeSet<Float>()
|
|
||||||
val deletedReadChapterNumbers = TreeSet<Float>()
|
|
||||||
val deletedBookmarkedChapterNumbers = TreeSet<Float>()
|
|
||||||
val deletedDownloadedChapterNumberToChapter = mutableMapOf<Float, ChapterDataClass>()
|
|
||||||
val deletedChapterNumberDateFetchMap = mutableMapOf<Float, Long>()
|
|
||||||
|
|
||||||
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
|
|
||||||
val chapterUrls = uniqueChapters.map { it.url }.toSet()
|
|
||||||
|
|
||||||
val chaptersIdsToDelete =
|
|
||||||
chaptersInDb.mapNotNull { dbChapter ->
|
|
||||||
if (!chapterUrls.contains(dbChapter.url)) {
|
|
||||||
if (dbChapter.read) deletedReadChapterNumbers.add(dbChapter.chapterNumber)
|
|
||||||
if (dbChapter.bookmarked) deletedBookmarkedChapterNumbers.add(dbChapter.chapterNumber)
|
|
||||||
if (dbChapter.downloaded) deletedDownloadedChapterNumberToChapter[dbChapter.chapterNumber] = dbChapter
|
|
||||||
deletedChapterNumbers.add(dbChapter.chapterNumber)
|
|
||||||
deletedChapterNumberDateFetchMap[dbChapter.chapterNumber] = dbChapter.fetchedAt
|
|
||||||
dbChapter.id
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction {
|
|
||||||
// we got some clean up due
|
|
||||||
if (chaptersIdsToDelete.isNotEmpty()) {
|
|
||||||
DownloadManager.dequeue(chaptersIdsToDelete)
|
|
||||||
PageTable.deleteWhere { chapter inList chaptersIdsToDelete }
|
|
||||||
ChapterTable.deleteWhere { id inList chaptersIdsToDelete }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chaptersToInsert.isNotEmpty()) {
|
|
||||||
ChapterTable
|
|
||||||
.batchInsert(chaptersToInsert) { chapter ->
|
|
||||||
this[ChapterTable.url] = chapter.url
|
|
||||||
this[ChapterTable.name] = chapter.name
|
|
||||||
this[ChapterTable.date_upload] = chapter.uploadDate
|
|
||||||
this[ChapterTable.chapter_number] = chapter.chapterNumber
|
|
||||||
this[ChapterTable.scanlator] = chapter.scanlator
|
|
||||||
this[ChapterTable.sourceOrder] = chapter.index
|
|
||||||
this[ChapterTable.fetchedAt] = chapter.fetchedAt
|
|
||||||
this[ChapterTable.manga] = chapter.mangaId
|
|
||||||
this[ChapterTable.realUrl] = chapter.realUrl
|
|
||||||
this[ChapterTable.isRead] = false
|
|
||||||
this[ChapterTable.isBookmarked] = false
|
|
||||||
this[ChapterTable.isDownloaded] = false
|
|
||||||
this[ChapterTable.lastModifiedAt] = chapter.lastModifiedAt
|
|
||||||
this[ChapterTable.version] = chapter.version
|
|
||||||
this[ChapterTable.pageCount] = -1
|
|
||||||
|
|
||||||
// is recognized chapter number
|
|
||||||
if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) {
|
|
||||||
this[ChapterTable.isRead] = chapter.chapterNumber in deletedReadChapterNumbers
|
|
||||||
this[ChapterTable.isBookmarked] = chapter.chapterNumber in deletedBookmarkedChapterNumbers
|
|
||||||
|
|
||||||
// Try to use the fetch date of the original entry to not pollute 'Updates' tab
|
|
||||||
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
|
|
||||||
this[ChapterTable.fetchedAt] = it
|
|
||||||
}
|
|
||||||
|
|
||||||
deletedDownloadedChapterNumberToChapter[chapter.chapterNumber]?.let {
|
|
||||||
val hasDownloadedPages = it.pageCount > 0
|
|
||||||
val isSameName = it.name == chapter.name
|
|
||||||
val isSameScanlator = it.scanlator == chapter.scanlator
|
|
||||||
|
|
||||||
// Only preserve download status for chapters with the same name and of the same scanlator; otherwise,
|
|
||||||
// the downloaded files won't be found anyway
|
|
||||||
val isDownloadPreservable = hasDownloadedPages && isSameName && isSameScanlator
|
|
||||||
if (isDownloadPreservable) {
|
|
||||||
this[ChapterTable.isDownloaded] = true
|
|
||||||
this[ChapterTable.pageCount] = it.pageCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.forEach { insertedChapterIds.add(it[ChapterTable.id].value) }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chaptersToUpdate.isNotEmpty()) {
|
|
||||||
BatchUpdateStatement(ChapterTable)
|
|
||||||
.apply {
|
|
||||||
chaptersToUpdate.forEach {
|
|
||||||
addBatch(EntityID(it.id, ChapterTable))
|
|
||||||
|
|
||||||
val currentChapter = chaptersInDb.find { dbChapter -> dbChapter.id == it.id }!!
|
|
||||||
|
|
||||||
this[ChapterTable.name] = it.name
|
|
||||||
this[ChapterTable.date_upload] = it.uploadDate
|
|
||||||
this[ChapterTable.chapter_number] = it.chapterNumber
|
|
||||||
this[ChapterTable.scanlator] = it.scanlator
|
|
||||||
this[ChapterTable.sourceOrder] = it.index
|
|
||||||
this[ChapterTable.realUrl] = it.realUrl
|
|
||||||
this[ChapterTable.lastModifiedAt] = it.lastModifiedAt
|
|
||||||
this[ChapterTable.version] = it.version
|
|
||||||
this[ChapterTable.isDownloaded] = currentChapter.downloaded
|
|
||||||
this[ChapterTable.pageCount] = currentChapter.pageCount
|
|
||||||
|
|
||||||
if (!currentChapter.downloaded) {
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
|
|
||||||
val isSameScanlator = currentChapter.scanlator == it.scanlator
|
|
||||||
val isSameName = currentChapter.name == it.name
|
|
||||||
|
|
||||||
val isDownloadPreservable = isSameName && isSameScanlator
|
|
||||||
if (!isDownloadPreservable) {
|
|
||||||
this[ChapterTable.isDownloaded] = false
|
|
||||||
this[ChapterTable.pageCount] = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.toExecutable()
|
|
||||||
.execute(this@transaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
|
||||||
it[chaptersLastFetchedAt] = Instant.now().epochSecond
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manga.inLibrary) {
|
|
||||||
// We have to query the inserted chapters to get the up-to-date data. I.e. "last_modified_at" is not returned by the insert statement, due to being set by a DB trigger
|
|
||||||
val insertedChapters =
|
|
||||||
transaction {
|
|
||||||
ChapterTable.selectAll().where { ChapterTable.id inList insertedChapterIds }.map(
|
|
||||||
ChapterTable::toDataClass,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
downloadNewChapters(mangaId, currentLatestChapterNumber, numberOfCurrentChapters, insertedChapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
uniqueChapters
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return chapterList
|
return chapterList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateChapterListDatabase(
|
||||||
|
mangaEntry: ResultRow,
|
||||||
|
chapters: List<SChapter>,
|
||||||
|
source: CatalogueSource,
|
||||||
|
): List<SChapter> {
|
||||||
|
val currentLatestChapterNumber = Manga.getLatestChapter(mangaEntry[MangaTable.id].value)?.chapterNumber ?: 0f
|
||||||
|
val numberOfCurrentChapters = getCountOfMangaChapters(mangaEntry[MangaTable.id].value)
|
||||||
|
// it's possible that the source returns a list containing chapters with the same url
|
||||||
|
// once such duplicated chapters have been added, they aren't being removed anymore as long as there is
|
||||||
|
// a chapter with the same url in the fetched chapter list, even if the duplicated chapter itself
|
||||||
|
// does not exist anymore on the source
|
||||||
|
val uniqueChapters = chapters.distinctBy { it.url }
|
||||||
|
|
||||||
|
if (uniqueChapters.isEmpty()) {
|
||||||
|
throw Exception("No chapters found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recognize number for new chapters.
|
||||||
|
val sManga = SManga.create().apply {
|
||||||
|
url = mangaEntry[MangaTable.url]
|
||||||
|
title = mangaEntry[MangaTable.title]
|
||||||
|
thumbnail_url = mangaEntry[MangaTable.thumbnail_url]
|
||||||
|
artist = mangaEntry[MangaTable.artist]
|
||||||
|
author = mangaEntry[MangaTable.author]
|
||||||
|
description = mangaEntry[MangaTable.description]
|
||||||
|
genre = mangaEntry[MangaTable.genre]
|
||||||
|
status = mangaEntry[MangaTable.status]
|
||||||
|
update_strategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy])
|
||||||
|
memo = mangaEntry[MangaTable.memo]
|
||||||
|
initialized = mangaEntry[MangaTable.initialized]
|
||||||
|
}
|
||||||
|
uniqueChapters.forEach { chapter ->
|
||||||
|
(source as? HttpSource)?.prepareNewChapter(chapter, sManga)
|
||||||
|
val chapterNumber = ChapterRecognition.parseChapterNumber(mangaEntry[MangaTable.title], chapter.name, chapter.chapter_number.toDouble())
|
||||||
|
chapter.chapter_number = chapterNumber.toFloat()
|
||||||
|
chapter.name = chapter.name.sanitize(mangaEntry[MangaTable.title])
|
||||||
|
chapter.scanlator = chapter.scanlator?.ifBlank { null }?.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
val now = Instant.now().epochSecond
|
||||||
|
// Used to not set upload date of older chapters
|
||||||
|
// to a higher value than newer chapters
|
||||||
|
var maxSeenUploadDate = 0L
|
||||||
|
|
||||||
|
val chaptersInDb =
|
||||||
|
transaction {
|
||||||
|
ChapterTable
|
||||||
|
.selectAll()
|
||||||
|
.where { ChapterTable.manga eq mangaEntry[MangaTable.id].value }
|
||||||
|
.map { ChapterTable.toDataClass(it) }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// new chapters after they have been added to the database for auto downloads
|
||||||
|
val insertedChapterIds = mutableListOf<Int>()
|
||||||
|
|
||||||
|
val chaptersToInsert = mutableListOf<ChapterDataClass>() // do not yet have an ID from the database
|
||||||
|
val chaptersToUpdate = mutableListOf<ChapterDataClass>()
|
||||||
|
|
||||||
|
uniqueChapters.reversed().forEachIndexed { index, fetchedChapter ->
|
||||||
|
val chapterEntry = chaptersInDb.find { it.url == fetchedChapter.url }
|
||||||
|
|
||||||
|
val chapterData =
|
||||||
|
ChapterDataClass.fromSChapter(
|
||||||
|
fetchedChapter,
|
||||||
|
chapterEntry?.id ?: 0,
|
||||||
|
index + 1,
|
||||||
|
now,
|
||||||
|
mangaEntry[MangaTable.id].value,
|
||||||
|
runCatching {
|
||||||
|
(source as? HttpSource)?.getChapterUrl(fetchedChapter)
|
||||||
|
}.getOrNull(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (chapterEntry == null) {
|
||||||
|
val newChapterData =
|
||||||
|
if (chapterData.uploadDate == 0L) {
|
||||||
|
val altDateUpload = if (maxSeenUploadDate == 0L) now else maxSeenUploadDate
|
||||||
|
chapterData.copy(uploadDate = altDateUpload)
|
||||||
|
} else {
|
||||||
|
maxSeenUploadDate = max(maxSeenUploadDate, chapterData.uploadDate)
|
||||||
|
chapterData
|
||||||
|
}
|
||||||
|
chaptersToInsert.add(newChapterData)
|
||||||
|
} else {
|
||||||
|
val newChapterData =
|
||||||
|
if (chapterData.uploadDate == 0L) {
|
||||||
|
chapterData.copy(uploadDate = chapterEntry.uploadDate)
|
||||||
|
} else {
|
||||||
|
chapterData
|
||||||
|
}
|
||||||
|
chaptersToUpdate.add(newChapterData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val deletedChapterNumbers = TreeSet<Float>()
|
||||||
|
val deletedReadChapterNumbers = TreeSet<Float>()
|
||||||
|
val deletedBookmarkedChapterNumbers = TreeSet<Float>()
|
||||||
|
val deletedDownloadedChapterNumberToChapter = mutableMapOf<Float, ChapterDataClass>()
|
||||||
|
val deletedChapterNumberDateFetchMap = mutableMapOf<Float, Long>()
|
||||||
|
|
||||||
|
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
|
||||||
|
val chapterUrls = uniqueChapters.map { it.url }.toSet()
|
||||||
|
|
||||||
|
val chaptersIdsToDelete =
|
||||||
|
chaptersInDb.mapNotNull { dbChapter ->
|
||||||
|
if (!chapterUrls.contains(dbChapter.url)) {
|
||||||
|
if (dbChapter.read) deletedReadChapterNumbers.add(dbChapter.chapterNumber)
|
||||||
|
if (dbChapter.bookmarked) deletedBookmarkedChapterNumbers.add(dbChapter.chapterNumber)
|
||||||
|
if (dbChapter.downloaded) deletedDownloadedChapterNumberToChapter[dbChapter.chapterNumber] = dbChapter
|
||||||
|
deletedChapterNumbers.add(dbChapter.chapterNumber)
|
||||||
|
deletedChapterNumberDateFetchMap[dbChapter.chapterNumber] = dbChapter.fetchedAt
|
||||||
|
dbChapter.id
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
// we got some clean up due
|
||||||
|
if (chaptersIdsToDelete.isNotEmpty()) {
|
||||||
|
DownloadManager.dequeue(chaptersIdsToDelete)
|
||||||
|
PageTable.deleteWhere { chapter inList chaptersIdsToDelete }
|
||||||
|
ChapterTable.deleteWhere { id inList chaptersIdsToDelete }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chaptersToInsert.isNotEmpty()) {
|
||||||
|
ChapterTable
|
||||||
|
.batchInsert(chaptersToInsert) { chapter ->
|
||||||
|
this[ChapterTable.url] = chapter.url
|
||||||
|
this[ChapterTable.name] = chapter.name
|
||||||
|
this[ChapterTable.date_upload] = chapter.uploadDate
|
||||||
|
this[ChapterTable.chapter_number] = chapter.chapterNumber
|
||||||
|
this[ChapterTable.scanlator] = chapter.scanlator
|
||||||
|
this[ChapterTable.sourceOrder] = chapter.index
|
||||||
|
this[ChapterTable.fetchedAt] = chapter.fetchedAt
|
||||||
|
this[ChapterTable.manga] = chapter.mangaId
|
||||||
|
this[ChapterTable.realUrl] = chapter.realUrl
|
||||||
|
this[ChapterTable.memo] = chapter.memo
|
||||||
|
this[ChapterTable.isRead] = false
|
||||||
|
this[ChapterTable.isBookmarked] = false
|
||||||
|
this[ChapterTable.isDownloaded] = false
|
||||||
|
this[ChapterTable.lastModifiedAt] = chapter.lastModifiedAt
|
||||||
|
this[ChapterTable.version] = chapter.version
|
||||||
|
this[ChapterTable.pageCount] = -1
|
||||||
|
|
||||||
|
// is recognized chapter number
|
||||||
|
if (chapter.chapterNumber >= 0f && chapter.chapterNumber in deletedChapterNumbers) {
|
||||||
|
this[ChapterTable.isRead] = chapter.chapterNumber in deletedReadChapterNumbers
|
||||||
|
this[ChapterTable.isBookmarked] = chapter.chapterNumber in deletedBookmarkedChapterNumbers
|
||||||
|
|
||||||
|
// Try to use the fetch date of the original entry to not pollute 'Updates' tab
|
||||||
|
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
|
||||||
|
this[ChapterTable.fetchedAt] = it
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedDownloadedChapterNumberToChapter[chapter.chapterNumber]?.let {
|
||||||
|
val hasDownloadedPages = it.pageCount > 0
|
||||||
|
val isSameName = it.name == chapter.name
|
||||||
|
val isSameScanlator = it.scanlator == chapter.scanlator
|
||||||
|
|
||||||
|
// Only preserve download status for chapters with the same name and of the same scanlator; otherwise,
|
||||||
|
// the downloaded files won't be found anyway
|
||||||
|
val isDownloadPreservable = hasDownloadedPages && isSameName && isSameScanlator
|
||||||
|
if (isDownloadPreservable) {
|
||||||
|
this[ChapterTable.isDownloaded] = true
|
||||||
|
this[ChapterTable.pageCount] = it.pageCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.forEach { insertedChapterIds.add(it[ChapterTable.id].value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chaptersToUpdate.isNotEmpty()) {
|
||||||
|
BatchUpdateStatement(ChapterTable)
|
||||||
|
.apply {
|
||||||
|
chaptersToUpdate.forEach {
|
||||||
|
addBatch(EntityID(it.id, ChapterTable))
|
||||||
|
|
||||||
|
val currentChapter = chaptersInDb.find { dbChapter -> dbChapter.id == it.id }!!
|
||||||
|
|
||||||
|
this[ChapterTable.name] = it.name
|
||||||
|
this[ChapterTable.date_upload] = it.uploadDate
|
||||||
|
this[ChapterTable.chapter_number] = it.chapterNumber
|
||||||
|
this[ChapterTable.scanlator] = it.scanlator
|
||||||
|
this[ChapterTable.sourceOrder] = it.index
|
||||||
|
this[ChapterTable.realUrl] = it.realUrl
|
||||||
|
this[ChapterTable.lastModifiedAt] = it.lastModifiedAt
|
||||||
|
this[ChapterTable.version] = it.version
|
||||||
|
this[ChapterTable.memo] = it.memo
|
||||||
|
this[ChapterTable.isDownloaded] = currentChapter.downloaded
|
||||||
|
this[ChapterTable.pageCount] = currentChapter.pageCount
|
||||||
|
|
||||||
|
if (!currentChapter.downloaded) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
|
||||||
|
val isSameScanlator = currentChapter.scanlator == it.scanlator
|
||||||
|
val isSameName = currentChapter.name == it.name
|
||||||
|
|
||||||
|
val isDownloadPreservable = isSameName && isSameScanlator
|
||||||
|
if (!isDownloadPreservable) {
|
||||||
|
this[ChapterTable.isDownloaded] = false
|
||||||
|
this[ChapterTable.pageCount] = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.toExecutable()
|
||||||
|
.execute(this@transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
MangaTable.update({ MangaTable.id eq mangaEntry[MangaTable.id].value }) {
|
||||||
|
it[chaptersLastFetchedAt] = Instant.now().epochSecond
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mangaEntry[MangaTable.inLibrary]) {
|
||||||
|
// We have to query the inserted chapters to get the up-to-date data. I.e. "last_modified_at" is not returned by the insert statement, due to being set by a DB trigger
|
||||||
|
val insertedChapters =
|
||||||
|
transaction {
|
||||||
|
ChapterTable.selectAll().where { ChapterTable.id inList insertedChapterIds }.map(
|
||||||
|
ChapterTable::toDataClass,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
downloadNewChapters(
|
||||||
|
mangaEntry[MangaTable.id].value,
|
||||||
|
currentLatestChapterNumber,
|
||||||
|
numberOfCurrentChapters,
|
||||||
|
insertedChapters
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueChapters
|
||||||
|
}
|
||||||
|
|
||||||
private fun downloadNewChapters(
|
private fun downloadNewChapters(
|
||||||
mangaId: Int,
|
mangaId: Int,
|
||||||
prevLatestChapterNumber: Float,
|
prevLatestChapterNumber: Float,
|
||||||
|
|||||||
@@ -11,13 +11,19 @@ import eu.kanade.tachiyomi.network.GET
|
|||||||
import eu.kanade.tachiyomi.network.HttpException
|
import eu.kanade.tachiyomi.network.HttpException
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.model.SMangaUpdate
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import io.github.oshai.kotlinlogging.KLogger
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import io.github.reactivecircus.cache4k.Cache
|
||||||
import io.javalin.http.HttpStatus
|
import io.javalin.http.HttpStatus
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
@@ -32,10 +38,7 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
|||||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
import org.jetbrains.exposed.v1.jdbc.update
|
import org.jetbrains.exposed.v1.jdbc.update
|
||||||
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
|
||||||
import suwayomi.tachidesk.manga.impl.Source.getSource
|
|
||||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.MissingThumbnailException
|
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.MissingThumbnailException
|
||||||
import suwayomi.tachidesk.manga.impl.track.Track
|
|
||||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||||
@@ -47,10 +50,8 @@ import suwayomi.tachidesk.manga.impl.util.updateMangaDownloadDir
|
|||||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude
|
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
|
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||||
import suwayomi.tachidesk.server.ApplicationDirs
|
import suwayomi.tachidesk.server.ApplicationDirs
|
||||||
@@ -59,10 +60,17 @@ import java.io.File
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger { }
|
private val logger = KotlinLogging.logger { }
|
||||||
|
|
||||||
object Manga {
|
object Manga {
|
||||||
|
val mangaInfoMutex: Cache<Int, Mutex> =
|
||||||
|
Cache
|
||||||
|
.Builder<Int, Mutex>()
|
||||||
|
.expireAfterAccess(10.minutes)
|
||||||
|
.build()
|
||||||
|
|
||||||
suspend fun getManga(
|
suspend fun getManga(
|
||||||
mangaId: Int,
|
mangaId: Int,
|
||||||
onlineFetch: Boolean = false,
|
onlineFetch: Boolean = false,
|
||||||
@@ -70,63 +78,83 @@ object Manga {
|
|||||||
var mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
var mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||||
|
|
||||||
return if (!onlineFetch && mangaEntry[MangaTable.initialized]) {
|
return if (!onlineFetch && mangaEntry[MangaTable.initialized]) {
|
||||||
getMangaDataClass(mangaId, mangaEntry)
|
MangaTable.toDataClass(mangaEntry)
|
||||||
} else { // initialize manga
|
} else { // initialize manga
|
||||||
val sManga = fetchManga(mangaId) ?: return getMangaDataClass(mangaId, mangaEntry)
|
fetchManga(mangaId) ?: return MangaTable.toDataClass(mangaEntry)
|
||||||
|
|
||||||
mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||||
|
|
||||||
MangaDataClass(
|
MangaTable.toDataClass(mangaEntry).copy(freshData = true)
|
||||||
id = mangaId,
|
|
||||||
sourceId = mangaEntry[MangaTable.sourceReference].toString(),
|
|
||||||
url = mangaEntry[MangaTable.url],
|
|
||||||
title = mangaEntry[MangaTable.title],
|
|
||||||
thumbnailUrl = proxyThumbnailUrl(mangaId),
|
|
||||||
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
|
|
||||||
initialized = true,
|
|
||||||
artist = sManga.artist,
|
|
||||||
author = sManga.author,
|
|
||||||
description = sManga.description,
|
|
||||||
genre = sManga.genre.toGenreList(),
|
|
||||||
status = MangaStatus.valueOf(sManga.status).name,
|
|
||||||
inLibrary = mangaEntry[MangaTable.inLibrary],
|
|
||||||
inLibraryAt = mangaEntry[MangaTable.inLibraryAt],
|
|
||||||
source = getSource(mangaEntry[MangaTable.sourceReference]),
|
|
||||||
realUrl = mangaEntry[MangaTable.realUrl],
|
|
||||||
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
|
|
||||||
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
|
|
||||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
|
|
||||||
freshData = true,
|
|
||||||
trackers = Track.getTrackRecordsByMangaId(mangaId),
|
|
||||||
lastModifiedAt = mangaEntry[MangaTable.lastModifiedAt],
|
|
||||||
version = mangaEntry[MangaTable.version],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchManga(mangaId: Int): SManga? {
|
suspend fun fetchMangaAndChapters(
|
||||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
mangaEntry: ResultRow,
|
||||||
|
source: CatalogueSource,
|
||||||
val source =
|
fetchDetails: Boolean,
|
||||||
getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
fetchChapters: Boolean,
|
||||||
?: return null
|
): SMangaUpdate {
|
||||||
val sManga =
|
val sManga =
|
||||||
source.getMangaDetails(
|
SManga.create().apply {
|
||||||
SManga.create().apply {
|
url = mangaEntry[MangaTable.url]
|
||||||
url = mangaEntry[MangaTable.url]
|
title = mangaEntry[MangaTable.title]
|
||||||
title = mangaEntry[MangaTable.title]
|
thumbnail_url = mangaEntry[MangaTable.thumbnail_url]
|
||||||
thumbnail_url = mangaEntry[MangaTable.thumbnail_url]
|
artist = mangaEntry[MangaTable.artist]
|
||||||
artist = mangaEntry[MangaTable.artist]
|
author = mangaEntry[MangaTable.author]
|
||||||
author = mangaEntry[MangaTable.author]
|
description = mangaEntry[MangaTable.description]
|
||||||
description = mangaEntry[MangaTable.description]
|
genre = mangaEntry[MangaTable.genre]
|
||||||
genre = mangaEntry[MangaTable.genre]
|
status = mangaEntry[MangaTable.status]
|
||||||
status = mangaEntry[MangaTable.status]
|
update_strategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy])
|
||||||
update_strategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy])
|
memo = mangaEntry[MangaTable.memo]
|
||||||
},
|
initialized = mangaEntry[MangaTable.initialized]
|
||||||
)
|
}
|
||||||
|
val sChapters = transaction {
|
||||||
|
ChapterTable.selectAll()
|
||||||
|
.where { ChapterTable.manga eq mangaEntry[MangaTable.id] }
|
||||||
|
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
|
||||||
|
.map {
|
||||||
|
SChapter.create().apply {
|
||||||
|
url = it[ChapterTable.url]
|
||||||
|
name = it[ChapterTable.name]
|
||||||
|
chapter_number = it[ChapterTable.chapter_number]
|
||||||
|
scanlator = it[ChapterTable.scanlator]
|
||||||
|
date_upload = it[ChapterTable.date_upload]
|
||||||
|
memo = it[ChapterTable.memo]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return source.getMangaUpdate(
|
||||||
|
sManga,
|
||||||
|
sChapters,
|
||||||
|
fetchDetails = fetchDetails,
|
||||||
|
fetchChapters = fetchChapters,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchManga(mangaId: Int): SManga? {
|
||||||
|
return mangaInfoMutex.get(mangaId) { Mutex() }.withLock {
|
||||||
|
val mangaEntry =
|
||||||
|
transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||||
|
val source = getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference]) ?: return null
|
||||||
|
val sManga = fetchMangaAndChapters(
|
||||||
|
mangaEntry,
|
||||||
|
source,
|
||||||
|
fetchDetails = true,
|
||||||
|
fetchChapters = false
|
||||||
|
).manga
|
||||||
|
|
||||||
|
updateMangaDatabase(mangaEntry, source, sManga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMangaDatabase(
|
||||||
|
mangaEntry: ResultRow,
|
||||||
|
source: CatalogueSource,
|
||||||
|
sManga: SManga,
|
||||||
|
): SManga {
|
||||||
transaction {
|
transaction {
|
||||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
MangaTable.update({ MangaTable.id eq mangaEntry[MangaTable.id] }) {
|
||||||
val remoteTitle =
|
val remoteTitle =
|
||||||
try {
|
try {
|
||||||
sManga.title
|
sManga.title
|
||||||
@@ -151,7 +179,7 @@ object Manga {
|
|||||||
if (!sManga.thumbnail_url.isNullOrEmpty()) {
|
if (!sManga.thumbnail_url.isNullOrEmpty()) {
|
||||||
it[MangaTable.thumbnail_url] = sManga.thumbnail_url
|
it[MangaTable.thumbnail_url] = sManga.thumbnail_url
|
||||||
it[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
|
it[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
|
||||||
clearThumbnail(mangaId)
|
clearThumbnail(mangaEntry[MangaTable.id].value)
|
||||||
}
|
}
|
||||||
|
|
||||||
it[MangaTable.realUrl] =
|
it[MangaTable.realUrl] =
|
||||||
@@ -221,35 +249,6 @@ object Manga {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMangaDataClass(
|
|
||||||
mangaId: Int,
|
|
||||||
mangaEntry: ResultRow,
|
|
||||||
) = MangaDataClass(
|
|
||||||
id = mangaId,
|
|
||||||
sourceId = mangaEntry[MangaTable.sourceReference].toString(),
|
|
||||||
url = mangaEntry[MangaTable.url],
|
|
||||||
title = mangaEntry[MangaTable.title],
|
|
||||||
thumbnailUrl = proxyThumbnailUrl(mangaId),
|
|
||||||
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
|
|
||||||
initialized = true,
|
|
||||||
artist = mangaEntry[MangaTable.artist],
|
|
||||||
author = mangaEntry[MangaTable.author],
|
|
||||||
description = mangaEntry[MangaTable.description],
|
|
||||||
genre = mangaEntry[MangaTable.genre].toGenreList(),
|
|
||||||
status = MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
|
|
||||||
inLibrary = mangaEntry[MangaTable.inLibrary],
|
|
||||||
inLibraryAt = mangaEntry[MangaTable.inLibraryAt],
|
|
||||||
source = getSource(mangaEntry[MangaTable.sourceReference]),
|
|
||||||
realUrl = mangaEntry[MangaTable.realUrl],
|
|
||||||
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
|
|
||||||
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
|
|
||||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
|
|
||||||
freshData = false,
|
|
||||||
trackers = Track.getTrackRecordsByMangaId(mangaId),
|
|
||||||
lastModifiedAt = mangaEntry[MangaTable.lastModifiedAt],
|
|
||||||
version = mangaEntry[MangaTable.version],
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getMangaMetaMap(mangaId: Int): Map<String, String> =
|
fun getMangaMetaMap(mangaId: Int): Map<String, String> =
|
||||||
transaction {
|
transaction {
|
||||||
MangaMetaTable
|
MangaMetaTable
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.model.MangasPage
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.model.SMangaUpdate
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
open class StubSource(
|
open class StubSource(
|
||||||
@@ -23,9 +24,13 @@ open class StubSource(
|
|||||||
override val name: String
|
override val name: String
|
||||||
get() = id.toString()
|
get() = id.toString()
|
||||||
|
|
||||||
|
override suspend fun getPopularManga(page: Int): MangasPage = throw getSourceNotInstalledException()
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
|
||||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
|
||||||
|
|
||||||
|
override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage = throw getSourceNotInstalledException()
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
|
||||||
override fun fetchSearchManga(
|
override fun fetchSearchManga(
|
||||||
page: Int,
|
page: Int,
|
||||||
@@ -33,17 +38,23 @@ open class StubSource(
|
|||||||
filters: FilterList,
|
filters: FilterList,
|
||||||
): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
|
): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
|
||||||
|
|
||||||
|
override suspend fun getLatestUpdates(page: Int): MangasPage = throw getSourceNotInstalledException()
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
|
||||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
|
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
|
||||||
|
|
||||||
override fun getFilterList(): FilterList = FilterList()
|
override fun getFilterList(): FilterList = FilterList()
|
||||||
|
|
||||||
|
override suspend fun getMangaUpdate(manga: SManga, chapters: List<SChapter>, fetchDetails: Boolean, fetchChapters: Boolean): SMangaUpdate = throw getSourceNotInstalledException()
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Observable.error(getSourceNotInstalledException())
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Observable.error(getSourceNotInstalledException())
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.error(getSourceNotInstalledException())
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.error(getSourceNotInstalledException())
|
||||||
|
|
||||||
|
override suspend fun getPageList(chapter: SChapter): List<Page> = throw getSourceNotInstalledException()
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.error(getSourceNotInstalledException())
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.error(getSourceNotInstalledException())
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package suwayomi.tachidesk.manga.model.dataclass
|
package suwayomi.tachidesk.manga.model.dataclass
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
import org.jetbrains.exposed.v1.core.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
@@ -43,6 +45,8 @@ data class ChapterDataClass(
|
|||||||
val pageCount: Int = -1,
|
val pageCount: Int = -1,
|
||||||
val lastModifiedAt: Long = 0,
|
val lastModifiedAt: Long = 0,
|
||||||
val version: Long = 0,
|
val version: Long = 0,
|
||||||
|
@JsonIgnore
|
||||||
|
val memo: JsonObject = JsonObject(emptyMap()),
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromSChapter(
|
fun fromSChapter(
|
||||||
@@ -60,6 +64,7 @@ data class ChapterDataClass(
|
|||||||
uploadDate = sChapter.date_upload,
|
uploadDate = sChapter.date_upload,
|
||||||
chapterNumber = sChapter.chapter_number,
|
chapterNumber = sChapter.chapter_number,
|
||||||
scanlator = sChapter.scanlator,
|
scanlator = sChapter.scanlator,
|
||||||
|
memo = sChapter.memo,
|
||||||
index = index,
|
index = index,
|
||||||
fetchedAt = fetchedAt,
|
fetchedAt = fetchedAt,
|
||||||
realUrl = realUrl,
|
realUrl = realUrl,
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ package suwayomi.tachidesk.manga.model.dataclass
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
import suwayomi.tachidesk.manga.impl.Manga.getMangaMetaMap
|
import suwayomi.tachidesk.manga.impl.Manga.getMangaMetaMap
|
||||||
import suwayomi.tachidesk.manga.impl.util.lang.trimAll
|
import suwayomi.tachidesk.manga.impl.util.lang.trimAll
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
import suwayomi.tachidesk.manga.model.table.MangaStatus
|
||||||
@@ -44,6 +46,8 @@ data class MangaDataClass(
|
|||||||
val trackers: List<MangaTrackerDataClass>? = null,
|
val trackers: List<MangaTrackerDataClass>? = null,
|
||||||
val lastModifiedAt: Long = 0,
|
val lastModifiedAt: Long = 0,
|
||||||
val version: Long = 0,
|
val version: Long = 0,
|
||||||
|
@JsonIgnore
|
||||||
|
val memo: JsonObject = JsonObject(emptyMap()),
|
||||||
) {
|
) {
|
||||||
override fun toString(): String = "\"$title\" (id= $id) (sourceId= $sourceId)"
|
override fun toString(): String = "\"$title\" (id= $id) (sourceId= $sourceId)"
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ package suwayomi.tachidesk.manga.model.table
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
import org.jetbrains.exposed.v1.core.ReferenceOption
|
import org.jetbrains.exposed.v1.core.ReferenceOption
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.v1.json.json
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||||
import suwayomi.tachidesk.manga.model.table.columns.truncatingVarchar
|
import suwayomi.tachidesk.manga.model.table.columns.truncatingVarchar
|
||||||
|
import suwayomi.tachidesk.server.database.DBManager
|
||||||
|
|
||||||
object ChapterTable : IntIdTable() {
|
object ChapterTable : IntIdTable() {
|
||||||
val url = varchar("url", 2048)
|
val url = varchar("url", 2048)
|
||||||
@@ -42,6 +45,8 @@ object ChapterTable : IntIdTable() {
|
|||||||
val lastModifiedAt = long("last_modified_at").default(0)
|
val lastModifiedAt = long("last_modified_at").default(0)
|
||||||
val version = long("version").default(0)
|
val version = long("version").default(0)
|
||||||
val isSyncing = bool("is_syncing").default(false)
|
val isSyncing = bool("is_syncing").default(false)
|
||||||
|
|
||||||
|
val memo = json<JsonObject>("memo", DBManager.format)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
|
fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
|
||||||
@@ -64,4 +69,5 @@ fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
|
|||||||
pageCount = chapterEntry[pageCount],
|
pageCount = chapterEntry[pageCount],
|
||||||
lastModifiedAt = chapterEntry[lastModifiedAt],
|
lastModifiedAt = chapterEntry[lastModifiedAt],
|
||||||
version = chapterEntry[version],
|
version = chapterEntry[version],
|
||||||
|
memo = chapterEntry[memo],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ package suwayomi.tachidesk.manga.model.table
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
import org.jetbrains.exposed.v1.core.ResultRow
|
import org.jetbrains.exposed.v1.core.ResultRow
|
||||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||||
|
import org.jetbrains.exposed.v1.json.json
|
||||||
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||||
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
|
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
|
||||||
import suwayomi.tachidesk.manga.model.table.columns.truncatingVarchar
|
import suwayomi.tachidesk.manga.model.table.columns.truncatingVarchar
|
||||||
import suwayomi.tachidesk.manga.model.table.columns.unlimitedVarchar
|
import suwayomi.tachidesk.manga.model.table.columns.unlimitedVarchar
|
||||||
|
import suwayomi.tachidesk.server.database.DBManager
|
||||||
|
|
||||||
object MangaTable : IntIdTable() {
|
object MangaTable : IntIdTable() {
|
||||||
val url = varchar("url", 2048)
|
val url = varchar("url", 2048)
|
||||||
@@ -48,6 +51,7 @@ object MangaTable : IntIdTable() {
|
|||||||
val lastModifiedAt = long("last_modified_at").default(0)
|
val lastModifiedAt = long("last_modified_at").default(0)
|
||||||
val version = long("version").default(0)
|
val version = long("version").default(0)
|
||||||
val isSyncing = bool("is_syncing").default(false)
|
val isSyncing = bool("is_syncing").default(false)
|
||||||
|
val memo = json<JsonObject>("memo", DBManager.format)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||||
@@ -72,6 +76,7 @@ fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
|||||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[updateStrategy]),
|
updateStrategy = UpdateStrategy.valueOf(mangaEntry[updateStrategy]),
|
||||||
lastModifiedAt = mangaEntry[lastModifiedAt],
|
lastModifiedAt = mangaEntry[lastModifiedAt],
|
||||||
version = mangaEntry[version],
|
version = mangaEntry[version],
|
||||||
|
memo = mangaEntry[memo],
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class MangaStatus(
|
enum class MangaStatus(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.zaxxer.hikari.HikariDataSource
|
|||||||
import de.neonew.exposed.migrations.loadMigrationsFrom
|
import de.neonew.exposed.migrations.loadMigrationsFrom
|
||||||
import de.neonew.exposed.migrations.runMigrations
|
import de.neonew.exposed.migrations.runMigrations
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.jetbrains.exposed.v1.core.DatabaseConfig
|
import org.jetbrains.exposed.v1.core.DatabaseConfig
|
||||||
import org.jetbrains.exposed.v1.core.ExperimentalKeywordApi
|
import org.jetbrains.exposed.v1.core.ExperimentalKeywordApi
|
||||||
import org.jetbrains.exposed.v1.core.Schema
|
import org.jetbrains.exposed.v1.core.Schema
|
||||||
@@ -140,6 +141,8 @@ object DBManager {
|
|||||||
"Idle: ${ds.hikariPoolMXBean.idleConnections}, " +
|
"Idle: ${ds.hikariPoolMXBean.idleConnections}, " +
|
||||||
"Waiting: ${ds.hikariPoolMXBean.threadsAwaitingConnection}"
|
"Waiting: ${ds.hikariPoolMXBean.threadsAwaitingConnection}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val format = Json { prettyPrint = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package suwayomi.tachidesk.server.database.migration
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0057_AddMangaChapterMemoFields : SQLMigration() {
|
||||||
|
override val sql =
|
||||||
|
"""
|
||||||
|
ALTER TABLE MANGA ADD COLUMN memo JSON DEFAULT '{}';
|
||||||
|
ALTER TABLE CHAPTER ADD COLUMN memo JSON DEFAULT '{}';
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user