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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

@@ -23,7 +23,7 @@ object GraphQLController {
ctx.future(
future {
server.execute(ctx)
}
},
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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?,
)
/**

View File

@@ -52,7 +52,7 @@ class TachideskDataLoaderRegistryFactory {
SourceDataLoader(),
SourcesForExtensionDataLoader(),
ExtensionDataLoader(),
ExtensionForSourceDataLoader()
ExtensionForSourceDataLoader(),
)
}
}

View File

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

View File

@@ -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()),
),
)
)

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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 {

View File

@@ -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("\\-"))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,17 +22,15 @@ import java.util.concurrent.CompletableFuture
class ExtensionType(
val apkName: String,
val iconUrl: String,
val name: String,
val pkgName: String,
val versionName: String,
val versionCode: Int,
val lang: String,
val isNsfw: Boolean,
val isInstalled: Boolean,
val hasUpdate: Boolean,
val isObsolete: Boolean
val isObsolete: Boolean,
) : Node {
constructor(row: ResultRow) : this(
apkName = row[ExtensionTable.apkName],
@@ -45,7 +43,7 @@ class ExtensionType(
isNsfw = row[ExtensionTable.isNsfw],
isInstalled = row[ExtensionTable.isInstalled],
hasUpdate = row[ExtensionTable.hasUpdate],
isObsolete = row[ExtensionTable.isObsolete]
isObsolete = row[ExtensionTable.isObsolete],
)
fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<SourceNodeList> {
@@ -57,11 +55,11 @@ data class ExtensionNodeList(
override val nodes: List<ExtensionType>,
override val edges: List<ExtensionEdge>,
override val pageInfo: PageInfo,
override val totalCount: Int
override val totalCount: Int,
) : NodeList() {
data class ExtensionEdge(
override val cursor: Cursor,
override val node: ExtensionType
override val node: ExtensionType,
) : Edge()
companion object {
@@ -69,13 +67,14 @@ data class ExtensionNodeList(
return ExtensionNodeList(
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,
)
}
@@ -84,12 +83,12 @@ data class ExtensionNodeList(
return listOf(
ExtensionEdge(
cursor = Cursor("0"),
node = first()
node = first(),
),
ExtensionEdge(
cursor = Cursor(lastIndex.toString()),
node = last()
)
node = last(),
),
)
}
}

View File

@@ -39,7 +39,7 @@ class MangaType(
val inLibraryAt: Long,
val realUrl: String?,
var lastFetchedAt: Long?, // todo
var chaptersLastFetchedAt: Long? // todo
var chaptersLastFetchedAt: Long?, // todo
) : Node {
constructor(row: ResultRow) : this(
row[MangaTable.id].value,
@@ -57,7 +57,7 @@ class MangaType(
row[MangaTable.inLibraryAt],
row[MangaTable.realUrl],
row[MangaTable.lastFetchedAt],
row[MangaTable.chaptersLastFetchedAt]
row[MangaTable.chaptersLastFetchedAt],
)
constructor(dataClass: MangaDataClass) : this(
@@ -76,7 +76,7 @@ class MangaType(
dataClass.inLibraryAt,
dataClass.realUrl,
dataClass.lastFetchedAt,
dataClass.chaptersLastFetchedAt
dataClass.chaptersLastFetchedAt,
)
fun downloadCount(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<Int> {
@@ -123,11 +123,11 @@ data class MangaNodeList(
override val nodes: List<MangaType>,
override val edges: List<MangaEdge>,
override val pageInfo: PageInfo,
override val totalCount: Int
override val totalCount: Int,
) : NodeList() {
data class MangaEdge(
override val cursor: Cursor,
override val node: MangaType
override val node: MangaType,
) : Edge()
companion object {
@@ -135,13 +135,14 @@ data class MangaNodeList(
return MangaNodeList(
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,
)
}
@@ -150,12 +151,12 @@ data class MangaNodeList(
return listOf(
MangaEdge(
cursor = Cursor("0"),
node = first()
node = first(),
),
MangaEdge(
cursor = Cursor(lastIndex.toString()),
node = last()
)
node = last(),
),
)
}
}

View File

@@ -22,12 +22,12 @@ interface MetaType : Node {
class ChapterMetaType(
override val key: String,
override val value: String,
val chapterId: Int
val chapterId: Int,
) : MetaType {
constructor(row: ResultRow) : this(
key = row[ChapterMetaTable.key],
value = row[ChapterMetaTable.value],
chapterId = row[ChapterMetaTable.ref].value
chapterId = row[ChapterMetaTable.ref].value,
)
fun chapter(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ChapterType> {
@@ -38,12 +38,12 @@ class ChapterMetaType(
class MangaMetaType(
override val key: String,
override val value: String,
val mangaId: Int
val mangaId: Int,
) : MetaType {
constructor(row: ResultRow) : this(
key = row[MangaMetaTable.key],
value = row[MangaMetaTable.value],
mangaId = row[MangaMetaTable.ref].value
mangaId = row[MangaMetaTable.ref].value,
)
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaType> {
@@ -54,12 +54,12 @@ class MangaMetaType(
class CategoryMetaType(
override val key: String,
override val value: String,
val categoryId: Int
val categoryId: Int,
) : MetaType {
constructor(row: ResultRow) : this(
key = row[CategoryMetaTable.key],
value = row[CategoryMetaTable.value],
categoryId = row[CategoryMetaTable.ref].value
categoryId = row[CategoryMetaTable.ref].value,
)
fun category(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<CategoryType> {
@@ -69,11 +69,11 @@ class CategoryMetaType(
class GlobalMetaType(
override val key: String,
override val value: String
override val value: String,
) : MetaType {
constructor(row: ResultRow) : this(
key = row[GlobalMetaTable.key],
value = row[GlobalMetaTable.value]
value = row[GlobalMetaTable.value],
)
}
@@ -81,11 +81,11 @@ data class GlobalMetaNodeList(
override val nodes: List<GlobalMetaType>,
override val edges: List<MetaEdge>,
override val pageInfo: PageInfo,
override val totalCount: Int
override val totalCount: Int,
) : NodeList() {
data class MetaEdge(
override val cursor: Cursor,
override val node: GlobalMetaType
override val node: GlobalMetaType,
) : Edge()
companion object {
@@ -93,13 +93,14 @@ data class GlobalMetaNodeList(
return GlobalMetaNodeList(
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 GlobalMetaNodeList(
return listOf(
MetaEdge(
cursor = Cursor("0"),
node = first()
node = first(),
),
MetaEdge(
cursor = Cursor(lastIndex.toString()),
node = last()
)
node = last(),
),
)
}
}

View File

@@ -69,12 +69,10 @@ interface Settings : Node {
data class PartialSettingsType(
override val ip: String?,
override val port: Int?,
// proxy
override val socksProxyEnabled: Boolean?,
override val socksProxyHost: String?,
override val socksProxyPort: String?,
// webUI
override val webUIFlavor: WebUIFlavor?,
override val initialOpenInBrowserEnabled: Boolean?,
@@ -82,49 +80,40 @@ data class PartialSettingsType(
override val electronPath: String?,
override val webUIChannel: WebUIChannel?,
override val webUIUpdateCheckInterval: Double?,
// downloader
override val downloadAsCbz: Boolean?,
override val downloadsPath: String?,
override val autoDownloadNewChapters: Boolean?,
// requests
override val maxSourcesInParallel: Int?,
// updater
override val excludeUnreadChapters: Boolean?,
override val excludeNotStarted: Boolean?,
override val excludeCompleted: Boolean?,
override val globalUpdateInterval: Double?,
// Authentication
override val basicAuthEnabled: Boolean?,
override val basicAuthUsername: String?,
override val basicAuthPassword: String?,
// misc
override val debugLogsEnabled: Boolean?,
override val systemTrayEnabled: Boolean?,
// backup
override val backupPath: String?,
override val backupTime: String?,
override val backupInterval: Int?,
override val backupTTL: Int?,
// local source
override val localSourcePath: String?
override val localSourcePath: String?,
) : Settings
class SettingsType(
override val ip: String,
override val port: Int,
// proxy
override val socksProxyEnabled: Boolean,
override val socksProxyHost: String,
override val socksProxyPort: String,
// webUI
override val webUIFlavor: WebUIFlavor,
override val initialOpenInBrowserEnabled: Boolean,
@@ -132,77 +121,61 @@ class SettingsType(
override val electronPath: String,
override val webUIChannel: WebUIChannel,
override val webUIUpdateCheckInterval: Double,
// downloader
override val downloadAsCbz: Boolean,
override val downloadsPath: String,
override val autoDownloadNewChapters: Boolean,
// requests
override val maxSourcesInParallel: Int,
// updater
override val excludeUnreadChapters: Boolean,
override val excludeNotStarted: Boolean,
override val excludeCompleted: Boolean,
override val globalUpdateInterval: Double,
// Authentication
override val basicAuthEnabled: Boolean,
override val basicAuthUsername: String,
override val basicAuthPassword: String,
// misc
override val debugLogsEnabled: Boolean,
override val systemTrayEnabled: Boolean,
// backup
override val backupPath: String,
override val backupTime: String,
override val backupInterval: Int,
override val backupTTL: Int,
// local source
override val localSourcePath: String
override val localSourcePath: String,
) : Settings {
constructor(config: ServerConfig = serverConfig) : this(
config.ip.value,
config.port.value,
config.socksProxyEnabled.value,
config.socksProxyHost.value,
config.socksProxyPort.value,
WebUIFlavor.from(config.webUIFlavor.value),
config.initialOpenInBrowserEnabled.value,
WebUIInterface.from(config.webUIInterface.value),
config.electronPath.value,
WebUIChannel.from(config.webUIChannel.value),
config.webUIUpdateCheckInterval.value,
config.downloadAsCbz.value,
config.downloadsPath.value,
config.autoDownloadNewChapters.value,
config.maxSourcesInParallel.value,
config.excludeUnreadChapters.value,
config.excludeNotStarted.value,
config.excludeCompleted.value,
config.globalUpdateInterval.value,
config.basicAuthEnabled.value,
config.basicAuthUsername.value,
config.basicAuthPassword.value,
config.debugLogsEnabled.value,
config.systemTrayEnabled.value,
config.backupPath.value,
config.backupTime.value,
config.backupInterval.value,
config.backupTTL.value,
config.localSourcePath.value
config.localSourcePath.value,
)
}

View File

@@ -43,7 +43,7 @@ class SourceType(
val supportsLatest: Boolean,
val isConfigurable: Boolean,
val isNsfw: Boolean,
val displayName: String
val displayName: String,
) : Node {
constructor(source: SourceDataClass) : this(
id = source.id.toLong(),
@@ -53,7 +53,7 @@ class SourceType(
supportsLatest = source.supportsLatest,
isConfigurable = source.isConfigurable,
isNsfw = source.isNsfw,
displayName = source.displayName
displayName = source.displayName,
)
constructor(row: ResultRow, sourceExtension: ResultRow, catalogueSource: CatalogueSource) : this(
@@ -64,7 +64,7 @@ class SourceType(
supportsLatest = catalogueSource.supportsLatest,
isConfigurable = catalogueSource is ConfigurableSource,
isNsfw = row[SourceTable.isNsfw],
displayName = catalogueSource.toString()
displayName = catalogueSource.toString(),
)
fun manga(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> {
@@ -84,17 +84,20 @@ class SourceType(
}
}
@Suppress("ktlint:standard:function-naming")
fun SourceType(row: ResultRow): SourceType? {
val catalogueSource = GetCatalogueSource
.getCatalogueSourceOrNull(row[SourceTable.id].value)
?: return null
val sourceExtension = if (row.hasValue(ExtensionTable.id)) {
row
} else {
ExtensionTable
.select { ExtensionTable.id eq row[SourceTable.extension] }
.first()
}
val catalogueSource =
GetCatalogueSource
.getCatalogueSourceOrNull(row[SourceTable.id].value)
?: return null
val sourceExtension =
if (row.hasValue(ExtensionTable.id)) {
row
} else {
ExtensionTable
.select { ExtensionTable.id eq row[SourceTable.extension] }
.first()
}
return SourceType(row, sourceExtension, catalogueSource)
}
@@ -103,11 +106,11 @@ data class SourceNodeList(
override val nodes: List<SourceType>,
override val edges: List<SourceEdge>,
override val pageInfo: PageInfo,
override val totalCount: Int
override val totalCount: Int,
) : NodeList() {
data class SourceEdge(
override val cursor: Cursor,
override val node: SourceType
override val node: SourceType,
) : Edge()
companion object {
@@ -115,13 +118,14 @@ data class SourceNodeList(
return SourceNodeList(
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,
)
}
@@ -130,12 +134,12 @@ data class SourceNodeList(
return listOf(
SourceEdge(
cursor = Cursor("0"),
node = first()
node = first(),
),
SourceEdge(
cursor = Cursor(lastIndex.toString()),
node = last()
)
node = last(),
),
)
}
}
@@ -156,7 +160,7 @@ data class CheckBoxFilter(val name: String, val default: Boolean) : Filter
enum class TriState {
IGNORE,
INCLUDE,
EXCLUDE
EXCLUDE,
}
data class TriStateFilter(val name: String, val default: TriState) : Filter
@@ -177,18 +181,20 @@ fun filterOf(filter: SourceFilter<*>): Filter {
is SourceFilter.Select<*> -> SelectFilter(filter.name, filter.displayValues, filter.state)
is SourceFilter.Text -> TextFilter(filter.name, filter.state)
is SourceFilter.CheckBox -> CheckBoxFilter(filter.name, filter.state)
is SourceFilter.TriState -> TriStateFilter(
filter.name,
when (filter.state) {
SourceFilter.TriState.STATE_INCLUDE -> TriState.INCLUDE
SourceFilter.TriState.STATE_EXCLUDE -> TriState.EXCLUDE
else -> TriState.IGNORE
}
)
is SourceFilter.Group<*> -> GroupFilter(
filter.name,
filter.state.map { filterOf(it as SourceFilter<*>) }
)
is SourceFilter.TriState ->
TriStateFilter(
filter.name,
when (filter.state) {
SourceFilter.TriState.STATE_INCLUDE -> TriState.INCLUDE
SourceFilter.TriState.STATE_EXCLUDE -> TriState.EXCLUDE
else -> TriState.IGNORE
},
)
is SourceFilter.Group<*> ->
GroupFilter(
filter.name,
filter.state.map { filterOf(it as SourceFilter<*>) },
)
is SourceFilter.Sort -> SortFilter(filter.name, filter.values.asList(), filter.state?.let(SortFilter::SortSelection))
else -> throw RuntimeException("sealed class cannot have more subtypes!")
}
@@ -239,10 +245,13 @@ data class FilterChange(
val checkBoxState: Boolean? = null,
val triState: TriState? = null,
val sortState: SortFilter.SortSelection? = null,
val groupChange: FilterChange? = null
val groupChange: FilterChange? = null,
)
fun updateFilterList(source: CatalogueSource, changes: List<FilterChange>?): FilterList {
fun updateFilterList(
source: CatalogueSource,
changes: List<FilterChange>?,
): FilterList {
val filterList = source.getFilterList()
changes?.forEach { change ->
@@ -254,32 +263,42 @@ fun updateFilterList(source: CatalogueSource, changes: List<FilterChange>?): Fil
// NOOP
}
is SourceFilter.Select<*> -> {
filter.state = change.selectState ?: throw Exception("Expected select state change at position ${change.position}")
filter.state = change.selectState
?: throw Exception("Expected select state change at position ${change.position}")
}
is SourceFilter.Text -> {
filter.state = change.textState ?: throw Exception("Expected text state change at position ${change.position}")
filter.state = change.textState
?: throw Exception("Expected text state change at position ${change.position}")
}
is SourceFilter.CheckBox -> {
filter.state = change.checkBoxState ?: throw Exception("Expected checkbox state change at position ${change.position}")
filter.state = change.checkBoxState
?: throw Exception("Expected checkbox state change at position ${change.position}")
}
is SourceFilter.TriState -> {
filter.state = change.triState?.ordinal ?: throw Exception("Expected tri state change at position ${change.position}")
filter.state = change.triState?.ordinal
?: throw Exception("Expected tri state change at position ${change.position}")
}
is SourceFilter.Group<*> -> {
val groupChange = change.groupChange ?: throw Exception("Expected group change at position ${change.position}")
val groupChange =
change.groupChange
?: throw Exception("Expected group change at position ${change.position}")
when (val groupFilter = filter.state[groupChange.position]) {
is SourceFilter.CheckBox -> {
groupFilter.state = groupChange.checkBoxState ?: throw Exception("Expected checkbox state change at position ${change.position}")
groupFilter.state = groupChange.checkBoxState
?: throw Exception("Expected checkbox state change at position ${change.position}")
}
is SourceFilter.TriState -> {
groupFilter.state = groupChange.triState?.ordinal ?: throw Exception("Expected tri state change at position ${change.position}")
groupFilter.state = groupChange.triState?.ordinal
?: throw Exception("Expected tri state change at position ${change.position}")
}
is SourceFilter.Text -> {
groupFilter.state = groupChange.textState ?: throw Exception("Expected text state change at position ${change.position}")
groupFilter.state = groupChange.textState
?: throw Exception("Expected text state change at position ${change.position}")
}
is SourceFilter.Select<*> -> {
groupFilter.state = groupChange.selectState ?: throw Exception("Expected select state change at position ${change.position}")
groupFilter.state = groupChange.selectState
?: throw Exception("Expected select state change at position ${change.position}")
}
}
}
@@ -301,7 +320,7 @@ data class SwitchPreference(
val summary: String?,
val visible: Boolean,
val currentValue: Boolean?,
val default: Boolean
val default: Boolean,
) : Preference
data class CheckBoxPreference(
@@ -310,7 +329,7 @@ data class CheckBoxPreference(
val summary: String?,
val visible: Boolean,
val currentValue: Boolean?,
val default: Boolean
val default: Boolean,
) : Preference
data class EditTextPreference(
@@ -322,7 +341,7 @@ data class EditTextPreference(
val default: String?,
val dialogTitle: String?,
val dialogMessage: String?,
val text: String?
val text: String?,
) : Preference
data class ListPreference(
@@ -333,7 +352,7 @@ data class ListPreference(
val currentValue: String?,
val default: String?,
val entries: List<String>,
val entryValues: List<String>
val entryValues: List<String>,
) : Preference
data class MultiSelectListPreference(
@@ -346,60 +365,65 @@ data class MultiSelectListPreference(
val dialogTitle: String?,
val dialogMessage: String?,
val entries: List<String>,
val entryValues: List<String>
val entryValues: List<String>,
) : Preference
fun preferenceOf(preference: SourcePreference): Preference {
return when (preference) {
is SourceSwitchPreference -> SwitchPreference(
preference.key,
preference.title.toString(),
preference.summary?.toString(),
preference.visible,
preference.currentValue as Boolean,
preference.defaultValue as Boolean,
)
is SourceCheckBoxPreference -> CheckBoxPreference(
preference.key,
preference.title.toString(),
preference.summary?.toString(),
preference.visible,
preference.currentValue as Boolean,
preference.defaultValue as Boolean
)
is SourceEditTextPreference -> EditTextPreference(
preference.key,
preference.title?.toString(),
preference.summary?.toString(),
preference.visible,
(preference.currentValue as CharSequence?)?.toString(),
(preference.defaultValue as CharSequence?)?.toString(),
preference.dialogTitle?.toString(),
preference.dialogMessage?.toString(),
preference.text
)
is SourceListPreference -> ListPreference(
preference.key,
preference.title?.toString(),
preference.summary?.toString(),
preference.visible,
(preference.currentValue as CharSequence?)?.toString(),
(preference.defaultValue as CharSequence?)?.toString(),
preference.entries.map { it.toString() },
preference.entryValues.map { it.toString() }
)
is SourceMultiSelectListPreference -> MultiSelectListPreference(
preference.key,
preference.title?.toString(),
preference.summary?.toString(),
preference.visible,
(preference.currentValue as Collection<*>?)?.map { it.toString() },
(preference.defaultValue as Collection<*>?)?.map { it.toString() },
preference.dialogTitle?.toString(),
preference.dialogMessage?.toString(),
preference.entries.map { it.toString() },
preference.entryValues.map { it.toString() }
)
is SourceSwitchPreference ->
SwitchPreference(
preference.key,
preference.title.toString(),
preference.summary?.toString(),
preference.visible,
preference.currentValue as Boolean,
preference.defaultValue as Boolean,
)
is SourceCheckBoxPreference ->
CheckBoxPreference(
preference.key,
preference.title.toString(),
preference.summary?.toString(),
preference.visible,
preference.currentValue as Boolean,
preference.defaultValue as Boolean,
)
is SourceEditTextPreference ->
EditTextPreference(
preference.key,
preference.title?.toString(),
preference.summary?.toString(),
preference.visible,
(preference.currentValue as CharSequence?)?.toString(),
(preference.defaultValue as CharSequence?)?.toString(),
preference.dialogTitle?.toString(),
preference.dialogMessage?.toString(),
preference.text,
)
is SourceListPreference ->
ListPreference(
preference.key,
preference.title?.toString(),
preference.summary?.toString(),
preference.visible,
(preference.currentValue as CharSequence?)?.toString(),
(preference.defaultValue as CharSequence?)?.toString(),
preference.entries.map { it.toString() },
preference.entryValues.map { it.toString() },
)
is SourceMultiSelectListPreference ->
MultiSelectListPreference(
preference.key,
preference.title?.toString(),
preference.summary?.toString(),
preference.visible,
(preference.currentValue as Collection<*>?)?.map { it.toString() },
(preference.defaultValue as Collection<*>?)?.map { it.toString() },
preference.dialogTitle?.toString(),
preference.dialogMessage?.toString(),
preference.entries.map { it.toString() },
preference.entryValues.map { it.toString() },
)
else -> throw RuntimeException("sealed class cannot have more subtypes!")
}
}

View File

@@ -16,7 +16,7 @@ class UpdateStatus(
val runningJobs: UpdateStatusType,
val completeJobs: UpdateStatusType,
val failedJobs: UpdateStatusType,
val skippedJobs: UpdateStatusType
val skippedJobs: UpdateStatusType,
) {
constructor(status: UpdateStatus) : this(
isRunning = status.running,
@@ -26,13 +26,13 @@ class UpdateStatus(
runningJobs = UpdateStatusType(status.mangaStatusMap[JobStatus.RUNNING]?.map { it.id }.orEmpty()),
completeJobs = UpdateStatusType(status.mangaStatusMap[JobStatus.COMPLETE]?.map { it.id }.orEmpty()),
failedJobs = UpdateStatusType(status.mangaStatusMap[JobStatus.FAILED]?.map { it.id }.orEmpty()),
skippedJobs = UpdateStatusType(status.mangaStatusMap[JobStatus.SKIPPED]?.map { it.id }.orEmpty())
skippedJobs = UpdateStatusType(status.mangaStatusMap[JobStatus.SKIPPED]?.map { it.id }.orEmpty()),
)
}
class UpdateStatusCategoryType(
@get:GraphQLIgnore
val categoryIds: List<Int>
val categoryIds: List<Int>,
) {
fun categories(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<CategoryNodeList> {
return dataFetchingEnvironment.getValueFromDataLoader("CategoryForIdsDataLoader", categoryIds)
@@ -41,7 +41,7 @@ class UpdateStatusCategoryType(
class UpdateStatusType(
@get:GraphQLIgnore
val mangaIds: List<Int>
val mangaIds: List<Int>,
) {
fun mangas(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> {
return dataFetchingEnvironment.getValueFromDataLoader<List<Int>, MangaNodeList>("MangaForIdsDataLoader", mangaIds)

View File

@@ -3,18 +3,18 @@ package suwayomi.tachidesk.graphql.types
data class WebUIUpdateInfo(
val channel: String,
val tag: String,
val updateAvailable: Boolean
val updateAvailable: Boolean,
)
enum class UpdateState {
STOPPED,
DOWNLOADING,
FINISHED,
ERROR
ERROR,
}
data class WebUIUpdateStatus(
val info: WebUIUpdateInfo,
val state: UpdateState,
val progress: Int
val progress: Int,
)

View File

@@ -17,154 +17,163 @@ import suwayomi.tachidesk.server.util.withOperation
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
object BackupController {
/** expects a Tachiyomi protobuf backup in the body */
val protobufImport = handler(
documentWith = {
withOperation {
summary("Restore a backup")
description("Expects a Tachiyomi protobuf backup in the body")
}
},
behaviorOf = { ctx ->
ctx.future(
future {
ProtoBackupImport.performRestore(ctx.bodyAsInputStream())
val protobufImport =
handler(
documentWith = {
withOperation {
summary("Restore a backup")
description("Expects a Tachiyomi protobuf backup in the body")
}
)
},
withResults = {
httpCode(HttpCode.OK)
}
)
},
behaviorOf = { ctx ->
ctx.future(
future {
ProtoBackupImport.performRestore(ctx.bodyAsInputStream())
},
)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */
val protobufImportFile = handler(
documentWith = {
withOperation {
summary("Restore a backup file")
description("Expects a Tachiyomi protobuf backup as a file upload, the file must be named \"backup.proto.gz\"")
}
uploadedFile("backup.proto.gz") {
it.description("Protobuf backup")
it.required(true)
}
},
behaviorOf = { ctx ->
// TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz"
ctx.future(
future {
ProtoBackupImport.performRestore(ctx.uploadedFile("backup.proto.gz")!!.content)
val protobufImportFile =
handler(
documentWith = {
withOperation {
summary("Restore a backup file")
description("Expects a Tachiyomi protobuf backup as a file upload, the file must be named \"backup.proto.gz\"")
}
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
uploadedFile("backup.proto.gz") {
it.description("Protobuf backup")
it.required(true)
}
},
behaviorOf = { ctx ->
// TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz"
ctx.future(
future {
ProtoBackupImport.performRestore(ctx.uploadedFile("backup.proto.gz")!!.content)
},
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
/** returns a Tachiyomi protobuf backup created from the current database as a body */
val protobufExport = handler(
documentWith = {
withOperation {
summary("Create a backup")
description("Returns a Tachiyomi protobuf backup created from the current database as a body")
}
},
behaviorOf = { ctx ->
ctx.contentType("application/octet-stream")
ctx.future(
future {
ProtoBackupExport.createBackup(
BackupFlags(
includeManga = true,
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true
)
)
val protobufExport =
handler(
documentWith = {
withOperation {
summary("Create a backup")
description("Returns a Tachiyomi protobuf backup created from the current database as a body")
}
)
},
withResults = {
stream(HttpCode.OK)
}
)
},
behaviorOf = { ctx ->
ctx.contentType("application/octet-stream")
ctx.future(
future {
ProtoBackupExport.createBackup(
BackupFlags(
includeManga = true,
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true,
),
)
},
)
},
withResults = {
stream(HttpCode.OK)
},
)
/** returns a Tachiyomi protobuf backup created from the current database as a file */
val protobufExportFile = handler(
documentWith = {
withOperation {
summary("Create a backup file")
description("Returns a Tachiyomi protobuf backup created from the current database as a file")
}
},
behaviorOf = { ctx ->
ctx.contentType("application/octet-stream")
ctx.header("Content-Disposition", """attachment; filename="${ProtoBackupExport.getBackupFilename()}"""")
ctx.future(
future {
ProtoBackupExport.createBackup(
BackupFlags(
includeManga = true,
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true
)
)
val protobufExportFile =
handler(
documentWith = {
withOperation {
summary("Create a backup file")
description("Returns a Tachiyomi protobuf backup created from the current database as a file")
}
)
},
withResults = {
stream(HttpCode.OK)
}
)
},
behaviorOf = { ctx ->
ctx.contentType("application/octet-stream")
ctx.header("Content-Disposition", """attachment; filename="${ProtoBackupExport.getBackupFilename()}"""")
ctx.future(
future {
ProtoBackupExport.createBackup(
BackupFlags(
includeManga = true,
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true,
),
)
},
)
},
withResults = {
stream(HttpCode.OK)
},
)
/** Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body */
val protobufValidate = handler(
documentWith = {
withOperation {
summary("Validate a backup")
description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body")
}
},
behaviorOf = { ctx ->
ctx.future(
future {
ProtoBackupValidator.validate(ctx.bodyAsInputStream())
val protobufValidate =
handler(
documentWith = {
withOperation {
summary("Validate a backup")
description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body")
}
)
},
withResults = {
json<ProtoBackupValidator.ValidationResult>(HttpCode.OK)
}
)
},
behaviorOf = { ctx ->
ctx.future(
future {
ProtoBackupValidator.validate(ctx.bodyAsInputStream())
},
)
},
withResults = {
json<ProtoBackupValidator.ValidationResult>(HttpCode.OK)
},
)
/** Reports missing sources and trackers, expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */
val protobufValidateFile = handler(
documentWith = {
withOperation {
summary("Validate a backup file")
description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup as a file upload, the file must be named \"backup.proto.gz\"")
}
uploadedFile("backup.proto.gz") {
it.description("Protobuf backup")
it.required(true)
}
},
behaviorOf = { ctx ->
ctx.future(
future {
ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content)
val protobufValidateFile =
handler(
documentWith = {
withOperation {
summary("Validate a backup file")
description(
"Reports missing sources and trackers, " +
"expects a Tachiyomi protobuf backup as a file upload, " +
"the file must be named \"backup.proto.gz\"",
)
}
)
},
withResults = {
json<ProtoBackupValidator.ValidationResult>(HttpCode.OK)
}
)
uploadedFile("backup.proto.gz") {
it.description("Protobuf backup")
it.required(true)
}
},
behaviorOf = { ctx ->
ctx.future(
future {
ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content)
},
)
},
withResults = {
json<ProtoBackupValidator.ValidationResult>(HttpCode.OK)
},
)
}

View File

@@ -19,136 +19,143 @@ import suwayomi.tachidesk.server.util.withOperation
object CategoryController {
/** category list */
val categoryList = handler(
documentWith = {
withOperation {
summary("Category list")
description("get a list of categories")
}
},
behaviorOf = { ctx ->
ctx.json(Category.getCategoryList())
},
withResults = {
json<Array<CategoryDataClass>>(HttpCode.OK)
}
)
val categoryList =
handler(
documentWith = {
withOperation {
summary("Category list")
description("get a list of categories")
}
},
behaviorOf = { ctx ->
ctx.json(Category.getCategoryList())
},
withResults = {
json<Array<CategoryDataClass>>(HttpCode.OK)
},
)
/** category create */
val categoryCreate = handler(
formParam<String>("name"),
documentWith = {
withOperation {
summary("Category create")
description("Create a category")
}
},
behaviorOf = { ctx, name ->
if (Category.createCategory(name) != -1) {
ctx.status(200)
} else {
ctx.status(HttpCode.BAD_REQUEST)
}
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.BAD_REQUEST)
}
)
val categoryCreate =
handler(
formParam<String>("name"),
documentWith = {
withOperation {
summary("Category create")
description("Create a category")
}
},
behaviorOf = { ctx, name ->
if (Category.createCategory(name) != -1) {
ctx.status(200)
} else {
ctx.status(HttpCode.BAD_REQUEST)
}
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.BAD_REQUEST)
},
)
/** category modification */
val categoryModify = handler(
pathParam<Int>("categoryId"),
formParam<String?>("name"),
formParam<Boolean?>("default"),
formParam<Int?>("includeInUpdate"),
documentWith = {
withOperation {
summary("Category modify")
description("Modify a category")
}
},
behaviorOf = { ctx, categoryId, name, isDefault, includeInUpdate ->
Category.updateCategory(categoryId, name, isDefault, includeInUpdate)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
val categoryModify =
handler(
pathParam<Int>("categoryId"),
formParam<String?>("name"),
formParam<Boolean?>("default"),
formParam<Int?>("includeInUpdate"),
documentWith = {
withOperation {
summary("Category modify")
description("Modify a category")
}
},
behaviorOf = { ctx, categoryId, name, isDefault, includeInUpdate ->
Category.updateCategory(categoryId, name, isDefault, includeInUpdate)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** category delete */
val categoryDelete = handler(
pathParam<Int>("categoryId"),
documentWith = {
withOperation {
summary("Category delete")
description("Delete a category")
}
},
behaviorOf = { ctx, categoryId ->
Category.removeCategory(categoryId)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
val categoryDelete =
handler(
pathParam<Int>("categoryId"),
documentWith = {
withOperation {
summary("Category delete")
description("Delete a category")
}
},
behaviorOf = { ctx, categoryId ->
Category.removeCategory(categoryId)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** returns the manga list associated with a category */
val categoryMangas = handler(
pathParam<Int>("categoryId"),
documentWith = {
withOperation {
summary("Category manga")
description("Returns the manga list associated with a category")
}
},
behaviorOf = { ctx, categoryId ->
ctx.json(CategoryManga.getCategoryMangaList(categoryId))
},
withResults = {
json<Array<MangaDataClass>>(HttpCode.OK)
}
)
val categoryMangas =
handler(
pathParam<Int>("categoryId"),
documentWith = {
withOperation {
summary("Category manga")
description("Returns the manga list associated with a category")
}
},
behaviorOf = { ctx, categoryId ->
ctx.json(CategoryManga.getCategoryMangaList(categoryId))
},
withResults = {
json<Array<MangaDataClass>>(HttpCode.OK)
},
)
/** category re-ordering */
val categoryReorder = handler(
formParam<Int>("from"),
formParam<Int>("to"),
documentWith = {
withOperation {
summary("Category re-ordering")
description("Re-order a category")
}
},
behaviorOf = { ctx, from, to ->
Category.reorderCategory(from, to)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
val categoryReorder =
handler(
formParam<Int>("from"),
formParam<Int>("to"),
documentWith = {
withOperation {
summary("Category re-ordering")
description("Re-order a category")
}
},
behaviorOf = { ctx, from, to ->
Category.reorderCategory(from, to)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** used to modify a category's meta parameters */
val meta = handler(
pathParam<Int>("categoryId"),
formParam<String>("key"),
formParam<String>("value"),
documentWith = {
withOperation {
summary("Add meta data to category")
description("A simple Key-Value storage in the manga object, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx, categoryId, key, value ->
Category.modifyMeta(categoryId, key, value)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
val meta =
handler(
pathParam<Int>("categoryId"),
formParam<String>("key"),
formParam<String>("value"),
documentWith = {
withOperation {
summary("Add meta data to category")
description("A simple Key-Value storage in the manga object, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx, categoryId, key, value ->
Category.modifyMeta(categoryId, key, value)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
}

View File

@@ -39,159 +39,167 @@ object DownloadController {
}
/** Start the downloader */
val start = handler(
documentWith = {
withOperation {
summary("Downloader start")
description("Start the downloader")
}
},
behaviorOf = {
DownloadManager.start()
},
withResults = {
httpCode(HttpCode.OK)
}
)
val start =
handler(
documentWith = {
withOperation {
summary("Downloader start")
description("Start the downloader")
}
},
behaviorOf = {
DownloadManager.start()
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** Stop the downloader */
val stop = handler(
documentWith = {
withOperation {
summary("Downloader stop")
description("Stop the downloader")
}
},
behaviorOf = { ctx ->
ctx.future(
future { DownloadManager.stop() }
)
},
withResults = {
httpCode(HttpCode.OK)
}
)
val stop =
handler(
documentWith = {
withOperation {
summary("Downloader stop")
description("Stop the downloader")
}
},
behaviorOf = { ctx ->
ctx.future(
future { DownloadManager.stop() },
)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** clear download queue */
val clear = handler(
documentWith = {
withOperation {
summary("Downloader clear")
description("Clear download queue")
}
},
behaviorOf = { ctx ->
ctx.future(
future { DownloadManager.clear() }
)
},
withResults = {
httpCode(HttpCode.OK)
}
)
val clear =
handler(
documentWith = {
withOperation {
summary("Downloader clear")
description("Clear download queue")
}
},
behaviorOf = { ctx ->
ctx.future(
future { DownloadManager.clear() },
)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** Queue single chapter for download */
val queueChapter = handler(
pathParam<Int>("chapterIndex"),
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Downloader add single chapter")
description("Queue single chapter for download")
}
},
behaviorOf = { ctx, chapterIndex, mangaId ->
ctx.future(
future {
DownloadManager.enqueueWithChapterIndex(mangaId, chapterIndex)
val queueChapter =
handler(
pathParam<Int>("chapterIndex"),
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Downloader add single chapter")
description("Queue single chapter for download")
}
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
},
behaviorOf = { ctx, chapterIndex, mangaId ->
ctx.future(
future {
DownloadManager.enqueueWithChapterIndex(mangaId, chapterIndex)
},
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
val queueChapters = handler(
documentWith = {
withOperation {
summary("Downloader add multiple chapters")
description("Queue multiple chapters for download")
}
body<EnqueueInput>()
},
behaviorOf = { ctx ->
val inputs = json.decodeFromString<EnqueueInput>(ctx.body())
ctx.future(
future {
DownloadManager.enqueue(inputs)
val queueChapters =
handler(
documentWith = {
withOperation {
summary("Downloader add multiple chapters")
description("Queue multiple chapters for download")
}
)
},
withResults = {
httpCode(HttpCode.OK)
}
)
body<EnqueueInput>()
},
behaviorOf = { ctx ->
val inputs = json.decodeFromString<EnqueueInput>(ctx.body())
ctx.future(
future {
DownloadManager.enqueue(inputs)
},
)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** delete multiple chapters from download queue */
val unqueueChapters = handler(
documentWith = {
withOperation {
summary("Downloader remove multiple downloads")
description("Remove multiple chapters downloads from queue")
}
body<EnqueueInput>()
},
behaviorOf = { ctx ->
val input = json.decodeFromString<EnqueueInput>(ctx.body())
ctx.future(
future {
DownloadManager.dequeue(input)
val unqueueChapters =
handler(
documentWith = {
withOperation {
summary("Downloader remove multiple downloads")
description("Remove multiple chapters downloads from queue")
}
)
},
withResults = {
httpCode(HttpCode.OK)
}
)
body<EnqueueInput>()
},
behaviorOf = { ctx ->
val input = json.decodeFromString<EnqueueInput>(ctx.body())
ctx.future(
future {
DownloadManager.dequeue(input)
},
)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** delete chapter from download queue */
val unqueueChapter = handler(
pathParam<Int>("chapterIndex"),
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Downloader remove chapter")
description("Delete chapter from download queue")
}
},
behaviorOf = { ctx, chapterIndex, mangaId ->
DownloadManager.dequeue(chapterIndex, mangaId)
val unqueueChapter =
handler(
pathParam<Int>("chapterIndex"),
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Downloader remove chapter")
description("Delete chapter from download queue")
}
},
behaviorOf = { ctx, chapterIndex, mangaId ->
DownloadManager.dequeue(chapterIndex, mangaId)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** clear download queue */
val reorderChapter = handler(
pathParam<Int>("chapterIndex"),
pathParam<Int>("mangaId"),
pathParam<Int>("to"),
documentWith = {
withOperation {
summary("Downloader reorder chapter")
description("Reorder chapter in download queue")
}
},
behaviorOf = { _, chapterIndex, mangaId, to ->
DownloadManager.reorder(chapterIndex, mangaId, to)
},
withResults = {
httpCode(HttpCode.OK)
}
)
val reorderChapter =
handler(
pathParam<Int>("chapterIndex"),
pathParam<Int>("mangaId"),
pathParam<Int>("to"),
documentWith = {
withOperation {
summary("Downloader reorder chapter")
description("Reorder chapter in download queue")
}
},
behaviorOf = { _, chapterIndex, mangaId, to ->
DownloadManager.reorder(chapterIndex, mangaId, to)
},
withResults = {
httpCode(HttpCode.OK)
},
)
}

View File

@@ -21,143 +21,149 @@ object ExtensionController {
private val logger = KotlinLogging.logger {}
/** list all extensions */
val list = handler(
documentWith = {
withOperation {
summary("Extension list")
description("List all extensions")
}
},
behaviorOf = { ctx ->
ctx.future(
future {
ExtensionsList.getExtensionList()
val list =
handler(
documentWith = {
withOperation {
summary("Extension list")
description("List all extensions")
}
)
},
withResults = {
json<Array<ExtensionDataClass>>(HttpCode.OK)
}
)
},
behaviorOf = { ctx ->
ctx.future(
future {
ExtensionsList.getExtensionList()
},
)
},
withResults = {
json<Array<ExtensionDataClass>>(HttpCode.OK)
},
)
/** install extension identified with "pkgName" */
val install = handler(
pathParam<String>("pkgName"),
documentWith = {
withOperation {
summary("Extension install")
description("install extension identified with \"pkgName\"")
}
},
behaviorOf = { ctx, pkgName ->
ctx.future(
future {
Extension.installExtension(pkgName)
val install =
handler(
pathParam<String>("pkgName"),
documentWith = {
withOperation {
summary("Extension install")
description("install extension identified with \"pkgName\"")
}
)
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
}
)
},
behaviorOf = { ctx, pkgName ->
ctx.future(
future {
Extension.installExtension(pkgName)
},
)
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
},
)
/** install the uploaded apk file */
val installFile = handler(
documentWith = {
withOperation {
summary("Extension install apk")
description("Install the uploaded apk file")
}
uploadedFile("file") {
it.description("Extension apk")
it.required(true)
}
},
behaviorOf = { ctx ->
val uploadedFile = ctx.uploadedFile("file")!!
logger.debug { "Uploaded extension file name: " + uploadedFile.filename }
ctx.future(
future {
Extension.installExternalExtension(uploadedFile.content, uploadedFile.filename)
val installFile =
handler(
documentWith = {
withOperation {
summary("Extension install apk")
description("Install the uploaded apk file")
}
)
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
}
)
uploadedFile("file") {
it.description("Extension apk")
it.required(true)
}
},
behaviorOf = { ctx ->
val uploadedFile = ctx.uploadedFile("file")!!
logger.debug { "Uploaded extension file name: " + uploadedFile.filename }
ctx.future(
future {
Extension.installExternalExtension(uploadedFile.content, uploadedFile.filename)
},
)
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
},
)
/** update extension identified with "pkgName" */
val update = handler(
pathParam<String>("pkgName"),
documentWith = {
withOperation {
summary("Extension update")
description("Update extension identified with \"pkgName\"")
}
},
behaviorOf = { ctx, pkgName ->
ctx.future(
future {
Extension.updateExtension(pkgName)
val update =
handler(
pathParam<String>("pkgName"),
documentWith = {
withOperation {
summary("Extension update")
description("Update extension identified with \"pkgName\"")
}
)
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.NOT_FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
}
)
},
behaviorOf = { ctx, pkgName ->
ctx.future(
future {
Extension.updateExtension(pkgName)
},
)
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.NOT_FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
},
)
/** uninstall extension identified with "pkgName" */
val uninstall = handler(
pathParam<String>("pkgName"),
documentWith = {
withOperation {
summary("Extension uninstall")
description("Uninstall extension identified with \"pkgName\"")
}
},
behaviorOf = { ctx, pkgName ->
Extension.uninstallExtension(pkgName)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.NOT_FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
}
)
val uninstall =
handler(
pathParam<String>("pkgName"),
documentWith = {
withOperation {
summary("Extension uninstall")
description("Uninstall extension identified with \"pkgName\"")
}
},
behaviorOf = { ctx, pkgName ->
Extension.uninstallExtension(pkgName)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.CREATED)
httpCode(HttpCode.FOUND)
httpCode(HttpCode.NOT_FOUND)
httpCode(HttpCode.INTERNAL_SERVER_ERROR)
},
)
/** icon for extension named `apkName` */
val icon = handler(
pathParam<String>("apkName"),
documentWith = {
withOperation {
summary("Extension icon")
description("Icon for extension named `apkName`")
}
},
behaviorOf = { ctx, apkName ->
ctx.future(
future { Extension.getExtensionIcon(apkName) }
.thenApply {
ctx.header("content-type", it.second)
it.first
}
)
},
withResults = {
image(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
val icon =
handler(
pathParam<String>("apkName"),
documentWith = {
withOperation {
summary("Extension icon")
description("Icon for extension named `apkName`")
}
},
behaviorOf = { ctx, apkName ->
ctx.future(
future { Extension.getExtensionIcon(apkName) }
.thenApply {
ctx.header("content-type", it.second)
it.first
},
)
},
withResults = {
image(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
}

View File

@@ -32,367 +32,390 @@ import kotlin.time.Duration.Companion.days
object MangaController {
private val json by DI.global.instance<Json>()
val retrieve = handler(
pathParam<Int>("mangaId"),
queryParam("onlineFetch", false),
documentWith = {
withOperation {
summary("Get manga info")
description("Get a manga from the database using a specific id.")
}
},
behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.future(
future {
Manga.getManga(mangaId, onlineFetch)
val retrieve =
handler(
pathParam<Int>("mangaId"),
queryParam("onlineFetch", false),
documentWith = {
withOperation {
summary("Get manga info")
description("Get a manga from the database using a specific id.")
}
)
},
withResults = {
json<MangaDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
},
behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.future(
future {
Manga.getManga(mangaId, onlineFetch)
},
)
},
withResults = {
json<MangaDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
/** get manga info with all data filled in */
val retrieveFull = handler(
pathParam<Int>("mangaId"),
queryParam("onlineFetch", false),
documentWith = {
withOperation {
summary("Get manga info with all data filled in")
description("Get a manga from the database using a specific id.")
}
},
behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.future(
future {
Manga.getMangaFull(mangaId, onlineFetch)
val retrieveFull =
handler(
pathParam<Int>("mangaId"),
queryParam("onlineFetch", false),
documentWith = {
withOperation {
summary("Get manga info with all data filled in")
description("Get a manga from the database using a specific id.")
}
)
},
withResults = {
json<MangaDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
},
behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.future(
future {
Manga.getMangaFull(mangaId, onlineFetch)
},
)
},
withResults = {
json<MangaDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
/** manga thumbnail */
val thumbnail = handler(
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Get a manga thumbnail")
description("Get a manga thumbnail from the source or the cache.")
}
},
behaviorOf = { ctx, mangaId ->
ctx.future(
future { Manga.getMangaThumbnail(mangaId) }
.thenApply {
ctx.header("content-type", it.second)
val httpCacheSeconds = 1.days.inWholeSeconds
ctx.header("cache-control", "max-age=$httpCacheSeconds")
it.first
}
)
},
withResults = {
image(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
val thumbnail =
handler(
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Get a manga thumbnail")
description("Get a manga thumbnail from the source or the cache.")
}
},
behaviorOf = { ctx, mangaId ->
ctx.future(
future { Manga.getMangaThumbnail(mangaId) }
.thenApply {
ctx.header("content-type", it.second)
val httpCacheSeconds = 1.days.inWholeSeconds
ctx.header("cache-control", "max-age=$httpCacheSeconds")
it.first
},
)
},
withResults = {
image(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
/** adds the manga to library */
val addToLibrary = handler(
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Add manga to library")
description("Use a manga id to add the manga to your library.\nWill do nothing if manga is already in your library.")
}
},
behaviorOf = { ctx, mangaId ->
ctx.future(
future { Library.addMangaToLibrary(mangaId) }
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
val addToLibrary =
handler(
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Add manga to library")
description("Use a manga id to add the manga to your library.\nWill do nothing if manga is already in your library.")
}
},
behaviorOf = { ctx, mangaId ->
ctx.future(
future { Library.addMangaToLibrary(mangaId) },
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
/** removes the manga from the library */
val removeFromLibrary = handler(
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Remove manga to library")
description("Use a manga id to remove the manga to your library.\nWill do nothing if manga not in your library.")
}
},
behaviorOf = { ctx, mangaId ->
ctx.future(
future { Library.removeMangaFromLibrary(mangaId) }
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
val removeFromLibrary =
handler(
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Remove manga to library")
description("Use a manga id to remove the manga to your library.\nWill do nothing if manga not in your library.")
}
},
behaviorOf = { ctx, mangaId ->
ctx.future(
future { Library.removeMangaFromLibrary(mangaId) },
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
/** list manga's categories */
val categoryList = handler(
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Get a manga's categories")
description("Get the list of categories for this manga")
}
},
behaviorOf = { ctx, mangaId ->
ctx.json(CategoryManga.getMangaCategories(mangaId))
},
withResults = {
json<Array<CategoryDataClass>>(HttpCode.OK)
}
)
val categoryList =
handler(
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Get a manga's categories")
description("Get the list of categories for this manga")
}
},
behaviorOf = { ctx, mangaId ->
ctx.json(CategoryManga.getMangaCategories(mangaId))
},
withResults = {
json<Array<CategoryDataClass>>(HttpCode.OK)
},
)
/** adds the manga to category */
val addToCategory = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("categoryId"),
documentWith = {
withOperation {
summary("Add manga to category")
description("Add a manga to a category using their ids.")
}
},
behaviorOf = { ctx, mangaId, categoryId ->
CategoryManga.addMangaToCategory(mangaId, categoryId)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
val addToCategory =
handler(
pathParam<Int>("mangaId"),
pathParam<Int>("categoryId"),
documentWith = {
withOperation {
summary("Add manga to category")
description("Add a manga to a category using their ids.")
}
},
behaviorOf = { ctx, mangaId, categoryId ->
CategoryManga.addMangaToCategory(mangaId, categoryId)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** removes the manga from the category */
val removeFromCategory = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("categoryId"),
documentWith = {
withOperation {
summary("Remove manga from category")
description("Remove a manga from a category using their ids.")
}
},
behaviorOf = { ctx, mangaId, categoryId ->
CategoryManga.removeMangaFromCategory(mangaId, categoryId)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
val removeFromCategory =
handler(
pathParam<Int>("mangaId"),
pathParam<Int>("categoryId"),
documentWith = {
withOperation {
summary("Remove manga from category")
description("Remove a manga from a category using their ids.")
}
},
behaviorOf = { ctx, mangaId, categoryId ->
CategoryManga.removeMangaFromCategory(mangaId, categoryId)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** used to modify a manga's meta parameters */
val meta = handler(
pathParam<Int>("mangaId"),
formParam<String>("key"),
formParam<String>("value"),
documentWith = {
withOperation {
summary("Add data to manga")
description("A simple Key-Value storage in the manga object, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx, mangaId, key, value ->
Manga.modifyMangaMeta(mangaId, key, value)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
val meta =
handler(
pathParam<Int>("mangaId"),
formParam<String>("key"),
formParam<String>("value"),
documentWith = {
withOperation {
summary("Add data to manga")
description("A simple Key-Value storage in the manga object, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx, mangaId, key, value ->
Manga.modifyMangaMeta(mangaId, key, value)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
/** get chapter list when showing a manga */
val chapterList = handler(
pathParam<Int>("mangaId"),
queryParam("onlineFetch", false),
documentWith = {
withOperation {
summary("Get manga chapter list")
description("Get the manga chapter list from the database or online. If there is no chapters in the database it fetches the chapters online. Use onlineFetch to update chapter list.")
}
},
behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.future(future { Chapter.getChapterList(mangaId, onlineFetch) })
},
withResults = {
json<Array<ChapterDataClass>>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
val chapterList =
handler(
pathParam<Int>("mangaId"),
queryParam("onlineFetch", false),
documentWith = {
withOperation {
summary("Get manga chapter list")
description(
"Get the manga chapter list from the database or online. " +
"If there is no chapters in the database it fetches the chapters online. " +
"Use onlineFetch to update chapter list.",
)
}
},
behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.future(future { Chapter.getChapterList(mangaId, onlineFetch) })
},
withResults = {
json<Array<ChapterDataClass>>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
/** batch edit chapters of single manga */
val chapterBatch = handler(
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Chapters update multiple")
description("Update multiple chapters of single manga. For batch marking as read, or bookmarking")
}
body<Chapter.MangaChapterBatchEditInput>()
},
behaviorOf = { ctx, mangaId ->
val input = json.decodeFromString<Chapter.MangaChapterBatchEditInput>(ctx.body())
Chapter.modifyChapters(input, mangaId)
},
withResults = {
httpCode(HttpCode.OK)
}
)
val chapterBatch =
handler(
pathParam<Int>("mangaId"),
documentWith = {
withOperation {
summary("Chapters update multiple")
description("Update multiple chapters of single manga. For batch marking as read, or bookmarking")
}
body<Chapter.MangaChapterBatchEditInput>()
},
behaviorOf = { ctx, mangaId ->
val input = json.decodeFromString<Chapter.MangaChapterBatchEditInput>(ctx.body())
Chapter.modifyChapters(input, mangaId)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** batch edit chapters from multiple manga */
val anyChapterBatch = handler(
documentWith = {
withOperation {
summary("Chapters update multiple")
description("Update multiple chapters on any manga. For batch marking as read, or bookmarking")
}
body<Chapter.ChapterBatchEditInput>()
},
behaviorOf = { ctx ->
val input = json.decodeFromString<Chapter.ChapterBatchEditInput>(ctx.body())
Chapter.modifyChapters(
Chapter.MangaChapterBatchEditInput(
input.chapterIds,
null,
input.change
val anyChapterBatch =
handler(
documentWith = {
withOperation {
summary("Chapters update multiple")
description("Update multiple chapters on any manga. For batch marking as read, or bookmarking")
}
body<Chapter.ChapterBatchEditInput>()
},
behaviorOf = { ctx ->
val input = json.decodeFromString<Chapter.ChapterBatchEditInput>(ctx.body())
Chapter.modifyChapters(
Chapter.MangaChapterBatchEditInput(
input.chapterIds,
null,
input.change,
),
)
)
},
withResults = {
httpCode(HttpCode.OK)
}
)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** used to display a chapter, get a chapter in order to show its pages */
val chapterRetrieve = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
documentWith = {
withOperation {
summary("Get a chapter")
description("Get the chapter from the manga id and chapter index. It will also retrieve the pages for this chapter.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex ->
ctx.future(future { getChapterDownloadReadyByIndex(chapterIndex, mangaId) })
},
withResults = {
json<ChapterDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
val chapterRetrieve =
handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
documentWith = {
withOperation {
summary("Get a chapter")
description("Get the chapter from the manga id and chapter index. It will also retrieve the pages for this chapter.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex ->
ctx.future(future { getChapterDownloadReadyByIndex(chapterIndex, mangaId) })
},
withResults = {
json<ChapterDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
/** used to modify a chapter's parameters */
val chapterModify = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
formParam<Boolean?>("read"),
formParam<Boolean?>("bookmarked"),
formParam<Boolean?>("markPrevRead"),
formParam<Int?>("lastPageRead"),
documentWith = {
withOperation {
summary("Modify a chapter")
description("Update user info for a given chapter, such as read status, bookmarked, and more.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead ->
Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead)
val chapterModify =
handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
formParam<Boolean?>("read"),
formParam<Boolean?>("bookmarked"),
formParam<Boolean?>("markPrevRead"),
formParam<Int?>("lastPageRead"),
documentWith = {
withOperation {
summary("Modify a chapter")
description("Update user info for a given chapter, such as read status, bookmarked, and more.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead ->
Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
}
)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** delete a downloaded chapter */
val chapterDelete = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
documentWith = {
withOperation {
summary("Delete a chapter download")
description("Delete the downloaded chapter and its files.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex ->
Chapter.deleteChapter(mangaId, chapterIndex)
val chapterDelete =
handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
documentWith = {
withOperation {
summary("Delete a chapter download")
description("Delete the downloaded chapter and its files.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex ->
Chapter.deleteChapter(mangaId, chapterIndex)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
/** used to modify a chapter's meta parameters */
val chapterMeta = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
formParam<String>("key"),
formParam<String>("value"),
documentWith = {
withOperation {
summary("Add data to chapter")
description("A simple Key-Value storage in the chapter object, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex, key, value ->
Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value)
val chapterMeta =
handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
formParam<String>("key"),
formParam<String>("value"),
documentWith = {
withOperation {
summary("Add data to chapter")
description("A simple Key-Value storage in the chapter object, you can set values for whatever you want inside it.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex, key, value ->
Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
ctx.status(200)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
/** get page at index "index" */
val pageRetrieve = handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
pathParam<Int>("index"),
documentWith = {
withOperation {
summary("Get a chapter page")
description("Get a chapter page for a given index. Cache use can be disabled so it only retrieves it directly from the source.")
}
},
behaviorOf = { ctx, mangaId, chapterIndex, index ->
ctx.future(
future { Page.getPageImage(mangaId, chapterIndex, index) }
.thenApply {
ctx.header("content-type", it.second)
val httpCacheSeconds = 1.days.inWholeSeconds
ctx.header("cache-control", "max-age=$httpCacheSeconds")
it.first
}
)
},
withResults = {
image(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
val pageRetrieve =
handler(
pathParam<Int>("mangaId"),
pathParam<Int>("chapterIndex"),
pathParam<Int>("index"),
documentWith = {
withOperation {
summary("Get a chapter page")
description(
"Get a chapter page for a given index. Cache use can be disabled so it only retrieves it directly from the source.",
)
}
},
behaviorOf = { ctx, mangaId, chapterIndex, index ->
ctx.future(
future { Page.getPageImage(mangaId, chapterIndex, index) }
.thenApply {
ctx.header("content-type", it.second)
val httpCacheSeconds = 1.days.inWholeSeconds
ctx.header("cache-control", "max-age=$httpCacheSeconds")
it.first
},
)
},
withResults = {
image(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
}

View File

@@ -29,217 +29,229 @@ import suwayomi.tachidesk.server.util.withOperation
object SourceController {
/** list of sources */
val list = handler(
documentWith = {
withOperation {
summary("Sources list")
description("List of sources")
}
},
behaviorOf = { ctx ->
ctx.json(Source.getSourceList())
},
withResults = {
json<Array<SourceDataClass>>(HttpCode.OK)
}
)
val list =
handler(
documentWith = {
withOperation {
summary("Sources list")
description("List of sources")
}
},
behaviorOf = { ctx ->
ctx.json(Source.getSourceList())
},
withResults = {
json<Array<SourceDataClass>>(HttpCode.OK)
},
)
/** fetch source with id `sourceId` */
val retrieve = handler(
pathParam<Long>("sourceId"),
documentWith = {
withOperation {
summary("Source fetch")
description("Fetch source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId ->
ctx.json(Source.getSource(sourceId)!!)
},
withResults = {
json<SourceDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
}
)
val retrieve =
handler(
pathParam<Long>("sourceId"),
documentWith = {
withOperation {
summary("Source fetch")
description("Fetch source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId ->
ctx.json(Source.getSource(sourceId)!!)
},
withResults = {
json<SourceDataClass>(HttpCode.OK)
httpCode(HttpCode.NOT_FOUND)
},
)
/** popular mangas from source with id `sourceId` */
val popular = handler(
pathParam<Long>("sourceId"),
pathParam<Int>("pageNum"),
documentWith = {
withOperation {
summary("Source popular manga")
description("Popular mangas from source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId, pageNum ->
ctx.future(
future {
MangaList.getMangaList(sourceId, pageNum, popular = true)
val popular =
handler(
pathParam<Long>("sourceId"),
pathParam<Int>("pageNum"),
documentWith = {
withOperation {
summary("Source popular manga")
description("Popular mangas from source with id `sourceId`")
}
)
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
}
)
},
behaviorOf = { ctx, sourceId, pageNum ->
ctx.future(
future {
MangaList.getMangaList(sourceId, pageNum, popular = true)
},
)
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
},
)
/** latest mangas from source with id `sourceId` */
val latest = handler(
pathParam<Long>("sourceId"),
pathParam<Int>("pageNum"),
documentWith = {
withOperation {
summary("Source latest manga")
description("Latest mangas from source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId, pageNum ->
ctx.future(
future {
MangaList.getMangaList(sourceId, pageNum, popular = false)
val latest =
handler(
pathParam<Long>("sourceId"),
pathParam<Int>("pageNum"),
documentWith = {
withOperation {
summary("Source latest manga")
description("Latest mangas from source with id `sourceId`")
}
)
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
}
)
},
behaviorOf = { ctx, sourceId, pageNum ->
ctx.future(
future {
MangaList.getMangaList(sourceId, pageNum, popular = false)
},
)
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
},
)
/** fetch preferences of source with id `sourceId` */
val getPreferences = handler(
pathParam<Long>("sourceId"),
documentWith = {
withOperation {
summary("Source preferences")
description("Fetch preferences of source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId ->
ctx.json(Source.getSourcePreferences(sourceId))
},
withResults = {
json<Array<Source.PreferenceObject>>(HttpCode.OK)
}
)
val getPreferences =
handler(
pathParam<Long>("sourceId"),
documentWith = {
withOperation {
summary("Source preferences")
description("Fetch preferences of source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId ->
ctx.json(Source.getSourcePreferences(sourceId))
},
withResults = {
json<Array<Source.PreferenceObject>>(HttpCode.OK)
},
)
/** set one preference of source with id `sourceId` */
val setPreference = handler(
pathParam<Long>("sourceId"),
documentWith = {
withOperation {
summary("Source preference set")
description("Set one preference of source with id `sourceId`")
}
body<SourcePreferenceChange>()
},
behaviorOf = { ctx, sourceId ->
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
ctx.json(Source.setSourcePreference(sourceId, preferenceChange.position, preferenceChange.value))
},
withResults = {
httpCode(HttpCode.OK)
}
)
val setPreference =
handler(
pathParam<Long>("sourceId"),
documentWith = {
withOperation {
summary("Source preference set")
description("Set one preference of source with id `sourceId`")
}
body<SourcePreferenceChange>()
},
behaviorOf = { ctx, sourceId ->
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
ctx.json(Source.setSourcePreference(sourceId, preferenceChange.position, preferenceChange.value))
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** fetch filters of source with id `sourceId` */
val getFilters = handler(
pathParam<Long>("sourceId"),
queryParam("reset", false),
documentWith = {
withOperation {
summary("Source filters")
description("Fetch filters of source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId, reset ->
ctx.json(Search.getFilterList(sourceId, reset))
},
withResults = {
json<Array<Search.FilterObject>>(HttpCode.OK)
}
)
val getFilters =
handler(
pathParam<Long>("sourceId"),
queryParam("reset", false),
documentWith = {
withOperation {
summary("Source filters")
description("Fetch filters of source with id `sourceId`")
}
},
behaviorOf = { ctx, sourceId, reset ->
ctx.json(Search.getFilterList(sourceId, reset))
},
withResults = {
json<Array<Search.FilterObject>>(HttpCode.OK)
},
)
private val json by DI.global.instance<Json>()
/** change filters of source with id `sourceId` */
val setFilters = handler(
pathParam<Long>("sourceId"),
documentWith = {
withOperation {
summary("Source filters set")
description("Change filters of source with id `sourceId`")
}
body<FilterChange>()
body<Array<FilterChange>>()
},
behaviorOf = { ctx, sourceId ->
val filterChange = try {
json.decodeFromString<List<FilterChange>>(ctx.body())
} catch (e: Exception) {
listOf(json.decodeFromString<FilterChange>(ctx.body()))
}
val setFilters =
handler(
pathParam<Long>("sourceId"),
documentWith = {
withOperation {
summary("Source filters set")
description("Change filters of source with id `sourceId`")
}
body<FilterChange>()
body<Array<FilterChange>>()
},
behaviorOf = { ctx, sourceId ->
val filterChange =
try {
json.decodeFromString<List<FilterChange>>(ctx.body())
} catch (e: Exception) {
listOf(json.decodeFromString<FilterChange>(ctx.body()))
}
ctx.json(Search.setFilter(sourceId, filterChange))
},
withResults = {
httpCode(HttpCode.OK)
}
)
ctx.json(Search.setFilter(sourceId, filterChange))
},
withResults = {
httpCode(HttpCode.OK)
},
)
/** single source search */
val searchSingle = handler(
pathParam<Long>("sourceId"),
queryParam("searchTerm", ""),
queryParam("pageNum", 1),
documentWith = {
withOperation {
summary("Source search")
description("Single source search")
}
},
behaviorOf = { ctx, sourceId, searchTerm, pageNum ->
ctx.future(future { Search.sourceSearch(sourceId, searchTerm, pageNum) })
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
}
)
val searchSingle =
handler(
pathParam<Long>("sourceId"),
queryParam("searchTerm", ""),
queryParam("pageNum", 1),
documentWith = {
withOperation {
summary("Source search")
description("Single source search")
}
},
behaviorOf = { ctx, sourceId, searchTerm, pageNum ->
ctx.future(future { Search.sourceSearch(sourceId, searchTerm, pageNum) })
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
},
)
/** quick search single source filter */
val quickSearchSingle = handler(
pathParam<Long>("sourceId"),
queryParam("pageNum", 1),
documentWith = {
withOperation {
summary("Source manga quick search")
description("Returns list of manga from source matching posted searchTerm and filter")
}
body<FilterData>()
},
behaviorOf = { ctx, sourceId, pageNum ->
val filter = json.decodeFromString<FilterData>(ctx.body())
ctx.future(future { Search.sourceFilter(sourceId, pageNum, filter) })
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
}
)
val quickSearchSingle =
handler(
pathParam<Long>("sourceId"),
queryParam("pageNum", 1),
documentWith = {
withOperation {
summary("Source manga quick search")
description("Returns list of manga from source matching posted searchTerm and filter")
}
body<FilterData>()
},
behaviorOf = { ctx, sourceId, pageNum ->
val filter = json.decodeFromString<FilterData>(ctx.body())
ctx.future(future { Search.sourceFilter(sourceId, pageNum, filter) })
},
withResults = {
json<PagedMangaListDataClass>(HttpCode.OK)
},
)
/** all source search */
val searchAll = handler(
pathParam<String>("searchTerm"),
documentWith = {
withOperation {
summary("Source global search")
description("All source search")
}
},
behaviorOf = { ctx, searchTerm -> // TODO
ctx.json(Search.sourceGlobalSearch(searchTerm))
},
withResults = {
httpCode(HttpCode.OK)
}
)
val searchAll =
handler(
pathParam<String>("searchTerm"),
documentWith = {
withOperation {
summary("Source global search")
description("All source search")
}
},
behaviorOf = { ctx, searchTerm -> // TODO
ctx.json(Search.sourceGlobalSearch(searchTerm))
},
withResults = {
httpCode(HttpCode.OK)
},
)
}

View File

@@ -30,25 +30,26 @@ object UpdateController {
private val logger = KotlinLogging.logger { }
/** get recently updated manga chapters */
val recentChapters = handler(
pathParam<Int>("pageNum"),
documentWith = {
withOperation {
summary("Updates fetch")
description("Get recently updated manga chapters")
}
},
behaviorOf = { ctx, pageNum ->
ctx.future(
future {
Chapter.getRecentChapters(pageNum)
val recentChapters =
handler(
pathParam<Int>("pageNum"),
documentWith = {
withOperation {
summary("Updates fetch")
description("Get recently updated manga chapters")
}
)
},
withResults = {
json<PagedMangaChapterListDataClass>(HttpCode.OK)
}
)
},
behaviorOf = { ctx, pageNum ->
ctx.future(
future {
Chapter.getRecentChapters(pageNum)
},
)
},
withResults = {
json<PagedMangaChapterListDataClass>(HttpCode.OK)
},
)
/**
* Class made for handling return type in the documentation for [recentChapters],
@@ -56,42 +57,43 @@ object UpdateController {
*/
private class PagedMangaChapterListDataClass : PaginatedList<MangaChapterDataClass>(emptyList(), false)
val categoryUpdate = handler(
formParam<Int?>("categoryId"),
documentWith = {
withOperation {
summary("Updater start")
description("Starts the updater")
}
},
behaviorOf = { ctx, categoryId ->
val updater by DI.global.instance<IUpdater>()
if (categoryId == null) {
logger.info { "Adding Library to Update Queue" }
updater.addCategoriesToUpdateQueue(
Category.getCategoryList(),
clear = true,
forceAll = false
)
} else {
val category = Category.getCategoryById(categoryId)
if (category != null) {
val categoryUpdate =
handler(
formParam<Int?>("categoryId"),
documentWith = {
withOperation {
summary("Updater start")
description("Starts the updater")
}
},
behaviorOf = { ctx, categoryId ->
val updater by DI.global.instance<IUpdater>()
if (categoryId == null) {
logger.info { "Adding Library to Update Queue" }
updater.addCategoriesToUpdateQueue(
listOf(category),
Category.getCategoryList(),
clear = true,
forceAll = true
forceAll = false,
)
} else {
logger.info { "No Category found" }
ctx.status(HttpCode.BAD_REQUEST)
val category = Category.getCategoryById(categoryId)
if (category != null) {
updater.addCategoriesToUpdateQueue(
listOf(category),
clear = true,
forceAll = true,
)
} else {
logger.info { "No Category found" }
ctx.status(HttpCode.BAD_REQUEST)
}
}
}
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.BAD_REQUEST)
}
)
},
withResults = {
httpCode(HttpCode.OK)
httpCode(HttpCode.BAD_REQUEST)
},
)
fun categoryUpdateWS(ws: WsConfig) {
ws.onConnect { ctx ->
@@ -105,42 +107,44 @@ object UpdateController {
}
}
val updateSummary = handler(
documentWith = {
withOperation {
summary("Updater summary")
description("Gets the latest updater summary")
}
},
behaviorOf = { ctx ->
val updater by DI.global.instance<IUpdater>()
ctx.json(updater.status.value)
},
withResults = {
json<UpdateStatus>(HttpCode.OK)
}
)
val reset = handler(
documentWith = {
withOperation {
summary("Updater reset")
description("Stops and resets the Updater")
}
},
behaviorOf = { ctx ->
val updater by DI.global.instance<IUpdater>()
logger.info { "Resetting Updater" }
ctx.future(
future {
updater.reset()
}.thenApply {
ctx.status(HttpCode.OK)
val updateSummary =
handler(
documentWith = {
withOperation {
summary("Updater summary")
description("Gets the latest updater summary")
}
)
},
withResults = {
httpCode(HttpCode.OK)
}
)
},
behaviorOf = { ctx ->
val updater by DI.global.instance<IUpdater>()
ctx.json(updater.status.value)
},
withResults = {
json<UpdateStatus>(HttpCode.OK)
},
)
val reset =
handler(
documentWith = {
withOperation {
summary("Updater reset")
description("Stops and resets the Updater")
}
},
behaviorOf = { ctx ->
val updater by DI.global.instance<IUpdater>()
logger.info { "Resetting Updater" }
ctx.future(
future {
updater.reset()
}.thenApply {
ctx.status(HttpCode.OK)
},
)
},
withResults = {
httpCode(HttpCode.OK)
},
)
}

View File

@@ -35,10 +35,11 @@ object Category {
return transaction {
if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null) {
val newCategoryId = CategoryTable.insertAndGetId {
it[CategoryTable.name] = name
it[CategoryTable.order] = Int.MAX_VALUE
}.value
val newCategoryId =
CategoryTable.insertAndGetId {
it[CategoryTable.name] = name
it[CategoryTable.order] = Int.MAX_VALUE
}.value
normalizeCategories()
@@ -49,10 +50,20 @@ object Category {
}
}
fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?, includeInUpdate: Int?) {
fun updateCategory(
categoryId: Int,
name: String?,
isDefault: Boolean?,
includeInUpdate: Int?,
) {
transaction {
CategoryTable.update({ CategoryTable.id eq categoryId }) {
if (categoryId != DEFAULT_CATEGORY_ID && name != null && !name.equals(DEFAULT_CATEGORY_NAME, ignoreCase = true)) it[CategoryTable.name] = name
if (
categoryId != DEFAULT_CATEGORY_ID && name != null &&
!name.equals(DEFAULT_CATEGORY_NAME, ignoreCase = true)
) {
it[CategoryTable.name] = name
}
if (categoryId != DEFAULT_CATEGORY_ID && isDefault != null) it[CategoryTable.isDefault] = isDefault
if (includeInUpdate != null) it[CategoryTable.includeInUpdate] = includeInUpdate
}
@@ -62,10 +73,16 @@ object Category {
/**
* Move the category from order number `from` to `to`
*/
fun reorderCategory(from: Int, to: Int) {
fun reorderCategory(
from: Int,
to: Int,
) {
if (from == 0 || to == 0) return
transaction {
val categories = CategoryTable.select { CategoryTable.id neq DEFAULT_CATEGORY_ID }.orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
val categories =
CategoryTable.select {
CategoryTable.id neq DEFAULT_CATEGORY_ID
}.orderBy(CategoryTable.order to SortOrder.ASC).toMutableList()
categories.add(to - 1, categories.removeAt(from - 1))
categories.forEachIndexed { index, cat ->
CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) {
@@ -98,14 +115,15 @@ object Category {
}
}
private fun needsDefaultCategory() = transaction {
MangaTable
.leftJoin(CategoryMangaTable)
.select { MangaTable.inLibrary eq true }
.andWhere { CategoryMangaTable.manga.isNull() }
.empty()
.not()
}
private fun needsDefaultCategory() =
transaction {
MangaTable
.leftJoin(CategoryMangaTable)
.select { MangaTable.inLibrary eq true }
.andWhere { CategoryMangaTable.manga.isNull() }
.empty()
.not()
}
const val DEFAULT_CATEGORY_ID = 0
const val DEFAULT_CATEGORY_NAME = "Default"
@@ -156,11 +174,16 @@ object Category {
}
}
fun modifyMeta(categoryId: Int, key: String, value: String) {
fun modifyMeta(
categoryId: Int,
key: String,
value: String,
) {
transaction {
val meta = transaction {
CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
}.firstOrNull()
val meta =
transaction {
CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }
}.firstOrNull()
if (meta == null) {
CategoryMetaTable.insert {

View File

@@ -31,9 +31,16 @@ import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.toDataClass
object CategoryManga {
fun addMangaToCategory(mangaId: Int, categoryId: Int) {
fun addMangaToCategory(
mangaId: Int,
categoryId: Int,
) {
if (categoryId == DEFAULT_CATEGORY_ID) return
fun notAlreadyInCategory() = CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.isEmpty()
fun notAlreadyInCategory() =
CategoryMangaTable.select {
(CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId)
}.isEmpty()
transaction {
if (notAlreadyInCategory()) {
@@ -45,7 +52,10 @@ object CategoryManga {
}
}
fun removeMangaFromCategory(mangaId: Int, categoryId: Int) {
fun removeMangaFromCategory(
mangaId: Int,
categoryId: Int,
) {
if (categoryId == DEFAULT_CATEGORY_ID) return
transaction {
CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }
@@ -57,12 +67,18 @@ object CategoryManga {
*/
fun getCategoryMangaList(categoryId: Int): List<MangaDataClass> {
// Select the required columns from the MangaTable and add the aggregate functions to compute unread, download, and chapter counts
val unreadCount = wrapAsExpression<Long>(
ChapterTable.slice(ChapterTable.id.count()).select((ChapterTable.isRead eq false) and (ChapterTable.manga eq MangaTable.id))
)
val downloadedCount = wrapAsExpression<Long>(
ChapterTable.slice(ChapterTable.id.count()).select((ChapterTable.isDownloaded eq true) and (ChapterTable.manga eq MangaTable.id))
)
val unreadCount =
wrapAsExpression<Long>(
ChapterTable.slice(
ChapterTable.id.count(),
).select((ChapterTable.isRead eq false) and (ChapterTable.manga eq MangaTable.id)),
)
val downloadedCount =
wrapAsExpression<Long>(
ChapterTable.slice(
ChapterTable.id.count(),
).select((ChapterTable.isDownloaded eq true) and (ChapterTable.manga eq MangaTable.id)),
)
val chapterCount = ChapterTable.id.count().alias("chapter_count")
val lastReadAt = ChapterTable.lastReadAt.max().alias("last_read_at")
@@ -80,19 +96,20 @@ object CategoryManga {
return transaction {
// Fetch data from the MangaTable and join with the CategoryMangaTable, if a category is specified
val query = if (categoryId == DEFAULT_CATEGORY_ID) {
MangaTable
.leftJoin(ChapterTable, { MangaTable.id }, { ChapterTable.manga })
.leftJoin(CategoryMangaTable)
.slice(columns = selectedColumns)
.select { (MangaTable.inLibrary eq true) and CategoryMangaTable.category.isNull() }
} else {
MangaTable
.innerJoin(CategoryMangaTable)
.leftJoin(ChapterTable, { MangaTable.id }, { ChapterTable.manga })
.slice(columns = selectedColumns)
.select { (MangaTable.inLibrary eq true) and (CategoryMangaTable.category eq categoryId) }
}
val query =
if (categoryId == DEFAULT_CATEGORY_ID) {
MangaTable
.leftJoin(ChapterTable, { MangaTable.id }, { ChapterTable.manga })
.leftJoin(CategoryMangaTable)
.slice(columns = selectedColumns)
.select { (MangaTable.inLibrary eq true) and CategoryMangaTable.category.isNull() }
} else {
MangaTable
.innerJoin(CategoryMangaTable)
.leftJoin(ChapterTable, { MangaTable.id }, { ChapterTable.manga })
.slice(columns = selectedColumns)
.select { (MangaTable.inLibrary eq true) and (CategoryMangaTable.category eq categoryId) }
}
// Join with the ChapterTable to fetch the last read chapter for each manga
query.groupBy(*MangaTable.columns.toTypedArray()).map(transform)
@@ -104,7 +121,9 @@ object CategoryManga {
*/
fun getMangaCategories(mangaId: Int): List<CategoryDataClass> {
return transaction {
CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryMangaTable.innerJoin(CategoryTable).select {
CategoryMangaTable.manga eq mangaId
}.orderBy(CategoryTable.order to SortOrder.ASC).map {
CategoryTable.toDataClass(it)
}
}

View File

@@ -45,7 +45,10 @@ object Chapter {
private val logger = KotlinLogging.logger { }
/** get chapter list when showing a manga */
suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean = false): List<ChapterDataClass> {
suspend fun getChapterList(
mangaId: Int,
onlineFetch: Boolean = false,
): List<ChapterDataClass> {
return if (onlineFetch) {
getSourceChapters(mangaId)
} else {
@@ -68,10 +71,11 @@ object Chapter {
private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
val chapterList = fetchChapterList(mangaId)
val dbChapterMap = transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.associateBy({ it[ChapterTable.url] }, { it })
}
val dbChapterMap =
transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.associateBy({ it[ChapterTable.url] }, { it })
}
val chapterIds = chapterList.map { dbChapterMap.getValue(it.url)[ChapterTable.id] }
val chapterMetas = getChaptersMetaMaps(chapterIds)
@@ -88,21 +92,17 @@ object Chapter {
chapterNumber = it.chapter_number,
scanlator = it.scanlator,
mangaId = mangaId,
read = dbChapter[ChapterTable.isRead],
bookmarked = dbChapter[ChapterTable.isBookmarked],
lastPageRead = dbChapter[ChapterTable.lastPageRead],
lastReadAt = dbChapter[ChapterTable.lastReadAt],
index = chapterList.size - index,
fetchedAt = dbChapter[ChapterTable.fetchedAt],
realUrl = dbChapter[ChapterTable.realUrl],
downloaded = dbChapter[ChapterTable.isDownloaded],
pageCount = dbChapter[ChapterTable.pageCount],
chapterCount = chapterList.size,
meta = chapterMetas.getValue(dbChapter[ChapterTable.id])
meta = chapterMetas.getValue(dbChapter[ChapterTable.id]),
)
}
}
@@ -111,10 +111,11 @@ object Chapter {
val manga = getManga(mangaId)
val source = getCatalogueSourceOrStub(manga.sourceId.toLong())
val sManga = SManga.create().apply {
title = manga.title
url = manga.url
}
val sManga =
SManga.create().apply {
title = manga.title
url = manga.url
}
val numberOfCurrentChapters = getCountOfMangaChapters(mangaId)
val chapterList = source.getChapterList(sManga)
@@ -143,9 +144,10 @@ object Chapter {
it[fetchedAt] = now++
it[ChapterTable.manga] = mangaId
it[realUrl] = runCatching {
(source as? HttpSource)?.getChapterUrl(fetchedChapter)
}.getOrNull()
it[realUrl] =
runCatching {
(source as? HttpSource)?.getChapterUrl(fetchedChapter)
}.getOrNull()
}
} else {
ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) {
@@ -157,9 +159,10 @@ object Chapter {
it[sourceOrder] = index + 1
it[ChapterTable.manga] = mangaId
it[realUrl] = runCatching {
(source as? HttpSource)?.getChapterUrl(fetchedChapter)
}.getOrNull()
it[realUrl] =
runCatching {
(source as? HttpSource)?.getChapterUrl(fetchedChapter)
}.getOrNull()
}
}
}
@@ -169,18 +172,20 @@ object Chapter {
}
}
val newChapters = transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC).toList()
}
val newChapters =
transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC).toList()
}
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
val dbChapterCount = newChapters.count()
if (dbChapterCount > chapterList.size) { // we got some clean up due
val dbChapterList = transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.orderBy(ChapterTable.url to ASC).toList()
}
val dbChapterList =
transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.orderBy(ChapterTable.url to ASC).toList()
}
val chapterUrls = chapterList.map { it.url }.toSet()
dbChapterList.forEachIndexed { index, dbChapter ->
@@ -203,7 +208,11 @@ object Chapter {
return chapterList
}
private fun downloadNewChapters(mangaId: Int, prevNumberOfChapters: Int, updatedChapterList: List<ResultRow>) {
private fun downloadNewChapters(
mangaId: Int,
prevNumberOfChapters: Int,
updatedChapterList: List<ResultRow>,
) {
// convert numbers to be index based
val currentNumberOfChapters = (prevNumberOfChapters - 1).coerceAtLeast(0)
val updatedNumberOfChapters = (updatedChapterList.size - 1).coerceAtLeast(0)
@@ -224,8 +233,9 @@ object Chapter {
// make sure to only consider the latest chapters. e.g. old unread chapters should be ignored
val latestReadChapterIndex =
updatedChapterList.indexOfFirst { it[ChapterTable.isRead] }.takeIf { it > -1 } ?: return
val unreadChapters = updatedChapterList.subList(numberOfNewChapters, latestReadChapterIndex)
.filter { !it[ChapterTable.isRead] }
val unreadChapters =
updatedChapterList.subList(numberOfNewChapters, latestReadChapterIndex)
.filter { !it[ChapterTable.isRead] }
val skipDueToUnreadChapters = serverConfig.excludeEntryWithUnreadChapters.value && unreadChapters.isNotEmpty()
if (skipDueToUnreadChapters) {
@@ -235,9 +245,10 @@ object Chapter {
val firstChapterToDownloadIndex =
(numberOfNewChapters - serverConfig.autoDownloadAheadLimit.value).coerceAtLeast(0)
val chapterIdsToDownload = newChapters.subList(firstChapterToDownloadIndex, numberOfNewChapters)
.filter { !it[ChapterTable.isRead] && !it[ChapterTable.isDownloaded] }
.map { it[ChapterTable.id].value }
val chapterIdsToDownload =
newChapters.subList(firstChapterToDownloadIndex, numberOfNewChapters)
.filter { !it[ChapterTable.isRead] && !it[ChapterTable.isDownloaded] }
.map { it[ChapterTable.id].value }
if (chapterIdsToDownload.isEmpty()) {
return
@@ -254,7 +265,7 @@ object Chapter {
isRead: Boolean?,
isBookmarked: Boolean?,
markPrevRead: Boolean?,
lastPageRead: Int?
lastPageRead: Int?,
) {
transaction {
if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) {
@@ -285,23 +296,26 @@ object Chapter {
val isRead: Boolean? = null,
val isBookmarked: Boolean? = null,
val lastPageRead: Int? = null,
val delete: Boolean? = null
val delete: Boolean? = null,
)
@Serializable
data class MangaChapterBatchEditInput(
val chapterIds: List<Int>? = null,
val chapterIndexes: List<Int>? = null,
val change: ChapterChange?
val change: ChapterChange?,
)
@Serializable
data class ChapterBatchEditInput(
val chapterIds: List<Int>? = null,
val change: ChapterChange?
val change: ChapterChange?,
)
fun modifyChapters(input: MangaChapterBatchEditInput, mangaId: Int? = null) {
fun modifyChapters(
input: MangaChapterBatchEditInput,
mangaId: Int? = null,
) {
// Make sure change is defined
if (input.change == null) return
val (isRead, isBookmarked, lastPageRead, delete) = input.change
@@ -315,25 +329,26 @@ object Chapter {
if (listOfNotNull(isRead, isBookmarked, lastPageRead).isEmpty()) return
// Make sure some filter is defined
val condition = when {
mangaId != null ->
// mangaId is not null, scope query under manga
when {
input.chapterIds != null ->
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.id inList input.chapterIds) }
input.chapterIndexes != null ->
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder inList input.chapterIndexes) }
else -> null
val condition =
when {
mangaId != null ->
// mangaId is not null, scope query under manga
when {
input.chapterIds != null ->
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.id inList input.chapterIds) }
input.chapterIndexes != null ->
Op.build { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder inList input.chapterIndexes) }
else -> null
}
else -> {
// mangaId is null, only chapterIndexes is valid for this case
when {
input.chapterIds != null ->
Op.build { (ChapterTable.id inList input.chapterIds) }
else -> null
}
}
else -> {
// mangaId is null, only chapterIndexes is valid for this case
when {
input.chapterIds != null ->
Op.build { (ChapterTable.id inList input.chapterIds) }
else -> null
}
}
} ?: return
} ?: return
transaction {
val now = Instant.now().epochSecond
@@ -368,7 +383,12 @@ object Chapter {
}
}
fun modifyChapterMeta(mangaId: Int, chapterIndex: Int, key: String, value: String) {
fun modifyChapterMeta(
mangaId: Int,
chapterIndex: Int,
key: String,
value: String,
) {
transaction {
val chapterId =
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
@@ -377,7 +397,11 @@ object Chapter {
}
}
fun modifyChapterMeta(chapterId: Int, key: String, value: String) {
fun modifyChapterMeta(
chapterId: Int,
key: String,
value: String,
) {
transaction {
val meta =
ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }
@@ -397,7 +421,10 @@ object Chapter {
}
}
fun deleteChapter(mangaId: Int, chapterIndex: Int) {
fun deleteChapter(
mangaId: Int,
chapterIndex: Int,
) {
transaction {
val chapterId =
ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }
@@ -411,19 +438,23 @@ object Chapter {
}
}
private fun deleteChapters(input: MangaChapterBatchEditInput, mangaId: Int? = null) {
private fun deleteChapters(
input: MangaChapterBatchEditInput,
mangaId: Int? = null,
) {
if (input.chapterIds != null) {
deleteChapters(input.chapterIds)
} else if (input.chapterIndexes != null && mangaId != null) {
transaction {
val chapterIds = ChapterTable.slice(ChapterTable.manga, ChapterTable.id)
.select { (ChapterTable.sourceOrder inList input.chapterIndexes) and (ChapterTable.manga eq mangaId) }
.map { row ->
val chapterId = row[ChapterTable.id].value
ChapterDownloadHelper.delete(mangaId, chapterId)
val chapterIds =
ChapterTable.slice(ChapterTable.manga, ChapterTable.id)
.select { (ChapterTable.sourceOrder inList input.chapterIndexes) and (ChapterTable.manga eq mangaId) }
.map { row ->
val chapterId = row[ChapterTable.id].value
ChapterDownloadHelper.delete(mangaId, chapterId)
chapterId
}
chapterId
}
ChapterTable.update({ ChapterTable.id inList chapterIds }) {
it[isDownloaded] = false
@@ -457,7 +488,7 @@ object Chapter {
.map {
MangaChapterDataClass(
MangaTable.toDataClass(it),
ChapterTable.toDataClass(it)
ChapterTable.toDataClass(it),
)
}
}

View File

@@ -12,11 +12,18 @@ import java.io.File
import java.io.InputStream
object ChapterDownloadHelper {
fun getImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, String> {
fun getImage(
mangaId: Int,
chapterId: Int,
index: Int,
): Pair<InputStream, String> {
return provider(mangaId, chapterId).getImage().execute(index)
}
fun delete(mangaId: Int, chapterId: Int): Boolean {
fun delete(
mangaId: Int,
chapterId: Int,
): Boolean {
return provider(mangaId, chapterId).delete()
}
@@ -25,13 +32,16 @@ object ChapterDownloadHelper {
chapterId: Int,
download: DownloadChapter,
scope: CoroutineScope,
step: suspend (DownloadChapter?, Boolean) -> Unit
step: suspend (DownloadChapter?, Boolean) -> Unit,
): Boolean {
return provider(mangaId, chapterId).download().execute(download, scope, step)
}
// return the appropriate provider based on how the download was saved. For the logic is simple but will evolve when new types of downloads are available
private fun provider(mangaId: Int, chapterId: Int): ChaptersFilesProvider {
private fun provider(
mangaId: Int,
chapterId: Int,
): ChaptersFilesProvider {
val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId))
val cbzFile = File(getChapterCbzPath(mangaId, chapterId))
if (cbzFile.exists()) return ArchiveProvider(mangaId, chapterId)

View File

@@ -29,9 +29,10 @@ object Library {
val manga = getManga(mangaId)
if (!manga.inLibrary) {
transaction {
val defaultCategories = CategoryTable.select {
(CategoryTable.isDefault eq true) and (CategoryTable.id neq Category.DEFAULT_CATEGORY_ID)
}.toList()
val defaultCategories =
CategoryTable.select {
(CategoryTable.isDefault eq true) and (CategoryTable.id neq Category.DEFAULT_CATEGORY_ID)
}.toList()
val existingCategories = CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.toList()
MangaTable.update({ MangaTable.id eq manga.id }) {
@@ -66,7 +67,10 @@ object Library {
}
}
fun handleMangaThumbnail(mangaId: Int, inLibrary: Boolean) {
fun handleMangaThumbnail(
mangaId: Int,
inLibrary: Boolean,
) {
scope.launch {
try {
if (inLibrary) {

View File

@@ -58,7 +58,10 @@ import java.util.concurrent.ConcurrentHashMap
private val logger = KotlinLogging.logger { }
object Manga {
private fun truncate(text: String?, maxLength: Int): String? {
private fun truncate(
text: String?,
maxLength: Int,
): String? {
return if (text?.length ?: 0 > maxLength) {
text?.take(maxLength - 3) + "..."
} else {
@@ -66,7 +69,10 @@ object Manga {
}
}
suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
suspend fun getManga(
mangaId: Int,
onlineFetch: Boolean = false,
): MangaDataClass {
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
return if (!onlineFetch && mangaEntry[MangaTable.initialized]) {
@@ -79,14 +85,11 @@ object Manga {
MangaDataClass(
id = mangaId,
sourceId = mangaEntry[MangaTable.sourceReference].toString(),
url = mangaEntry[MangaTable.url],
title = mangaEntry[MangaTable.title],
thumbnailUrl = proxyThumbnailUrl(mangaId),
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
initialized = true,
artist = sManga.artist,
author = sManga.author,
description = sManga.description,
@@ -100,7 +103,7 @@ object Manga {
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
freshData = true
freshData = true,
)
}
}
@@ -108,12 +111,14 @@ object Manga {
suspend fun fetchManga(mangaId: Int): SManga? {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference])
?: return null
val sManga = SManga.create().apply {
url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title]
}
val source =
getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference])
?: return null
val sManga =
SManga.create().apply {
url = mangaEntry[MangaTable.url]
title = mangaEntry[MangaTable.title]
}
val networkManga = source.getMangaDetails(sManga)
sManga.copyFrom(networkManga)
@@ -139,9 +144,10 @@ object Manga {
clearThumbnail(mangaId)
}
it[MangaTable.realUrl] = runCatching {
(source as? HttpSource)?.getMangaUrl(sManga)
}.getOrNull()
it[MangaTable.realUrl] =
runCatching {
(source as? HttpSource)?.getMangaUrl(sManga)
}.getOrNull()
it[MangaTable.lastFetchedAt] = Instant.now().epochSecond
@@ -152,7 +158,10 @@ object Manga {
return sManga
}
suspend fun getMangaFull(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass {
suspend fun getMangaFull(
mangaId: Int,
onlineFetch: Boolean = false,
): MangaDataClass {
val mangaDaaClass = getManga(mangaId, onlineFetch)
return transaction {
@@ -186,17 +195,17 @@ object Manga {
}
}
private fun getMangaDataClass(mangaId: Int, mangaEntry: ResultRow) = MangaDataClass(
private fun getMangaDataClass(
mangaId: Int,
mangaEntry: ResultRow,
) = MangaDataClass(
id = mangaId,
sourceId = mangaEntry[MangaTable.sourceReference].toString(),
url = mangaEntry[MangaTable.url],
title = mangaEntry[MangaTable.title],
thumbnailUrl = proxyThumbnailUrl(mangaId),
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
initialized = true,
artist = mangaEntry[MangaTable.artist],
author = mangaEntry[MangaTable.author],
description = mangaEntry[MangaTable.description],
@@ -210,7 +219,7 @@ object Manga {
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
freshData = false
freshData = false,
)
fun getMangaMetaMap(mangaId: Int): Map<String, String> {
@@ -220,7 +229,11 @@ object Manga {
}
}
fun modifyMangaMeta(mangaId: Int, key: String, value: String) {
fun modifyMangaMeta(
mangaId: Int,
key: String,
value: String,
) {
transaction {
val meta =
MangaMetaTable.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
@@ -251,45 +264,51 @@ object Manga {
val sourceId = mangaEntry[MangaTable.sourceReference]
return when (val source = getCatalogueSourceOrStub(sourceId)) {
is HttpSource -> getImageResponse(cacheSaveDir, fileName) {
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
?: if (!mangaEntry[MangaTable.initialized]) {
// initialize then try again
getManga(mangaId)
transaction {
MangaTable.select { MangaTable.id eq mangaId }.first()
}[MangaTable.thumbnail_url]!!
} else {
// source provides no thumbnail url for this manga
throw NullPointerException("No thumbnail found")
}
is HttpSource ->
getImageResponse(cacheSaveDir, fileName) {
val thumbnailUrl =
mangaEntry[MangaTable.thumbnail_url]
?: if (!mangaEntry[MangaTable.initialized]) {
// initialize then try again
getManga(mangaId)
transaction {
MangaTable.select { MangaTable.id eq mangaId }.first()
}[MangaTable.thumbnail_url]!!
} else {
// source provides no thumbnail url for this manga
throw NullPointerException("No thumbnail found")
}
source.client.newCall(
GET(thumbnailUrl, source.headers)
).await()
}
source.client.newCall(
GET(thumbnailUrl, source.headers),
).await()
}
is LocalSource -> {
val imageFile = mangaEntry[MangaTable.thumbnail_url]?.let {
val file = File(it)
if (file.exists()) {
file
} else {
null
}
} ?: throw IOException("Thumbnail does not exist")
val contentType = ImageUtil.findImageType { imageFile.inputStream() }?.mime
?: "image/jpeg"
val imageFile =
mangaEntry[MangaTable.thumbnail_url]?.let {
val file = File(it)
if (file.exists()) {
file
} else {
null
}
} ?: throw IOException("Thumbnail does not exist")
val contentType =
ImageUtil.findImageType { imageFile.inputStream() }?.mime
?: "image/jpeg"
imageFile.inputStream() to contentType
}
is StubSource -> getImageResponse(cacheSaveDir, fileName) {
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
?: throw NullPointerException("No thumbnail found")
network.client.newCall(
GET(thumbnailUrl)
).await()
}
is StubSource ->
getImageResponse(cacheSaveDir, fileName) {
val thumbnailUrl =
mangaEntry[MangaTable.thumbnail_url]
?: throw NullPointerException("No thumbnail found")
network.client.newCall(
GET(thumbnailUrl),
).await()
}
else -> throw IllegalArgumentException("Unknown source")
}
@@ -319,14 +338,18 @@ object Manga {
private val downloadAheadQueue = ConcurrentHashMap<String, ConcurrentHashMap.KeySetView<Int, Boolean>>()
private var downloadAheadTimer: Timer? = null
fun downloadAhead(mangaIds: List<Int>, latestReadChapterIds: List<Int> = emptyList()) {
private const val MANGAS_KEY = "mangaIds"
private const val CHAPTERS_KEY = "chapterIds"
fun downloadAhead(
mangaIds: List<Int>,
latestReadChapterIds: List<Int> = emptyList(),
) {
if (serverConfig.autoDownloadAheadLimit.value == 0) {
return
}
val MANGAS_KEY = "mangaIds"
val CHAPTERS_KEY = "chapterIds"
val updateDownloadAheadQueue = { key: String, ids: List<Int> ->
val idSet = downloadAheadQueue[key] ?: ConcurrentHashMap.newKeySet()
idSet.addAll(ids)
@@ -339,17 +362,21 @@ object Manga {
// handle cases where this function gets called multiple times in quick succession.
// this could happen in case e.g. multiple chapters get marked as read without batching the operation
downloadAheadTimer?.cancel()
downloadAheadTimer = Timer().apply {
schedule(
object : TimerTask() {
override fun run() {
downloadAheadChapters(downloadAheadQueue[MANGAS_KEY]!!.toList(), downloadAheadQueue[CHAPTERS_KEY]!!.toList())
downloadAheadQueue.clear()
}
},
5000
)
}
downloadAheadTimer =
Timer().apply {
schedule(
object : TimerTask() {
override fun run() {
downloadAheadChapters(
downloadAheadQueue[MANGAS_KEY]?.toList().orEmpty(),
downloadAheadQueue[CHAPTERS_KEY]?.toList().orEmpty(),
)
downloadAheadQueue.clear()
}
},
5000,
)
}
}
/**
@@ -370,29 +397,38 @@ object Manga {
*
* will download the unread chapters starting from chapter 15
*/
private fun downloadAheadChapters(mangaIds: List<Int>, latestReadChapterIds: List<Int>) {
val mangaToLatestReadChapterIndex = transaction {
ChapterTable.select { (ChapterTable.manga inList mangaIds) and (ChapterTable.isRead eq true) }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC).groupBy { it[ChapterTable.manga].value }
}.mapValues { (_, chapters) -> chapters.firstOrNull()?.let { it[ChapterTable.sourceOrder] } ?: 0 }
private fun downloadAheadChapters(
mangaIds: List<Int>,
latestReadChapterIds: List<Int>,
) {
val mangaToLatestReadChapterIndex =
transaction {
ChapterTable.select { (ChapterTable.manga inList mangaIds) and (ChapterTable.isRead eq true) }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC).groupBy { it[ChapterTable.manga].value }
}.mapValues { (_, chapters) -> chapters.firstOrNull()?.let { it[ChapterTable.sourceOrder] } ?: 0 }
val mangaToUnreadChaptersMap = transaction {
ChapterTable.select { (ChapterTable.manga inList mangaIds) and (ChapterTable.isRead eq false) }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.groupBy { it[ChapterTable.manga].value }
}
val mangaToUnreadChaptersMap =
transaction {
ChapterTable.select { (ChapterTable.manga inList mangaIds) and (ChapterTable.isRead eq false) }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.groupBy { it[ChapterTable.manga].value }
}
val chapterIdsToDownload = mangaToUnreadChaptersMap.map { (mangaId, unreadChapters) ->
val latestReadChapterIndex = mangaToLatestReadChapterIndex[mangaId] ?: 0
val lastChapterToDownloadIndex =
unreadChapters.indexOfLast { it[ChapterTable.sourceOrder] > latestReadChapterIndex && it[ChapterTable.id].value !in latestReadChapterIds }
val unreadChaptersToConsider = unreadChapters.subList(0, lastChapterToDownloadIndex + 1)
val firstChapterToDownloadIndex =
(unreadChaptersToConsider.size - serverConfig.autoDownloadAheadLimit.value).coerceAtLeast(0)
unreadChaptersToConsider.subList(firstChapterToDownloadIndex, lastChapterToDownloadIndex + 1)
.filter { !it[ChapterTable.isDownloaded] }
.map { it[ChapterTable.id].value }
}.flatten()
val chapterIdsToDownload =
mangaToUnreadChaptersMap.map { (mangaId, unreadChapters) ->
val latestReadChapterIndex = mangaToLatestReadChapterIndex[mangaId] ?: 0
val lastChapterToDownloadIndex =
unreadChapters.indexOfLast {
it[ChapterTable.sourceOrder] > latestReadChapterIndex &&
it[ChapterTable.id].value !in latestReadChapterIds
}
val unreadChaptersToConsider = unreadChapters.subList(0, lastChapterToDownloadIndex + 1)
val firstChapterToDownloadIndex =
(unreadChaptersToConsider.size - serverConfig.autoDownloadAheadLimit.value).coerceAtLeast(0)
unreadChaptersToConsider.subList(firstChapterToDownloadIndex, lastChapterToDownloadIndex + 1)
.filter { !it[ChapterTable.isDownloaded] }
.map { it[ChapterTable.id].value }
}.flatten()
logger.info { "downloadAheadChapters: download chapters [${chapterIdsToDownload.joinToString(", ")}]" }

View File

@@ -26,29 +26,35 @@ object MangaList {
return "/api/v1/manga/$mangaId/thumbnail"
}
suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass {
suspend fun getMangaList(
sourceId: Long,
pageNum: Int = 1,
popular: Boolean,
): PagedMangaListDataClass {
require(pageNum > 0) {
"pageNum = $pageNum is not in valid range"
}
val source = getCatalogueSourceOrStub(sourceId)
val mangasPage = if (popular) {
source.getPopularManga(pageNum)
} else {
if (source.supportsLatest) {
source.getLatestUpdates(pageNum)
val mangasPage =
if (popular) {
source.getPopularManga(pageNum)
} else {
throw Exception("Source $source doesn't support latest")
if (source.supportsLatest) {
source.getLatestUpdates(pageNum)
} else {
throw Exception("Source $source doesn't support latest")
}
}
}
return mangasPage.processEntries(sourceId)
}
fun MangasPage.insertOrGet(sourceId: Long): List<Int> {
return transaction {
mangas.map { manga ->
val mangaEntry = MangaTable.select {
(MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId)
}.firstOrNull()
val mangaEntry =
MangaTable.select {
(MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId)
}.firstOrNull()
if (mangaEntry == null) { // create manga entry
MangaTable.insertAndGetId {
it[url] = manga.url
@@ -73,89 +79,87 @@ object MangaList {
fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass {
val mangasPage = this
val mangaList = transaction {
return@transaction mangasPage.mangas.map { manga ->
var mangaEntry = MangaTable.select {
(MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId)
}.firstOrNull()
if (mangaEntry == null) { // create manga entry
val mangaId = MangaTable.insertAndGetId {
it[url] = manga.url
it[title] = manga.title
val mangaList =
transaction {
return@transaction mangasPage.mangas.map { manga ->
var mangaEntry =
MangaTable.select {
(MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId)
}.firstOrNull()
if (mangaEntry == null) { // create manga entry
val mangaId =
MangaTable.insertAndGetId {
it[url] = manga.url
it[title] = manga.title
it[artist] = manga.artist
it[author] = manga.author
it[description] = manga.description
it[genre] = manga.genre
it[status] = manga.status
it[thumbnail_url] = manga.thumbnail_url
it[updateStrategy] = manga.update_strategy.name
it[artist] = manga.artist
it[author] = manga.author
it[description] = manga.description
it[genre] = manga.genre
it[status] = manga.status
it[thumbnail_url] = manga.thumbnail_url
it[updateStrategy] = manga.update_strategy.name
it[sourceReference] = sourceId
}.value
it[sourceReference] = sourceId
}.value
mangaEntry = MangaTable.select {
(MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId)
}.first()
mangaEntry =
MangaTable.select {
(MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId)
}.first()
MangaDataClass(
id = mangaId,
sourceId = sourceId.toString(),
url = manga.url,
title = manga.title,
thumbnailUrl = proxyThumbnailUrl(mangaId),
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
initialized = manga.initialized,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre.toGenreList(),
status = MangaStatus.valueOf(manga.status).name,
inLibrary = false, // It's a new manga entry
inLibraryAt = 0,
meta = getMangaMetaMap(mangaId),
realUrl = mangaEntry[MangaTable.realUrl],
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
freshData = true
)
} else {
val mangaId = mangaEntry[MangaTable.id].value
MangaDataClass(
id = mangaId,
sourceId = sourceId.toString(),
url = manga.url,
title = manga.title,
thumbnailUrl = proxyThumbnailUrl(mangaId),
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
initialized = true,
artist = mangaEntry[MangaTable.artist],
author = mangaEntry[MangaTable.author],
description = mangaEntry[MangaTable.description],
genre = mangaEntry[MangaTable.genre].toGenreList(),
status = MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
inLibrary = mangaEntry[MangaTable.inLibrary],
inLibraryAt = mangaEntry[MangaTable.inLibraryAt],
meta = getMangaMetaMap(mangaId),
realUrl = mangaEntry[MangaTable.realUrl],
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
freshData = false
)
MangaDataClass(
id = mangaId,
sourceId = sourceId.toString(),
url = manga.url,
title = manga.title,
thumbnailUrl = proxyThumbnailUrl(mangaId),
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
initialized = manga.initialized,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.genre.toGenreList(),
status = MangaStatus.valueOf(manga.status).name,
inLibrary = false, // It's a new manga entry
inLibraryAt = 0,
meta = getMangaMetaMap(mangaId),
realUrl = mangaEntry[MangaTable.realUrl],
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
freshData = true,
)
} else {
val mangaId = mangaEntry[MangaTable.id].value
MangaDataClass(
id = mangaId,
sourceId = sourceId.toString(),
url = manga.url,
title = manga.title,
thumbnailUrl = proxyThumbnailUrl(mangaId),
thumbnailUrlLastFetched = mangaEntry[MangaTable.thumbnailUrlLastFetched],
initialized = true,
artist = mangaEntry[MangaTable.artist],
author = mangaEntry[MangaTable.author],
description = mangaEntry[MangaTable.description],
genre = mangaEntry[MangaTable.genre].toGenreList(),
status = MangaStatus.valueOf(mangaEntry[MangaTable.status]).name,
inLibrary = mangaEntry[MangaTable.inLibrary],
inLibraryAt = mangaEntry[MangaTable.inLibraryAt],
meta = getMangaMetaMap(mangaId),
realUrl = mangaEntry[MangaTable.realUrl],
lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt],
chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt],
updateStrategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy]),
freshData = false,
)
}
}
}
}
return PagedMangaListDataClass(
mangaList,
mangasPage.hasNextPage
mangasPage.hasNextPage,
)
}
}

View File

@@ -31,21 +31,30 @@ object Page {
* A page might have a imageUrl ready from the get go, or we might need to
* go an extra step and call fetchImageUrl to get it.
*/
suspend fun getTrueImageUrl(page: Page, source: HttpSource): String {
suspend fun getTrueImageUrl(
page: Page,
source: HttpSource,
): String {
if (page.imageUrl == null) {
page.imageUrl = source.getImageUrl(page)
}
return page.imageUrl!!
}
suspend fun getPageImage(mangaId: Int, chapterIndex: Int, index: Int, progressFlow: ((StateFlow<Int>) -> Unit)? = null): Pair<InputStream, String> {
suspend fun getPageImage(
mangaId: Int,
chapterIndex: Int,
index: Int,
progressFlow: ((StateFlow<Int>) -> Unit)? = null,
): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
val chapterEntry = transaction {
ChapterTable.select {
(ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.first()
}
val chapterEntry =
transaction {
ChapterTable.select {
(ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId)
}.first()
}
val chapterId = chapterEntry[ChapterTable.id].value
val pageEntry =
@@ -54,11 +63,12 @@ object Page {
.orderBy(PageTable.index to SortOrder.ASC)
.limit(1, index.toLong()).first()
}
val tachiyomiPage = Page(
pageEntry[PageTable.index],
pageEntry[PageTable.url],
pageEntry[PageTable.imageUrl]
)
val tachiyomiPage =
Page(
pageEntry[PageTable.index],
pageEntry[PageTable.url],
pageEntry[PageTable.imageUrl],
)
progressFlow?.invoke(tachiyomiPage.progress)
// we treat Local source differently

View File

@@ -20,13 +20,21 @@ import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogue
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
object Search {
suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass {
suspend fun sourceSearch(
sourceId: Long,
searchTerm: String,
pageNum: Int,
): PagedMangaListDataClass {
val source = getCatalogueSourceOrStub(sourceId)
val searchManga = source.getSearchManga(pageNum, searchTerm, getFilterListOf(source))
return searchManga.processEntries(sourceId)
}
suspend fun sourceFilter(sourceId: Long, pageNum: Int, filter: FilterData): PagedMangaListDataClass {
suspend fun sourceFilter(
sourceId: Long,
pageNum: Int,
filter: FilterData,
): PagedMangaListDataClass {
val source = getCatalogueSourceOrStub(sourceId)
val filterList = if (filter.filter != null) buildFilterList(sourceId, filter.filter) else source.getFilterList()
val searchManga = source.getSearchManga(pageNum, filter.searchTerm ?: "", filterList)
@@ -35,14 +43,20 @@ object Search {
private val filterListCache = mutableMapOf<Long, FilterList>()
private fun getFilterListOf(source: CatalogueSource, reset: Boolean = false): FilterList {
private fun getFilterListOf(
source: CatalogueSource,
reset: Boolean = false,
): FilterList {
if (reset || !filterListCache.containsKey(source.id)) {
filterListCache[source.id] = source.getFilterList()
}
return filterListCache[source.id]!!
}
fun getFilterList(sourceId: Long, reset: Boolean): List<FilterObject> {
fun getFilterList(
sourceId: Long,
reset: Boolean,
): List<FilterObject> {
val source = getCatalogueSourceOrStub(sourceId)
return getFilterListOf(source, reset).list.map {
@@ -70,30 +84,37 @@ object Search {
is Filter.Select<*> -> FilterObject("Select", item)
else -> throw RuntimeException("Illegal Group item type!")
}
}
},
)
}
else -> it
}
},
)
}
}
private fun Filter.Select<*>.getValuesType(): String = values::class.java.componentType!!.simpleName
class SerializableGroup(name: String, state: List<FilterObject>) : Filter<List<FilterObject>>(name, state)
data class FilterObject(
val type: String,
val filter: Filter<*>
val filter: Filter<*>,
)
fun setFilter(sourceId: Long, changes: List<FilterChange>) {
fun setFilter(
sourceId: Long,
changes: List<FilterChange>,
) {
val source = getCatalogueSourceOrStub(sourceId)
val filterList = getFilterListOf(source, false)
updateFilterList(filterList, changes)
}
private fun updateFilterList(filterList: FilterList, changes: List<FilterChange>): FilterList {
private fun updateFilterList(
filterList: FilterList,
changes: List<FilterChange>,
): FilterList {
changes.forEach { change ->
when (val filter = filterList[change.position]) {
is Filter.Header -> {
@@ -124,7 +145,10 @@ object Search {
return filterList
}
fun buildFilterList(sourceId: Long, changes: List<FilterChange>): FilterList {
fun buildFilterList(
sourceId: Long,
changes: List<FilterChange>,
): FilterList {
val source = getCatalogueSourceOrStub(sourceId)
val filterList = source.getFilterList()
return updateFilterList(filterList, changes)
@@ -135,13 +159,13 @@ object Search {
@Serializable
data class FilterChange(
val position: Int,
val state: String
val state: String,
)
@Serializable
data class FilterData(
val searchTerm: String?,
val filter: List<FilterChange>?
val filter: List<FilterChange>?,
)
@Suppress("UNUSED_PARAMETER")

View File

@@ -46,7 +46,7 @@ object Source {
catalogueSource.supportsLatest,
catalogueSource is ConfigurableSource,
it[SourceTable.isNsfw],
catalogueSource.toString()
catalogueSource.toString(),
)
}
}
@@ -63,12 +63,12 @@ object Source {
source[SourceTable.name],
source[SourceTable.lang],
getExtensionIconUrl(
extension[ExtensionTable.apkName]
extension[ExtensionTable.apkName],
),
catalogueSource.supportsLatest,
catalogueSource is ConfigurableSource,
source[SourceTable.isNsfw],
catalogueSource.toString()
catalogueSource.toString(),
)
}
}
@@ -85,7 +85,7 @@ object Source {
*/
data class PreferenceObject(
val type: String,
val props: Any
val props: Any,
)
var preferenceScreenMap: MutableMap<Long, PreferenceScreen> = mutableMapOf()
@@ -119,7 +119,7 @@ object Source {
data class SourcePreferenceChange(
val position: Int,
val value: String
val value: String,
)
private val jsonMapper by DI.global.instance<JsonMapper>()
@@ -137,7 +137,7 @@ object Source {
"Set<String>" -> jsonMapper.fromJsonString(value, List::class.java as Class<List<String>>).toSet()
else -> throw RuntimeException("Unsupported type conversion")
}
}
},
) {
val screen = preferenceScreenMap[sourceId]!!
val pref = screen.preferences[position]

View File

@@ -12,5 +12,5 @@ data class BackupFlags(
val includeCategories: Boolean,
val includeChapters: Boolean,
val includeTracking: Boolean,
val includeHistory: Boolean
val includeHistory: Boolean,
)

View File

@@ -1,23 +0,0 @@
package suwayomi.tachidesk.manga.impl.backup.models
import java.io.Serializable
interface Category : Serializable {
var id: Int?
var name: String
var order: Int
var flags: Int
companion object {
fun create(name: String): Category = CategoryImpl().apply {
this.name = name
}
fun createDefault(): Category = create("Default").apply { id = 0 }
}
}

View File

@@ -1,24 +0,0 @@
package suwayomi.tachidesk.manga.impl.backup.models
class CategoryImpl : Category {
override var id: Int? = null
override lateinit var name: String
override var order: Int = 0
override var flags: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val category = other as Category
return name == category.name
}
override fun hashCode(): Int {
return name.hashCode()
}
}

View File

@@ -1,10 +1,11 @@
@file:Suppress("ktlint:standard:property-naming")
package suwayomi.tachidesk.manga.impl.backup.models
import eu.kanade.tachiyomi.source.model.SChapter
import java.io.Serializable
interface Chapter : SChapter, Serializable {
var id: Long?
var manga_id: Long?
@@ -23,9 +24,9 @@ interface Chapter : SChapter, Serializable {
get() = chapter_number >= 0f
companion object {
fun create(): Chapter = ChapterImpl().apply {
chapter_number = -1f
}
fun create(): Chapter =
ChapterImpl().apply {
chapter_number = -1f
}
}
}

View File

@@ -1,10 +1,11 @@
@file:Suppress("ktlint:standard:property-naming")
package suwayomi.tachidesk.manga.impl.backup.models
import org.jetbrains.exposed.sql.ResultRow
import suwayomi.tachidesk.manga.model.table.ChapterTable
class ChapterImpl : Chapter {
override var id: Long? = null
override var manga_id: Long? = null

View File

@@ -1,42 +0,0 @@
package suwayomi.tachidesk.manga.impl.backup.models
import java.io.Serializable
/**
* Object containing the history statistics of a chapter
*/
interface History : Serializable {
/**
* Id of history object.
*/
var id: Long?
/**
* Chapter id of history object.
*/
var chapter_id: Long
/**
* Last time chapter was read in time long format
*/
var last_read: Long
/**
* Total time chapter was read - todo not yet implemented
*/
var time_read: Long
companion object {
/**
* History constructor
*
* @param chapter chapter object
* @return history object
*/
fun create(chapter: Chapter): History = HistoryImpl().apply {
this.chapter_id = chapter.id!!
}
}
}

View File

@@ -1,27 +0,0 @@
package suwayomi.tachidesk.manga.impl.backup.models
/**
* Object containing the history statistics of a chapter
*/
class HistoryImpl : History {
/**
* Id of history object.
*/
override var id: Long? = null
/**
* Chapter id of history object.
*/
override var chapter_id: Long = 0
/**
* Last time chapter was read in time long format
*/
override var last_read: Long = 0
/**
* Total time chapter was read - todo not yet implemented
*/
override var time_read: Long = 0
}

View File

@@ -1,3 +1,5 @@
@file:Suppress("ktlint:standard:property-naming")
package suwayomi.tachidesk.manga.impl.backup.models
import eu.kanade.tachiyomi.source.model.SManga
@@ -15,7 +17,6 @@ object ReadingModeType {
}
interface Manga : SManga {
var id: Long?
var source: Long
@@ -48,11 +49,17 @@ interface Manga : SManga {
return genre?.split(", ")?.map { it.trim() }
}
private fun setChapterFlags(flag: Int, mask: Int) {
private fun setChapterFlags(
flag: Int,
mask: Int,
) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
}
private fun setViewerFlags(flag: Int, mask: Int) {
private fun setViewerFlags(
flag: Int,
mask: Int,
) {
viewer_flags = viewer_flags and mask.inv() or (flag and mask)
}
@@ -86,7 +93,6 @@ interface Manga : SManga {
set(rotationType) = setViewerFlags(rotationType, OrientationType.MASK)
companion object {
// Generic filter that does not filter anything
const val SHOW_ALL = 0x00000000
@@ -115,15 +121,21 @@ interface Manga : SManga {
const val CHAPTER_DISPLAY_NUMBER = 0x00100000
const val CHAPTER_DISPLAY_MASK = 0x00100000
fun create(source: Long): Manga = MangaImpl().apply {
this.source = source
}
fun create(source: Long): Manga =
MangaImpl().apply {
this.source = source
}
fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply {
url = pathUrl
this.title = title
this.source = source
}
fun create(
pathUrl: String,
title: String,
source: Long = 0,
): Manga =
MangaImpl().apply {
url = pathUrl
this.title = title
this.source = source
}
}
}

View File

@@ -1,20 +0,0 @@
package suwayomi.tachidesk.manga.impl.backup.models
class MangaCategory {
var id: Long? = null
var manga_id: Long = 0
var category_id: Int = 0
companion object {
fun create(manga: Manga, category: Category): MangaCategory {
val mc = MangaCategory()
mc.manga_id = manga.id!!
mc.category_id = category.id!!
return mc
}
}
}

View File

@@ -1,3 +0,0 @@
package suwayomi.tachidesk.manga.impl.backup.models
class MangaChapter(val manga: Manga, val chapter: Chapter)

View File

@@ -1,10 +0,0 @@
package suwayomi.tachidesk.manga.impl.backup.models
/**
* Object containing manga, chapter and history
*
* @param manga object containing manga
* @param chapter object containing chater
* @param history object containing history
*/
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)

View File

@@ -1,3 +1,5 @@
@file:Suppress("ktlint:standard:property-naming")
package suwayomi.tachidesk.manga.impl.backup.models
import eu.kanade.tachiyomi.source.model.UpdateStrategy
@@ -5,7 +7,6 @@ import org.jetbrains.exposed.sql.ResultRow
import suwayomi.tachidesk.manga.model.table.MangaTable
open class MangaImpl : Manga {
override var id: Long? = null
override var source: Long = -1

View File

@@ -1,9 +1,10 @@
@file:Suppress("ktlint:standard:property-naming")
package suwayomi.tachidesk.manga.impl.backup.models
import java.io.Serializable
interface Track : Serializable {
var id: Long?
var manga_id: Long
@@ -39,8 +40,9 @@ interface Track : Serializable {
}
companion object {
fun create(serviceId: Int): Track = TrackImpl().apply {
sync_id = serviceId
}
fun create(serviceId: Int): Track =
TrackImpl().apply {
sync_id = serviceId
}
}
}

View File

@@ -1,7 +1,8 @@
@file:Suppress("ktlint:standard:property-naming")
package suwayomi.tachidesk.manga.impl.backup.models
class TrackImpl : Track {
override var id: Long? = null
override var manga_id: Long = 0

View File

@@ -51,7 +51,7 @@ object ProtoBackupExport : ProtoBackupBase() {
private val logger = KotlinLogging.logger { }
private val applicationDirs by DI.global.instance<ApplicationDirs>()
private var backupSchedulerJobId: String = ""
private const val lastAutomatedBackupKey = "lastAutomatedBackupKey"
private const val LAST_AUTOMATED_BACKUP_KEY = "lastAutomatedBackupKey"
private val preferences = Preferences.userNodeForPackage(ProtoBackupExport::class.java)
init {
@@ -59,10 +59,10 @@ object ProtoBackupExport : ProtoBackupBase() {
combine(serverConfig.backupInterval, serverConfig.backupTime) { interval, timeOfDay ->
Pair(
interval,
timeOfDay
timeOfDay,
)
},
::scheduleAutomatedBackupTask
::scheduleAutomatedBackupTask,
)
}
@@ -77,7 +77,7 @@ object ProtoBackupExport : ProtoBackupBase() {
val task = {
cleanupAutomatedBackups()
createAutomatedBackup()
preferences.putLong(lastAutomatedBackupKey, System.currentTimeMillis())
preferences.putLong(LAST_AUTOMATED_BACKUP_KEY, System.currentTimeMillis())
}
val (hour, minute) = serverConfig.backupTime.value.split(":").map { it.toInt() }
@@ -86,7 +86,7 @@ object ProtoBackupExport : ProtoBackupBase() {
val backupInterval = serverConfig.backupInterval.value.days.coerceAtLeast(1.days)
// trigger last backup in case the server wasn't running on the scheduled time
val lastAutomatedBackup = preferences.getLong(lastAutomatedBackupKey, System.currentTimeMillis())
val lastAutomatedBackup = preferences.getLong(LAST_AUTOMATED_BACKUP_KEY, System.currentTimeMillis())
val wasPreviousBackupTriggered =
(System.currentTimeMillis() - lastAutomatedBackup) < backupInterval.inWholeMilliseconds
if (!wasPreviousBackupTriggered) {
@@ -105,8 +105,8 @@ object ProtoBackupExport : ProtoBackupBase() {
includeCategories = true,
includeChapters = true,
includeTracking = true,
includeHistory = true
)
includeHistory = true,
),
).use { input ->
val automatedBackupDir = File(applicationDirs.automatedBackupRoot)
automatedBackupDir.mkdirs()
@@ -162,14 +162,15 @@ object ProtoBackupExport : ProtoBackupBase() {
val databaseManga = transaction { MangaTable.select { MangaTable.inLibrary eq true } }
val backup: Backup = transaction {
Backup(
backupManga(databaseManga, flags),
backupCategories(),
emptyList(),
backupExtensionInfo(databaseManga)
)
}
val backup: Backup =
transaction {
Backup(
backupManga(databaseManga, flags),
backupCategories(),
emptyList(),
backupExtensionInfo(databaseManga),
)
}
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
@@ -179,48 +180,54 @@ object ProtoBackupExport : ProtoBackupBase() {
return byteStream.toByteArray().inputStream()
}
private fun backupManga(databaseManga: Query, flags: BackupFlags): List<BackupManga> {
private fun backupManga(
databaseManga: Query,
flags: BackupFlags,
): List<BackupManga> {
return databaseManga.map { mangaRow ->
val backupManga = BackupManga(
source = mangaRow[MangaTable.sourceReference],
url = mangaRow[MangaTable.url],
title = mangaRow[MangaTable.title],
artist = mangaRow[MangaTable.artist],
author = mangaRow[MangaTable.author],
description = mangaRow[MangaTable.description],
genre = mangaRow[MangaTable.genre]?.split(", ") ?: emptyList(),
status = MangaStatus.valueOf(mangaRow[MangaTable.status]).value,
thumbnailUrl = mangaRow[MangaTable.thumbnail_url],
dateAdded = TimeUnit.SECONDS.toMillis(mangaRow[MangaTable.inLibraryAt]),
viewer = 0, // not supported in Tachidesk
updateStrategy = UpdateStrategy.valueOf(mangaRow[MangaTable.updateStrategy])
)
val backupManga =
BackupManga(
source = mangaRow[MangaTable.sourceReference],
url = mangaRow[MangaTable.url],
title = mangaRow[MangaTable.title],
artist = mangaRow[MangaTable.artist],
author = mangaRow[MangaTable.author],
description = mangaRow[MangaTable.description],
genre = mangaRow[MangaTable.genre]?.split(", ") ?: emptyList(),
status = MangaStatus.valueOf(mangaRow[MangaTable.status]).value,
thumbnailUrl = mangaRow[MangaTable.thumbnail_url],
dateAdded = TimeUnit.SECONDS.toMillis(mangaRow[MangaTable.inLibraryAt]),
viewer = 0, // not supported in Tachidesk
updateStrategy = UpdateStrategy.valueOf(mangaRow[MangaTable.updateStrategy]),
)
val mangaId = mangaRow[MangaTable.id].value
if (flags.includeChapters) {
val chapters = transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.map {
ChapterTable.toDataClass(it)
}
}
val chapters =
transaction {
ChapterTable.select { ChapterTable.manga eq mangaId }
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
.map {
ChapterTable.toDataClass(it)
}
}
backupManga.chapters = chapters.map {
BackupChapter(
it.url,
it.name,
it.scanlator,
it.read,
it.bookmarked,
it.lastPageRead,
TimeUnit.SECONDS.toMillis(it.fetchedAt),
it.uploadDate,
it.chapterNumber,
chapters.size - it.index
)
}
backupManga.chapters =
chapters.map {
BackupChapter(
it.url,
it.name,
it.scanlator,
it.read,
it.bookmarked,
it.lastPageRead,
TimeUnit.SECONDS.toMillis(it.fetchedAt),
it.uploadDate,
it.chapterNumber,
chapters.size - it.index,
)
}
}
if (flags.includeCategories) {
@@ -246,7 +253,7 @@ object ProtoBackupExport : ProtoBackupBase() {
BackupCategory(
it.name,
it.order,
0 // not supported in Tachidesk
0, // not supported in Tachidesk
)
}
}
@@ -260,7 +267,7 @@ object ProtoBackupExport : ProtoBackupBase() {
val sourceRow = SourceTable.select { SourceTable.id eq it }.firstOrNull()
BackupSource(
sourceRow?.get(SourceTable.name) ?: "",
it
it,
)
}
.toList()

View File

@@ -47,9 +47,12 @@ object ProtoBackupImport : ProtoBackupBase() {
private val errors = mutableListOf<Pair<Date, String>>()
private val backupMutex = Mutex()
sealed class BackupRestoreState {
data object Idle : BackupRestoreState()
data class RestoringCategories(val totalManga: Int) : BackupRestoreState()
data class RestoringManga(val current: Int, val totalManga: Int, val title: String) : BackupRestoreState()
}
@@ -70,26 +73,28 @@ object ProtoBackupImport : ProtoBackupBase() {
restoreCategories(backup.backupCategories)
}
val categoryMapping = transaction {
backup.backupCategories.associate {
it.order to CategoryTable.select { CategoryTable.name eq it.name }.first()[CategoryTable.id].value
val categoryMapping =
transaction {
backup.backupCategories.associate {
it.order to CategoryTable.select { CategoryTable.name eq it.name }.first()[CategoryTable.id].value
}
}
}
// Store source mapping for error messages
sourceMapping = backup.getSourceMap()
// Restore individual manga
backup.backupManga.forEachIndexed { index, manga ->
backupRestoreState.value = BackupRestoreState.RestoringManga(
current = index + 1,
totalManga = backup.backupManga.size,
title = manga.title
)
backupRestoreState.value =
BackupRestoreState.RestoringManga(
current = index + 1,
totalManga = backup.backupManga.size,
title = manga.title,
)
restoreManga(
backupManga = manga,
backupCategories = backup.backupCategories,
categoryMapping = categoryMapping
categoryMapping = categoryMapping,
)
}
@@ -126,7 +131,7 @@ object ProtoBackupImport : ProtoBackupBase() {
private fun restoreManga(
backupManga: BackupManga,
backupCategories: List<BackupCategory>,
categoryMapping: Map<Int, Int>
categoryMapping: Map<Int, Int>,
) {
val manga = backupManga.getMangaImpl()
val chapters = backupManga.getChaptersImpl()
@@ -150,36 +155,38 @@ object ProtoBackupImport : ProtoBackupBase() {
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>,
categoryMapping: Map<Int, Int>
categoryMapping: Map<Int, Int>,
) {
val dbManga = transaction {
MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }
.firstOrNull()
}
val dbManga =
transaction {
MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }
.firstOrNull()
}
if (dbManga == null) { // Manga not in database
transaction {
// insert manga to database
val mangaId = MangaTable.insertAndGetId {
it[url] = manga.url
it[title] = manga.title
val mangaId =
MangaTable.insertAndGetId {
it[url] = manga.url
it[title] = manga.title
it[artist] = manga.artist
it[author] = manga.author
it[description] = manga.description
it[genre] = manga.genre
it[status] = manga.status
it[thumbnail_url] = manga.thumbnail_url
it[updateStrategy] = manga.update_strategy.name
it[artist] = manga.artist
it[author] = manga.author
it[description] = manga.description
it[genre] = manga.genre
it[status] = manga.status
it[thumbnail_url] = manga.thumbnail_url
it[updateStrategy] = manga.update_strategy.name
it[sourceReference] = manga.source
it[sourceReference] = manga.source
it[initialized] = manga.description != null
it[initialized] = manga.description != null
it[inLibrary] = manga.favorite
it[inLibrary] = manga.favorite
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
}.value
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
}.value
// insert chapter data
val chaptersLength = chapters.size

View File

@@ -24,7 +24,7 @@ object ProtoBackupValidator {
val missingTrackers: List<String>,
val mangasMissingSources: List<String>,
@JsonIgnore
val missingSourceIds: List<Pair<Long, String>>
val missingSourceIds: List<Pair<Long, String>>,
)
fun validate(backup: Backup): ValidationResult {
@@ -34,9 +34,10 @@ object ProtoBackupValidator {
val sources = backup.getSourceMap()
val missingSources = transaction {
sources.filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null }
}
val missingSources =
transaction {
sources.filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null }
}
// val trackers = backup.backupManga
// .flatMap { it.tracking }
@@ -56,7 +57,7 @@ object ProtoBackupValidator {
.sorted(),
missingTrackers,
emptyList(),
missingSources.toList()
missingSources.toList(),
)
}

View File

@@ -9,7 +9,7 @@ data class Backup(
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
// Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var brokenBackupSources: List<BrokenBackupSource> = emptyList(),
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList()
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
) {
fun getSourceMap(): Map<Long, String> {
return (brokenBackupSources.map { BackupSource(it.name, it.sourceId) } + backupSources)

View File

@@ -2,8 +2,6 @@ package suwayomi.tachidesk.manga.impl.backup.proto.models
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import suwayomi.tachidesk.manga.impl.backup.models.Category
import suwayomi.tachidesk.manga.impl.backup.models.CategoryImpl
@Serializable
class BackupCategory(
@@ -11,23 +9,5 @@ class BackupCategory(
@ProtoNumber(2) var order: Int = 0,
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
// Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var flags: Int = 0
) {
fun getCategoryImpl(): CategoryImpl {
return CategoryImpl().apply {
name = this@BackupCategory.name
flags = this@BackupCategory.flags
order = this@BackupCategory.order
}
}
companion object {
fun copyFrom(category: Category): BackupCategory {
return BackupCategory(
name = category.name,
order = category.order,
flags = category.flags
)
}
}
}
@ProtoNumber(100) var flags: Int = 0,
)

View File

@@ -20,7 +20,7 @@ data class BackupChapter(
@ProtoNumber(8) var dateUpload: Long = 0,
// chapterNumber is called number is 1.x
@ProtoNumber(9) var chapterNumber: Float = 0F,
@ProtoNumber(10) var sourceOrder: Int = 0
@ProtoNumber(10) var sourceOrder: Int = 0,
) {
fun toChapterImpl(): ChapterImpl {
return ChapterImpl().apply {
@@ -49,7 +49,7 @@ data class BackupChapter(
lastPageRead = chapter.last_page_read,
dateFetch = chapter.date_fetch,
dateUpload = chapter.date_upload,
sourceOrder = chapter.source_order
sourceOrder = chapter.source_order,
)
}
}

View File

@@ -6,11 +6,11 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BrokenBackupHistory(
@ProtoNumber(0) var url: String,
@ProtoNumber(1) var lastRead: Long
@ProtoNumber(1) var lastRead: Long,
)
@Serializable
data class BackupHistory(
@ProtoNumber(1) var url: String,
@ProtoNumber(2) var lastRead: Long
@ProtoNumber(2) var lastRead: Long,
)

View File

@@ -37,7 +37,7 @@ data class BackupManga(
@ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(),
@ProtoNumber(103) var viewer_flags: Int? = null,
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
) {
fun getMangaImpl(): MangaImpl {
return MangaImpl().apply {
@@ -86,7 +86,7 @@ data class BackupManga(
dateAdded = manga.date_added,
viewer = manga.readingModeType,
viewer_flags = manga.viewer_flags,
chapterFlags = manga.chapter_flags
chapterFlags = manga.chapter_flags,
)
}
}

View File

@@ -7,19 +7,19 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BrokenBackupSource(
@ProtoNumber(0) var name: String = "",
@ProtoNumber(1) var sourceId: Long
@ProtoNumber(1) var sourceId: Long,
)
@Serializable
data class BackupSource(
@ProtoNumber(1) var name: String = "",
@ProtoNumber(2) var sourceId: Long
@ProtoNumber(2) var sourceId: Long,
) {
companion object {
fun copyFrom(source: Source): BackupSource {
return BackupSource(
name = source.name,
sourceId = source.id
sourceId = source.id,
)
}
}

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