Switch to a new Ktlint Formatter (#705)

* Switch to new Ktlint plugin

* Add ktlintCheck to PR builds

* Run formatter

* Put ktlint version in libs toml

* Fix lint

* Use Zip4Java from libs.toml
This commit is contained in:
Mitchell Syer
2023-10-06 23:38:39 -04:00
committed by GitHub
parent 3cd3cb0186
commit 849acfca3d
277 changed files with 6709 additions and 5090 deletions
@@ -9,15 +9,11 @@ package eu.kanade.tachiyomi
import android.app.Application
import android.content.Context
// import android.content.res.Configuration
// import android.support.multidex.MultiDex
// import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.registry.default.DefaultRegistrar
open class App : Application() {
override fun onCreate() {
super.onCreate()
Injekt = InjektScope(DefaultRegistrar())
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
import suwayomi.tachidesk.server.generated.BuildConfig
/**
* Used by extensions.
@@ -14,14 +15,14 @@ object AppInfo {
*
* @since extension-lib 1.3
*/
fun getVersionCode() = suwayomi.tachidesk.server.BuildConfig.REVISION.substring(1).toInt()
fun getVersionCode() = BuildConfig.REVISION.substring(1).toInt()
/**
* should be something like "0.13.1"
*
* @since extension-lib 1.3
*/
fun getVersionName() = suwayomi.tachidesk.server.BuildConfig.VERSION.substring(1)
fun getVersionName() = BuildConfig.VERSION.substring(1)
/**
* A list of supported image MIME types by the reader.
@@ -28,7 +28,6 @@ import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
@@ -9,7 +9,6 @@ import kotlinx.coroutines.withContext
* Util for evaluating JavaScript in sources.
*/
class JavaScriptEngine(context: Context) {
/**
* Evaluate arbitrary JavaScript code and get the result as a primitive type
* (e.g., String, Int).
@@ -19,9 +18,10 @@ class JavaScriptEngine(context: Context) {
* @return Result of JavaScript code as a primitive type.
*/
@Suppress("UNUSED", "UNCHECKED_CAST")
suspend fun <T> evaluate(script: String): T = withContext(Dispatchers.IO) {
QuickJs.create().use {
it.evaluate(script) as T
suspend fun <T> evaluate(script: String): T =
withContext(Dispatchers.IO) {
QuickJs.create().use {
it.evaluate(script) as T
}
}
}
}
@@ -33,7 +33,10 @@ class MemoryCookieJar : CookieJar {
}
@Synchronized
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
override fun saveFromResponse(
url: HttpUrl,
cookies: List<Cookie>,
) {
val cookiesToAdd = cookies.map { WrappedCookie.wrap(it) }
cache.removeAll(cookiesToAdd)
@@ -27,8 +27,7 @@ import java.util.concurrent.TimeUnit
@Suppress("UNUSED_PARAMETER")
class NetworkHelper(context: Context) {
// private val preferences: PreferencesHelper by injectLazy()
// private val preferences: PreferencesHelper by injectLazy()
// private val cacheDir = File(context.cacheDir, "network_cache")
@@ -36,31 +35,36 @@ class NetworkHelper(context: Context) {
// Tachidesk -->
val cookieStore = PersistentCookieStore(context)
init {
CookieHandler.setDefault(
CookieManager(cookieStore, CookiePolicy.ACCEPT_ALL)
CookieManager(cookieStore, CookiePolicy.ACCEPT_ALL),
)
}
// Tachidesk <--
private val baseClientBuilder: OkHttpClient.Builder
get() {
val builder = OkHttpClient.Builder()
.cookieJar(PersistentCookieJar(cookieStore))
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.callTimeout(2, TimeUnit.MINUTES)
.addInterceptor(UserAgentInterceptor())
val builder =
OkHttpClient.Builder()
.cookieJar(PersistentCookieJar(cookieStore))
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.callTimeout(2, TimeUnit.MINUTES)
.addInterceptor(UserAgentInterceptor())
val httpLoggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
val logger = KotlinLogging.logger { }
val httpLoggingInterceptor =
HttpLoggingInterceptor(
object : HttpLoggingInterceptor.Logger {
val logger = KotlinLogging.logger { }
override fun log(message: String) {
logger.debug { message }
override fun log(message: String) {
logger.debug { message }
}
},
).apply {
level = HttpLoggingInterceptor.Level.BASIC
}
}).apply {
level = HttpLoggingInterceptor.Level.BASIC
}
builder.addInterceptor(httpLoggingInterceptor)
// when (preferences.dohProvider()) {
@@ -25,31 +25,32 @@ fun Call.asObservable(): Observable<Response> {
val call = clone()
// Wrap the call in a helper which handles both unsubscription and backpressure.
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
override fun request(n: Long) {
if (n == 0L || !compareAndSet(false, true)) return
val requestArbiter =
object : AtomicBoolean(), Producer, Subscription {
override fun request(n: Long) {
if (n == 0L || !compareAndSet(false, true)) return
try {
val response = call.execute()
if (!subscriber.isUnsubscribed) {
subscriber.onNext(response)
subscriber.onCompleted()
}
} catch (error: Exception) {
if (!subscriber.isUnsubscribed) {
subscriber.onError(error)
try {
val response = call.execute()
if (!subscriber.isUnsubscribed) {
subscriber.onNext(response)
subscriber.onCompleted()
}
} catch (error: Exception) {
if (!subscriber.isUnsubscribed) {
subscriber.onError(error)
}
}
}
}
override fun unsubscribe() {
// call.cancel()
}
override fun unsubscribe() {
// call.cancel()
}
override fun isUnsubscribed(): Boolean {
return call.isCanceled()
override fun isUnsubscribed(): Boolean {
return call.isCanceled()
}
}
}
subscriber.add(requestArbiter)
subscriber.setProducer(requestArbiter)
@@ -72,13 +73,19 @@ private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
return suspendCancellableCoroutine { continuation ->
val callback =
object : Callback {
override fun onResponse(call: Call, response: Response) {
override fun onResponse(
call: Call,
response: Response,
) {
continuation.resume(response) {
response.body.close()
}
}
override fun onFailure(call: Call, e: IOException) {
override fun onFailure(
call: Call,
e: IOException,
) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
val exception = IOException(e.message, e).apply { stackTrace = callStack }
@@ -116,16 +123,20 @@ suspend fun Call.awaitSuccess(): Response {
return response
}
fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body, listener))
.build()
}
.build()
fun OkHttpClient.newCachelessCallWithProgress(
request: Request,
listener: ProgressListener,
): Call {
val progressClient =
newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body, listener))
.build()
}
.build()
return progressClient.newCall(request)
}
@@ -139,7 +150,7 @@ context(Json)
@OptIn(ExperimentalSerializationApi::class)
fun <T> decodeFromJsonResponse(
deserializer: DeserializationStrategy<T>,
response: Response
response: Response,
): T {
return response.body.source().use {
decodeFromBufferedSource(deserializer, it)
@@ -6,8 +6,10 @@ import okhttp3.HttpUrl
// from TachiWeb-Server
class PersistentCookieJar(private val store: PersistentCookieStore) : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
override fun saveFromResponse(
url: HttpUrl,
cookies: List<Cookie>,
) {
store.addAll(url, cookies)
}
@@ -15,7 +15,6 @@ import kotlin.time.Duration.Companion.seconds
// from TachiWeb-Server
class PersistentCookieStore(context: Context) : CookieStore {
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
@@ -28,8 +27,9 @@ class PersistentCookieStore(context: Context) : CookieStore {
if (cookies != null) {
try {
val url = "http://$key".toHttpUrlOrNull() ?: continue
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
.filter { !it.hasExpired() }
val nonExpiredCookies =
cookies.mapNotNull { Cookie.parse(url, it) }
.filter { !it.hasExpired() }
cookieMap.put(key, nonExpiredCookies)
} catch (e: Exception) {
// Ignore
@@ -38,7 +38,10 @@ class PersistentCookieStore(context: Context) : CookieStore {
}
}
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
fun addAll(
url: HttpUrl,
cookies: List<Cookie>,
) {
lock.withLock {
val uri = url.toUri()
@@ -75,13 +78,17 @@ class PersistentCookieStore(context: Context) : CookieStore {
}
}
override fun get(uri: URI): List<HttpCookie> = get(uri.host).map {
it.toHttpCookie()
}
override fun get(uri: URI): List<HttpCookie> =
get(uri.host).map {
it.toHttpCookie()
}
fun get(url: HttpUrl) = get(url.toUri().host)
override fun add(uri: URI?, cookie: HttpCookie) {
override fun add(
uri: URI?,
cookie: HttpCookie,
) {
@Suppress("NAME_SHADOWING")
val uri = uri ?: URI("http://" + cookie.domain.removePrefix("."))
lock.withLock {
@@ -105,15 +112,19 @@ class PersistentCookieStore(context: Context) : CookieStore {
}
}
override fun remove(uri: URI?, cookie: HttpCookie): Boolean {
override fun remove(
uri: URI?,
cookie: HttpCookie,
): Boolean {
@Suppress("NAME_SHADOWING")
val uri = uri ?: URI("http://" + cookie.domain.removePrefix("."))
return lock.withLock {
val cookies = cookieMap[uri.host].orEmpty()
val index = cookies.indexOfFirst {
it.name == cookie.name &&
it.path == cookie.path
}
val index =
cookies.indexOfFirst {
it.name == cookie.name &&
it.path == cookie.path
}
if (index >= 0) {
val newList = cookies.toMutableList()
newList.removeAt(index)
@@ -132,45 +143,47 @@ class PersistentCookieStore(context: Context) : CookieStore {
private fun saveToDisk(uri: URI) {
// Get cookies to be stored in disk
val newValues = cookieMap[uri.host]
.orEmpty()
.asSequence()
.filter { it.persistent && !it.hasExpired() }
.map(Cookie::toString)
.toSet()
val newValues =
cookieMap[uri.host]
.orEmpty()
.asSequence()
.filter { it.persistent && !it.hasExpired() }
.map(Cookie::toString)
.toSet()
prefs.edit().putStringSet(uri.host, newValues).apply()
}
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt
private fun HttpCookie.toCookie(uri: URI) = Cookie.Builder()
.name(name)
.value(value)
.domain(uri.host)
.path(path ?: "/")
.let {
if (maxAge != -1L) {
it.expiresAt(System.currentTimeMillis() + maxAge.seconds.inWholeMilliseconds)
} else {
it.expiresAt(Long.MAX_VALUE)
private fun HttpCookie.toCookie(uri: URI) =
Cookie.Builder()
.name(name)
.value(value)
.domain(uri.host)
.path(path ?: "/")
.let {
if (maxAge != -1L) {
it.expiresAt(System.currentTimeMillis() + maxAge.seconds.inWholeMilliseconds)
} else {
it.expiresAt(Long.MAX_VALUE)
}
}
}
.let {
if (secure) {
it.secure()
} else {
it
.let {
if (secure) {
it.secure()
} else {
it
}
}
}
.let {
if (isHttpOnly) {
it.httpOnly()
} else {
it
.let {
if (isHttpOnly) {
it.httpOnly()
} else {
it
}
}
}
.build()
.build()
private fun Cookie.toHttpCookie(): HttpCookie {
val it = this
@@ -178,11 +191,12 @@ class PersistentCookieStore(context: Context) : CookieStore {
domain = it.domain
path = it.path
secure = it.secure
maxAge = if (it.persistent) {
-1
} else {
(it.expiresAt.milliseconds - System.currentTimeMillis().milliseconds).inWholeSeconds
}
maxAge =
if (it.persistent) {
-1
} else {
(it.expiresAt.milliseconds - System.currentTimeMillis().milliseconds).inWholeSeconds
}
isHttpOnly = it.httpOnly
}
@@ -1,5 +1,9 @@
package eu.kanade.tachiyomi.network
interface ProgressListener {
fun update(bytesRead: Long, contentLength: Long, done: Boolean)
fun update(
bytesRead: Long,
contentLength: Long,
done: Boolean,
)
}
@@ -10,7 +10,6 @@ import okio.buffer
import java.io.IOException
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
private val bufferedSource: BufferedSource by lazy {
source(responseBody.source()).buffer()
}
@@ -32,7 +31,10 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p
var totalBytesRead = 0L
@Throws(IOException::class)
override fun read(sink: Buffer, byteCount: Long): Long {
override fun read(
sink: Buffer,
byteCount: Long,
): Long {
val bytesRead = super.read(sink, byteCount)
// read() returns the number of bytes read, or -1 if this source is exhausted.
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
@@ -1,3 +1,5 @@
@file:Suppress("ktlint:standard:function-naming")
package eu.kanade.tachiyomi.network
import okhttp3.CacheControl
@@ -15,7 +17,7 @@ private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
fun GET(
url: String,
headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL
cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request {
return Request.Builder()
.url(url)
@@ -30,7 +32,7 @@ fun GET(
fun GET(
url: HttpUrl,
headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL
cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request {
return Request.Builder()
.url(url)
@@ -43,7 +45,7 @@ fun POST(
url: String,
headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL
cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request {
return Request.Builder()
.url(url)
@@ -81,51 +81,56 @@ object CFClearance {
logger.debug { "resolveWithWebView($url)" }
val cookies = Playwright.create().use { playwright ->
playwright.chromium().launch(
LaunchOptions()
.setHeadless(false)
.apply {
if (serverConfig.socksProxyEnabled.value) {
setProxy("socks5://${serverConfig.socksProxyHost.value}:${serverConfig.socksProxyPort.value}")
val cookies =
Playwright.create().use { playwright ->
playwright.chromium().launch(
LaunchOptions()
.setHeadless(false)
.apply {
if (serverConfig.socksProxyEnabled.value) {
setProxy("socks5://${serverConfig.socksProxyHost.value}:${serverConfig.socksProxyPort.value}")
}
},
).use { browser ->
val userAgent = originalRequest.header("User-Agent")
if (userAgent != null) {
browser.newContext(Browser.NewContextOptions().setUserAgent(userAgent)).use { browserContext ->
browserContext.newPage().use { getCookies(it, url) }
}
} else {
browser.newPage().use { getCookies(it, url) }
}
).use { browser ->
val userAgent = originalRequest.header("User-Agent")
if (userAgent != null) {
browser.newContext(Browser.NewContextOptions().setUserAgent(userAgent)).use { browserContext ->
browserContext.newPage().use { getCookies(it, url) }
}
} else {
browser.newPage().use { getCookies(it, url) }
}
}
}
// Copy cookies to cookie store
cookies.groupBy { it.domain }.forEach { (domain, cookies) ->
network.cookieStore.addAll(
url = HttpUrl.Builder()
.scheme("http")
.host(domain)
.build(),
cookies = cookies
url =
HttpUrl.Builder()
.scheme("http")
.host(domain)
.build(),
cookies = cookies,
)
}
// Merge new and existing cookies for this request
// Find the cookies that we need to merge into this request
val convertedForThisRequest = cookies.filter {
it.matches(originalRequest.url)
}
val convertedForThisRequest =
cookies.filter {
it.matches(originalRequest.url)
}
// Extract cookies from current request
val existingCookies = Cookie.parseAll(
originalRequest.url,
originalRequest.headers
)
val existingCookies =
Cookie.parseAll(
originalRequest.url,
originalRequest.headers,
)
// Filter out existing values of cookies that we are about to merge in
val filteredExisting = existingCookies.filter { existing ->
convertedForThisRequest.none { converted -> converted.name == existing.name }
}
val filteredExisting =
existingCookies.filter { existing ->
convertedForThisRequest.none { converted -> converted.name == existing.name }
}
logger.trace { "Existing cookies" }
logger.trace { existingCookies.joinToString("; ") }
val newCookies = filteredExisting + convertedForThisRequest
@@ -143,7 +148,7 @@ object CFClearance {
Playwright.create().use { playwright ->
playwright.chromium().launch(
LaunchOptions()
.setHeadless(true)
.setHeadless(true),
).use { browser ->
browser.newPage().use { page ->
val userAgent = page.evaluate("() => {return navigator.userAgent}") as String
@@ -158,7 +163,10 @@ object CFClearance {
}
}
private fun getCookies(page: Page, url: String): List<Cookie> {
private fun getCookies(
page: Page,
url: String,
): List<Cookie> {
applyStealthInitScripts(page)
page.navigate(url)
val challengeResolved = waitForChallengeResolve(page)
@@ -198,7 +206,7 @@ object CFClearance {
ServerConfig::class.java.getResource("/cloudflare-js/navigator.permissions.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/navigator.webdriver.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/chrome.runtime.js")!!.readText(),
ServerConfig::class.java.getResource("/cloudflare-js/chrome.plugin.js")!!.readText()
ServerConfig::class.java.getResource("/cloudflare-js/chrome.plugin.js")!!.readText(),
)
}
@@ -215,12 +223,13 @@ object CFClearance {
val timeoutSeconds = 120
repeat(timeoutSeconds) {
page.waitForTimeout(1.seconds.toDouble(DurationUnit.MILLISECONDS))
val success = try {
page.querySelector("#challenge-form") == null
} catch (e: Exception) {
logger.debug(e) { "query Error" }
false
}
val success =
try {
page.querySelector("#challenge-form") == null
} catch (e: Exception) {
logger.debug(e) { "query Error" }
false
}
if (success) return true
}
return false
@@ -34,7 +34,7 @@ import kotlin.time.toDurationUnit
fun OkHttpClient.Builder.rateLimit(
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(null, permits, period.toDuration(unit.toDurationUnit())))
/**
@@ -50,17 +50,18 @@ fun OkHttpClient.Builder.rateLimit(
* @param permits [Int] Number of requests allowed within a period of units.
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
*/
fun OkHttpClient.Builder.rateLimit(permits: Int, period: Duration = 1.seconds) =
addInterceptor(RateLimitInterceptor(null, permits, period))
fun OkHttpClient.Builder.rateLimit(
permits: Int,
period: Duration = 1.seconds,
) = addInterceptor(RateLimitInterceptor(null, permits, period))
/** We can probably accept domains or wildcards by comparing with [endsWith], etc. */
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
internal class RateLimitInterceptor(
private val host: String?,
private val permits: Int,
period: Duration
period: Duration,
) : Interceptor {
private val requestQueue = ArrayDeque<Long>(permits)
private val rateLimitMillis = period.inWholeMilliseconds
private val fairLock = Semaphore(1, true)
@@ -98,7 +99,8 @@ internal class RateLimitInterceptor(
} else if (hasRemovedExpired) {
break
} else {
try { // wait for the first entry to expire, or notified by cached response
try {
// wait for the first entry to expire, or notified by cached response
(requestQueue as Object).wait(requestQueue.first - periodStart)
} catch (_: InterruptedException) {
continue
@@ -29,7 +29,7 @@ fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl,
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())))
/**
@@ -49,7 +49,7 @@ fun OkHttpClient.Builder.rateLimitHost(
fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl,
permits: Int,
period: Duration = 1.seconds
period: Duration = 1.seconds,
): OkHttpClient.Builder = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period))
/**
@@ -69,5 +69,5 @@ fun OkHttpClient.Builder.rateLimitHost(
fun OkHttpClient.Builder.rateLimitHost(
url: String,
permits: Int,
period: Duration = 1.seconds
period: Duration = 1.seconds,
): OkHttpClient.Builder = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))
@@ -9,11 +9,12 @@ class UserAgentInterceptor : Interceptor {
val originalRequest = chain.request()
return if (originalRequest.header("User-Agent").isNullOrEmpty()) {
val newRequest = originalRequest
.newBuilder()
.removeHeader("User-Agent")
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT)
.build()
val newRequest =
originalRequest
.newBuilder()
.removeHeader("User-Agent")
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT)
.build()
chain.proceed(newRequest)
} else {
chain.proceed(originalRequest)
@@ -6,7 +6,6 @@ import rx.Observable
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
interface CatalogueSource : Source {
/**
* An ISO 639-1 compliant language code (two letters in lower case).
*/
@@ -37,7 +36,11 @@ interface CatalogueSource : Source {
* @param filters the list of filters to apply.
*/
@Suppress("DEPRECATION")
suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
suspend fun getSearchManga(
page: Int,
query: String,
filters: FilterList,
): MangasPage {
return fetchSearchManga(page, query, filters).awaitSingle()
}
@@ -59,22 +62,23 @@ interface CatalogueSource : Source {
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getPopularManga")
ReplaceWith("getPopularManga"),
)
fun fetchPopularManga(page: Int): Observable<MangasPage> =
throw IllegalStateException("Not used")
fun fetchPopularManga(page: Int): Observable<MangasPage> = throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getSearchManga")
ReplaceWith("getSearchManga"),
)
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
throw IllegalStateException("Not used")
fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> = throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getLatestUpdates")
ReplaceWith("getLatestUpdates"),
)
fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
throw IllegalStateException("Not used")
fun fetchLatestUpdates(page: Int): Observable<MangasPage> = throw IllegalStateException("Not used")
}
@@ -8,14 +8,12 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
interface ConfigurableSource : Source {
/**
* Gets instance of [SharedPreferences] scoped to the specific source.
*
* @since extensions-lib 1.5
*/
fun getSourcePreferences(): SharedPreferences =
Injekt.get<Application>().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE)
fun getSourcePreferences(): SharedPreferences = Injekt.get<Application>().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE)
fun setupPreferenceScreen(screen: PreferenceScreen)
}
@@ -10,7 +10,6 @@ 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...
*/
interface Source {
/**
* Id for the source. Must be unique.
*/
@@ -60,19 +59,19 @@ interface Source {
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getMangaDetails")
ReplaceWith("getMangaDetails"),
)
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getChapterList")
ReplaceWith("getChapterList"),
)
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getPageList")
ReplaceWith("getPageList"),
)
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.empty()
}
@@ -1,3 +1,5 @@
@file:Suppress("ktlint:standard:property-naming")
package eu.kanade.tachiyomi.source.local
import eu.kanade.tachiyomi.source.CatalogueSource
@@ -55,9 +57,8 @@ import com.github.junrar.Archive as JunrarArchive
class LocalSource(
private val fileSystem: LocalSourceFileSystem,
private val coverManager: LocalCoverManager
private val coverManager: LocalCoverManager,
) : CatalogueSource, UnmeteredSource {
private val json: Json by injectLazy()
private val xml: XML by injectLazy()
@@ -79,56 +80,64 @@ class LocalSource(
override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS)
override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
override suspend fun getSearchManga(
page: Int,
query: String,
filters: FilterList,
): MangasPage {
val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
var mangaDirs = baseDirsFiles
// Filter out files that are hidden and is not a folder
.filter { it.isDirectory && !it.name.startsWith('.') }
.distinctBy { it.name }
.filter { // Filter by query or last modified
if (lastModifiedLimit == 0L) {
it.name.contains(query, ignoreCase = true)
} else {
it.lastModified() >= lastModifiedLimit
var mangaDirs =
baseDirsFiles
// Filter out files that are hidden and is not a folder
.filter { it.isDirectory && !it.name.startsWith('.') }
.distinctBy { it.name }
.filter { // Filter by query or last modified
if (lastModifiedLimit == 0L) {
it.name.contains(query, ignoreCase = true)
} else {
it.lastModified() >= lastModifiedLimit
}
}
}
filters.forEach { filter ->
when (filter) {
is OrderBy.Popular -> {
mangaDirs = if (filter.state!!.ascending) {
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
} else {
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
}
mangaDirs =
if (filter.state!!.ascending) {
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
} else {
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
}
}
is OrderBy.Latest -> {
mangaDirs = if (filter.state!!.ascending) {
mangaDirs.sortedBy(File::lastModified)
} else {
mangaDirs.sortedByDescending(File::lastModified)
}
mangaDirs =
if (filter.state!!.ascending) {
mangaDirs.sortedBy(File::lastModified)
} else {
mangaDirs.sortedByDescending(File::lastModified)
}
}
else -> {
/* Do nothing */
// Do nothing
}
}
}
// Transform mangaDirs to list of SManga
val mangas = mangaDirs.map { mangaDir ->
SManga.create().apply {
title = mangaDir.name
url = mangaDir.name
val mangas =
mangaDirs.map { mangaDir ->
SManga.create().apply {
title = mangaDir.name
url = mangaDir.name
// Try to find the cover
coverManager.find(mangaDir.name)
?.takeIf(File::exists)
?.let { thumbnail_url = it.absolutePath }
// Try to find the cover
coverManager.find(mangaDir.name)
?.takeIf(File::exists)
?.let { thumbnail_url = it.absolutePath }
}
}
}
// Fetch chapters of all the manga
mangas.forEach { manga ->
@@ -156,67 +165,75 @@ class LocalSource(
}
// Manga details related
override suspend fun getMangaDetails(manga: SManga): SManga = withContext(Dispatchers.IO) {
coverManager.find(manga.url)?.let {
manga.thumbnail_url = it.absolutePath
}
// Augment manga details based on metadata files
try {
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList()
val comicInfoFile = mangaDirFiles
.firstOrNull { it.name == COMIC_INFO_FILE }
val noXmlFile = mangaDirFiles
.firstOrNull { it.name == ".noxml" }
val legacyJsonDetailsFile = mangaDirFiles
.firstOrNull { it.extension == "json" }
when {
// Top level ComicInfo.xml
comicInfoFile != null -> {
noXmlFile?.delete()
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
}
// TODO: automatically convert these to ComicInfo.xml
legacyJsonDetailsFile != null -> {
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
title?.let { manga.title = it }
author?.let { manga.author = it }
artist?.let { manga.artist = it }
description?.let { manga.description = it }
genre?.let { manga.genre = it.joinToString() }
status?.let { manga.status = it }
}
}
// Copy ComicInfo.xml from chapter archive to top level if found
noXmlFile == null -> {
val chapterArchives = mangaDirFiles
.filter(Archive::isSupported)
.toList()
val mangaDir = fileSystem.getMangaDirectory(manga.url)
val folderPath = mangaDir?.absolutePath
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
if (copiedFile != null) {
setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga)
} else {
// Avoid re-scanning
File("$folderPath/.noxml").createNewFile()
}
}
override suspend fun getMangaDetails(manga: SManga): SManga =
withContext(Dispatchers.IO) {
coverManager.find(manga.url)?.let {
manga.thumbnail_url = it.absolutePath
}
} catch (e: Throwable) {
logger.error(e) { "Error setting manga details from local metadata for ${manga.title}" }
// Augment manga details based on metadata files
try {
val mangaDirFiles = fileSystem.getFilesInMangaDirectory(manga.url).toList()
val comicInfoFile =
mangaDirFiles
.firstOrNull { it.name == COMIC_INFO_FILE }
val noXmlFile =
mangaDirFiles
.firstOrNull { it.name == ".noxml" }
val legacyJsonDetailsFile =
mangaDirFiles
.firstOrNull { it.extension == "json" }
when {
// Top level ComicInfo.xml
comicInfoFile != null -> {
noXmlFile?.delete()
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
}
// TODO: automatically convert these to ComicInfo.xml
legacyJsonDetailsFile != null -> {
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
title?.let { manga.title = it }
author?.let { manga.author = it }
artist?.let { manga.artist = it }
description?.let { manga.description = it }
genre?.let { manga.genre = it.joinToString() }
status?.let { manga.status = it }
}
}
// Copy ComicInfo.xml from chapter archive to top level if found
noXmlFile == null -> {
val chapterArchives =
mangaDirFiles
.filter(Archive::isSupported)
.toList()
val mangaDir = fileSystem.getMangaDirectory(manga.url)
val folderPath = mangaDir?.absolutePath
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
if (copiedFile != null) {
setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga)
} else {
// Avoid re-scanning
File("$folderPath/.noxml").createNewFile()
}
}
}
} catch (e: Throwable) {
logger.error(e) { "Error setting manga details from local metadata for ${manga.title}" }
}
return@withContext manga
}
return@withContext manga
}
private fun copyComicInfoFileFromArchive(chapterArchives: List<File>, folderPath: String?): File? {
private fun copyComicInfoFileFromArchive(
chapterArchives: List<File>,
folderPath: String?,
): File? {
for (chapter in chapterArchives) {
when (Format.valueOf(chapter)) {
is Format.Zip -> {
@@ -243,7 +260,10 @@ class LocalSource(
return null
}
private fun copyComicInfoFile(comicInfoFileStream: InputStream, folderPath: String?): File {
private fun copyComicInfoFile(
comicInfoFileStream: InputStream,
folderPath: String?,
): File {
return File("$folderPath/$COMIC_INFO_FILE").apply {
outputStream().use { outputStream ->
comicInfoFileStream.use { it.copyTo(outputStream) }
@@ -251,10 +271,14 @@ class LocalSource(
}
}
private fun setMangaDetailsFromComicInfoFile(stream: InputStream, manga: SManga) {
val comicInfo = KtXmlReader(stream, StandardCharsets.UTF_8.name()).use {
xml.decodeFromReader<ComicInfo>(it)
}
private fun setMangaDetailsFromComicInfoFile(
stream: InputStream,
manga: SManga,
) {
val comicInfo =
KtXmlReader(stream, StandardCharsets.UTF_8.name()).use {
xml.decodeFromReader<ComicInfo>(it)
}
manga.copyFromComicInfo(comicInfo)
}
@@ -267,15 +291,17 @@ class LocalSource(
.map { chapterFile ->
SChapter.create().apply {
url = "${manga.url}/${chapterFile.name}"
name = if (chapterFile.isDirectory) {
chapterFile.name
} else {
chapterFile.nameWithoutExtension
}
name =
if (chapterFile.isDirectory) {
chapterFile.name
} else {
chapterFile.nameWithoutExtension
}
date_upload = chapterFile.lastModified()
chapter_number = ChapterRecognition
.parseChapterNumber(manga.title, this.name, this.chapter_number.toDouble())
.toFloat()
chapter_number =
ChapterRecognition
.parseChapterNumber(manga.title, this.name, this.chapter_number.toDouble())
.toFloat()
val format = Format.valueOf(chapterFile)
if (format is Format.Epub) {
@@ -305,7 +331,7 @@ class LocalSource(
.mapIndexed { index, page ->
Page(
index,
imageUrl = applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name
imageUrl = applicationDirs.localMangaRoot + "/" + chapter.url + "/" + page.name,
)
}
}
@@ -347,39 +373,46 @@ class LocalSource(
}
}
private fun updateCover(chapter: SChapter, manga: SManga): File? {
private fun updateCover(
chapter: SChapter,
manga: SManga,
): File? {
return try {
when (val format = getFormat(chapter)) {
is Format.Directory -> {
val entry = format.file.listFiles()
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
val entry =
format.file.listFiles()
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
entry?.let { coverManager.update(manga, it.inputStream()) }
}
is Format.Zip -> {
ZipFile(format.file).use { zip ->
val entry = zip.entries.toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
val entry =
zip.entries.toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
entry?.let { coverManager.update(manga, zip.getInputStream(it)) }
}
}
is Format.Rar -> {
JunrarArchive(format.file).use { archive ->
val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
val entry =
archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
entry?.let { coverManager.update(manga, archive.getInputStream(it)) }
}
}
is Format.Epub -> {
EpubFile(format.file).use { epub ->
val entry = epub.getImagesFromPages()
.firstOrNull()
?.let { epub.getEntry(it) }
val entry =
epub.getImagesFromPages()
.firstOrNull()
?.let { epub.getEntry(it) }
entry?.let { coverManager.update(manga, epub.getInputStream(it)) }
}
@@ -412,16 +445,17 @@ class LocalSource(
if (sourceRecord == null) {
// must do this to avoid database integrity errors
val extensionId = ExtensionTable.insertAndGetId {
it[apkName] = "localSource"
it[name] = EXTENSION_NAME
it[pkgName] = LocalSource::class.java.`package`.name
it[versionName] = "1.2"
it[versionCode] = 0
it[lang] = LANG
it[isNsfw] = false
it[isInstalled] = true
}
val extensionId =
ExtensionTable.insertAndGetId {
it[apkName] = "localSource"
it[name] = EXTENSION_NAME
it[pkgName] = LocalSource::class.java.`package`.name
it[versionName] = "1.2"
it[versionCode] = 0
it[lang] = LANG
it[isNsfw] = false
it[isInstalled] = true
}
SourceTable.insert {
it[id] = ID
@@ -5,8 +5,9 @@ import eu.kanade.tachiyomi.source.model.Filter
sealed class OrderBy(selection: Selection) : Filter.Sort(
"Order by",
arrayOf("Title", "Date"),
selection
selection,
) {
class Popular() : OrderBy(Selection(0, true))
class Latest() : OrderBy(Selection(1, false))
}
@@ -9,9 +9,8 @@ import java.io.InputStream
private const val DEFAULT_COVER_NAME = "cover.jpg"
class LocalCoverManager(
private val fileSystem: LocalSourceFileSystem
private val fileSystem: LocalSourceFileSystem,
) {
fun find(mangaUrl: String): File? {
return fileSystem.getFilesInMangaDirectory(mangaUrl)
// Get all file whose names start with 'cover'
@@ -24,7 +23,7 @@ class LocalCoverManager(
fun update(
manga: SManga,
inputStream: InputStream
inputStream: InputStream,
): File? {
val directory = fileSystem.getMangaDirectory(manga.url)
if (directory == null) {
@@ -3,10 +3,10 @@ package eu.kanade.tachiyomi.source.local.io
import java.io.File
object Archive {
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
fun isSupported(file: File): Boolean = with(file) {
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
}
fun isSupported(file: File): Boolean =
with(file) {
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
}
}
@@ -4,22 +4,25 @@ import java.io.File
sealed interface Format {
data class Directory(val file: File) : Format
data class Zip(val file: File) : Format
data class Rar(val file: File) : Format
data class Epub(val file: File) : Format
class UnknownFormatException : Exception()
companion object {
fun valueOf(file: File) = with(file) {
when {
isDirectory -> Directory(this)
extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)
extension.equals("rar", true) || extension.equals("cbr", true) -> Rar(this)
extension.equals("epub", true) -> Epub(this)
else -> throw UnknownFormatException()
fun valueOf(file: File) =
with(file) {
when {
isDirectory -> Directory(this)
extension.equals("zip", true) || extension.equals("cbz", true) -> Zip(this)
extension.equals("rar", true) || extension.equals("cbr", true) -> Rar(this)
extension.equals("epub", true) -> Epub(this)
else -> throw UnknownFormatException()
}
}
}
}
}
@@ -4,9 +4,8 @@ import suwayomi.tachidesk.server.ApplicationDirs
import java.io.File
class LocalSourceFileSystem(
private val applicationDirs: ApplicationDirs
private val applicationDirs: ApplicationDirs,
) {
fun getBaseDirectories(): Sequence<File> {
return sequenceOf(File(applicationDirs.localMangaRoot))
}
@@ -7,7 +7,6 @@ import java.io.File
* Loader used to load a chapter from a .epub file.
*/
class EpubPageLoader(file: File) : PageLoader {
private val epub = EpubFile(file)
override suspend fun getPages(): List<ReaderPage> {
@@ -13,7 +13,6 @@ import java.io.PipedOutputStream
* Loader used to load a chapter from a .rar or .cbr file.
*/
class RarPageLoader(file: File) : PageLoader {
private val rar = Archive(file)
override suspend fun getPages(): List<ReaderPage> {
@@ -35,7 +34,10 @@ class RarPageLoader(file: File) : PageLoader {
/**
* Returns an input stream for the given [header].
*/
private fun getStream(rar: Archive, header: FileHeader): InputStream {
private fun getStream(
rar: Archive,
header: FileHeader,
): InputStream {
val pipeIn = PipedInputStream()
val pipeOut = PipedOutputStream(pipeIn)
synchronized(this) {
@@ -7,5 +7,5 @@ class ReaderPage(
index: Int,
url: String = "",
imageUrl: String? = null,
var stream: (() -> InputStream)? = null
var stream: (() -> InputStream)? = null,
) : Page(index, url, imageUrl, null)
@@ -9,7 +9,6 @@ import java.io.File
* Loader used to load a chapter from a .zip or .cbz file.
*/
class ZipPageLoader(file: File) : PageLoader {
private val zip = ZipFile(file)
override suspend fun getPages(): List<ReaderPage> {
@@ -16,7 +16,7 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
listOfNotNull(
comicInfo.genre?.value,
comicInfo.tags?.value,
comicInfo.categories?.value
comicInfo.categories?.value,
)
.distinct()
.joinToString(", ") { it.trim() }
@@ -28,7 +28,7 @@ fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
comicInfo.inker?.value,
comicInfo.colorist?.value,
comicInfo.letterer?.value,
comicInfo.coverArtist?.value
comicInfo.coverArtist?.value,
)
.flatMap { it.split(", ") }
.distinct()
@@ -57,7 +57,7 @@ data class ComicInfo(
val tags: Tags?,
val web: Web?,
val publishingStatus: PublishingStatusTachiyomi?,
val categories: CategoriesTachiyomi?
val categories: CategoriesTachiyomi?,
) {
@Suppress("UNUSED")
@XmlElement(false)
@@ -71,73 +71,105 @@ data class ComicInfo(
@Serializable
@XmlSerialName("Title", "", "")
data class Title(@XmlValue(true) val value: String = "")
data class Title(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("Series", "", "")
data class Series(@XmlValue(true) val value: String = "")
data class Series(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("Number", "", "")
data class Number(@XmlValue(true) val value: String = "")
data class Number(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("Summary", "", "")
data class Summary(@XmlValue(true) val value: String = "")
data class Summary(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("Writer", "", "")
data class Writer(@XmlValue(true) val value: String = "")
data class Writer(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("Penciller", "", "")
data class Penciller(@XmlValue(true) val value: String = "")
data class Penciller(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("Inker", "", "")
data class Inker(@XmlValue(true) val value: String = "")
data class Inker(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("Colorist", "", "")
data class Colorist(@XmlValue(true) val value: String = "")
data class Colorist(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("Letterer", "", "")
data class Letterer(@XmlValue(true) val value: String = "")
data class Letterer(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("CoverArtist", "", "")
data class CoverArtist(@XmlValue(true) val value: String = "")
data class CoverArtist(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("Translator", "", "")
data class Translator(@XmlValue(true) val value: String = "")
data class Translator(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("Genre", "", "")
data class Genre(@XmlValue(true) val value: String = "")
data class Genre(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("Tags", "", "")
data class Tags(@XmlValue(true) val value: String = "")
data class Tags(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("Web", "", "")
data class Web(@XmlValue(true) val value: String = "")
data class Web(
@XmlValue(true) val value: String = "",
)
// The spec doesn't have a good field for this
@Serializable
@XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty")
data class PublishingStatusTachiyomi(@XmlValue(true) val value: String = "")
data class PublishingStatusTachiyomi(
@XmlValue(true) val value: String = "",
)
@Serializable
@XmlSerialName("Categories", "http://www.w3.org/2001/XMLSchema", "ty")
data class CategoriesTachiyomi(@XmlValue(true) val value: String = "")
data class CategoriesTachiyomi(
@XmlValue(true) val value: String = "",
)
}
enum class ComicInfoPublishingStatus(
val comicInfoValue: String,
val sMangaModelValue: Int
val sMangaModelValue: Int,
) {
ONGOING("Ongoing", SManga.ONGOING),
COMPLETED("Completed", SManga.COMPLETED),
@@ -145,7 +177,7 @@ enum class ComicInfoPublishingStatus(
PUBLISHING_FINISHED("Publishing finished", SManga.PUBLISHING_FINISHED),
CANCELLED("Cancelled", SManga.CANCELLED),
ON_HIATUS("On hiatus", SManga.ON_HIATUS),
UNKNOWN("Unknown", SManga.UNKNOWN)
UNKNOWN("Unknown", SManga.UNKNOWN),
;
companion object {
@@ -9,5 +9,5 @@ class MangaDetails(
val artist: String? = null,
val description: String? = null,
val genre: List<String>? = null,
val status: Int? = null
val status: Int? = null,
)
@@ -4,15 +4,22 @@ package eu.kanade.tachiyomi.source.model
// sealed class Filter<T>(val name: String, var state: T) {
open class Filter<T>(val name: String, var state: T) {
open class Header(name: String) : Filter<Any>(name, 0)
open class Separator(name: String = "") : Filter<Any>(name, 0)
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state) {
val displayValues get() = values.map { it.toString() }
}
abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {
fun isIgnored() = state == STATE_IGNORE
fun isIncluded() = state == STATE_INCLUDE
fun isExcluded() = state == STATE_EXCLUDE
companion object {
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.source.model
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
}
@@ -9,18 +9,23 @@ open class Page(
val index: Int,
val url: String = "",
var imageUrl: String? = null,
@Transient var uri: Uri? = null // Deprecated but can't be deleted due to extensions
// Deprecated but can't be deleted due to extensions
@Transient var uri: Uri? = null,
) : ProgressListener {
private val _progress = MutableStateFlow(0)
val progress = _progress.asStateFlow()
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
_progress.value = if (contentLength > 0) {
(100 * bytesRead / contentLength).toInt()
} else {
-1
}
override fun update(
bytesRead: Long,
contentLength: Long,
done: Boolean,
) {
_progress.value =
if (contentLength > 0) {
(100 * bytesRead / contentLength).toInt()
} else {
-1
}
}
companion object {
@@ -1,9 +1,10 @@
@file:Suppress("ktlint:standard:property-naming")
package eu.kanade.tachiyomi.source.model
import java.io.Serializable
interface SChapter : Serializable {
var url: String
var name: String
@@ -1,7 +1,8 @@
@file:Suppress("ktlint:standard:property-naming")
package eu.kanade.tachiyomi.source.model
class SChapterImpl : SChapter {
override lateinit var url: String
override lateinit var name: String
@@ -1,9 +1,10 @@
@file:Suppress("ktlint:standard:property-naming")
package eu.kanade.tachiyomi.source.model
import java.io.Serializable
interface SManga : Serializable {
var url: String
var title: String
@@ -1,7 +1,8 @@
@file:Suppress("ktlint:standard:property-naming")
package eu.kanade.tachiyomi.source.model
class SMangaImpl : SManga {
override lateinit var url: String
override lateinit var title: String
@@ -2,5 +2,5 @@ package eu.kanade.tachiyomi.source.model
enum class UpdateStrategy {
ALWAYS_UPDATE,
ONLY_FETCH_ONCE
ONLY_FETCH_ONCE,
}
@@ -19,7 +19,6 @@ import okhttp3.Response
import rx.Observable
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
import uy.kohesive.injekt.injectLazy
// import uy.kohesive.injekt.injectLazy
import java.net.URI
import java.net.URISyntaxException
import java.security.MessageDigest
@@ -28,7 +27,6 @@ import java.security.MessageDigest
* A simple implementation for sources from a website.
*/
abstract class HttpSource : CatalogueSource {
/**
* Network service.
*/
@@ -91,7 +89,11 @@ abstract class HttpSource : CatalogueSource {
* @param versionId [Int] the version ID of the source
* @return a unique ID for the source
*/
protected fun generateId(name: String, lang: String, versionId: Int): Long {
protected fun generateId(
name: String,
lang: String,
versionId: Int,
): Long {
val key = "${name.lowercase()}/$lang/$versionId"
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
@@ -100,9 +102,10 @@ abstract class HttpSource : CatalogueSource {
/**
* Headers builder for requests. Implementations can override this method for custom headers.
*/
protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", DEFAULT_USER_AGENT)
}
protected open fun headersBuilder() =
Headers.Builder().apply {
add("User-Agent", DEFAULT_USER_AGENT)
}
/**
* Visible name of the source.
@@ -147,7 +150,11 @@ abstract class HttpSource : CatalogueSource {
* @param filters the list of filters to apply.
*/
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
override fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> {
return client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
@@ -162,7 +169,11 @@ abstract class HttpSource : CatalogueSource {
* @param query the search query.
* @param filters the list of filters to apply.
*/
protected abstract fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request
protected abstract fun searchMangaRequest(
page: Int,
query: String,
filters: FilterList,
): Request
/**
* Parses the response from the site and returns a [MangasPage] object.
@@ -450,7 +461,10 @@ abstract class HttpSource : CatalogueSource {
* @param chapter the chapter to be added.
* @param manga the manga of the chapter.
*/
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {}
open fun prepareNewChapter(
chapter: SChapter,
manga: SManga,
) {}
/**
* Returns the list of filters for the source.
@@ -13,7 +13,6 @@ import org.jsoup.nodes.Element
* A simple implementation for sources from a website using Jsoup, an HTML parser.
*/
abstract class ParsedHttpSource : HttpSource() {
/**
* Parses the response from the site and returns a [MangasPage] object.
*
@@ -22,13 +21,15 @@ abstract class ParsedHttpSource : HttpSource() {
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(popularMangaSelector()).map { element ->
popularMangaFromElement(element)
}
val mangas =
document.select(popularMangaSelector()).map { element ->
popularMangaFromElement(element)
}
val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
val hasNextPage =
popularMangaNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
@@ -60,13 +61,15 @@ abstract class ParsedHttpSource : HttpSource() {
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(searchMangaSelector()).map { element ->
searchMangaFromElement(element)
}
val mangas =
document.select(searchMangaSelector()).map { element ->
searchMangaFromElement(element)
}
val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
val hasNextPage =
searchMangaNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
@@ -98,13 +101,15 @@ abstract class ParsedHttpSource : HttpSource() {
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector()).map { element ->
latestUpdatesFromElement(element)
}
val mangas =
document.select(latestUpdatesSelector()).map { element ->
latestUpdatesFromElement(element)
}
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
val hasNextPage =
latestUpdatesNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.model.SManga
*/
@Suppress("unused")
interface ResolvableSource : Source {
/**
* Whether this source may potentially handle the given URI.
*
@@ -5,11 +5,17 @@ import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
fun Element.selectText(css: String, defaultValue: String? = null): String? {
fun Element.selectText(
css: String,
defaultValue: String? = null,
): String? {
return select(css).first()?.text() ?: defaultValue
}
fun Element.selectInt(css: String, defaultValue: Int = 0): Int {
fun Element.selectInt(
css: String,
defaultValue: Int = 0,
): Int {
return select(css).first()?.text()?.toInt() ?: defaultValue
}
@@ -4,7 +4,6 @@ package eu.kanade.tachiyomi.util.chapter
* -R> = regex conversion.
*/
object ChapterRecognition {
private const val NUMBER_PATTERN = """([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?"""
/**
@@ -30,7 +29,11 @@ object ChapterRecognition {
*/
private val unwantedWhiteSpace = Regex("""\s(?=extra|special|omake)""")
fun parseChapterNumber(mangaTitle: String, chapterName: String, chapterNumber: Double? = null): Double {
fun parseChapterNumber(
mangaTitle: String,
chapterName: String,
chapterNumber: Double? = null,
): Double {
// If chapter number is known return.
if (chapterNumber != null && (chapterNumber == -2.0 || chapterNumber > -1.0)) {
return chapterNumber
@@ -81,7 +84,10 @@ object ChapterRecognition {
* @param alpha alpha value of regex
* @return decimal/alpha float value
*/
private fun checkForDecimal(decimal: String?, alpha: String?): Double {
private fun checkForDecimal(
decimal: String?,
alpha: String?,
): Double {
if (!decimal.isNullOrEmpty()) {
return decimal.toDouble()
}
@@ -3,11 +3,11 @@ package eu.kanade.tachiyomi.util.lang
import java.security.MessageDigest
object Hash {
private val chars = charArrayOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f'
)
private val chars =
charArrayOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f',
)
private val MD5 get() = MessageDigest.getInstance("MD5")
@@ -7,7 +7,10 @@ import kotlin.math.floor
* Replaces the given string to have at most [count] characters using [replacement] at its end.
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.
*/
fun String.chop(count: Int, replacement: String = ""): String {
fun String.chop(
count: Int,
replacement: String = "",
): String {
return if (length > count) {
take(count - replacement.length) + replacement
} else {
@@ -19,7 +22,10 @@ fun String.chop(count: Int, replacement: String = "…"): String {
* Replaces the given string to have at most [count] characters using [replacement] near the center.
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.
*/
fun String.truncateCenter(count: Int, replacement: String = "..."): String {
fun String.truncateCenter(
count: Int,
replacement: String = "...",
): String {
if (length <= count) {
return this
}
@@ -12,7 +12,6 @@ import java.io.InputStream
* Wrapper over ZipFile to load files in epub format.
*/
class EpubFile(file: File) : Closeable {
/**
* Zip file of this epub.
*/
@@ -81,9 +80,10 @@ class EpubFile(file: File) : Closeable {
* Returns all the pages from the epub.
*/
fun getPagesFromDocument(document: Document): List<String> {
val pages = document.select("manifest > item")
.filter { node -> "application/xhtml+xml" == node.attr("media-type") }
.associateBy { it.attr("id") }
val pages =
document.select("manifest > item")
.filter { node -> "application/xhtml+xml" == node.attr("media-type") }
.associateBy { it.attr("id") }
val spine = document.select("spine > itemref").map { it.attr("idref") }
return spine.mapNotNull { pages[it] }.map { it.attr("href") }
@@ -92,7 +92,10 @@ class EpubFile(file: File) : Closeable {
/**
* Returns all the images contained in every page from the epub.
*/
private fun getImagesFromPages(pages: List<String>, packageHref: String): List<String> {
private fun getImagesFromPages(
pages: List<String>,
packageHref: String,
): List<String> {
val result = mutableListOf<String>()
val basePath = getParentDirectory(packageHref)
pages.forEach { page ->
@@ -128,7 +131,10 @@ class EpubFile(file: File) : Closeable {
/**
* Resolves a zip path from base and relative components and a path separator.
*/
private fun resolveZipPath(basePath: String, relativePath: String): String {
private fun resolveZipPath(
basePath: String,
relativePath: String,
): String {
if (relativePath.startsWith(pathSeparator)) {
// Path is absolute, so return as-is.
return relativePath
@@ -15,39 +15,41 @@ import suwayomi.tachidesk.server.util.withOperation
object GlobalMetaController {
/** used to modify a category's meta parameters */
val getMeta = handler(
documentWith = {
withOperation {
summary("Server level meta mapping")
description("Get a list of globally stored key-value mapping, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx ->
ctx.json(GlobalMeta.getMetaMap())
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
val getMeta =
handler(
documentWith = {
withOperation {
summary("Server level meta mapping")
description("Get a list of globally stored key-value mapping, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx ->
ctx.json(GlobalMeta.getMetaMap())
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** used to modify global meta parameters */
val modifyMeta = handler(
formParam<String>("key"),
formParam<String>("value"),
documentWith = {
withOperation {
summary("Add meta data to the global meta mapping")
description("A simple Key-Value stored at server global level, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx, key, value ->
GlobalMeta.modifyMeta(key, value)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
val modifyMeta =
handler(
formParam<String>("key"),
formParam<String>("value"),
documentWith = {
withOperation {
summary("Add meta data to the global meta mapping")
description("A simple Key-Value stored at server global level, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx, key, value ->
GlobalMeta.modifyMeta(key, value)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
}
@@ -19,36 +19,38 @@ import suwayomi.tachidesk.server.util.withOperation
/** Settings Page/Screen */
object SettingsController {
/** returns some static info about the current app build */
val about = handler(
documentWith = {
withOperation {
summary("About Tachidesk")
description("Returns some static info about the current app build")
}
},
behaviorOf = { ctx ->
ctx.json(About.getAbout())
},
withResults = {
json<AboutDataClass>(HttpCode.OK)
}
)
val about =
handler(
documentWith = {
withOperation {
summary("About Tachidesk")
description("Returns some static info about the current app build")
}
},
behaviorOf = { ctx ->
ctx.json(About.getAbout())
},
withResults = {
json<AboutDataClass>(HttpCode.OK)
},
)
/** check for app updates */
val checkUpdate = handler(
documentWith = {
withOperation {
summary("Tachidesk update check")
description("Check for app updates")
}
},
behaviorOf = { ctx ->
ctx.future(
future { AppUpdate.checkUpdate() }
)
},
withResults = {
json<Array<UpdateDataClass>>(HttpCode.OK)
}
)
val checkUpdate =
handler(
documentWith = {
withOperation {
summary("Tachidesk update check")
description("Check for app updates")
}
},
behaviorOf = { ctx ->
ctx.future(
future { AppUpdate.checkUpdate() },
)
},
withResults = {
json<Array<UpdateDataClass>>(HttpCode.OK)
},
)
}
@@ -7,7 +7,7 @@ package suwayomi.tachidesk.global.impl
* 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 suwayomi.tachidesk.server.BuildConfig
import suwayomi.tachidesk.server.generated.BuildConfig
data class AboutDataClass(
val name: String,
@@ -16,7 +16,7 @@ data class AboutDataClass(
val buildType: String,
val buildTime: Long,
val github: String,
val discord: String
val discord: String,
)
object About {
@@ -28,7 +28,7 @@ object About {
BuildConfig.BUILD_TYPE,
BuildConfig.BUILD_TIME,
BuildConfig.GITHUB,
BuildConfig.DISCORD
BuildConfig.DISCORD,
)
}
}
@@ -19,7 +19,7 @@ data class UpdateDataClass(
/** [channel] mirrors [suwayomi.tachidesk.server.BuildConfig.BUILD_TYPE] */
val channel: String,
val tag: String,
val url: String
val url: String,
)
object AppUpdate {
@@ -30,29 +30,31 @@ object AppUpdate {
private val network: NetworkHelper by injectLazy()
suspend fun checkUpdate(): List<UpdateDataClass> {
val stableJson = json.parseToJsonElement(
network.client.newCall(
GET(LATEST_STABLE_CHANNEL_URL)
).await().body.string()
).jsonObject
val stableJson =
json.parseToJsonElement(
network.client.newCall(
GET(LATEST_STABLE_CHANNEL_URL),
).await().body.string(),
).jsonObject
val previewJson = json.parseToJsonElement(
network.client.newCall(
GET(LATEST_PREVIEW_CHANNEL_URL)
).await().body.string()
).jsonObject
val previewJson =
json.parseToJsonElement(
network.client.newCall(
GET(LATEST_PREVIEW_CHANNEL_URL),
).await().body.string(),
).jsonObject
return listOf(
UpdateDataClass(
"Stable",
stableJson["tag_name"]!!.jsonPrimitive.content,
stableJson["html_url"]!!.jsonPrimitive.content
stableJson["html_url"]!!.jsonPrimitive.content,
),
UpdateDataClass(
"Preview",
previewJson["tag_name"]!!.jsonPrimitive.content,
previewJson["html_url"]!!.jsonPrimitive.content
)
previewJson["html_url"]!!.jsonPrimitive.content,
),
)
}
}
@@ -15,11 +15,15 @@ import suwayomi.tachidesk.global.model.table.GlobalMetaTable
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
object GlobalMeta {
fun modifyMeta(key: String, value: String) {
fun modifyMeta(
key: String,
value: String,
) {
transaction {
val meta = transaction {
GlobalMetaTable.select { GlobalMetaTable.key eq key }
}.firstOrNull()
val meta =
transaction {
GlobalMetaTable.select { GlobalMetaTable.key eq key }
}.firstOrNull()
if (meta == null) {
GlobalMetaTable.insert {
@@ -23,7 +23,7 @@ object GraphQLController {
ctx.future(
future {
server.execute(ctx)
}
},
)
}
@@ -23,48 +23,56 @@ import suwayomi.tachidesk.server.JavalinSetup.future
class CategoryDataLoader : KotlinDataLoader<Int, CategoryType> {
override val dataLoaderName = "CategoryDataLoader"
override fun getDataLoader(): DataLoader<Int, CategoryType> = DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val categories = CategoryTable.select { CategoryTable.id inList ids }
.map { CategoryType(it) }
.associateBy { it.id }
ids.map { categories[it] }
override fun getDataLoader(): DataLoader<Int, CategoryType> =
DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val categories =
CategoryTable.select { CategoryTable.id inList ids }
.map { CategoryType(it) }
.associateBy { it.id }
ids.map { categories[it] }
}
}
}
}
}
class CategoryForIdsDataLoader : KotlinDataLoader<List<Int>, CategoryNodeList> {
override val dataLoaderName = "CategoryForIdsDataLoader"
override fun getDataLoader(): DataLoader<List<Int>, CategoryNodeList> = DataLoaderFactory.newDataLoader { categoryIds ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val ids = categoryIds.flatten().distinct()
val categories = CategoryTable.select { CategoryTable.id inList ids }.map { CategoryType(it) }
categoryIds.map { categoryIds ->
categories.filter { it.id in categoryIds }.toNodeList()
override fun getDataLoader(): DataLoader<List<Int>, CategoryNodeList> =
DataLoaderFactory.newDataLoader { categoryIds ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val ids = categoryIds.flatten().distinct()
val categories = CategoryTable.select { CategoryTable.id inList ids }.map { CategoryType(it) }
categoryIds.map { categoryIds ->
categories.filter { it.id in categoryIds }.toNodeList()
}
}
}
}
}
}
class CategoriesForMangaDataLoader : KotlinDataLoader<Int, CategoryNodeList> {
override val dataLoaderName = "CategoriesForMangaDataLoader"
override fun getDataLoader(): DataLoader<Int, CategoryNodeList> = DataLoaderFactory.newDataLoader<Int, CategoryNodeList> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val itemsByRef = CategoryMangaTable.innerJoin(CategoryTable)
.select { CategoryMangaTable.manga inList ids }
.map { Pair(it[CategoryMangaTable.manga].value, CategoryType(it)) }
.groupBy { it.first }
.mapValues { it.value.map { pair -> pair.second } }
ids.map { (itemsByRef[it] ?: emptyList()).toNodeList() }
override fun getDataLoader(): DataLoader<Int, CategoryNodeList> =
DataLoaderFactory.newDataLoader<Int, CategoryNodeList> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val itemsByRef =
CategoryMangaTable.innerJoin(CategoryTable)
.select { CategoryMangaTable.manga inList ids }
.map { Pair(it[CategoryMangaTable.manga].value, CategoryType(it)) }
.groupBy { it.first }
.mapValues { it.value.map { pair -> pair.second } }
ids.map { (itemsByRef[it] ?: emptyList()).toNodeList() }
}
}
}
}
}
@@ -25,82 +25,95 @@ import suwayomi.tachidesk.server.JavalinSetup.future
class ChapterDataLoader : KotlinDataLoader<Int, ChapterType?> {
override val dataLoaderName = "ChapterDataLoader"
override fun getDataLoader(): DataLoader<Int, ChapterType?> = DataLoaderFactory.newDataLoader<Int, ChapterType> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val chapters = ChapterTable.select { ChapterTable.id inList ids }
.map { ChapterType(it) }
.associateBy { it.id }
ids.map { chapters[it] }
override fun getDataLoader(): DataLoader<Int, ChapterType?> =
DataLoaderFactory.newDataLoader<Int, ChapterType> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val chapters =
ChapterTable.select { ChapterTable.id inList ids }
.map { ChapterType(it) }
.associateBy { it.id }
ids.map { chapters[it] }
}
}
}
}
}
class ChaptersForMangaDataLoader : KotlinDataLoader<Int, ChapterNodeList> {
override val dataLoaderName = "ChaptersForMangaDataLoader"
override fun getDataLoader(): DataLoader<Int, ChapterNodeList> = DataLoaderFactory.newDataLoader<Int, ChapterNodeList> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val chaptersByMangaId = ChapterTable.select { ChapterTable.manga inList ids }
.map { ChapterType(it) }
.groupBy { it.mangaId }
ids.map { (chaptersByMangaId[it] ?: emptyList()).toNodeList() }
override fun getDataLoader(): DataLoader<Int, ChapterNodeList> =
DataLoaderFactory.newDataLoader<Int, ChapterNodeList> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val chaptersByMangaId =
ChapterTable.select { ChapterTable.manga inList ids }
.map { ChapterType(it) }
.groupBy { it.mangaId }
ids.map { (chaptersByMangaId[it] ?: emptyList()).toNodeList() }
}
}
}
}
}
class DownloadedChapterCountForMangaDataLoader : KotlinDataLoader<Int, Int> {
override val dataLoaderName = "DownloadedChapterCountForMangaDataLoader"
override fun getDataLoader(): DataLoader<Int, Int> = DataLoaderFactory.newDataLoader<Int, Int> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val downloadedChapterCountByMangaId =
ChapterTable
.slice(ChapterTable.manga, ChapterTable.isDownloaded.count())
.select { (ChapterTable.manga inList ids) and (ChapterTable.isDownloaded eq true) }
.groupBy(ChapterTable.manga)
.associate { it[ChapterTable.manga].value to it[ChapterTable.isDownloaded.count()] }
ids.map { downloadedChapterCountByMangaId[it]?.toInt() ?: 0 }
override fun getDataLoader(): DataLoader<Int, Int> =
DataLoaderFactory.newDataLoader<Int, Int> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val downloadedChapterCountByMangaId =
ChapterTable
.slice(ChapterTable.manga, ChapterTable.isDownloaded.count())
.select { (ChapterTable.manga inList ids) and (ChapterTable.isDownloaded eq true) }
.groupBy(ChapterTable.manga)
.associate { it[ChapterTable.manga].value to it[ChapterTable.isDownloaded.count()] }
ids.map { downloadedChapterCountByMangaId[it]?.toInt() ?: 0 }
}
}
}
}
}
class UnreadChapterCountForMangaDataLoader : KotlinDataLoader<Int, Int> {
override val dataLoaderName = "UnreadChapterCountForMangaDataLoader"
override fun getDataLoader(): DataLoader<Int, Int> = DataLoaderFactory.newDataLoader<Int, Int> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val unreadChapterCountByMangaId =
ChapterTable
.slice(ChapterTable.manga, ChapterTable.isRead.count())
.select { (ChapterTable.manga inList ids) and (ChapterTable.isRead eq false) }
.groupBy(ChapterTable.manga)
.associate { it[ChapterTable.manga].value to it[ChapterTable.isRead.count()] }
ids.map { unreadChapterCountByMangaId[it]?.toInt() ?: 0 }
override fun getDataLoader(): DataLoader<Int, Int> =
DataLoaderFactory.newDataLoader<Int, Int> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val unreadChapterCountByMangaId =
ChapterTable
.slice(ChapterTable.manga, ChapterTable.isRead.count())
.select { (ChapterTable.manga inList ids) and (ChapterTable.isRead eq false) }
.groupBy(ChapterTable.manga)
.associate { it[ChapterTable.manga].value to it[ChapterTable.isRead.count()] }
ids.map { unreadChapterCountByMangaId[it]?.toInt() ?: 0 }
}
}
}
}
}
class LastReadChapterForMangaDataLoader : KotlinDataLoader<Int, ChapterType?> {
override val dataLoaderName = "LastReadChapterForMangaDataLoader"
override fun getDataLoader(): DataLoader<Int, ChapterType?> = DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val lastReadChaptersByMangaId = ChapterTable
.select { (ChapterTable.manga inList ids) and (ChapterTable.isRead eq true) }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.groupBy { it[ChapterTable.manga].value }
ids.map { id -> lastReadChaptersByMangaId[id]?.let { chapters -> ChapterType(chapters.first()) } }
override fun getDataLoader(): DataLoader<Int, ChapterType?> =
DataLoaderFactory.newDataLoader<Int, ChapterType?> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val lastReadChaptersByMangaId =
ChapterTable
.select { (ChapterTable.manga inList ids) and (ChapterTable.isRead eq true) }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.groupBy { it[ChapterTable.manga].value }
ids.map { id -> lastReadChaptersByMangaId[id]?.let { chapters -> ChapterType(chapters.first()) } }
}
}
}
}
}
@@ -21,43 +21,50 @@ import suwayomi.tachidesk.server.JavalinSetup.future
class ExtensionDataLoader : KotlinDataLoader<String, ExtensionType?> {
override val dataLoaderName = "ExtensionDataLoader"
override fun getDataLoader(): DataLoader<String, ExtensionType?> = DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val extensions = ExtensionTable.select { ExtensionTable.pkgName inList ids }
.map { ExtensionType(it) }
.associateBy { it.pkgName }
ids.map { extensions[it] }
override fun getDataLoader(): DataLoader<String, ExtensionType?> =
DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val extensions =
ExtensionTable.select { ExtensionTable.pkgName inList ids }
.map { ExtensionType(it) }
.associateBy { it.pkgName }
ids.map { extensions[it] }
}
}
}
}
}
class ExtensionForSourceDataLoader : KotlinDataLoader<Long, ExtensionType?> {
override val dataLoaderName = "ExtensionForSourceDataLoader"
override fun getDataLoader(): DataLoader<Long, ExtensionType?> = DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val extensions = ExtensionTable.innerJoin(SourceTable)
.select { SourceTable.id inList ids }
.toList()
.map { Triple(it[SourceTable.id].value, it[ExtensionTable.pkgName], it) }
.let { triples ->
val sources = buildMap {
triples.forEach {
if (!containsKey(it.second)) {
put(it.second, ExtensionType(it.third))
override fun getDataLoader(): DataLoader<Long, ExtensionType?> =
DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val extensions =
ExtensionTable.innerJoin(SourceTable)
.select { SourceTable.id inList ids }
.toList()
.map { Triple(it[SourceTable.id].value, it[ExtensionTable.pkgName], it) }
.let { triples ->
val sources =
buildMap {
triples.forEach {
if (!containsKey(it.second)) {
put(it.second, ExtensionType(it.third))
}
}
}
triples.associate {
it.first to sources[it.second]
}
}
}
triples.associate {
it.first to sources[it.second]
}
}
ids.map { extensions[it] }
ids.map { extensions[it] }
}
}
}
}
}
@@ -24,76 +24,89 @@ import suwayomi.tachidesk.server.JavalinSetup.future
class MangaDataLoader : KotlinDataLoader<Int, MangaType?> {
override val dataLoaderName = "MangaDataLoader"
override fun getDataLoader(): DataLoader<Int, MangaType?> = DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val manga = MangaTable.select { MangaTable.id inList ids }
.map { MangaType(it) }
.associateBy { it.id }
ids.map { manga[it] }
override fun getDataLoader(): DataLoader<Int, MangaType?> =
DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val manga =
MangaTable.select { MangaTable.id inList ids }
.map { MangaType(it) }
.associateBy { it.id }
ids.map { manga[it] }
}
}
}
}
}
class MangaForCategoryDataLoader : KotlinDataLoader<Int, MangaNodeList> {
override val dataLoaderName = "MangaForCategoryDataLoader"
override fun getDataLoader(): DataLoader<Int, MangaNodeList> = DataLoaderFactory.newDataLoader<Int, MangaNodeList> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val itemsByRef = if (ids.contains(0)) {
MangaTable
.leftJoin(CategoryMangaTable)
.select { MangaTable.inLibrary eq true }
.andWhere { CategoryMangaTable.manga.isNull() }
.map { MangaType(it) }
.let {
mapOf(0 to it)
}
} else {
emptyMap()
} + CategoryMangaTable.innerJoin(MangaTable)
.select { CategoryMangaTable.category inList ids }
.map { Pair(it[CategoryMangaTable.category].value, MangaType(it)) }
.groupBy { it.first }
.mapValues { it.value.map { pair -> pair.second } }
ids.map { (itemsByRef[it] ?: emptyList()).toNodeList() }
override fun getDataLoader(): DataLoader<Int, MangaNodeList> =
DataLoaderFactory.newDataLoader<Int, MangaNodeList> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val itemsByRef =
if (ids.contains(0)) {
MangaTable
.leftJoin(CategoryMangaTable)
.select { MangaTable.inLibrary eq true }
.andWhere { CategoryMangaTable.manga.isNull() }
.map { MangaType(it) }
.let {
mapOf(0 to it)
}
} else {
emptyMap()
} +
CategoryMangaTable.innerJoin(MangaTable)
.select { CategoryMangaTable.category inList ids }
.map { Pair(it[CategoryMangaTable.category].value, MangaType(it)) }
.groupBy { it.first }
.mapValues { it.value.map { pair -> pair.second } }
ids.map { (itemsByRef[it] ?: emptyList()).toNodeList() }
}
}
}
}
}
class MangaForSourceDataLoader : KotlinDataLoader<Long, MangaNodeList> {
override val dataLoaderName = "MangaForSourceDataLoader"
override fun getDataLoader(): DataLoader<Long, MangaNodeList> = DataLoaderFactory.newDataLoader<Long, MangaNodeList> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val mangaBySourceId = MangaTable.select { MangaTable.sourceReference inList ids }
.map { MangaType(it) }
.groupBy { it.sourceId }
ids.map { (mangaBySourceId[it] ?: emptyList()).toNodeList() }
override fun getDataLoader(): DataLoader<Long, MangaNodeList> =
DataLoaderFactory.newDataLoader<Long, MangaNodeList> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val mangaBySourceId =
MangaTable.select { MangaTable.sourceReference inList ids }
.map { MangaType(it) }
.groupBy { it.sourceId }
ids.map { (mangaBySourceId[it] ?: emptyList()).toNodeList() }
}
}
}
}
}
class MangaForIdsDataLoader : KotlinDataLoader<List<Int>, MangaNodeList> {
override val dataLoaderName = "MangaForIdsDataLoader"
override fun getDataLoader(): DataLoader<List<Int>, MangaNodeList> = DataLoaderFactory.newDataLoader { mangaIds ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val ids = mangaIds.flatten().distinct()
val manga = MangaTable.select { MangaTable.id inList ids }
.map { MangaType(it) }
mangaIds.map { mangaIds ->
manga.filter { it.id in mangaIds }.toNodeList()
override fun getDataLoader(): DataLoader<List<Int>, MangaNodeList> =
DataLoaderFactory.newDataLoader { mangaIds ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val ids = mangaIds.flatten().distinct()
val manga =
MangaTable.select { MangaTable.id inList ids }
.map { MangaType(it) }
mangaIds.map { mangaIds ->
manga.filter { it.id in mangaIds }.toNodeList()
}
}
}
}
}
}
@@ -19,60 +19,72 @@ import suwayomi.tachidesk.server.JavalinSetup.future
class GlobalMetaDataLoader : KotlinDataLoader<String, GlobalMetaType?> {
override val dataLoaderName = "GlobalMetaDataLoader"
override fun getDataLoader(): DataLoader<String, GlobalMetaType?> = DataLoaderFactory.newDataLoader<String, GlobalMetaType?> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val metasByRefId = GlobalMetaTable.select { GlobalMetaTable.key inList ids }
.map { GlobalMetaType(it) }
.associateBy { it.key }
ids.map { metasByRefId[it] }
override fun getDataLoader(): DataLoader<String, GlobalMetaType?> =
DataLoaderFactory.newDataLoader<String, GlobalMetaType?> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val metasByRefId =
GlobalMetaTable.select { GlobalMetaTable.key inList ids }
.map { GlobalMetaType(it) }
.associateBy { it.key }
ids.map { metasByRefId[it] }
}
}
}
}
}
class ChapterMetaDataLoader : KotlinDataLoader<Int, List<ChapterMetaType>> {
override val dataLoaderName = "ChapterMetaDataLoader"
override fun getDataLoader(): DataLoader<Int, List<ChapterMetaType>> = DataLoaderFactory.newDataLoader<Int, List<ChapterMetaType>> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val metasByRefId = ChapterMetaTable.select { ChapterMetaTable.ref inList ids }
.map { ChapterMetaType(it) }
.groupBy { it.chapterId }
ids.map { metasByRefId[it].orEmpty() }
override fun getDataLoader(): DataLoader<Int, List<ChapterMetaType>> =
DataLoaderFactory.newDataLoader<Int, List<ChapterMetaType>> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val metasByRefId =
ChapterMetaTable.select { ChapterMetaTable.ref inList ids }
.map { ChapterMetaType(it) }
.groupBy { it.chapterId }
ids.map { metasByRefId[it].orEmpty() }
}
}
}
}
}
class MangaMetaDataLoader : KotlinDataLoader<Int, List<MangaMetaType>> {
override val dataLoaderName = "MangaMetaDataLoader"
override fun getDataLoader(): DataLoader<Int, List<MangaMetaType>> = DataLoaderFactory.newDataLoader<Int, List<MangaMetaType>> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val metasByRefId = MangaMetaTable.select { MangaMetaTable.ref inList ids }
.map { MangaMetaType(it) }
.groupBy { it.mangaId }
ids.map { metasByRefId[it].orEmpty() }
override fun getDataLoader(): DataLoader<Int, List<MangaMetaType>> =
DataLoaderFactory.newDataLoader<Int, List<MangaMetaType>> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val metasByRefId =
MangaMetaTable.select { MangaMetaTable.ref inList ids }
.map { MangaMetaType(it) }
.groupBy { it.mangaId }
ids.map { metasByRefId[it].orEmpty() }
}
}
}
}
}
class CategoryMetaDataLoader : KotlinDataLoader<Int, List<CategoryMetaType>> {
override val dataLoaderName = "CategoryMetaDataLoader"
override fun getDataLoader(): DataLoader<Int, List<CategoryMetaType>> = DataLoaderFactory.newDataLoader<Int, List<CategoryMetaType>> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val metasByRefId = CategoryMetaTable.select { CategoryMetaTable.ref inList ids }
.map { CategoryMetaType(it) }
.groupBy { it.categoryId }
ids.map { metasByRefId[it].orEmpty() }
override fun getDataLoader(): DataLoader<Int, List<CategoryMetaType>> =
DataLoaderFactory.newDataLoader<Int, List<CategoryMetaType>> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val metasByRefId =
CategoryMetaTable.select { CategoryMetaTable.ref inList ids }
.map { CategoryMetaType(it) }
.groupBy { it.categoryId }
ids.map { metasByRefId[it].orEmpty() }
}
}
}
}
}
@@ -23,34 +23,40 @@ import suwayomi.tachidesk.server.JavalinSetup.future
class SourceDataLoader : KotlinDataLoader<Long, SourceType?> {
override val dataLoaderName = "SourceDataLoader"
override fun getDataLoader(): DataLoader<Long, SourceType?> = DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val source = SourceTable.select { SourceTable.id inList ids }
.mapNotNull { SourceType(it) }
.associateBy { it.id }
ids.map { source[it] }
override fun getDataLoader(): DataLoader<Long, SourceType?> =
DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val source =
SourceTable.select { SourceTable.id inList ids }
.mapNotNull { SourceType(it) }
.associateBy { it.id }
ids.map { source[it] }
}
}
}
}
}
class SourcesForExtensionDataLoader : KotlinDataLoader<String, SourceNodeList> {
override val dataLoaderName = "SourcesForExtensionDataLoader"
override fun getDataLoader(): DataLoader<String, SourceNodeList> = DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val sourcesByExtensionPkg = SourceTable.innerJoin(ExtensionTable)
.select { ExtensionTable.pkgName inList ids }
.map { Pair(it[ExtensionTable.pkgName], SourceType(it)) }
.groupBy { it.first }
.mapValues { it.value.mapNotNull { pair -> pair.second } }
override fun getDataLoader(): DataLoader<String, SourceNodeList> =
DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
ids.map { (sourcesByExtensionPkg[it] ?: emptyList()).toNodeList() }
val sourcesByExtensionPkg =
SourceTable.innerJoin(ExtensionTable)
.select { ExtensionTable.pkgName inList ids }
.map { Pair(it[ExtensionTable.pkgName], SourceType(it)) }
.groupBy { it.first }
.mapValues { it.value.mapNotNull { pair -> pair.second } }
ids.map { (sourcesByExtensionPkg[it] ?: emptyList()).toNodeList() }
}
}
}
}
}
@@ -20,17 +20,16 @@ import kotlin.time.Duration.Companion.seconds
class BackupMutation {
data class RestoreBackupInput(
val clientMutationId: String? = null,
val backup: UploadedFile
val backup: UploadedFile,
)
data class RestoreBackupPayload(
val clientMutationId: String?,
val status: BackupRestoreStatus
val status: BackupRestoreStatus,
)
@OptIn(DelicateCoroutinesApi::class)
fun restoreBackup(
input: RestoreBackupInput
): CompletableFuture<RestoreBackupPayload> {
fun restoreBackup(input: RestoreBackupInput): CompletableFuture<RestoreBackupPayload> {
val (clientMutationId, backup) = input
return future {
@@ -38,11 +37,12 @@ class BackupMutation {
ProtoBackupImport.performRestore(backup.content)
}
val status = withTimeout(10.seconds) {
ProtoBackupImport.backupRestoreState.first {
it != ProtoBackupImport.BackupRestoreState.Idle
}.toStatus()
}
val status =
withTimeout(10.seconds) {
ProtoBackupImport.backupRestoreState.first {
it != ProtoBackupImport.BackupRestoreState.Idle
}.toStatus()
}
RestoreBackupPayload(clientMutationId, status)
}
@@ -51,32 +51,33 @@ class BackupMutation {
data class CreateBackupInput(
val clientMutationId: String? = null,
val includeChapters: Boolean? = null,
val includeCategories: Boolean? = null
val includeCategories: Boolean? = null,
)
data class CreateBackupPayload(
val clientMutationId: String?,
val url: String
val url: String,
)
fun createBackup(
input: CreateBackupInput? = null
): CreateBackupPayload {
fun createBackup(input: CreateBackupInput? = null): CreateBackupPayload {
val filename = ProtoBackupExport.getBackupFilename()
val backup = ProtoBackupExport.createBackup(
BackupFlags(
includeManga = true,
includeCategories = input?.includeCategories ?: true,
includeChapters = input?.includeChapters ?: true,
includeTracking = true,
includeHistory = true
val backup =
ProtoBackupExport.createBackup(
BackupFlags(
includeManga = true,
includeCategories = input?.includeCategories ?: true,
includeChapters = input?.includeChapters ?: true,
includeTracking = true,
includeHistory = true,
),
)
)
TemporaryFileStorage.saveFile(filename, backup)
return CreateBackupPayload(
clientMutationId = input?.clientMutationId,
url = "/api/graphql/files/backup/$filename"
url = "/api/graphql/files/backup/$filename",
)
}
}
@@ -27,15 +27,15 @@ import suwayomi.tachidesk.manga.model.table.MangaTable
class CategoryMutation {
data class SetCategoryMetaInput(
val clientMutationId: String? = null,
val meta: CategoryMetaType
val meta: CategoryMetaType,
)
data class SetCategoryMetaPayload(
val clientMutationId: String?,
val meta: CategoryMetaType
val meta: CategoryMetaType,
)
fun setCategoryMeta(
input: SetCategoryMetaInput
): SetCategoryMetaPayload {
fun setCategoryMeta(input: SetCategoryMetaInput): SetCategoryMetaPayload {
val (clientMutationId, meta) = input
Category.modifyMeta(meta.categoryId, meta.key, meta.value)
@@ -46,65 +46,73 @@ class CategoryMutation {
data class DeleteCategoryMetaInput(
val clientMutationId: String? = null,
val categoryId: Int,
val key: String
val key: String,
)
data class DeleteCategoryMetaPayload(
val clientMutationId: String?,
val meta: CategoryMetaType?,
val category: CategoryType
val category: CategoryType,
)
fun deleteCategoryMeta(
input: DeleteCategoryMetaInput
): DeleteCategoryMetaPayload {
fun deleteCategoryMeta(input: DeleteCategoryMetaInput): DeleteCategoryMetaPayload {
val (clientMutationId, categoryId, key) = input
val (meta, category) = transaction {
val meta = CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
.firstOrNull()
val (meta, category) =
transaction {
val meta =
CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
.firstOrNull()
CategoryMetaTable.deleteWhere { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
CategoryMetaTable.deleteWhere { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
val category = transaction {
CategoryType(CategoryTable.select { CategoryTable.id eq categoryId }.first())
val category =
transaction {
CategoryType(CategoryTable.select { CategoryTable.id eq categoryId }.first())
}
if (meta != null) {
CategoryMetaType(meta)
} else {
null
} to category
}
if (meta != null) {
CategoryMetaType(meta)
} else {
null
} to category
}
return DeleteCategoryMetaPayload(clientMutationId, meta, category)
}
data class UpdateCategoryPatch(
val name: String? = null,
val default: Boolean? = null,
val includeInUpdate: IncludeInUpdate? = null
val includeInUpdate: IncludeInUpdate? = null,
)
data class UpdateCategoryPayload(
val clientMutationId: String?,
val category: CategoryType
val category: CategoryType,
)
data class UpdateCategoryInput(
val clientMutationId: String? = null,
val id: Int,
val patch: UpdateCategoryPatch
val patch: UpdateCategoryPatch,
)
data class UpdateCategoriesPayload(
val clientMutationId: String?,
val categories: List<CategoryType>
val categories: List<CategoryType>,
)
data class UpdateCategoriesInput(
val clientMutationId: String? = null,
val ids: List<Int>,
val patch: UpdateCategoryPatch
val patch: UpdateCategoryPatch,
)
private fun updateCategories(ids: List<Int>, patch: UpdateCategoryPatch) {
private fun updateCategories(
ids: List<Int>,
patch: UpdateCategoryPatch,
) {
transaction {
if (patch.name != null) {
CategoryTable.update({ CategoryTable.id inList ids }) { update ->
@@ -135,13 +143,14 @@ class CategoryMutation {
updateCategories(listOf(id), patch)
val category = transaction {
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
}
val category =
transaction {
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
}
return UpdateCategoryPayload(
clientMutationId = clientMutationId,
category = category
category = category,
)
}
@@ -150,24 +159,26 @@ class CategoryMutation {
updateCategories(ids, patch)
val categories = transaction {
CategoryTable.select { CategoryTable.id inList ids }.map { CategoryType(it) }
}
val categories =
transaction {
CategoryTable.select { CategoryTable.id inList ids }.map { CategoryType(it) }
}
return UpdateCategoriesPayload(
clientMutationId = clientMutationId,
categories = categories
categories = categories,
)
}
data class UpdateCategoryOrderPayload(
val clientMutationId: String?,
val categories: List<CategoryType>
val categories: List<CategoryType>,
)
data class UpdateCategoryOrderInput(
val clientMutationId: String? = null,
val id: Int,
val position: Int
val position: Int,
)
fun updateCategoryOrder(input: UpdateCategoryOrderInput): UpdateCategoryOrderPayload {
@@ -177,9 +188,10 @@ class CategoryMutation {
}
transaction {
val currentOrder = CategoryTable
.select { CategoryTable.id eq categoryId }
.first()[CategoryTable.order]
val currentOrder =
CategoryTable
.select { CategoryTable.id eq categoryId }
.first()[CategoryTable.order]
if (currentOrder != position) {
if (position < currentOrder) {
@@ -200,13 +212,14 @@ class CategoryMutation {
Category.normalizeCategories()
val categories = transaction {
CategoryTable.selectAll().orderBy(CategoryTable.order).map { CategoryType(it) }
}
val categories =
transaction {
CategoryTable.selectAll().orderBy(CategoryTable.order).map { CategoryType(it) }
}
return UpdateCategoryOrderPayload(
clientMutationId = clientMutationId,
categories = categories
categories = categories,
)
}
@@ -215,15 +228,15 @@ class CategoryMutation {
val name: String,
val order: Int? = null,
val default: Boolean? = null,
val includeInUpdate: IncludeInUpdate? = null
val includeInUpdate: IncludeInUpdate? = null,
)
data class CreateCategoryPayload(
val clientMutationId: String?,
val category: CategoryType
val category: CategoryType,
)
fun createCategory(
input: CreateCategoryInput
): CreateCategoryPayload {
fun createCategory(input: CreateCategoryInput): CreateCategoryPayload {
val (clientMutationId, name, order, default, includeInUpdate) = input
transaction {
require(CategoryTable.select { CategoryTable.name eq input.name }.isEmpty()) {
@@ -239,104 +252,114 @@ class CategoryMutation {
}
}
val category = transaction {
if (order != null) {
CategoryTable.update({ CategoryTable.order greaterEq order }) {
it[CategoryTable.order] = CategoryTable.order + 1
val category =
transaction {
if (order != null) {
CategoryTable.update({ CategoryTable.order greaterEq order }) {
it[CategoryTable.order] = CategoryTable.order + 1
}
}
val id =
CategoryTable.insertAndGetId {
it[CategoryTable.name] = input.name
it[CategoryTable.order] = order ?: Int.MAX_VALUE
if (default != null) {
it[CategoryTable.isDefault] = default
}
if (includeInUpdate != null) {
it[CategoryTable.includeInUpdate] = includeInUpdate.value
}
}
Category.normalizeCategories()
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
}
val id = CategoryTable.insertAndGetId {
it[CategoryTable.name] = input.name
it[CategoryTable.order] = order ?: Int.MAX_VALUE
if (default != null) {
it[CategoryTable.isDefault] = default
}
if (includeInUpdate != null) {
it[CategoryTable.includeInUpdate] = includeInUpdate.value
}
}
Category.normalizeCategories()
CategoryType(CategoryTable.select { CategoryTable.id eq id }.first())
}
return CreateCategoryPayload(clientMutationId, category)
}
data class DeleteCategoryInput(
val clientMutationId: String? = null,
val categoryId: Int
val categoryId: Int,
)
data class DeleteCategoryPayload(
val clientMutationId: String?,
val category: CategoryType?,
val mangas: List<MangaType>
val mangas: List<MangaType>,
)
fun deleteCategory(
input: DeleteCategoryInput
): DeleteCategoryPayload {
fun deleteCategory(input: DeleteCategoryInput): DeleteCategoryPayload {
val (clientMutationId, categoryId) = input
if (categoryId == 0) { // Don't delete default category
return DeleteCategoryPayload(
clientMutationId,
null,
emptyList()
emptyList(),
)
}
val (category, mangas) = transaction {
val category = CategoryTable.select { CategoryTable.id eq categoryId }
.firstOrNull()
val (category, mangas) =
transaction {
val category =
CategoryTable.select { CategoryTable.id eq categoryId }
.firstOrNull()
val mangas = transaction {
MangaTable.innerJoin(CategoryMangaTable)
.select { CategoryMangaTable.category eq categoryId }
.map { MangaType(it) }
val mangas =
transaction {
MangaTable.innerJoin(CategoryMangaTable)
.select { CategoryMangaTable.category eq categoryId }
.map { MangaType(it) }
}
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
Category.normalizeCategories()
if (category != null) {
CategoryType(category)
} else {
null
} to mangas
}
CategoryTable.deleteWhere { CategoryTable.id eq categoryId }
Category.normalizeCategories()
if (category != null) {
CategoryType(category)
} else {
null
} to mangas
}
return DeleteCategoryPayload(clientMutationId, category, mangas)
}
data class UpdateMangaCategoriesPatch(
val clearCategories: Boolean? = null,
val addToCategories: List<Int>? = null,
val removeFromCategories: List<Int>? = null
val removeFromCategories: List<Int>? = null,
)
data class UpdateMangaCategoriesPayload(
val clientMutationId: String?,
val manga: MangaType
val manga: MangaType,
)
data class UpdateMangaCategoriesInput(
val clientMutationId: String? = null,
val id: Int,
val patch: UpdateMangaCategoriesPatch
val patch: UpdateMangaCategoriesPatch,
)
data class UpdateMangasCategoriesPayload(
val clientMutationId: String?,
val mangas: List<MangaType>
val mangas: List<MangaType>,
)
data class UpdateMangasCategoriesInput(
val clientMutationId: String? = null,
val ids: List<Int>,
val patch: UpdateMangaCategoriesPatch
val patch: UpdateMangaCategoriesPatch,
)
private fun updateMangas(ids: List<Int>, patch: UpdateMangaCategoriesPatch) {
private fun updateMangas(
ids: List<Int>,
patch: UpdateMangaCategoriesPatch,
) {
transaction {
if (patch.clearCategories == true) {
CategoryMangaTable.deleteWhere { CategoryMangaTable.manga inList ids }
@@ -346,19 +369,21 @@ class CategoryMutation {
}
}
if (!patch.addToCategories.isNullOrEmpty()) {
val newCategories = buildList {
ids.forEach { mangaId ->
patch.addToCategories.forEach { categoryId ->
val existingMapping = CategoryMangaTable.select {
(CategoryMangaTable.manga eq mangaId) and (CategoryMangaTable.category eq categoryId)
}.isNotEmpty()
val newCategories =
buildList {
ids.forEach { mangaId ->
patch.addToCategories.forEach { categoryId ->
val existingMapping =
CategoryMangaTable.select {
(CategoryMangaTable.manga eq mangaId) and (CategoryMangaTable.category eq categoryId)
}.isNotEmpty()
if (!existingMapping) {
add(mangaId to categoryId)
if (!existingMapping) {
add(mangaId to categoryId)
}
}
}
}
}
CategoryMangaTable.batchInsert(newCategories) { (manga, category) ->
this[CategoryMangaTable.manga] = manga
@@ -373,13 +398,14 @@ class CategoryMutation {
updateMangas(listOf(id), patch)
val manga = transaction {
MangaType(MangaTable.select { MangaTable.id eq id }.first())
}
val manga =
transaction {
MangaType(MangaTable.select { MangaTable.id eq id }.first())
}
return UpdateMangaCategoriesPayload(
clientMutationId = clientMutationId,
manga = manga
manga = manga,
)
}
@@ -388,13 +414,14 @@ class CategoryMutation {
updateMangas(ids, patch)
val mangas = transaction {
MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) }
}
val mangas =
transaction {
MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) }
}
return UpdateMangasCategoriesPayload(
clientMutationId = clientMutationId,
mangas = mangas
mangas = mangas,
)
}
}
@@ -25,30 +25,35 @@ class ChapterMutation {
data class UpdateChapterPatch(
val isBookmarked: Boolean? = null,
val isRead: Boolean? = null,
val lastPageRead: Int? = null
val lastPageRead: Int? = null,
)
data class UpdateChapterPayload(
val clientMutationId: String?,
val chapter: ChapterType
val chapter: ChapterType,
)
data class UpdateChapterInput(
val clientMutationId: String? = null,
val id: Int,
val patch: UpdateChapterPatch
val patch: UpdateChapterPatch,
)
data class UpdateChaptersPayload(
val clientMutationId: String?,
val chapters: List<ChapterType>
val chapters: List<ChapterType>,
)
data class UpdateChaptersInput(
val clientMutationId: String? = null,
val ids: List<Int>,
val patch: UpdateChapterPatch
val patch: UpdateChapterPatch,
)
private fun updateChapters(ids: List<Int>, patch: UpdateChapterPatch) {
private fun updateChapters(
ids: List<Int>,
patch: UpdateChapterPatch,
) {
transaction {
if (patch.isRead != null || patch.isBookmarked != null || patch.lastPageRead != null) {
val now = Instant.now().epochSecond
@@ -68,81 +73,79 @@ class ChapterMutation {
}
}
fun updateChapter(
input: UpdateChapterInput
): UpdateChapterPayload {
fun updateChapter(input: UpdateChapterInput): UpdateChapterPayload {
val (clientMutationId, id, patch) = input
updateChapters(listOf(id), patch)
val chapter = transaction {
ChapterType(ChapterTable.select { ChapterTable.id eq id }.first())
}
val chapter =
transaction {
ChapterType(ChapterTable.select { ChapterTable.id eq id }.first())
}
return UpdateChapterPayload(
clientMutationId = clientMutationId,
chapter = chapter
chapter = chapter,
)
}
fun updateChapters(
input: UpdateChaptersInput
): UpdateChaptersPayload {
fun updateChapters(input: UpdateChaptersInput): UpdateChaptersPayload {
val (clientMutationId, ids, patch) = input
updateChapters(ids, patch)
val chapters = transaction {
ChapterTable.select { ChapterTable.id inList ids }.map { ChapterType(it) }
}
val chapters =
transaction {
ChapterTable.select { ChapterTable.id inList ids }.map { ChapterType(it) }
}
return UpdateChaptersPayload(
clientMutationId = clientMutationId,
chapters = chapters
chapters = chapters,
)
}
data class FetchChaptersInput(
val clientMutationId: String? = null,
val mangaId: Int
)
data class FetchChaptersPayload(
val clientMutationId: String?,
val chapters: List<ChapterType>
val mangaId: Int,
)
fun fetchChapters(
input: FetchChaptersInput
): CompletableFuture<FetchChaptersPayload> {
data class FetchChaptersPayload(
val clientMutationId: String?,
val chapters: List<ChapterType>,
)
fun fetchChapters(input: FetchChaptersInput): CompletableFuture<FetchChaptersPayload> {
val (clientMutationId, mangaId) = input
return future {
Chapter.fetchChapterList(mangaId)
}.thenApply {
val chapters = transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.orderBy(ChapterTable.sourceOrder)
.map { ChapterType(it) }
}
val chapters =
transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.orderBy(ChapterTable.sourceOrder)
.map { ChapterType(it) }
}
FetchChaptersPayload(
clientMutationId = clientMutationId,
chapters = chapters
chapters = chapters,
)
}
}
data class SetChapterMetaInput(
val clientMutationId: String? = null,
val meta: ChapterMetaType
val meta: ChapterMetaType,
)
data class SetChapterMetaPayload(
val clientMutationId: String?,
val meta: ChapterMetaType
val meta: ChapterMetaType,
)
fun setChapterMeta(
input: SetChapterMetaInput
): SetChapterMetaPayload {
fun setChapterMeta(input: SetChapterMetaInput): SetChapterMetaPayload {
val (clientMutationId, meta) = input
Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value)
@@ -153,50 +156,53 @@ class ChapterMutation {
data class DeleteChapterMetaInput(
val clientMutationId: String? = null,
val chapterId: Int,
val key: String
val key: String,
)
data class DeleteChapterMetaPayload(
val clientMutationId: String?,
val meta: ChapterMetaType?,
val chapter: ChapterType
val chapter: ChapterType,
)
fun deleteChapterMeta(
input: DeleteChapterMetaInput
): DeleteChapterMetaPayload {
fun deleteChapterMeta(input: DeleteChapterMetaInput): DeleteChapterMetaPayload {
val (clientMutationId, chapterId, key) = input
val (meta, chapter) = transaction {
val meta = ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
.firstOrNull()
val (meta, chapter) =
transaction {
val meta =
ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
.firstOrNull()
ChapterMetaTable.deleteWhere { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
ChapterMetaTable.deleteWhere { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
val chapter = transaction {
ChapterType(ChapterTable.select { ChapterTable.id eq chapterId }.first())
val chapter =
transaction {
ChapterType(ChapterTable.select { ChapterTable.id eq chapterId }.first())
}
if (meta != null) {
ChapterMetaType(meta)
} else {
null
} to chapter
}
if (meta != null) {
ChapterMetaType(meta)
} else {
null
} to chapter
}
return DeleteChapterMetaPayload(clientMutationId, meta, chapter)
}
data class FetchChapterPagesInput(
val clientMutationId: String? = null,
val chapterId: Int
val chapterId: Int,
)
data class FetchChapterPagesPayload(
val clientMutationId: String?,
val pages: List<String>,
val chapter: ChapterType
val chapter: ChapterType,
)
fun fetchChapterPages(
input: FetchChapterPagesInput
): CompletableFuture<FetchChapterPagesPayload> {
fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture<FetchChapterPagesPayload> {
val (clientMutationId, chapterId) = input
return future {
@@ -204,10 +210,11 @@ class ChapterMutation {
}.thenApply { chapter ->
FetchChapterPagesPayload(
clientMutationId = clientMutationId,
pages = List(chapter.pageCount) { index ->
"/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}/page/$index"
},
chapter = ChapterType(chapter)
pages =
List(chapter.pageCount) { index ->
"/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}/page/$index"
},
chapter = ChapterType(chapter),
)
}
}
@@ -16,14 +16,14 @@ import java.util.concurrent.CompletableFuture
import kotlin.time.Duration.Companion.seconds
class DownloadMutation {
data class DeleteDownloadedChaptersInput(
val clientMutationId: String? = null,
val ids: List<Int>
val ids: List<Int>,
)
data class DeleteDownloadedChaptersPayload(
val clientMutationId: String?,
val chapters: List<ChapterType>
val chapters: List<ChapterType>,
)
fun deleteDownloadedChapters(input: DeleteDownloadedChaptersInput): DeleteDownloadedChaptersPayload {
@@ -33,20 +33,22 @@ class DownloadMutation {
return DeleteDownloadedChaptersPayload(
clientMutationId = clientMutationId,
chapters = transaction {
ChapterTable.select { ChapterTable.id inList chapters }
.map { ChapterType(it) }
}
chapters =
transaction {
ChapterTable.select { ChapterTable.id inList chapters }
.map { ChapterType(it) }
},
)
}
data class DeleteDownloadedChapterInput(
val clientMutationId: String? = null,
val id: Int
val id: Int,
)
data class DeleteDownloadedChapterPayload(
val clientMutationId: String?,
val chapters: ChapterType
val chapters: ChapterType,
)
fun deleteDownloadedChapter(input: DeleteDownloadedChapterInput): DeleteDownloadedChapterPayload {
@@ -56,24 +58,24 @@ class DownloadMutation {
return DeleteDownloadedChapterPayload(
clientMutationId = clientMutationId,
chapters = transaction {
ChapterType(ChapterTable.select { ChapterTable.id eq chapter }.first())
}
chapters =
transaction {
ChapterType(ChapterTable.select { ChapterTable.id eq chapter }.first())
},
)
}
data class EnqueueChapterDownloadsInput(
val clientMutationId: String? = null,
val ids: List<Int>
)
data class EnqueueChapterDownloadsPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus
val ids: List<Int>,
)
fun enqueueChapterDownloads(
input: EnqueueChapterDownloadsInput
): CompletableFuture<EnqueueChapterDownloadsPayload> {
data class EnqueueChapterDownloadsPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus,
)
fun enqueueChapterDownloads(input: EnqueueChapterDownloadsInput): CompletableFuture<EnqueueChapterDownloadsPayload> {
val (clientMutationId, chapters) = input
DownloadManager.enqueue(DownloadManager.EnqueueInput(chapters))
@@ -81,25 +83,25 @@ class DownloadMutation {
return future {
EnqueueChapterDownloadsPayload(
clientMutationId = clientMutationId,
downloadStatus = withTimeout(30.seconds) {
DownloadStatus(DownloadManager.status.first { it.queue.any { it.chapter.id in chapters } })
}
downloadStatus =
withTimeout(30.seconds) {
DownloadStatus(DownloadManager.status.first { it.queue.any { it.chapter.id in chapters } })
},
)
}
}
data class EnqueueChapterDownloadInput(
val clientMutationId: String? = null,
val id: Int
)
data class EnqueueChapterDownloadPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus
val id: Int,
)
fun enqueueChapterDownload(
input: EnqueueChapterDownloadInput
): CompletableFuture<EnqueueChapterDownloadPayload> {
data class EnqueueChapterDownloadPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus,
)
fun enqueueChapterDownload(input: EnqueueChapterDownloadInput): CompletableFuture<EnqueueChapterDownloadPayload> {
val (clientMutationId, chapter) = input
DownloadManager.enqueue(DownloadManager.EnqueueInput(listOf(chapter)))
@@ -107,25 +109,25 @@ class DownloadMutation {
return future {
EnqueueChapterDownloadPayload(
clientMutationId = clientMutationId,
downloadStatus = withTimeout(30.seconds) {
DownloadStatus(DownloadManager.status.first { it.queue.any { it.chapter.id == chapter } })
}
downloadStatus =
withTimeout(30.seconds) {
DownloadStatus(DownloadManager.status.first { it.queue.any { it.chapter.id == chapter } })
},
)
}
}
data class DequeueChapterDownloadsInput(
val clientMutationId: String? = null,
val ids: List<Int>
)
data class DequeueChapterDownloadsPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus
val ids: List<Int>,
)
fun dequeueChapterDownloads(
input: DequeueChapterDownloadsInput
): CompletableFuture<DequeueChapterDownloadsPayload> {
data class DequeueChapterDownloadsPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus,
)
fun dequeueChapterDownloads(input: DequeueChapterDownloadsInput): CompletableFuture<DequeueChapterDownloadsPayload> {
val (clientMutationId, chapters) = input
DownloadManager.dequeue(DownloadManager.EnqueueInput(chapters))
@@ -133,25 +135,25 @@ class DownloadMutation {
return future {
DequeueChapterDownloadsPayload(
clientMutationId = clientMutationId,
downloadStatus = withTimeout(30.seconds) {
DownloadStatus(DownloadManager.status.first { it.queue.none { it.chapter.id in chapters } })
}
downloadStatus =
withTimeout(30.seconds) {
DownloadStatus(DownloadManager.status.first { it.queue.none { it.chapter.id in chapters } })
},
)
}
}
data class DequeueChapterDownloadInput(
val clientMutationId: String? = null,
val id: Int
)
data class DequeueChapterDownloadPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus
val id: Int,
)
fun dequeueChapterDownload(
input: DequeueChapterDownloadInput
): CompletableFuture<DequeueChapterDownloadPayload> {
data class DequeueChapterDownloadPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus,
)
fun dequeueChapterDownload(input: DequeueChapterDownloadInput): CompletableFuture<DequeueChapterDownloadPayload> {
val (clientMutationId, chapter) = input
DownloadManager.dequeue(DownloadManager.EnqueueInput(listOf(chapter)))
@@ -159,19 +161,21 @@ class DownloadMutation {
return future {
DequeueChapterDownloadPayload(
clientMutationId = clientMutationId,
downloadStatus = withTimeout(30.seconds) {
DownloadStatus(DownloadManager.status.first { it.queue.none { it.chapter.id == chapter } })
}
downloadStatus =
withTimeout(30.seconds) {
DownloadStatus(DownloadManager.status.first { it.queue.none { it.chapter.id == chapter } })
},
)
}
}
data class StartDownloaderInput(
val clientMutationId: String? = null
val clientMutationId: String? = null,
)
data class StartDownloaderPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus
val downloadStatus: DownloadStatus,
)
fun startDownloader(input: StartDownloaderInput): CompletableFuture<StartDownloaderPayload> {
@@ -180,21 +184,23 @@ class DownloadMutation {
return future {
StartDownloaderPayload(
input.clientMutationId,
downloadStatus = withTimeout(30.seconds) {
DownloadStatus(
DownloadManager.status.first { it.status == Status.Started }
)
}
downloadStatus =
withTimeout(30.seconds) {
DownloadStatus(
DownloadManager.status.first { it.status == Status.Started },
)
},
)
}
}
data class StopDownloaderInput(
val clientMutationId: String? = null
val clientMutationId: String? = null,
)
data class StopDownloaderPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus
val downloadStatus: DownloadStatus,
)
fun stopDownloader(input: StopDownloaderInput): CompletableFuture<StopDownloaderPayload> {
@@ -202,21 +208,23 @@ class DownloadMutation {
DownloadManager.stop()
StopDownloaderPayload(
input.clientMutationId,
downloadStatus = withTimeout(30.seconds) {
DownloadStatus(
DownloadManager.status.first { it.status == Status.Stopped }
)
}
downloadStatus =
withTimeout(30.seconds) {
DownloadStatus(
DownloadManager.status.first { it.status == Status.Stopped },
)
},
)
}
}
data class ClearDownloaderInput(
val clientMutationId: String? = null
val clientMutationId: String? = null,
)
data class ClearDownloaderPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus
val downloadStatus: DownloadStatus,
)
fun clearDownloader(input: ClearDownloaderInput): CompletableFuture<ClearDownloaderPayload> {
@@ -224,11 +232,12 @@ class DownloadMutation {
DownloadManager.clear()
ClearDownloaderPayload(
input.clientMutationId,
downloadStatus = withTimeout(30.seconds) {
DownloadStatus(
DownloadManager.status.first { it.status == Status.Stopped && it.queue.isEmpty() }
)
}
downloadStatus =
withTimeout(30.seconds) {
DownloadStatus(
DownloadManager.status.first { it.status == Status.Stopped && it.queue.isEmpty() },
)
},
)
}
}
@@ -236,11 +245,12 @@ class DownloadMutation {
data class ReorderChapterDownloadInput(
val clientMutationId: String? = null,
val chapterId: Int,
val to: Int
val to: Int,
)
data class ReorderChapterDownloadPayload(
val clientMutationId: String?,
val downloadStatus: DownloadStatus
val downloadStatus: DownloadStatus,
)
fun reorderChapterDownload(input: ReorderChapterDownloadInput): CompletableFuture<ReorderChapterDownloadPayload> {
@@ -250,11 +260,12 @@ class DownloadMutation {
return future {
ReorderChapterDownloadPayload(
clientMutationId,
downloadStatus = withTimeout(30.seconds) {
DownloadStatus(
DownloadManager.status.first { it.queue.indexOfFirst { it.chapter.id == chapter } <= to }
)
}
downloadStatus =
withTimeout(30.seconds) {
DownloadStatus(
DownloadManager.status.first { it.queue.indexOfFirst { it.chapter.id == chapter } <= to },
)
},
)
}
}
@@ -262,7 +273,7 @@ class DownloadMutation {
data class DownloadAheadInput(
val clientMutationId: String? = null,
val mangaIds: List<Int> = emptyList(),
val latestReadChapterIds: List<Int>? = null
val latestReadChapterIds: List<Int>? = null,
)
data class DownloadAheadPayload(val clientMutationId: String?)
@@ -15,34 +15,40 @@ class ExtensionMutation {
data class UpdateExtensionPatch(
val install: Boolean? = null,
val update: Boolean? = null,
val uninstall: Boolean? = null
val uninstall: Boolean? = null,
)
data class UpdateExtensionPayload(
val clientMutationId: String?,
val extension: ExtensionType
val extension: ExtensionType,
)
data class UpdateExtensionInput(
val clientMutationId: String? = null,
val id: String,
val patch: UpdateExtensionPatch
val patch: UpdateExtensionPatch,
)
data class UpdateExtensionsPayload(
val clientMutationId: String?,
val extensions: List<ExtensionType>
val extensions: List<ExtensionType>,
)
data class UpdateExtensionsInput(
val clientMutationId: String? = null,
val ids: List<String>,
val patch: UpdateExtensionPatch
val patch: UpdateExtensionPatch,
)
private suspend fun updateExtensions(ids: List<String>, patch: UpdateExtensionPatch) {
val extensions = transaction {
ExtensionTable.select { ExtensionTable.pkgName inList ids }
.map { ExtensionType(it) }
}
private suspend fun updateExtensions(
ids: List<String>,
patch: UpdateExtensionPatch,
) {
val extensions =
transaction {
ExtensionTable.select { ExtensionTable.pkgName inList ids }
.map { ExtensionType(it) }
}
if (patch.update == true) {
extensions.filter { it.hasUpdate }.forEach {
@@ -69,13 +75,14 @@ class ExtensionMutation {
return future {
updateExtensions(listOf(id), patch)
}.thenApply {
val extension = transaction {
ExtensionType(ExtensionTable.select { ExtensionTable.pkgName eq id }.first())
}
val extension =
transaction {
ExtensionType(ExtensionTable.select { ExtensionTable.pkgName eq id }.first())
}
UpdateExtensionPayload(
clientMutationId = clientMutationId,
extension = extension
extension = extension,
)
}
}
@@ -86,54 +93,55 @@ class ExtensionMutation {
return future {
updateExtensions(ids, patch)
}.thenApply {
val extensions = transaction {
ExtensionTable.select { ExtensionTable.pkgName inList ids }
.map { ExtensionType(it) }
}
val extensions =
transaction {
ExtensionTable.select { ExtensionTable.pkgName inList ids }
.map { ExtensionType(it) }
}
UpdateExtensionsPayload(
clientMutationId = clientMutationId,
extensions = extensions
extensions = extensions,
)
}
}
data class FetchExtensionsInput(
val clientMutationId: String? = null
)
data class FetchExtensionsPayload(
val clientMutationId: String?,
val extensions: List<ExtensionType>
val clientMutationId: String? = null,
)
fun fetchExtensions(
input: FetchExtensionsInput
): CompletableFuture<FetchExtensionsPayload> {
data class FetchExtensionsPayload(
val clientMutationId: String?,
val extensions: List<ExtensionType>,
)
fun fetchExtensions(input: FetchExtensionsInput): CompletableFuture<FetchExtensionsPayload> {
val (clientMutationId) = input
return future {
ExtensionsList.fetchExtensions()
}.thenApply {
val extensions = transaction {
ExtensionTable.select { ExtensionTable.name neq LocalSource.EXTENSION_NAME }
.map { ExtensionType(it) }
}
val extensions =
transaction {
ExtensionTable.select { ExtensionTable.name neq LocalSource.EXTENSION_NAME }
.map { ExtensionType(it) }
}
FetchExtensionsPayload(
clientMutationId = clientMutationId,
extensions = extensions
extensions = extensions,
)
}
}
data class InstallExternalExtensionInput(
val clientMutationId: String? = null,
val extensionFile: UploadedFile
val extensionFile: UploadedFile,
)
data class InstallExternalExtensionPayload(
val clientMutationId: String?,
val extension: ExtensionType
val extension: ExtensionType,
)
fun installExternalExtension(input: InstallExternalExtensionInput): CompletableFuture<InstallExternalExtensionPayload> {
@@ -146,7 +154,7 @@ class ExtensionMutation {
InstallExternalExtensionPayload(
clientMutationId,
extension = ExtensionType(dbExtension)
extension = ExtensionType(dbExtension),
)
}
}
@@ -14,12 +14,12 @@ import kotlin.time.Duration.Companion.seconds
class InfoMutation {
data class WebUIUpdateInput(
val clientMutationId: String? = null
val clientMutationId: String? = null,
)
data class WebUIUpdatePayload(
val clientMutationId: String?,
val updateStatus: WebUIUpdateStatus
val updateStatus: WebUIUpdateStatus,
)
fun updateWebUI(input: WebUIUpdateInput): CompletableFuture<WebUIUpdatePayload> {
@@ -35,14 +35,15 @@ class InfoMutation {
return@withTimeout WebUIUpdatePayload(
input.clientMutationId,
WebUIUpdateStatus(
info = WebUIUpdateInfo(
channel = serverConfig.webUIChannel.value,
tag = version,
updateAvailable
),
info =
WebUIUpdateInfo(
channel = serverConfig.webUIChannel.value,
tag = version,
updateAvailable,
),
state = STOPPED,
progress = 0
)
progress = 0,
),
)
}
try {
@@ -53,7 +54,7 @@ class InfoMutation {
WebUIUpdatePayload(
input.clientMutationId,
updateStatus = WebInterfaceManager.status.first { it.state == DOWNLOADING }
updateStatus = WebInterfaceManager.status.first { it.state == DOWNLOADING },
)
}
}
@@ -22,30 +22,35 @@ import java.util.concurrent.CompletableFuture
*/
class MangaMutation {
data class UpdateMangaPatch(
val inLibrary: Boolean? = null
val inLibrary: Boolean? = null,
)
data class UpdateMangaPayload(
val clientMutationId: String?,
val manga: MangaType
val manga: MangaType,
)
data class UpdateMangaInput(
val clientMutationId: String? = null,
val id: Int,
val patch: UpdateMangaPatch
val patch: UpdateMangaPatch,
)
data class UpdateMangasPayload(
val clientMutationId: String?,
val mangas: List<MangaType>
val mangas: List<MangaType>,
)
data class UpdateMangasInput(
val clientMutationId: String? = null,
val ids: List<Int>,
val patch: UpdateMangaPatch
val patch: UpdateMangaPatch,
)
private suspend fun updateMangas(ids: List<Int>, patch: UpdateMangaPatch) {
private suspend fun updateMangas(
ids: List<Int>,
patch: UpdateMangaPatch,
) {
transaction {
if (patch.inLibrary != null) {
MangaTable.update({ MangaTable.id inList ids }) { update ->
@@ -69,13 +74,14 @@ class MangaMutation {
return future {
updateMangas(listOf(id), patch)
}.thenApply {
val manga = transaction {
MangaType(MangaTable.select { MangaTable.id eq id }.first())
}
val manga =
transaction {
MangaType(MangaTable.select { MangaTable.id eq id }.first())
}
UpdateMangaPayload(
clientMutationId = clientMutationId,
manga = manga
manga = manga,
)
}
}
@@ -86,55 +92,56 @@ class MangaMutation {
return future {
updateMangas(ids, patch)
}.thenApply {
val mangas = transaction {
MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) }
}
val mangas =
transaction {
MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) }
}
UpdateMangasPayload(
clientMutationId = clientMutationId,
mangas = mangas
mangas = mangas,
)
}
}
data class FetchMangaInput(
val clientMutationId: String? = null,
val id: Int
)
data class FetchMangaPayload(
val clientMutationId: String?,
val manga: MangaType
val id: Int,
)
fun fetchManga(
input: FetchMangaInput
): CompletableFuture<FetchMangaPayload> {
data class FetchMangaPayload(
val clientMutationId: String?,
val manga: MangaType,
)
fun fetchManga(input: FetchMangaInput): CompletableFuture<FetchMangaPayload> {
val (clientMutationId, id) = input
return future {
Manga.fetchManga(id)
}.thenApply {
val manga = transaction {
MangaTable.select { MangaTable.id eq id }.first()
}
val manga =
transaction {
MangaTable.select { MangaTable.id eq id }.first()
}
FetchMangaPayload(
clientMutationId = clientMutationId,
manga = MangaType(manga)
manga = MangaType(manga),
)
}
}
data class SetMangaMetaInput(
val clientMutationId: String? = null,
val meta: MangaMetaType
val meta: MangaMetaType,
)
data class SetMangaMetaPayload(
val clientMutationId: String?,
val meta: MangaMetaType
val meta: MangaMetaType,
)
fun setMangaMeta(
input: SetMangaMetaInput
): SetMangaMetaPayload {
fun setMangaMeta(input: SetMangaMetaInput): SetMangaMetaPayload {
val (clientMutationId, meta) = input
Manga.modifyMangaMeta(meta.mangaId, meta.key, meta.value)
@@ -145,35 +152,38 @@ class MangaMutation {
data class DeleteMangaMetaInput(
val clientMutationId: String? = null,
val mangaId: Int,
val key: String
val key: String,
)
data class DeleteMangaMetaPayload(
val clientMutationId: String?,
val meta: MangaMetaType?,
val manga: MangaType
val manga: MangaType,
)
fun deleteMangaMeta(
input: DeleteMangaMetaInput
): DeleteMangaMetaPayload {
fun deleteMangaMeta(input: DeleteMangaMetaInput): DeleteMangaMetaPayload {
val (clientMutationId, mangaId, key) = input
val (meta, manga) = transaction {
val meta = MangaMetaTable.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
.firstOrNull()
val (meta, manga) =
transaction {
val meta =
MangaMetaTable.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
.firstOrNull()
MangaMetaTable.deleteWhere { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
MangaMetaTable.deleteWhere { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
val manga = transaction {
MangaType(MangaTable.select { MangaTable.id eq mangaId }.first())
val manga =
transaction {
MangaType(MangaTable.select { MangaTable.id eq mangaId }.first())
}
if (meta != null) {
MangaMetaType(meta)
} else {
null
} to manga
}
if (meta != null) {
MangaMetaType(meta)
} else {
null
} to manga
}
return DeleteMangaMetaPayload(clientMutationId, meta, manga)
}
}
@@ -9,18 +9,17 @@ import suwayomi.tachidesk.global.model.table.GlobalMetaTable
import suwayomi.tachidesk.graphql.types.GlobalMetaType
class MetaMutation {
data class SetGlobalMetaInput(
val clientMutationId: String? = null,
val meta: GlobalMetaType
val meta: GlobalMetaType,
)
data class SetGlobalMetaPayload(
val clientMutationId: String?,
val meta: GlobalMetaType
val meta: GlobalMetaType,
)
fun setGlobalMeta(
input: SetGlobalMetaInput
): SetGlobalMetaPayload {
fun setGlobalMeta(input: SetGlobalMetaInput): SetGlobalMetaPayload {
val (clientMutationId, meta) = input
GlobalMeta.modifyMeta(meta.key, meta.value)
@@ -30,29 +29,31 @@ class MetaMutation {
data class DeleteGlobalMetaInput(
val clientMutationId: String? = null,
val key: String
val key: String,
)
data class DeleteGlobalMetaPayload(
val clientMutationId: String?,
val meta: GlobalMetaType?
val meta: GlobalMetaType?,
)
fun deleteGlobalMeta(
input: DeleteGlobalMetaInput
): DeleteGlobalMetaPayload {
fun deleteGlobalMeta(input: DeleteGlobalMetaInput): DeleteGlobalMetaPayload {
val (clientMutationId, key) = input
val meta = transaction {
val meta = GlobalMetaTable.select { GlobalMetaTable.key eq key }
.firstOrNull()
val meta =
transaction {
val meta =
GlobalMetaTable.select { GlobalMetaTable.key eq key }
.firstOrNull()
GlobalMetaTable.deleteWhere { GlobalMetaTable.key eq key }
GlobalMetaTable.deleteWhere { GlobalMetaTable.key eq key }
if (meta != null) {
GlobalMetaType(meta)
} else {
null
if (meta != null) {
GlobalMetaType(meta)
} else {
null
}
}
}
return DeleteGlobalMetaPayload(clientMutationId, meta)
}
@@ -11,53 +11,107 @@ import xyz.nulldev.ts.config.GlobalConfigManager
class SettingsMutation {
data class SetSettingsInput(
val clientMutationId: String? = null,
val settings: PartialSettingsType
val settings: PartialSettingsType,
)
data class SetSettingsPayload(
val clientMutationId: String?,
val settings: SettingsType
val settings: SettingsType,
)
private fun updateSettings(settings: Settings) {
if (settings.ip != null) serverConfig.ip.value = settings.ip!!
if (settings.port != null) serverConfig.port.value = settings.port!!
if (settings.socksProxyEnabled != null) serverConfig.socksProxyEnabled.value = settings.socksProxyEnabled!!
if (settings.socksProxyHost != null) serverConfig.socksProxyHost.value = settings.socksProxyHost!!
if (settings.socksProxyPort != null) serverConfig.socksProxyPort.value = settings.socksProxyPort!!
if (settings.socksProxyEnabled != null) {
serverConfig.socksProxyEnabled.value = settings.socksProxyEnabled!!
}
if (settings.socksProxyHost != null) {
serverConfig.socksProxyHost.value = settings.socksProxyHost!!
}
if (settings.socksProxyPort != null) {
serverConfig.socksProxyPort.value = settings.socksProxyPort!!
}
if (settings.webUIFlavor != null) serverConfig.webUIFlavor.value = settings.webUIFlavor!!.uiName
if (settings.initialOpenInBrowserEnabled != null) serverConfig.initialOpenInBrowserEnabled.value = settings.initialOpenInBrowserEnabled!!
if (settings.webUIInterface != null) serverConfig.webUIInterface.value = settings.webUIInterface!!.name.lowercase()
if (settings.electronPath != null) serverConfig.electronPath.value = settings.electronPath!!
if (settings.webUIChannel != null) serverConfig.webUIChannel.value = settings.webUIChannel!!.name.lowercase()
if (settings.webUIUpdateCheckInterval != null) serverConfig.webUIUpdateCheckInterval.value = settings.webUIUpdateCheckInterval!!
if (settings.webUIFlavor != null) {
serverConfig.webUIFlavor.value = settings.webUIFlavor!!.uiName
}
if (settings.initialOpenInBrowserEnabled != null) {
serverConfig.initialOpenInBrowserEnabled.value = settings.initialOpenInBrowserEnabled!!
}
if (settings.webUIInterface != null) {
serverConfig.webUIInterface.value = settings.webUIInterface!!.name.lowercase()
}
if (settings.electronPath != null) {
serverConfig.electronPath.value = settings.electronPath!!
}
if (settings.webUIChannel != null) {
serverConfig.webUIChannel.value = settings.webUIChannel!!.name.lowercase()
}
if (settings.webUIUpdateCheckInterval != null) {
serverConfig.webUIUpdateCheckInterval.value = settings.webUIUpdateCheckInterval!!
}
if (settings.downloadAsCbz != null) serverConfig.downloadAsCbz.value = settings.downloadAsCbz!!
if (settings.downloadsPath != null) serverConfig.downloadsPath.value = settings.downloadsPath!!
if (settings.autoDownloadNewChapters != null) serverConfig.autoDownloadNewChapters.value = settings.autoDownloadNewChapters!!
if (settings.downloadAsCbz != null) {
serverConfig.downloadAsCbz.value = settings.downloadAsCbz!!
}
if (settings.downloadsPath != null) {
serverConfig.downloadsPath.value = settings.downloadsPath!!
}
if (settings.autoDownloadNewChapters != null) {
serverConfig.autoDownloadNewChapters.value = settings.autoDownloadNewChapters!!
}
if (settings.maxSourcesInParallel != null) serverConfig.maxSourcesInParallel.value = settings.maxSourcesInParallel!!
if (settings.maxSourcesInParallel != null) {
serverConfig.maxSourcesInParallel.value = settings.maxSourcesInParallel!!
}
if (settings.excludeUnreadChapters != null) serverConfig.excludeUnreadChapters.value = settings.excludeUnreadChapters!!
if (settings.excludeNotStarted != null) serverConfig.excludeNotStarted.value = settings.excludeNotStarted!!
if (settings.excludeCompleted != null) serverConfig.excludeCompleted.value = settings.excludeCompleted!!
if (settings.globalUpdateInterval != null) serverConfig.globalUpdateInterval.value = settings.globalUpdateInterval!!
if (settings.excludeUnreadChapters != null) {
serverConfig.excludeUnreadChapters.value = settings.excludeUnreadChapters!!
}
if (settings.excludeNotStarted != null) {
serverConfig.excludeNotStarted.value = settings.excludeNotStarted!!
}
if (settings.excludeCompleted != null) {
serverConfig.excludeCompleted.value = settings.excludeCompleted!!
}
if (settings.globalUpdateInterval != null) {
serverConfig.globalUpdateInterval.value = settings.globalUpdateInterval!!
}
if (settings.basicAuthEnabled != null) serverConfig.basicAuthEnabled.value = settings.basicAuthEnabled!!
if (settings.basicAuthUsername != null) serverConfig.basicAuthUsername.value = settings.basicAuthUsername!!
if (settings.basicAuthPassword != null) serverConfig.basicAuthPassword.value = settings.basicAuthPassword!!
if (settings.basicAuthEnabled != null) {
serverConfig.basicAuthEnabled.value = settings.basicAuthEnabled!!
}
if (settings.basicAuthUsername != null) {
serverConfig.basicAuthUsername.value = settings.basicAuthUsername!!
}
if (settings.basicAuthPassword != null) {
serverConfig.basicAuthPassword.value = settings.basicAuthPassword!!
}
if (settings.debugLogsEnabled != null) serverConfig.debugLogsEnabled.value = settings.debugLogsEnabled!!
if (settings.systemTrayEnabled != null) serverConfig.systemTrayEnabled.value = settings.systemTrayEnabled!!
if (settings.debugLogsEnabled != null) {
serverConfig.debugLogsEnabled.value = settings.debugLogsEnabled!!
}
if (settings.systemTrayEnabled != null) {
serverConfig.systemTrayEnabled.value = settings.systemTrayEnabled!!
}
if (settings.backupPath != null) serverConfig.backupPath.value = settings.backupPath!!
if (settings.backupTime != null) serverConfig.backupTime.value = settings.backupTime!!
if (settings.backupInterval != null) serverConfig.backupInterval.value = settings.backupInterval!!
if (settings.backupTTL != null) serverConfig.backupTTL.value = settings.backupTTL!!
if (settings.backupPath != null) {
serverConfig.backupPath.value = settings.backupPath!!
}
if (settings.backupTime != null) {
serverConfig.backupTime.value = settings.backupTime!!
}
if (settings.backupInterval != null) {
serverConfig.backupInterval.value = settings.backupInterval!!
}
if (settings.backupTTL != null) {
serverConfig.backupTTL.value = settings.backupTTL!!
}
if (settings.localSourcePath != null) serverConfig.localSourcePath.value = settings.localSourcePath!!
if (settings.localSourcePath != null) {
serverConfig.localSourcePath.value = settings.localSourcePath!!
}
}
fun setSettings(input: SetSettingsInput): SetSettingsPayload {
@@ -72,7 +126,7 @@ class SettingsMutation {
data class ResetSettingsPayload(
val clientMutationId: String?,
val settings: SettingsType
val settings: SettingsType,
)
fun resetSettings(input: ResetSettingsInput): ResetSettingsPayload {
@@ -20,63 +20,64 @@ import suwayomi.tachidesk.server.JavalinSetup.future
import java.util.concurrent.CompletableFuture
class SourceMutation {
enum class FetchSourceMangaType {
SEARCH,
POPULAR,
LATEST
LATEST,
}
data class FetchSourceMangaInput(
val clientMutationId: String? = null,
val source: Long,
val type: FetchSourceMangaType,
val page: Int,
val query: String? = null,
val filters: List<FilterChange>? = null
val filters: List<FilterChange>? = null,
)
data class FetchSourceMangaPayload(
val clientMutationId: String?,
val mangas: List<MangaType>,
val hasNextPage: Boolean
val hasNextPage: Boolean,
)
fun fetchSourceManga(
input: FetchSourceMangaInput
): CompletableFuture<FetchSourceMangaPayload> {
fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture<FetchSourceMangaPayload> {
val (clientMutationId, sourceId, type, page, query, filters) = input
return future {
val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!!
val mangasPage = when (type) {
FetchSourceMangaType.SEARCH -> {
source.getSearchManga(
page = page,
query = query.orEmpty(),
filters = updateFilterList(source, filters)
)
val mangasPage =
when (type) {
FetchSourceMangaType.SEARCH -> {
source.getSearchManga(
page = page,
query = query.orEmpty(),
filters = updateFilterList(source, filters),
)
}
FetchSourceMangaType.POPULAR -> {
source.getPopularManga(page)
}
FetchSourceMangaType.LATEST -> {
if (!source.supportsLatest) throw Exception("Source does not support latest")
source.getLatestUpdates(page)
}
}
FetchSourceMangaType.POPULAR -> {
source.getPopularManga(page)
}
FetchSourceMangaType.LATEST -> {
if (!source.supportsLatest) throw Exception("Source does not support latest")
source.getLatestUpdates(page)
}
}
val mangaIds = mangasPage.insertOrGet(sourceId)
val mangas = transaction {
MangaTable.select { MangaTable.id inList mangaIds }
.map { MangaType(it) }
}.sortedBy {
mangaIds.indexOf(it.id)
}
val mangas =
transaction {
MangaTable.select { MangaTable.id inList mangaIds }
.map { MangaType(it) }
}.sortedBy {
mangaIds.indexOf(it.id)
}
FetchSourceMangaPayload(
clientMutationId = clientMutationId,
mangas = mangas,
hasNextPage = mangasPage.hasNextPage
hasNextPage = mangasPage.hasNextPage,
)
}
}
@@ -87,21 +88,21 @@ class SourceMutation {
val checkBoxState: Boolean? = null,
val editTextState: String? = null,
val listState: String? = null,
val multiSelectState: List<String>? = null
val multiSelectState: List<String>? = null,
)
data class UpdateSourcePreferenceInput(
val clientMutationId: String? = null,
val source: Long,
val change: SourcePreferenceChange
)
data class UpdateSourcePreferencePayload(
val clientMutationId: String?,
val preferences: List<Preference>
val change: SourcePreferenceChange,
)
fun updateSourcePreference(
input: UpdateSourcePreferenceInput
): UpdateSourcePreferencePayload {
data class UpdateSourcePreferencePayload(
val clientMutationId: String?,
val preferences: List<Preference>,
)
fun updateSourcePreference(input: UpdateSourcePreferenceInput): UpdateSourcePreferencePayload {
val (clientMutationId, sourceId, change) = input
Source.setSourcePreference(sourceId, change.position, "") { preference ->
@@ -117,7 +118,7 @@ class SourceMutation {
return UpdateSourcePreferencePayload(
clientMutationId = clientMutationId,
preferences = Source.getSourcePreferencesRaw(sourceId).map { preferenceOf(it) }
preferences = Source.getSourcePreferencesRaw(sourceId).map { preferenceOf(it) },
)
}
}
@@ -15,18 +15,19 @@ class UpdateMutation {
private val updater by DI.global.instance<IUpdater>()
data class UpdateLibraryMangaInput(
val clientMutationId: String? = null
val clientMutationId: String? = null,
)
data class UpdateLibraryMangaPayload(
val clientMutationId: String?,
val updateStatus: UpdateStatus
val updateStatus: UpdateStatus,
)
fun updateLibraryManga(input: UpdateLibraryMangaInput): UpdateLibraryMangaPayload {
updater.addCategoriesToUpdateQueue(
Category.getCategoryList(),
clear = true,
forceAll = false
forceAll = false,
)
return UpdateLibraryMangaPayload(input.clientMutationId, UpdateStatus(updater.status.value))
@@ -34,32 +35,35 @@ class UpdateMutation {
data class UpdateCategoryMangaInput(
val clientMutationId: String? = null,
val categories: List<Int>
val categories: List<Int>,
)
data class UpdateCategoryMangaPayload(
val clientMutationId: String?,
val updateStatus: UpdateStatus
val updateStatus: UpdateStatus,
)
fun updateCategoryManga(input: UpdateCategoryMangaInput): UpdateCategoryMangaPayload {
val categories = transaction {
CategoryTable.select { CategoryTable.id inList input.categories }.map {
CategoryTable.toDataClass(it)
val categories =
transaction {
CategoryTable.select { CategoryTable.id inList input.categories }.map {
CategoryTable.toDataClass(it)
}
}
}
updater.addCategoriesToUpdateQueue(categories, clear = true, forceAll = true)
return UpdateCategoryMangaPayload(
clientMutationId = input.clientMutationId,
updateStatus = UpdateStatus(updater.status.value)
updateStatus = UpdateStatus(updater.status.value),
)
}
data class UpdateStopInput(
val clientMutationId: String? = null
val clientMutationId: String? = null,
)
data class UpdateStopPayload(
val clientMutationId: String?
val clientMutationId: String?,
)
fun updateStop(input: UpdateStopInput): UpdateStopPayload {
@@ -8,21 +8,22 @@ import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator
class BackupQuery {
data class ValidateBackupInput(
val backup: UploadedFile
val backup: UploadedFile,
)
data class ValidateBackupSource(
val id: Long,
val name: String
val name: String,
)
data class ValidateBackupResult(
val missingSources: List<ValidateBackupSource>
val missingSources: List<ValidateBackupSource>,
)
fun validateBackup(
input: ValidateBackupInput
): ValidateBackupResult {
fun validateBackup(input: ValidateBackupInput): ValidateBackupResult {
val result = ProtoBackupValidator.validate(input.backup.content)
return ValidateBackupResult(
result.missingSourceIds.map { ValidateBackupSource(it.first, it.second) }
result.missingSourceIds.map { ValidateBackupSource(it.first, it.second) },
)
}
@@ -46,14 +46,18 @@ import suwayomi.tachidesk.manga.model.table.CategoryTable
import java.util.concurrent.CompletableFuture
class CategoryQuery {
fun category(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture<CategoryType> {
fun category(
dataFetchingEnvironment: DataFetchingEnvironment,
id: Int,
): CompletableFuture<CategoryType> {
return dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id)
}
enum class CategoryOrderBy(override val column: Column<out Comparable<*>>) : OrderBy<CategoryType> {
ID(CategoryTable.id),
NAME(CategoryTable.name),
ORDER(CategoryTable.order);
ORDER(CategoryTable.order),
;
override fun greater(cursor: Cursor): Op<Boolean> {
return when (this) {
@@ -72,11 +76,12 @@ class CategoryQuery {
}
override fun asCursor(type: CategoryType): Cursor {
val value = when (this) {
ID -> type.id.toString()
NAME -> type.id.toString() + "-" + type.name
ORDER -> type.id.toString() + "-" + type.order
}
val value =
when (this) {
ID -> type.id.toString()
NAME -> type.id.toString() + "-" + type.name
ORDER -> type.id.toString() + "-" + type.order
}
return Cursor(value)
}
}
@@ -85,7 +90,7 @@ class CategoryQuery {
val id: Int? = null,
val order: Int? = null,
val name: String? = null,
val default: Boolean? = null
val default: Boolean? = null,
) : HasGetOp {
override fun getOp(): Op<Boolean>? {
val opAnd = OpAnd()
@@ -105,14 +110,14 @@ class CategoryQuery {
val default: BooleanFilter? = null,
override val and: List<CategoryFilter>? = null,
override val or: List<CategoryFilter>? = null,
override val not: CategoryFilter? = null
override val not: CategoryFilter? = null,
) : Filter<CategoryFilter> {
override fun getOpList(): List<Op<Boolean>> {
return listOfNotNull(
andFilterWithCompareEntity(CategoryTable.id, id),
andFilterWithCompare(CategoryTable.order, order),
andFilterWithCompareString(CategoryTable.name, name),
andFilterWithCompare(CategoryTable.isDefault, default)
andFilterWithCompare(CategoryTable.isDefault, default),
)
}
}
@@ -126,55 +131,56 @@ class CategoryQuery {
after: Cursor? = null,
first: Int? = null,
last: Int? = null,
offset: Int? = null
offset: Int? = null,
): CategoryNodeList {
val queryResults = transaction {
val res = CategoryTable.selectAll()
val queryResults =
transaction {
val res = CategoryTable.selectAll()
res.applyOps(condition, filter)
res.applyOps(condition, filter)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: CategoryTable.id
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: CategoryTable.id
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy == CategoryOrderBy.ID || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
CategoryTable.id to SortOrder.ASC
)
}
}
val total = res.count()
val firstResult = res.firstOrNull()?.get(CategoryTable.id)?.value
val lastResult = res.lastOrNull()?.get(CategoryTable.id)?.value
if (after != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: CategoryOrderBy.ID).less(after)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: CategoryOrderBy.ID).greater(after)
if (orderBy == CategoryOrderBy.ID || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
CategoryTable.id to SortOrder.ASC,
)
}
}
} else if (before != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: CategoryOrderBy.ID).greater(before)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: CategoryOrderBy.ID).less(before)
val total = res.count()
val firstResult = res.firstOrNull()?.get(CategoryTable.id)?.value
val lastResult = res.lastOrNull()?.get(CategoryTable.id)?.value
if (after != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: CategoryOrderBy.ID).less(after)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: CategoryOrderBy.ID).greater(after)
}
}
} else if (before != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: CategoryOrderBy.ID).greater(before)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: CategoryOrderBy.ID).less(before)
}
}
}
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
QueryResults(total, firstResult, lastResult, res.toList())
}
QueryResults(total, firstResult, lastResult, res.toList())
}
val getAsCursor: (CategoryType) -> Cursor = (orderBy ?: CategoryOrderBy.ID)::asCursor
@@ -189,24 +195,25 @@ class CategoryQuery {
resultsAsType.firstOrNull()?.let {
CategoryNodeList.CategoryEdge(
getAsCursor(it),
it
it,
)
},
resultsAsType.lastOrNull()?.let {
CategoryNodeList.CategoryEdge(
getAsCursor(it),
it
it,
)
}
},
)
},
pageInfo = PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) }
),
totalCount = queryResults.total.toInt()
pageInfo =
PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) },
),
totalCount = queryResults.total.toInt(),
)
}
}
@@ -55,7 +55,10 @@ import java.util.concurrent.CompletableFuture
* - Get page list?
*/
class ChapterQuery {
fun chapter(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture<ChapterType> {
fun chapter(
dataFetchingEnvironment: DataFetchingEnvironment,
id: Int,
): CompletableFuture<ChapterType> {
return dataFetchingEnvironment.getValueFromDataLoader("ChapterDataLoader", id)
}
@@ -66,7 +69,8 @@ class ChapterQuery {
UPLOAD_DATE(ChapterTable.date_upload),
CHAPTER_NUMBER(ChapterTable.chapter_number),
LAST_READ_AT(ChapterTable.lastReadAt),
FETCHED_AT(ChapterTable.fetchedAt);
FETCHED_AT(ChapterTable.fetchedAt),
;
override fun greater(cursor: Cursor): Op<Boolean> {
return when (this) {
@@ -93,15 +97,16 @@ class ChapterQuery {
}
override fun asCursor(type: ChapterType): Cursor {
val value = when (this) {
ID -> type.id.toString()
SOURCE_ORDER -> type.id.toString() + "-" + type.sourceOrder
NAME -> type.id.toString() + "-" + type.name
UPLOAD_DATE -> type.id.toString() + "-" + type.uploadDate
CHAPTER_NUMBER -> type.id.toString() + "-" + type.chapterNumber
LAST_READ_AT -> type.id.toString() + "-" + type.lastReadAt
FETCHED_AT -> type.id.toString() + "-" + type.fetchedAt
}
val value =
when (this) {
ID -> type.id.toString()
SOURCE_ORDER -> type.id.toString() + "-" + type.sourceOrder
NAME -> type.id.toString() + "-" + type.name
UPLOAD_DATE -> type.id.toString() + "-" + type.uploadDate
CHAPTER_NUMBER -> type.id.toString() + "-" + type.chapterNumber
LAST_READ_AT -> type.id.toString() + "-" + type.lastReadAt
FETCHED_AT -> type.id.toString() + "-" + type.fetchedAt
}
return Cursor(value)
}
}
@@ -122,7 +127,7 @@ class ChapterQuery {
val realUrl: String? = null,
val fetchedAt: Long? = null,
val isDownloaded: Boolean? = null,
val pageCount: Int? = null
val pageCount: Int? = null,
) : HasGetOp {
override fun getOp(): Op<Boolean>? {
val opAnd = OpAnd()
@@ -167,7 +172,7 @@ class ChapterQuery {
val inLibrary: BooleanFilter? = null,
override val and: List<ChapterFilter>? = null,
override val or: List<ChapterFilter>? = null,
override val not: ChapterFilter? = null
override val not: ChapterFilter? = null,
) : Filter<ChapterFilter> {
override fun getOpList(): List<Op<Boolean>> {
return listOfNotNull(
@@ -186,7 +191,7 @@ class ChapterQuery {
andFilterWithCompareString(ChapterTable.realUrl, realUrl),
andFilterWithCompare(ChapterTable.fetchedAt, fetchedAt),
andFilterWithCompare(ChapterTable.isDownloaded, isDownloaded),
andFilterWithCompare(ChapterTable.pageCount, pageCount)
andFilterWithCompare(ChapterTable.pageCount, pageCount),
)
}
@@ -202,63 +207,64 @@ class ChapterQuery {
after: Cursor? = null,
first: Int? = null,
last: Int? = null,
offset: Int? = null
offset: Int? = null,
): ChapterNodeList {
val queryResults = transaction {
val res = ChapterTable.selectAll()
val queryResults =
transaction {
val res = ChapterTable.selectAll()
val libraryOp = filter?.getLibraryOp()
if (libraryOp != null) {
res.adjustColumnSet {
innerJoin(MangaTable)
val libraryOp = filter?.getLibraryOp()
if (libraryOp != null) {
res.adjustColumnSet {
innerJoin(MangaTable)
}
res.andWhere { libraryOp }
}
res.andWhere { libraryOp }
}
res.applyOps(condition, filter)
res.applyOps(condition, filter)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: ChapterTable.id
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: ChapterTable.id
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy == ChapterOrderBy.ID || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
ChapterTable.id to SortOrder.ASC
)
}
}
val total = res.count()
val firstResult = res.firstOrNull()?.get(ChapterTable.id)?.value
val lastResult = res.lastOrNull()?.get(ChapterTable.id)?.value
if (after != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: ID).less(after)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: ID).greater(after)
if (orderBy == ChapterOrderBy.ID || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
ChapterTable.id to SortOrder.ASC,
)
}
}
} else if (before != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: ID).greater(before)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: ID).less(before)
val total = res.count()
val firstResult = res.firstOrNull()?.get(ChapterTable.id)?.value
val lastResult = res.lastOrNull()?.get(ChapterTable.id)?.value
if (after != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: ID).less(after)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: ID).greater(after)
}
}
} else if (before != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: ID).greater(before)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: ID).less(before)
}
}
}
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
QueryResults(total, firstResult, lastResult, res.toList())
}
QueryResults(total, firstResult, lastResult, res.toList())
}
val getAsCursor: (ChapterType) -> Cursor = (orderBy ?: ChapterOrderBy.ID)::asCursor
@@ -273,24 +279,25 @@ class ChapterQuery {
resultsAsType.firstOrNull()?.let {
ChapterNodeList.ChapterEdge(
getAsCursor(it),
it
it,
)
},
resultsAsType.lastOrNull()?.let {
ChapterNodeList.ChapterEdge(
getAsCursor(it),
it
it,
)
}
},
)
},
pageInfo = PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) }
),
totalCount = queryResults.total.toInt()
pageInfo =
PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) },
),
totalCount = queryResults.total.toInt(),
)
}
}
@@ -7,7 +7,6 @@ import suwayomi.tachidesk.server.JavalinSetup.future
import java.util.concurrent.CompletableFuture
class DownloadQuery {
fun downloadStatus(): CompletableFuture<DownloadStatus> {
return future {
DownloadStatus(DownloadManager.status.first())
@@ -47,14 +47,18 @@ import suwayomi.tachidesk.manga.model.table.ExtensionTable
import java.util.concurrent.CompletableFuture
class ExtensionQuery {
fun extension(dataFetchingEnvironment: DataFetchingEnvironment, pkgName: String): CompletableFuture<ExtensionType> {
fun extension(
dataFetchingEnvironment: DataFetchingEnvironment,
pkgName: String,
): CompletableFuture<ExtensionType> {
return dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName)
}
enum class ExtensionOrderBy(override val column: Column<out Comparable<*>>) : OrderBy<ExtensionType> {
PKG_NAME(ExtensionTable.pkgName),
NAME(ExtensionTable.name),
APK_NAME(ExtensionTable.apkName);
APK_NAME(ExtensionTable.apkName),
;
override fun greater(cursor: Cursor): Op<Boolean> {
return when (this) {
@@ -73,11 +77,12 @@ class ExtensionQuery {
}
override fun asCursor(type: ExtensionType): Cursor {
val value = when (this) {
PKG_NAME -> type.pkgName
NAME -> type.pkgName + "\\-" + type.name
APK_NAME -> type.pkgName + "\\-" + type.apkName
}
val value =
when (this) {
PKG_NAME -> type.pkgName
NAME -> type.pkgName + "\\-" + type.name
APK_NAME -> type.pkgName + "\\-" + type.apkName
}
return Cursor(value)
}
}
@@ -93,7 +98,7 @@ class ExtensionQuery {
val isNsfw: Boolean? = null,
val isInstalled: Boolean? = null,
val hasUpdate: Boolean? = null,
val isObsolete: Boolean? = null
val isObsolete: Boolean? = null,
) : HasGetOp {
override fun getOp(): Op<Boolean>? {
val opAnd = OpAnd()
@@ -126,7 +131,7 @@ class ExtensionQuery {
val isObsolete: BooleanFilter? = null,
override val and: List<ExtensionFilter>? = null,
override val or: List<ExtensionFilter>? = null,
override val not: ExtensionFilter? = null
override val not: ExtensionFilter? = null,
) : Filter<ExtensionFilter> {
override fun getOpList(): List<Op<Boolean>> {
return listOfNotNull(
@@ -140,7 +145,7 @@ class ExtensionQuery {
andFilterWithCompare(ExtensionTable.isNsfw, isNsfw),
andFilterWithCompare(ExtensionTable.isInstalled, isInstalled),
andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate),
andFilterWithCompare(ExtensionTable.isObsolete, isObsolete)
andFilterWithCompare(ExtensionTable.isObsolete, isObsolete),
)
}
}
@@ -154,57 +159,58 @@ class ExtensionQuery {
after: Cursor? = null,
first: Int? = null,
last: Int? = null,
offset: Int? = null
offset: Int? = null,
): ExtensionNodeList {
val queryResults = transaction {
val res = ExtensionTable.selectAll()
val queryResults =
transaction {
val res = ExtensionTable.selectAll()
res.adjustWhere { ExtensionTable.name neq LocalSource.EXTENSION_NAME }
res.adjustWhere { ExtensionTable.name neq LocalSource.EXTENSION_NAME }
res.applyOps(condition, filter)
res.applyOps(condition, filter)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: ExtensionTable.pkgName
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: ExtensionTable.pkgName
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy == ExtensionOrderBy.PKG_NAME || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
ExtensionTable.pkgName to SortOrder.ASC
)
}
}
val total = res.count()
val firstResult = res.firstOrNull()?.get(ExtensionTable.pkgName)
val lastResult = res.lastOrNull()?.get(ExtensionTable.pkgName)
if (after != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: ExtensionOrderBy.PKG_NAME).less(after)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: ExtensionOrderBy.PKG_NAME).greater(after)
if (orderBy == ExtensionOrderBy.PKG_NAME || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
ExtensionTable.pkgName to SortOrder.ASC,
)
}
}
} else if (before != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: ExtensionOrderBy.PKG_NAME).greater(before)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: ExtensionOrderBy.PKG_NAME).less(before)
val total = res.count()
val firstResult = res.firstOrNull()?.get(ExtensionTable.pkgName)
val lastResult = res.lastOrNull()?.get(ExtensionTable.pkgName)
if (after != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: ExtensionOrderBy.PKG_NAME).less(after)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: ExtensionOrderBy.PKG_NAME).greater(after)
}
}
} else if (before != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: ExtensionOrderBy.PKG_NAME).greater(before)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: ExtensionOrderBy.PKG_NAME).less(before)
}
}
}
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
QueryResults(total, firstResult, lastResult, res.toList())
}
QueryResults(total, firstResult, lastResult, res.toList())
}
val getAsCursor: (ExtensionType) -> Cursor = (orderBy ?: ExtensionOrderBy.PKG_NAME)::asCursor
@@ -219,24 +225,25 @@ class ExtensionQuery {
resultsAsType.firstOrNull()?.let {
ExtensionNodeList.ExtensionEdge(
getAsCursor(it),
it
it,
)
},
resultsAsType.lastOrNull()?.let {
ExtensionNodeList.ExtensionEdge(
getAsCursor(it),
it
it,
)
}
},
)
},
pageInfo = PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.pkgName,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.pkgName,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) }
),
totalCount = queryResults.total.toInt()
pageInfo =
PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.pkgName,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.pkgName,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) },
),
totalCount = queryResults.total.toInt(),
)
}
}
@@ -3,8 +3,8 @@ package suwayomi.tachidesk.graphql.queries
import suwayomi.tachidesk.global.impl.AppUpdate
import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
import suwayomi.tachidesk.server.BuildConfig
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.generated.BuildConfig
import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.server.util.WebInterfaceManager
import java.util.concurrent.CompletableFuture
@@ -17,7 +17,7 @@ class InfoQuery {
val buildType: String,
val buildTime: Long,
val github: String,
val discord: String
val discord: String,
)
fun about(): AboutPayload {
@@ -28,7 +28,7 @@ class InfoQuery {
BuildConfig.BUILD_TYPE,
BuildConfig.BUILD_TIME,
BuildConfig.GITHUB,
BuildConfig.DISCORD
BuildConfig.DISCORD,
)
}
@@ -36,7 +36,7 @@ class InfoQuery {
/** [channel] mirrors [suwayomi.tachidesk.server.BuildConfig.BUILD_TYPE] */
val channel: String,
val tag: String,
val url: String
val url: String,
)
fun checkForServerUpdates(): CompletableFuture<List<CheckForServerUpdatesPayload>> {
@@ -45,7 +45,7 @@ class InfoQuery {
CheckForServerUpdatesPayload(
channel = it.channel,
tag = it.tag,
url = it.url
url = it.url,
)
}
}
@@ -57,7 +57,7 @@ class InfoQuery {
WebUIUpdateInfo(
channel = serverConfig.webUIChannel.value,
tag = version,
updateAvailable
updateAvailable,
)
}
}
@@ -49,7 +49,10 @@ import suwayomi.tachidesk.manga.model.table.MangaTable
import java.util.concurrent.CompletableFuture
class MangaQuery {
fun manga(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture<MangaType> {
fun manga(
dataFetchingEnvironment: DataFetchingEnvironment,
id: Int,
): CompletableFuture<MangaType> {
return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id)
}
@@ -57,7 +60,8 @@ class MangaQuery {
ID(MangaTable.id),
TITLE(MangaTable.title),
IN_LIBRARY_AT(MangaTable.inLibraryAt),
LAST_FETCHED_AT(MangaTable.lastFetchedAt);
LAST_FETCHED_AT(MangaTable.lastFetchedAt),
;
override fun greater(cursor: Cursor): Op<Boolean> {
return when (this) {
@@ -78,12 +82,13 @@ class MangaQuery {
}
override fun asCursor(type: MangaType): Cursor {
val value = when (this) {
ID -> type.id.toString()
TITLE -> type.id.toString() + "-" + type.title
IN_LIBRARY_AT -> type.id.toString() + "-" + type.inLibraryAt.toString()
LAST_FETCHED_AT -> type.id.toString() + "-" + type.lastFetchedAt.toString()
}
val value =
when (this) {
ID -> type.id.toString()
TITLE -> type.id.toString() + "-" + type.title
IN_LIBRARY_AT -> type.id.toString() + "-" + type.inLibraryAt.toString()
LAST_FETCHED_AT -> type.id.toString() + "-" + type.lastFetchedAt.toString()
}
return Cursor(value)
}
}
@@ -104,7 +109,7 @@ class MangaQuery {
val inLibraryAt: Long? = null,
val realUrl: String? = null,
val lastFetchedAt: Long? = null,
val chaptersLastFetchedAt: Long? = null
val chaptersLastFetchedAt: Long? = null,
) : HasGetOp {
override fun getOp(): Op<Boolean>? {
val opAnd = OpAnd()
@@ -140,21 +145,21 @@ class MangaQuery {
override val lessThan: MangaStatus? = null,
override val lessThanOrEqualTo: MangaStatus? = null,
override val greaterThan: MangaStatus? = null,
override val greaterThanOrEqualTo: MangaStatus? = null
override val greaterThanOrEqualTo: MangaStatus? = null,
) : ComparableScalarFilter<MangaStatus> {
fun asIntFilter() = IntFilter(
equalTo = equalTo?.value,
notEqualTo = notEqualTo?.value,
distinctFrom = distinctFrom?.value,
notDistinctFrom = notDistinctFrom?.value,
`in` = `in`?.map { it.value },
notIn = notIn?.map { it.value },
lessThan = lessThan?.value,
lessThanOrEqualTo = lessThanOrEqualTo?.value,
greaterThan = greaterThan?.value,
greaterThanOrEqualTo = greaterThanOrEqualTo?.value
)
fun asIntFilter() =
IntFilter(
equalTo = equalTo?.value,
notEqualTo = notEqualTo?.value,
distinctFrom = distinctFrom?.value,
notDistinctFrom = notDistinctFrom?.value,
`in` = `in`?.map { it.value },
notIn = notIn?.map { it.value },
lessThan = lessThan?.value,
lessThanOrEqualTo = lessThanOrEqualTo?.value,
greaterThan = greaterThan?.value,
greaterThanOrEqualTo = greaterThanOrEqualTo?.value,
)
}
data class MangaFilter(
@@ -176,7 +181,7 @@ class MangaQuery {
val chaptersLastFetchedAt: LongFilter? = null,
override val and: List<MangaFilter>? = null,
override val or: List<MangaFilter>? = null,
override val not: MangaFilter? = null
override val not: MangaFilter? = null,
) : Filter<MangaFilter> {
override fun getOpList(): List<Op<Boolean>> {
return listOfNotNull(
@@ -194,7 +199,7 @@ class MangaQuery {
andFilterWithCompare(MangaTable.inLibraryAt, inLibraryAt),
andFilterWithCompareString(MangaTable.realUrl, realUrl),
andFilterWithCompare(MangaTable.lastFetchedAt, lastFetchedAt),
andFilterWithCompare(MangaTable.chaptersLastFetchedAt, chaptersLastFetchedAt)
andFilterWithCompare(MangaTable.chaptersLastFetchedAt, chaptersLastFetchedAt),
)
}
}
@@ -208,55 +213,56 @@ class MangaQuery {
after: Cursor? = null,
first: Int? = null,
last: Int? = null,
offset: Int? = null
offset: Int? = null,
): MangaNodeList {
val queryResults = transaction {
val res = MangaTable.selectAll()
val queryResults =
transaction {
val res = MangaTable.selectAll()
res.applyOps(condition, filter)
res.applyOps(condition, filter)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: MangaTable.id
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: MangaTable.id
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy == MangaOrderBy.ID || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
MangaTable.id to SortOrder.ASC
)
}
}
val total = res.count()
val firstResult = res.firstOrNull()?.get(MangaTable.id)?.value
val lastResult = res.lastOrNull()?.get(MangaTable.id)?.value
if (after != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: MangaOrderBy.ID).less(after)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: MangaOrderBy.ID).greater(after)
if (orderBy == MangaOrderBy.ID || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
MangaTable.id to SortOrder.ASC,
)
}
}
} else if (before != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: MangaOrderBy.ID).greater(before)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: MangaOrderBy.ID).less(before)
val total = res.count()
val firstResult = res.firstOrNull()?.get(MangaTable.id)?.value
val lastResult = res.lastOrNull()?.get(MangaTable.id)?.value
if (after != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: MangaOrderBy.ID).less(after)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: MangaOrderBy.ID).greater(after)
}
}
} else if (before != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: MangaOrderBy.ID).greater(before)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: MangaOrderBy.ID).less(before)
}
}
}
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
QueryResults(total, firstResult, lastResult, res.toList())
}
QueryResults(total, firstResult, lastResult, res.toList())
}
val getAsCursor: (MangaType) -> Cursor = (orderBy ?: MangaOrderBy.ID)::asCursor
@@ -271,24 +277,25 @@ class MangaQuery {
resultsAsType.firstOrNull()?.let {
MangaNodeList.MangaEdge(
getAsCursor(it),
it
it,
)
},
resultsAsType.lastOrNull()?.let {
MangaNodeList.MangaEdge(
getAsCursor(it),
it
it,
)
}
},
)
},
pageInfo = PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) }
),
totalCount = queryResults.total.toInt()
pageInfo =
PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) },
),
totalCount = queryResults.total.toInt(),
)
}
}
@@ -42,13 +42,17 @@ import suwayomi.tachidesk.graphql.types.GlobalMetaType
import java.util.concurrent.CompletableFuture
class MetaQuery {
fun meta(dataFetchingEnvironment: DataFetchingEnvironment, key: String): CompletableFuture<GlobalMetaType> {
fun meta(
dataFetchingEnvironment: DataFetchingEnvironment,
key: String,
): CompletableFuture<GlobalMetaType> {
return dataFetchingEnvironment.getValueFromDataLoader("GlobalMetaDataLoader", key)
}
enum class MetaOrderBy(override val column: Column<out Comparable<*>>) : OrderBy<GlobalMetaType> {
KEY(GlobalMetaTable.key),
VALUE(GlobalMetaTable.value);
VALUE(GlobalMetaTable.value),
;
override fun greater(cursor: Cursor): Op<Boolean> {
return when (this) {
@@ -65,17 +69,18 @@ class MetaQuery {
}
override fun asCursor(type: GlobalMetaType): Cursor {
val value = when (this) {
KEY -> type.key
VALUE -> type.key + "\\-" + type.value
}
val value =
when (this) {
KEY -> type.key
VALUE -> type.key + "\\-" + type.value
}
return Cursor(value)
}
}
data class MetaCondition(
val key: String? = null,
val value: String? = null
val value: String? = null,
) : HasGetOp {
override fun getOp(): Op<Boolean>? {
val opAnd = OpAnd()
@@ -91,12 +96,12 @@ class MetaQuery {
val value: StringFilter? = null,
override val and: List<MetaFilter>? = null,
override val or: List<MetaFilter>? = null,
override val not: MetaFilter? = null
override val not: MetaFilter? = null,
) : Filter<MetaFilter> {
override fun getOpList(): List<Op<Boolean>> {
return listOfNotNull(
andFilterWithCompareString(GlobalMetaTable.key, key),
andFilterWithCompareString(GlobalMetaTable.value, value)
andFilterWithCompareString(GlobalMetaTable.value, value),
)
}
}
@@ -110,55 +115,56 @@ class MetaQuery {
after: Cursor? = null,
first: Int? = null,
last: Int? = null,
offset: Int? = null
offset: Int? = null,
): GlobalMetaNodeList {
val queryResults = transaction {
val res = GlobalMetaTable.selectAll()
val queryResults =
transaction {
val res = GlobalMetaTable.selectAll()
res.applyOps(condition, filter)
res.applyOps(condition, filter)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: GlobalMetaTable.key
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: GlobalMetaTable.key
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy == MetaOrderBy.KEY || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
GlobalMetaTable.key to SortOrder.ASC
)
}
}
val total = res.count()
val firstResult = res.firstOrNull()?.get(GlobalMetaTable.key)
val lastResult = res.lastOrNull()?.get(GlobalMetaTable.key)
if (after != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: MetaOrderBy.KEY).less(after)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: MetaOrderBy.KEY).greater(after)
if (orderBy == MetaOrderBy.KEY || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
GlobalMetaTable.key to SortOrder.ASC,
)
}
}
} else if (before != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: MetaOrderBy.KEY).greater(before)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: MetaOrderBy.KEY).less(before)
val total = res.count()
val firstResult = res.firstOrNull()?.get(GlobalMetaTable.key)
val lastResult = res.lastOrNull()?.get(GlobalMetaTable.key)
if (after != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: MetaOrderBy.KEY).less(after)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: MetaOrderBy.KEY).greater(after)
}
}
} else if (before != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: MetaOrderBy.KEY).greater(before)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: MetaOrderBy.KEY).less(before)
}
}
}
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
QueryResults(total, firstResult, lastResult, res.toList())
}
QueryResults(total, firstResult, lastResult, res.toList())
}
val getAsCursor: (GlobalMetaType) -> Cursor = (orderBy ?: MetaOrderBy.KEY)::asCursor
@@ -173,24 +179,25 @@ class MetaQuery {
resultsAsType.firstOrNull()?.let {
GlobalMetaNodeList.MetaEdge(
getAsCursor(it),
it
it,
)
},
resultsAsType.lastOrNull()?.let {
GlobalMetaNodeList.MetaEdge(
getAsCursor(it),
it
it,
)
}
},
)
},
pageInfo = PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.key,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.key,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) }
),
totalCount = queryResults.total.toInt()
pageInfo =
PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.key,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.key,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) },
),
totalCount = queryResults.total.toInt(),
)
}
}
@@ -46,14 +46,18 @@ import suwayomi.tachidesk.manga.model.table.SourceTable
import java.util.concurrent.CompletableFuture
class SourceQuery {
fun source(dataFetchingEnvironment: DataFetchingEnvironment, id: Long): CompletableFuture<SourceType> {
fun source(
dataFetchingEnvironment: DataFetchingEnvironment,
id: Long,
): CompletableFuture<SourceType> {
return dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id)
}
enum class SourceOrderBy(override val column: Column<out Comparable<*>>) : OrderBy<SourceType> {
ID(SourceTable.id),
NAME(SourceTable.name),
LANG(SourceTable.lang);
LANG(SourceTable.lang),
;
override fun greater(cursor: Cursor): Op<Boolean> {
return when (this) {
@@ -72,11 +76,12 @@ class SourceQuery {
}
override fun asCursor(type: SourceType): Cursor {
val value = when (this) {
ID -> type.id.toString()
NAME -> type.id.toString() + "-" + type.name
LANG -> type.id.toString() + "-" + type.lang
}
val value =
when (this) {
ID -> type.id.toString()
NAME -> type.id.toString() + "-" + type.name
LANG -> type.id.toString() + "-" + type.lang
}
return Cursor(value)
}
}
@@ -85,7 +90,7 @@ class SourceQuery {
val id: Long? = null,
val name: String? = null,
val lang: String? = null,
val isNsfw: Boolean? = null
val isNsfw: Boolean? = null,
) : HasGetOp {
override fun getOp(): Op<Boolean>? {
val opAnd = OpAnd()
@@ -105,14 +110,14 @@ class SourceQuery {
val isNsfw: BooleanFilter? = null,
override val and: List<SourceFilter>? = null,
override val or: List<SourceFilter>? = null,
override val not: SourceFilter? = null
override val not: SourceFilter? = null,
) : Filter<SourceFilter> {
override fun getOpList(): List<Op<Boolean>> {
return listOfNotNull(
andFilterWithCompareEntity(SourceTable.id, id),
andFilterWithCompareString(SourceTable.name, name),
andFilterWithCompareString(SourceTable.lang, lang),
andFilterWithCompare(SourceTable.isNsfw, isNsfw)
andFilterWithCompare(SourceTable.isNsfw, isNsfw),
)
}
}
@@ -126,57 +131,58 @@ class SourceQuery {
after: Cursor? = null,
first: Int? = null,
last: Int? = null,
offset: Int? = null
offset: Int? = null,
): SourceNodeList {
val (queryResults, resultsAsType) = transaction {
val res = SourceTable.selectAll()
val (queryResults, resultsAsType) =
transaction {
val res = SourceTable.selectAll()
res.applyOps(condition, filter)
res.applyOps(condition, filter)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: SourceTable.id
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy != null || (last != null || before != null)) {
val orderByColumn = orderBy?.column ?: SourceTable.id
val orderType = orderByType.maybeSwap(last ?: before)
if (orderBy == SourceOrderBy.ID || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
SourceTable.id to SortOrder.ASC
)
}
}
val total = res.count()
val firstResult = res.firstOrNull()?.get(SourceTable.id)?.value
val lastResult = res.lastOrNull()?.get(SourceTable.id)?.value
if (after != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: SourceOrderBy.ID).less(after)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: SourceOrderBy.ID).greater(after)
if (orderBy == SourceOrderBy.ID || orderBy == null) {
res.orderBy(orderByColumn to orderType)
} else {
res.orderBy(
orderByColumn to orderType,
SourceTable.id to SortOrder.ASC,
)
}
}
} else if (before != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: SourceOrderBy.ID).greater(before)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: SourceOrderBy.ID).less(before)
val total = res.count()
val firstResult = res.firstOrNull()?.get(SourceTable.id)?.value
val lastResult = res.lastOrNull()?.get(SourceTable.id)?.value
if (after != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: SourceOrderBy.ID).less(after)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: SourceOrderBy.ID).greater(after)
}
}
} else if (before != null) {
res.andWhere {
when (orderByType) {
DESC, DESC_NULLS_FIRST, DESC_NULLS_LAST -> (orderBy ?: SourceOrderBy.ID).greater(before)
null, ASC, ASC_NULLS_FIRST, ASC_NULLS_LAST -> (orderBy ?: SourceOrderBy.ID).less(before)
}
}
}
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
if (first != null) {
res.limit(first, offset?.toLong() ?: 0)
} else if (last != null) {
res.limit(last)
}
QueryResults(total, firstResult, lastResult, res.toList()).let {
it to it.results.mapNotNull { SourceType(it) }
QueryResults(total, firstResult, lastResult, res.toList()).let {
it to it.results.mapNotNull { SourceType(it) }
}
}
}
val getAsCursor: (SourceType) -> Cursor = (orderBy ?: SourceOrderBy.ID)::asCursor
@@ -189,24 +195,25 @@ class SourceQuery {
resultsAsType.firstOrNull()?.let {
SourceNodeList.SourceEdge(
getAsCursor(it),
it
it,
)
},
resultsAsType.lastOrNull()?.let {
SourceNodeList.SourceEdge(
getAsCursor(it),
it
it,
)
}
},
)
},
pageInfo = PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) }
),
totalCount = queryResults.total.toInt()
pageInfo =
PageInfo(
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id,
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id,
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) },
),
totalCount = queryResults.total.toInt(),
)
}
}
@@ -17,7 +17,11 @@ import org.jetbrains.exposed.sql.or
import org.jetbrains.exposed.sql.stringParam
import org.jetbrains.exposed.sql.upperCase
class ILikeEscapeOp(expr1: Expression<*>, expr2: Expression<*>, like: Boolean, val escapeChar: Char?) : ComparisonOp(expr1, expr2, if (like) "ILIKE" else "NOT ILIKE") {
class ILikeEscapeOp(expr1: Expression<*>, expr2: Expression<*>, like: Boolean, val escapeChar: Char?) : ComparisonOp(
expr1,
expr2,
if (like) "ILIKE" else "NOT ILIKE",
) {
override fun toQueryBuilder(queryBuilder: QueryBuilder) {
super.toQueryBuilder(queryBuilder)
if (escapeChar != null) {
@@ -29,43 +33,93 @@ class ILikeEscapeOp(expr1: Expression<*>, expr2: Expression<*>, like: Boolean, v
}
companion object {
fun <T : String?> iLike(expression: Expression<T>, pattern: String): ILikeEscapeOp = iLike(expression, LikePattern(pattern))
fun <T : String?> iNotLike(expression: Expression<T>, pattern: String): ILikeEscapeOp = iNotLike(expression, LikePattern(pattern))
fun <T : String?> iLike(expression: Expression<T>, pattern: LikePattern): ILikeEscapeOp = ILikeEscapeOp(expression, stringParam(pattern.pattern), true, pattern.escapeChar)
fun <T : String?> iNotLike(expression: Expression<T>, pattern: LikePattern): ILikeEscapeOp = ILikeEscapeOp(expression, stringParam(pattern.pattern), false, pattern.escapeChar)
fun <T : String?> iLike(
expression: Expression<T>,
pattern: String,
): ILikeEscapeOp = iLike(expression, LikePattern(pattern))
fun <T : String?> iNotLike(
expression: Expression<T>,
pattern: String,
): ILikeEscapeOp = iNotLike(expression, LikePattern(pattern))
fun <T : String?> iLike(
expression: Expression<T>,
pattern: LikePattern,
): ILikeEscapeOp =
ILikeEscapeOp(
expression,
stringParam(pattern.pattern),
true,
pattern.escapeChar,
)
fun <T : String?> iNotLike(
expression: Expression<T>,
pattern: LikePattern,
): ILikeEscapeOp =
ILikeEscapeOp(
expression,
stringParam(pattern.pattern),
false,
pattern.escapeChar,
)
}
}
class DistinctFromOp(expr1: Expression<*>, expr2: Expression<*>, not: Boolean) : ComparisonOp(expr1, expr2, if (not) "IS NOT DISTINCT FROM" else "IS DISTINCT FROM") {
class DistinctFromOp(expr1: Expression<*>, expr2: Expression<*>, not: Boolean) : ComparisonOp(
expr1,
expr2,
if (not) "IS NOT DISTINCT FROM" else "IS DISTINCT FROM",
) {
companion object {
fun <T> distinctFrom(expression: ExpressionWithColumnType<T>, t: T): DistinctFromOp = DistinctFromOp(
expression,
with(SqlExpressionBuilder) {
expression.wrap(t)
},
false
)
fun <T> notDistinctFrom(expression: ExpressionWithColumnType<T>, t: T): DistinctFromOp = DistinctFromOp(
expression,
with(SqlExpressionBuilder) {
expression.wrap(t)
},
true
)
fun <T : Comparable<T>> distinctFrom(expression: ExpressionWithColumnType<EntityID<T>>, t: T): DistinctFromOp = DistinctFromOp(
expression,
with(SqlExpressionBuilder) {
expression.wrap(t)
},
false
)
fun <T : Comparable<T>> notDistinctFrom(expression: ExpressionWithColumnType<EntityID<T>>, t: T): DistinctFromOp = DistinctFromOp(
expression,
with(SqlExpressionBuilder) {
expression.wrap(t)
},
true
)
fun <T> distinctFrom(
expression: ExpressionWithColumnType<T>,
t: T,
): DistinctFromOp =
DistinctFromOp(
expression,
with(SqlExpressionBuilder) {
expression.wrap(t)
},
false,
)
fun <T> notDistinctFrom(
expression: ExpressionWithColumnType<T>,
t: T,
): DistinctFromOp =
DistinctFromOp(
expression,
with(SqlExpressionBuilder) {
expression.wrap(t)
},
true,
)
fun <T : Comparable<T>> distinctFrom(
expression: ExpressionWithColumnType<EntityID<T>>,
t: T,
): DistinctFromOp =
DistinctFromOp(
expression,
with(SqlExpressionBuilder) {
expression.wrap(t)
},
false,
)
fun <T : Comparable<T>> notDistinctFrom(
expression: ExpressionWithColumnType<EntityID<T>>,
t: T,
): DistinctFromOp =
DistinctFromOp(
expression,
with(SqlExpressionBuilder) {
expression.wrap(t)
},
true,
)
}
}
@@ -88,9 +142,10 @@ interface Filter<T : Filter<T>> : HasGetOp {
override fun getOp(): Op<Boolean>? {
var op: Op<Boolean>? = null
fun newOp(
otherOp: Op<Boolean>?,
operator: (Op<Boolean>, Op<Boolean>) -> Op<Boolean>
operator: (Op<Boolean>, Op<Boolean>) -> Op<Boolean>,
) {
when {
op == null && otherOp == null -> Unit
@@ -99,9 +154,11 @@ interface Filter<T : Filter<T>> : HasGetOp {
op != null && otherOp != null -> op = operator(op!!, otherOp)
}
}
fun andOp(andOp: Op<Boolean>?) {
newOp(andOp, Op<Boolean>::and)
}
fun orOp(orOp: Op<Boolean>?) {
newOp(orOp, Op<Boolean>::or)
}
@@ -127,6 +184,8 @@ interface ScalarFilter<T> {
val notEqualTo: T?
val distinctFrom: T?
val notDistinctFrom: T?
@Suppress("ktlint:standard:property-naming")
val `in`: List<T>?
val notIn: List<T>?
}
@@ -155,7 +214,7 @@ data class LongFilter(
override val lessThan: Long? = null,
override val lessThanOrEqualTo: Long? = null,
override val greaterThan: Long? = null,
override val greaterThanOrEqualTo: Long? = null
override val greaterThanOrEqualTo: Long? = null,
) : ComparableScalarFilter<Long>
data class BooleanFilter(
@@ -169,7 +228,7 @@ data class BooleanFilter(
override val lessThan: Boolean? = null,
override val lessThanOrEqualTo: Boolean? = null,
override val greaterThan: Boolean? = null,
override val greaterThanOrEqualTo: Boolean? = null
override val greaterThanOrEqualTo: Boolean? = null,
) : ComparableScalarFilter<Boolean>
data class IntFilter(
@@ -183,7 +242,7 @@ data class IntFilter(
override val lessThan: Int? = null,
override val lessThanOrEqualTo: Int? = null,
override val greaterThan: Int? = null,
override val greaterThanOrEqualTo: Int? = null
override val greaterThanOrEqualTo: Int? = null,
) : ComparableScalarFilter<Int>
data class FloatFilter(
@@ -197,7 +256,7 @@ data class FloatFilter(
override val lessThan: Float? = null,
override val lessThanOrEqualTo: Float? = null,
override val greaterThan: Float? = null,
override val greaterThanOrEqualTo: Float? = null
override val greaterThanOrEqualTo: Float? = null,
) : ComparableScalarFilter<Float>
data class StringFilter(
@@ -235,7 +294,7 @@ data class StringFilter(
val lessThanInsensitive: String? = null,
val lessThanOrEqualToInsensitive: String? = null,
val greaterThanInsensitive: String? = null,
val greaterThanOrEqualToInsensitive: String? = null
val greaterThanOrEqualToInsensitive: String? = null,
) : ComparableScalarFilter<String>
data class StringListFilter(
@@ -251,13 +310,13 @@ data class StringListFilter(
override val hasNone: List<String>? = null,
val hasAnyInsensitive: List<String>? = null,
val hasAllInsensitive: List<String>? = null,
val hasNoneInsensitive: List<String>? = null
val hasNoneInsensitive: List<String>? = null,
) : ListScalarFilter<String, List<String>>
@Suppress("UNCHECKED_CAST")
fun <T : String, S : T?> andFilterWithCompareString(
column: Column<S>,
filter: StringFilter?
filter: StringFilter?,
): Op<Boolean>? {
filter ?: return null
val opAnd = OpAnd()
@@ -314,19 +373,29 @@ fun <T : String, S : T?> andFilterWithCompareString(
}
class OpAnd(var op: Op<Boolean>? = null) {
fun <T> andWhere(value: T?, andPart: SqlExpressionBuilder.(T & Any) -> Op<Boolean>) {
fun <T> andWhere(
value: T?,
andPart: SqlExpressionBuilder.(T & Any) -> Op<Boolean>,
) {
value ?: return
val expr = Op.build { andPart(value) }
op = if (op == null) expr else (op!! and expr)
}
fun <T> eq(value: T?, column: Column<T>) = andWhere(value) { column eq it }
fun <T : Comparable<T>> eq(value: T?, column: Column<EntityID<T>>) = andWhere(value) { column eq it }
fun <T> eq(
value: T?,
column: Column<T>,
) = andWhere(value) { column eq it }
fun <T : Comparable<T>> eq(
value: T?,
column: Column<EntityID<T>>,
) = andWhere(value) { column eq it }
}
fun <T : Comparable<T>> andFilterWithCompare(
column: Column<T>,
filter: ComparableScalarFilter<T>?
filter: ComparableScalarFilter<T>?,
): Op<Boolean>? {
filter ?: return null
val opAnd = OpAnd(andFilter(column, filter))
@@ -341,7 +410,7 @@ fun <T : Comparable<T>> andFilterWithCompare(
fun <T : Comparable<T>> andFilterWithCompareEntity(
column: Column<EntityID<T>>,
filter: ComparableScalarFilter<T>?
filter: ComparableScalarFilter<T>?,
): Op<Boolean>? {
filter ?: return null
val opAnd = OpAnd(andFilterEntity(column, filter))
@@ -356,7 +425,7 @@ fun <T : Comparable<T>> andFilterWithCompareEntity(
fun <T : Comparable<T>> andFilter(
column: Column<T>,
filter: ScalarFilter<T>?
filter: ScalarFilter<T>?,
): Op<Boolean>? {
filter ?: return null
val opAnd = OpAnd()
@@ -377,7 +446,7 @@ fun <T : Comparable<T>> andFilter(
fun <T : Comparable<T>> andFilterEntity(
column: Column<EntityID<T>>,
filter: ScalarFilter<T>?
filter: ScalarFilter<T>?,
): Op<Boolean>? {
filter ?: return null
val opAnd = OpAnd()
@@ -17,36 +17,39 @@ import io.javalin.plugin.json.jsonMapper
import java.io.IOException
class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE", "UNCHECKED_CAST")
override suspend fun parseRequest(context: Context): GraphQLServerRequest? {
return try {
val formParam = context.formParam("operations")
?: return context.bodyAsClass(GraphQLServerRequest::class.java)
val formParam =
context.formParam("operations")
?: return context.bodyAsClass(GraphQLServerRequest::class.java)
val request = context.jsonMapper().fromJsonString(
formParam,
GraphQLServerRequest::class.java
)
val map = context.formParam("map")?.let {
val request =
context.jsonMapper().fromJsonString(
it,
Map::class.java as Class<Map<String, List<String>>>
formParam,
GraphQLServerRequest::class.java,
)
}.orEmpty()
val mapItems = map.flatMap { (key, variables) ->
val file = context.uploadedFile(key)
variables.map { fullVariable ->
val variable = fullVariable.removePrefix("variables.").substringBefore('.')
val listIndex = fullVariable.substringAfterLast('.').toIntOrNull()
MapItem(
variable,
listIndex,
file
val map =
context.formParam("map")?.let {
context.jsonMapper().fromJsonString(
it,
Map::class.java as Class<Map<String, List<String>>>,
)
}
}.groupBy { it.variable }
}.orEmpty()
val mapItems =
map.flatMap { (key, variables) ->
val file = context.uploadedFile(key)
variables.map { fullVariable ->
val variable = fullVariable.removePrefix("variables.").substringBefore('.')
val listIndex = fullVariable.substringAfterLast('.').toIntOrNull()
MapItem(
variable,
listIndex,
file,
)
}
}.groupBy { it.variable }
when (request) {
is GraphQLRequest -> {
@@ -54,11 +57,12 @@ class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
}
is GraphQLBatchRequest -> {
request.copy(
requests = request.requests.map {
it.copy(
variables = it.variables?.modifyFiles(mapItems)
)
}
requests =
request.requests.map {
it.copy(
variables = it.variables?.modifyFiles(mapItems),
)
},
)
}
}
@@ -70,7 +74,7 @@ class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
data class MapItem(
val variable: String,
val listIndex: Int?,
val file: UploadedFile?
val file: UploadedFile?,
)
/**
@@ -52,7 +52,7 @@ class TachideskDataLoaderRegistryFactory {
SourceDataLoader(),
SourcesForExtensionDataLoader(),
ExtensionDataLoader(),
ExtensionForSourceDataLoader()
ExtensionForSourceDataLoader(),
)
}
}
@@ -37,5 +37,4 @@ class TachideskGraphQLContextFactory : GraphQLContextFactory<GraphQLContext, Con
* Create a [GraphQLContext] from [this] map
* @return a new [GraphQLContext]
*/
fun Map<*, Any?>.toGraphQLContext(): graphql.GraphQLContext =
graphql.GraphQLContext.of(this)
fun Map<*, Any?>.toGraphQLContext(): graphql.GraphQLContext = graphql.GraphQLContext.of(this)
@@ -46,49 +46,55 @@ import kotlin.reflect.KClass
import kotlin.reflect.KType
class CustomSchemaGeneratorHooks : FlowSubscriptionSchemaGeneratorHooks() {
override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) {
Long::class -> GraphQLLongAsString // encode to string for JS
Cursor::class -> GraphQLCursor
UploadedFile::class -> GraphQLUpload
else -> super.willGenerateGraphQLType(type)
}
override fun willGenerateGraphQLType(type: KType): GraphQLType? =
when (type.classifier as? KClass<*>) {
Long::class -> GraphQLLongAsString // encode to string for JS
Cursor::class -> GraphQLCursor
UploadedFile::class -> GraphQLUpload
else -> super.willGenerateGraphQLType(type)
}
}
val schema = toSchema(
config = SchemaGeneratorConfig(
supportedPackages = listOf("suwayomi.tachidesk.graphql"),
introspectionEnabled = true,
hooks = CustomSchemaGeneratorHooks()
),
queries = listOf(
TopLevelObject(BackupQuery()),
TopLevelObject(CategoryQuery()),
TopLevelObject(ChapterQuery()),
TopLevelObject(DownloadQuery()),
TopLevelObject(ExtensionQuery()),
TopLevelObject(InfoQuery()),
TopLevelObject(MangaQuery()),
TopLevelObject(MetaQuery()),
TopLevelObject(SettingsQuery()),
TopLevelObject(SourceQuery()),
TopLevelObject(UpdateQuery())
),
mutations = listOf(
TopLevelObject(BackupMutation()),
TopLevelObject(CategoryMutation()),
TopLevelObject(ChapterMutation()),
TopLevelObject(DownloadMutation()),
TopLevelObject(ExtensionMutation()),
TopLevelObject(InfoMutation()),
TopLevelObject(MangaMutation()),
TopLevelObject(MetaMutation()),
TopLevelObject(SettingsMutation()),
TopLevelObject(SourceMutation()),
TopLevelObject(UpdateMutation())
),
subscriptions = listOf(
TopLevelObject(DownloadSubscription()),
TopLevelObject(InfoSubscription()),
TopLevelObject(UpdateSubscription())
val schema =
toSchema(
config =
SchemaGeneratorConfig(
supportedPackages = listOf("suwayomi.tachidesk.graphql"),
introspectionEnabled = true,
hooks = CustomSchemaGeneratorHooks(),
),
queries =
listOf(
TopLevelObject(BackupQuery()),
TopLevelObject(CategoryQuery()),
TopLevelObject(ChapterQuery()),
TopLevelObject(DownloadQuery()),
TopLevelObject(ExtensionQuery()),
TopLevelObject(InfoQuery()),
TopLevelObject(MangaQuery()),
TopLevelObject(MetaQuery()),
TopLevelObject(SettingsQuery()),
TopLevelObject(SourceQuery()),
TopLevelObject(UpdateQuery()),
),
mutations =
listOf(
TopLevelObject(BackupMutation()),
TopLevelObject(CategoryMutation()),
TopLevelObject(ChapterMutation()),
TopLevelObject(DownloadMutation()),
TopLevelObject(ExtensionMutation()),
TopLevelObject(InfoMutation()),
TopLevelObject(MangaMutation()),
TopLevelObject(MetaMutation()),
TopLevelObject(SettingsMutation()),
TopLevelObject(SourceMutation()),
TopLevelObject(UpdateMutation()),
),
subscriptions =
listOf(
TopLevelObject(DownloadSubscription()),
TopLevelObject(InfoSubscription()),
TopLevelObject(UpdateSubscription()),
),
)
)
@@ -25,7 +25,7 @@ class TachideskGraphQLServer(
requestParser: JavalinGraphQLRequestParser,
contextFactory: TachideskGraphQLContextFactory,
requestHandler: GraphQLRequestHandler,
subscriptionHandler: GraphQLSubscriptionHandler
subscriptionHandler: GraphQLSubscriptionHandler,
) : GraphQLServer<Context>(requestParser, contextFactory, requestHandler) {
private val objectMapper = jacksonObjectMapper()
private val subscriptionProtocolHandler = ApolloSubscriptionProtocolHandler(contextFactory, subscriptionHandler, objectMapper)
@@ -42,9 +42,10 @@ class TachideskGraphQLServer(
}
companion object {
private fun getGraphQLObject(): GraphQL = GraphQL.newGraphQL(schema)
.subscriptionExecutionStrategy(FlowSubscriptionExecutionStrategy())
.build()
private fun getGraphQLObject(): GraphQL =
GraphQL.newGraphQL(schema)
.subscriptionExecutionStrategy(FlowSubscriptionExecutionStrategy())
.build()
fun create(): TachideskGraphQLServer {
val graphQL = getGraphQLObject()
@@ -22,12 +22,15 @@ object TemporaryFileStorage {
Runtime.getRuntime().addShutdownHook(
thread(start = false) {
folder.deleteRecursively()
}
},
)
}
@OptIn(DelicateCoroutinesApi::class)
fun saveFile(name: String, content: InputStream) {
fun saveFile(
name: String,
content: InputStream,
) {
val file = folder.resolve(name)
content.use { inStream ->
file.outputStream().use {
@@ -14,36 +14,45 @@ import java.util.Locale
data class Cursor(val value: String)
val GraphQLCursor: GraphQLScalarType = GraphQLScalarType.newScalar()
.name("Cursor").description("A location in a connection that can be used for resuming pagination.").coercing(GraphqlCursorCoercing()).build()
val GraphQLCursor: GraphQLScalarType =
GraphQLScalarType.newScalar()
.name(
"Cursor",
).description("A location in a connection that can be used for resuming pagination.").coercing(GraphqlCursorCoercing()).build()
private class GraphqlCursorCoercing : Coercing<Cursor, String> {
private fun toStringImpl(input: Any): String? {
return (input as? Cursor)?.value
}
private fun parseValueImpl(input: Any, locale: Locale): Cursor {
private fun parseValueImpl(
input: Any,
locale: Locale,
): Cursor {
if (input !is String) {
throw CoercingParseValueException(
CoercingUtil.i18nMsg(
locale,
"String.unexpectedRawValueType",
CoercingUtil.typeName(input)
)
CoercingUtil.typeName(input),
),
)
}
return Cursor(input)
}
private fun parseLiteralImpl(input: Any, locale: Locale): Cursor {
private fun parseLiteralImpl(
input: Any,
locale: Locale,
): Cursor {
if (input !is StringValue) {
throw CoercingParseLiteralException(
CoercingUtil.i18nMsg(
locale,
"Scalar.unexpectedAstType",
"StringValue",
CoercingUtil.typeName(input)
)
CoercingUtil.typeName(input),
),
)
}
return Cursor(input.value)
@@ -59,8 +68,8 @@ private class GraphqlCursorCoercing : Coercing<Cursor, String> {
CoercingUtil.i18nMsg(
Locale.getDefault(),
"String.unexpectedRawValueType",
CoercingUtil.typeName(dataFetcherResult)
)
CoercingUtil.typeName(dataFetcherResult),
),
)
}
@@ -68,14 +77,14 @@ private class GraphqlCursorCoercing : Coercing<Cursor, String> {
override fun serialize(
dataFetcherResult: Any,
graphQLContext: GraphQLContext,
locale: Locale
locale: Locale,
): String {
return toStringImpl(dataFetcherResult) ?: throw CoercingSerializeException(
CoercingUtil.i18nMsg(
locale,
"String.unexpectedRawValueType",
CoercingUtil.typeName(dataFetcherResult)
)
CoercingUtil.typeName(dataFetcherResult),
),
)
}
@@ -85,7 +94,11 @@ private class GraphqlCursorCoercing : Coercing<Cursor, String> {
}
@Throws(CoercingParseValueException::class)
override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Cursor {
override fun parseValue(
input: Any,
graphQLContext: GraphQLContext,
locale: Locale,
): Cursor {
return parseValueImpl(input, locale)
}
@@ -99,7 +112,7 @@ private class GraphqlCursorCoercing : Coercing<Cursor, String> {
input: Value<*>,
variables: CoercedVariables,
graphQLContext: GraphQLContext,
locale: Locale
locale: Locale,
): Cursor {
return parseLiteralImpl(input, locale)
}
@@ -112,7 +125,7 @@ private class GraphqlCursorCoercing : Coercing<Cursor, String> {
override fun valueToLiteral(
input: Any,
graphQLContext: GraphQLContext,
locale: Locale
locale: Locale,
): Value<*> {
return valueToLiteralImpl(input)
}
@@ -12,36 +12,43 @@ import graphql.schema.CoercingSerializeException
import graphql.schema.GraphQLScalarType
import java.util.Locale
val GraphQLLongAsString: GraphQLScalarType = GraphQLScalarType.newScalar()
.name("LongString").description("A 64-bit signed integer as a String").coercing(GraphqlLongAsStringCoercing()).build()
val GraphQLLongAsString: GraphQLScalarType =
GraphQLScalarType.newScalar()
.name("LongString").description("A 64-bit signed integer as a String").coercing(GraphqlLongAsStringCoercing()).build()
private class GraphqlLongAsStringCoercing : Coercing<Long, String> {
private fun toStringImpl(input: Any): String {
return input.toString()
}
private fun parseValueImpl(input: Any, locale: Locale): Long {
private fun parseValueImpl(
input: Any,
locale: Locale,
): Long {
if (input !is String) {
throw CoercingParseValueException(
CoercingUtil.i18nMsg(
locale,
"String.unexpectedRawValueType",
CoercingUtil.typeName(input)
)
CoercingUtil.typeName(input),
),
)
}
return input.toLong()
}
private fun parseLiteralImpl(input: Any, locale: Locale): Long {
private fun parseLiteralImpl(
input: Any,
locale: Locale,
): Long {
if (input !is StringValue) {
throw CoercingParseLiteralException(
CoercingUtil.i18nMsg(
locale,
"Scalar.unexpectedAstType",
"StringValue",
CoercingUtil.typeName(input)
)
CoercingUtil.typeName(input),
),
)
}
return input.value.toLong()
@@ -60,7 +67,7 @@ private class GraphqlLongAsStringCoercing : Coercing<Long, String> {
override fun serialize(
dataFetcherResult: Any,
graphQLContext: GraphQLContext,
locale: Locale
locale: Locale,
): String {
return toStringImpl(dataFetcherResult)
}
@@ -71,7 +78,11 @@ private class GraphqlLongAsStringCoercing : Coercing<Long, String> {
}
@Throws(CoercingParseValueException::class)
override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Long {
override fun parseValue(
input: Any,
graphQLContext: GraphQLContext,
locale: Locale,
): Long {
return parseValueImpl(input, locale)
}
@@ -85,7 +96,7 @@ private class GraphqlLongAsStringCoercing : Coercing<Long, String> {
input: Value<*>,
variables: CoercedVariables,
graphQLContext: GraphQLContext,
locale: Locale
locale: Locale,
): Long {
return parseLiteralImpl(input, locale)
}
@@ -98,7 +109,7 @@ private class GraphqlLongAsStringCoercing : Coercing<Long, String> {
override fun valueToLiteral(
input: Any,
graphQLContext: GraphQLContext,
locale: Locale
locale: Locale,
): Value<*> {
return valueToLiteralImpl(input)
}
@@ -26,7 +26,7 @@ data class PageInfo(
@GraphQLDescription("When paginating backwards, the cursor to continue.")
val startCursor: Cursor?,
@GraphQLDescription("When paginating forwards, the cursor to continue.")
val endCursor: Cursor?
val endCursor: Cursor?,
)
abstract class Edge {
@@ -41,7 +41,7 @@ fun <T : Comparable<T>> greaterNotUnique(
column: Column<T>,
idColumn: Column<EntityID<Int>>,
cursor: Cursor,
toValue: (String) -> T
toValue: (String) -> T,
): Op<Boolean> {
return greaterNotUniqueImpl(column, idColumn, cursor, String::toInt, toValue)
}
@@ -51,7 +51,7 @@ fun <T : Comparable<T>> greaterNotUnique(
column: Column<T>,
idColumn: Column<EntityID<Long>>,
cursor: Cursor,
toValue: (String) -> T
toValue: (String) -> T,
): Op<Boolean> {
return greaterNotUniqueImpl(column, idColumn, cursor, String::toLong, toValue)
}
@@ -61,7 +61,7 @@ private fun <K : Comparable<K>, V : Comparable<V>> greaterNotUniqueImpl(
idColumn: Column<EntityID<K>>,
cursor: Cursor,
toKey: (String) -> K,
toValue: (String) -> V
toValue: (String) -> V,
): Op<Boolean> {
val id = toKey(cursor.value.substringBefore('-'))
val value = toValue(cursor.value.substringAfter('-'))
@@ -73,7 +73,7 @@ fun <T : Comparable<T>> greaterNotUnique(
column: Column<T>,
idColumn: Column<String>,
cursor: Cursor,
toValue: (String) -> T
toValue: (String) -> T,
): Op<Boolean> {
val id = cursor.value.substringBefore("\\-")
val value = toValue(cursor.value.substringAfter("\\-"))
@@ -85,7 +85,7 @@ fun <T : Comparable<T>> lessNotUnique(
column: Column<T>,
idColumn: Column<EntityID<Int>>,
cursor: Cursor,
toValue: (String) -> T
toValue: (String) -> T,
): Op<Boolean> {
return lessNotUniqueImpl(column, idColumn, cursor, String::toInt, toValue)
}
@@ -95,7 +95,7 @@ fun <T : Comparable<T>> lessNotUnique(
column: Column<T>,
idColumn: Column<EntityID<Long>>,
cursor: Cursor,
toValue: (String) -> T
toValue: (String) -> T,
): Op<Boolean> {
return lessNotUniqueImpl(column, idColumn, cursor, String::toLong, toValue)
}
@@ -105,7 +105,7 @@ private fun <K : Comparable<K>, V : Comparable<V>> lessNotUniqueImpl(
idColumn: Column<EntityID<K>>,
cursor: Cursor,
toKey: (String) -> K,
toValue: (String) -> V
toValue: (String) -> V,
): Op<Boolean> {
val id = toKey(cursor.value.substringBefore('-'))
val value = toValue(cursor.value.substringAfter('-'))
@@ -117,7 +117,7 @@ fun <T : Comparable<T>> lessNotUnique(
column: Column<T>,
idColumn: Column<String>,
cursor: Cursor,
toValue: (String) -> T
toValue: (String) -> T,
): Op<Boolean> {
val id = cursor.value.substringBefore("\\-")
val value = toValue(cursor.value.substringAfter("\\-"))
@@ -9,21 +9,25 @@ import graphql.schema.GraphQLScalarType
import io.javalin.http.UploadedFile
import java.util.Locale
val GraphQLUpload = GraphQLScalarType.newScalar()
.name("Upload")
.description("A file part in a multipart request")
.coercing(GraphqlUploadCoercing())
.build()
val GraphQLUpload =
GraphQLScalarType.newScalar()
.name("Upload")
.description("A file part in a multipart request")
.coercing(GraphqlUploadCoercing())
.build()
private class GraphqlUploadCoercing : Coercing<UploadedFile, Void?> {
private fun parseValueImpl(input: Any, locale: Locale): UploadedFile {
private fun parseValueImpl(
input: Any,
locale: Locale,
): UploadedFile {
if (input !is UploadedFile) {
throw CoercingParseValueException(
CoercingUtil.i18nMsg(
locale,
"String.unexpectedRawValueType",
CoercingUtil.typeName(input)
)
CoercingUtil.typeName(input),
),
)
}
return input
@@ -38,7 +42,7 @@ private class GraphqlUploadCoercing : Coercing<UploadedFile, Void?> {
override fun serialize(
dataFetcherResult: Any,
graphQLContext: GraphQLContext,
locale: Locale
locale: Locale,
): Void? {
throw CoercingSerializeException("Upload is an input-only type")
}
@@ -49,7 +53,11 @@ private class GraphqlUploadCoercing : Coercing<UploadedFile, Void?> {
}
@Throws(CoercingParseValueException::class)
override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): UploadedFile {
override fun parseValue(
input: Any,
graphQLContext: GraphQLContext,
locale: Locale,
): UploadedFile {
return parseValueImpl(input, locale)
}
@@ -45,7 +45,7 @@ import suwayomi.tachidesk.server.serverConfig
class ApolloSubscriptionProtocolHandler(
private val contextFactory: TachideskGraphQLContextFactory,
private val subscriptionHandler: GraphQLSubscriptionHandler,
private val objectMapper: ObjectMapper
private val objectMapper: ObjectMapper,
) {
private val sessionState = ApolloSubscriptionSessionState()
private val logger = KotlinLogging.logger {}
@@ -68,13 +68,13 @@ class ApolloSubscriptionProtocolHandler(
val operationMessage = convertToMessageOrNull(context.message()) ?: return flowOf(basicConnectionErrorMessage)
logger.debug {
"GraphQL subscription client message, sessionId=${context.sessionId} type=${operationMessage.type} operationName=${
getOperationName(operationMessage.payload)
getOperationName(operationMessage.payload)
} ${
if (serverConfig.gqlDebugLogsEnabled.value) {
"operationMessage=$operationMessage"
} else {
""
}
if (serverConfig.gqlDebugLogsEnabled.value) {
"operationMessage=$operationMessage"
} else {
""
}
}"
}
@@ -108,7 +108,7 @@ class ApolloSubscriptionProtocolHandler(
@Suppress("Detekt.TooGenericExceptionCaught")
private fun startSubscription(
operationMessage: SubscriptionOperationMessage,
context: WsContext
context: WsContext,
): Flow<SubscriptionOperationMessage> {
if (operationMessage.id == null) {
logger.error("GraphQL subscription operation id is required")
@@ -149,7 +149,10 @@ class ApolloSubscriptionProtocolHandler(
}
}
private fun onInit(operationMessage: SubscriptionOperationMessage, context: WsContext): Flow<SubscriptionOperationMessage> {
private fun onInit(
operationMessage: SubscriptionOperationMessage,
context: WsContext,
): Flow<SubscriptionOperationMessage> {
saveContext(operationMessage, context)
return flowOf(acknowledgeMessage)
}
@@ -157,7 +160,10 @@ class ApolloSubscriptionProtocolHandler(
/**
* Generate the context and save it for all future messages.
*/
private fun saveContext(operationMessage: SubscriptionOperationMessage, context: WsContext) {
private fun saveContext(
operationMessage: SubscriptionOperationMessage,
context: WsContext,
) {
runBlocking {
val graphQLContext = contextFactory.generateContextMap(context).toGraphQLContext()
sessionState.saveContext(context, graphQLContext)
@@ -169,7 +175,7 @@ class ApolloSubscriptionProtocolHandler(
*/
private fun onComplete(
operationMessage: SubscriptionOperationMessage,
context: WsContext
context: WsContext,
): Flow<SubscriptionOperationMessage> {
return sessionState.completeOperation(operationMessage)
}
@@ -183,7 +189,10 @@ class ApolloSubscriptionProtocolHandler(
return emptyFlow()
}
private fun onUnknownOperation(operationMessage: SubscriptionOperationMessage, context: WsContext): Flow<SubscriptionOperationMessage> {
private fun onUnknownOperation(
operationMessage: SubscriptionOperationMessage,
context: WsContext,
): Flow<SubscriptionOperationMessage> {
logger.error("Unknown subscription operation $operationMessage")
sessionState.completeOperation(operationMessage)
return emptyFlow()
@@ -19,7 +19,6 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
internal class ApolloSubscriptionSessionState {
// Operations are saved by web socket session id, then operation id
internal val activeOperations = ConcurrentHashMap<String, Job>()
@@ -33,21 +32,29 @@ internal class ApolloSubscriptionSessionState {
* This allows us to include some initial state to be used when handling all the messages.
* This will be removed in [terminateSession].
*/
fun saveContext(context: WsContext, graphQLContext: GraphQLContext) {
fun saveContext(
context: WsContext,
graphQLContext: GraphQLContext,
) {
cachedGraphQLContext[context.sessionId] = graphQLContext
}
/**
* Return the graphQL context for this session.
*/
fun getGraphQLContext(context: WsContext): GraphQLContext = cachedGraphQLContext[context.sessionId] ?: emptyMap<Any, Any>().toGraphQLContext()
fun getGraphQLContext(context: WsContext): GraphQLContext =
cachedGraphQLContext[context.sessionId] ?: emptyMap<Any, Any>().toGraphQLContext()
/**
* Save the operation that is sending data to the client.
* This will override values without cancelling the subscription so it is the responsibility of the consumer to cancel.
* These messages will be stopped on [stopOperation].
*/
fun saveOperation(context: WsContext, operationMessage: SubscriptionOperationMessage, subscription: Job) {
fun saveOperation(
context: WsContext,
operationMessage: SubscriptionOperationMessage,
subscription: Job,
) {
val id = operationMessage.id
if (id != null) {
activeOperations[id] = subscription
@@ -78,7 +85,10 @@ internal class ApolloSubscriptionSessionState {
/**
* Terminate the session, cancelling the keep alive messages and all operations active for this session.
*/
fun terminateSession(context: WsContext, code: CloseStatus) {
fun terminateSession(
context: WsContext,
code: CloseStatus,
) {
sessionToOperationId.remove(context.sessionId)?.forEach {
activeOperations[it]?.cancel()
}
@@ -89,6 +99,5 @@ internal class ApolloSubscriptionSessionState {
/**
* Looks up the operation for the client, to check if it already exists
*/
fun doesOperationExist(operationMessage: SubscriptionOperationMessage): Boolean =
activeOperations.containsKey(operationMessage.id)
fun doesOperationExist(operationMessage: SubscriptionOperationMessage): Boolean = activeOperations.containsKey(operationMessage.id)
}
@@ -23,11 +23,11 @@ import kotlinx.coroutines.flow.map
open class GraphQLSubscriptionHandler(
private val graphQL: GraphQL,
private val dataLoaderRegistryFactory: KotlinDataLoaderRegistryFactory? = null
private val dataLoaderRegistryFactory: KotlinDataLoaderRegistryFactory? = null,
) {
open fun executeSubscription(
graphQLRequest: GraphQLRequest,
graphQLContext: GraphQLContext = GraphQLContext.of(emptyMap<Any, Any>())
graphQLContext: GraphQLContext = GraphQLContext.of(emptyMap<Any, Any>()),
): Flow<GraphQLResponse<*>> {
val dataLoaderRegistry = dataLoaderRegistryFactory?.generate()
val input = graphQLRequest.toExecutionInput(dataLoaderRegistry, graphQLContext)
@@ -21,22 +21,22 @@ import com.fasterxml.jackson.annotation.JsonInclude
data class SubscriptionOperationMessage(
val type: String,
val id: String? = null,
val payload: Any? = null
val payload: Any? = null,
) {
enum class CommonMessages(val type: String) {
GQL_PING("ping"),
GQL_PONG("pong"),
GQL_COMPLETE("complete")
GQL_COMPLETE("complete"),
}
enum class ClientMessages(val type: String) {
GQL_CONNECTION_INIT("connection_init"),
GQL_SUBSCRIBE("subscribe")
GQL_SUBSCRIBE("subscribe"),
}
enum class ServerMessages(val type: String) {
GQL_CONNECTION_ACK("connection_ack"),
GQL_NEXT("next"),
GQL_ERROR("error")
GQL_ERROR("error"),
}
}
@@ -13,7 +13,6 @@ import suwayomi.tachidesk.graphql.types.DownloadStatus
import suwayomi.tachidesk.manga.impl.download.DownloadManager
class DownloadSubscription {
fun downloadChanged(): Flow<DownloadStatus> {
return DownloadManager.status.map { downloadStatus ->
DownloadStatus(downloadStatus)
@@ -5,31 +5,34 @@ import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
enum class BackupRestoreState {
IDLE,
RESTORING_CATEGORIES,
RESTORING_MANGA
RESTORING_MANGA,
}
data class BackupRestoreStatus(
val state: BackupRestoreState,
val totalManga: Int,
val mangaProgress: Int
val mangaProgress: Int,
)
fun ProtoBackupImport.BackupRestoreState.toStatus(): BackupRestoreStatus {
return when (this) {
ProtoBackupImport.BackupRestoreState.Idle -> BackupRestoreStatus(
state = BackupRestoreState.IDLE,
totalManga = 0,
mangaProgress = 0
)
is ProtoBackupImport.BackupRestoreState.RestoringCategories -> BackupRestoreStatus(
state = BackupRestoreState.RESTORING_CATEGORIES,
totalManga = totalManga,
mangaProgress = 0
)
is ProtoBackupImport.BackupRestoreState.RestoringManga -> BackupRestoreStatus(
state = BackupRestoreState.RESTORING_MANGA,
totalManga = totalManga,
mangaProgress = current
)
ProtoBackupImport.BackupRestoreState.Idle ->
BackupRestoreStatus(
state = BackupRestoreState.IDLE,
totalManga = 0,
mangaProgress = 0,
)
is ProtoBackupImport.BackupRestoreState.RestoringCategories ->
BackupRestoreStatus(
state = BackupRestoreState.RESTORING_CATEGORIES,
totalManga = totalManga,
mangaProgress = 0,
)
is ProtoBackupImport.BackupRestoreState.RestoringManga ->
BackupRestoreStatus(
state = BackupRestoreState.RESTORING_MANGA,
totalManga = totalManga,
mangaProgress = current,
)
}
}
@@ -24,14 +24,14 @@ class CategoryType(
val order: Int,
val name: String,
val default: Boolean,
val includeInUpdate: IncludeInUpdate
val includeInUpdate: IncludeInUpdate,
) : Node {
constructor(row: ResultRow) : this(
row[CategoryTable.id].value,
row[CategoryTable.order],
row[CategoryTable.name],
row[CategoryTable.isDefault],
IncludeInUpdate.fromValue(row[CategoryTable.includeInUpdate])
IncludeInUpdate.fromValue(row[CategoryTable.includeInUpdate]),
)
fun mangas(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> {
@@ -47,11 +47,11 @@ data class CategoryNodeList(
override val nodes: List<CategoryType>,
override val edges: List<CategoryEdge>,
override val pageInfo: PageInfo,
override val totalCount: Int
override val totalCount: Int,
) : NodeList() {
data class CategoryEdge(
override val cursor: Cursor,
override val node: CategoryType
override val node: CategoryType,
) : Edge()
companion object {
@@ -59,13 +59,14 @@ data class CategoryNodeList(
return CategoryNodeList(
nodes = this,
edges = getEdges(),
pageInfo = PageInfo(
hasNextPage = false,
hasPreviousPage = false,
startCursor = Cursor(0.toString()),
endCursor = Cursor(lastIndex.toString())
),
totalCount = size
pageInfo =
PageInfo(
hasNextPage = false,
hasPreviousPage = false,
startCursor = Cursor(0.toString()),
endCursor = Cursor(lastIndex.toString()),
),
totalCount = size,
)
}
@@ -74,12 +75,12 @@ data class CategoryNodeList(
return listOf(
CategoryEdge(
cursor = Cursor("0"),
node = first()
node = first(),
),
CategoryEdge(
cursor = Cursor(lastIndex.toString()),
node = last()
)
node = last(),
),
)
}
}
@@ -35,7 +35,7 @@ class ChapterType(
val realUrl: String?,
val fetchedAt: Long,
val isDownloaded: Boolean,
val pageCount: Int
val pageCount: Int,
// val chapterCount: Int?,
) : Node {
constructor(row: ResultRow) : this(
@@ -54,7 +54,7 @@ class ChapterType(
row[ChapterTable.realUrl],
row[ChapterTable.fetchedAt],
row[ChapterTable.isDownloaded],
row[ChapterTable.pageCount]
row[ChapterTable.pageCount],
// transaction { ChapterTable.select { manga eq chapterEntry[manga].value }.count().toInt() },
)
@@ -74,7 +74,7 @@ class ChapterType(
dataClass.realUrl,
dataClass.fetchedAt,
dataClass.downloaded,
dataClass.pageCount
dataClass.pageCount,
)
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaType> {
@@ -90,11 +90,11 @@ data class ChapterNodeList(
override val nodes: List<ChapterType>,
override val edges: List<ChapterEdge>,
override val pageInfo: PageInfo,
override val totalCount: Int
override val totalCount: Int,
) : NodeList() {
data class ChapterEdge(
override val cursor: Cursor,
override val node: ChapterType
override val node: ChapterType,
) : Edge()
companion object {
@@ -102,13 +102,14 @@ data class ChapterNodeList(
return ChapterNodeList(
nodes = this,
edges = getEdges(),
pageInfo = PageInfo(
hasNextPage = false,
hasPreviousPage = false,
startCursor = Cursor(0.toString()),
endCursor = Cursor(lastIndex.toString())
),
totalCount = size
pageInfo =
PageInfo(
hasNextPage = false,
hasPreviousPage = false,
startCursor = Cursor(0.toString()),
endCursor = Cursor(lastIndex.toString()),
),
totalCount = size,
)
}
@@ -117,12 +118,12 @@ data class ChapterNodeList(
return listOf(
ChapterEdge(
cursor = Cursor("0"),
node = first()
node = first(),
),
ChapterEdge(
cursor = Cursor(lastIndex.toString()),
node = last()
)
node = last(),
),
)
}
}
@@ -23,14 +23,14 @@ import suwayomi.tachidesk.manga.impl.download.model.DownloadState as OtherDownlo
data class DownloadStatus(
val state: DownloaderState,
val queue: List<DownloadType>
val queue: List<DownloadType>,
) {
constructor(downloadStatus: DownloadStatus) : this(
when (downloadStatus.status) {
Status.Stopped -> DownloaderState.STOPPED
Status.Started -> DownloaderState.STARTED
},
downloadStatus.queue.map { DownloadType(it) }
downloadStatus.queue.map { DownloadType(it) },
)
}
@@ -41,7 +41,7 @@ class DownloadType(
val mangaId: Int,
val state: DownloadState,
val progress: Float,
val tries: Int
val tries: Int,
) : Node {
constructor(downloadChapter: DownloadChapter) : this(
downloadChapter.chapter.id,
@@ -53,7 +53,7 @@ class DownloadType(
OtherDownloadState.Error -> DownloadState.ERROR
},
downloadChapter.progress,
downloadChapter.tries
downloadChapter.tries,
)
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaType> {
@@ -69,23 +69,23 @@ enum class DownloadState {
QUEUED,
DOWNLOADING,
FINISHED,
ERROR
ERROR,
}
enum class DownloaderState {
STARTED,
STOPPED
STOPPED,
}
data class DownloadNodeList(
override val nodes: List<DownloadType>,
override val edges: List<DownloadEdge>,
override val pageInfo: PageInfo,
override val totalCount: Int
override val totalCount: Int,
) : NodeList() {
data class DownloadEdge(
override val cursor: Cursor,
override val node: DownloadType
override val node: DownloadType,
) : Edge()
companion object {
@@ -93,13 +93,14 @@ data class DownloadNodeList(
return DownloadNodeList(
nodes = this,
edges = getEdges(),
pageInfo = PageInfo(
hasNextPage = false,
hasPreviousPage = false,
startCursor = Cursor(0.toString()),
endCursor = Cursor(lastIndex.toString())
),
totalCount = size
pageInfo =
PageInfo(
hasNextPage = false,
hasPreviousPage = false,
startCursor = Cursor(0.toString()),
endCursor = Cursor(lastIndex.toString()),
),
totalCount = size,
)
}
@@ -108,12 +109,12 @@ data class DownloadNodeList(
return listOf(
DownloadEdge(
cursor = Cursor("0"),
node = first()
node = first(),
),
DownloadEdge(
cursor = Cursor(lastIndex.toString()),
node = last()
)
node = last(),
),
)
}
}

Some files were not shown because too many files have changed in this diff Show More