mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-03 10:54:38 -05:00
Fetch Manga and Chapters in GQL (#555)
This commit is contained in:
@@ -3,10 +3,13 @@ package suwayomi.tachidesk.graphql.mutations
|
|||||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||||
import com.expediagroup.graphql.server.extensions.getValuesFromDataLoader
|
import com.expediagroup.graphql.server.extensions.getValuesFromDataLoader
|
||||||
import graphql.schema.DataFetchingEnvironment
|
import graphql.schema.DataFetchingEnvironment
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
import suwayomi.tachidesk.graphql.types.ChapterType
|
import suwayomi.tachidesk.graphql.types.ChapterType
|
||||||
|
import suwayomi.tachidesk.manga.impl.Chapter
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
@@ -63,7 +66,10 @@ class ChapterMutation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateChapter(dataFetchingEnvironment: DataFetchingEnvironment, input: UpdateChapterInput): CompletableFuture<UpdateChapterPayload> {
|
fun updateChapter(
|
||||||
|
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||||
|
input: UpdateChapterInput
|
||||||
|
): CompletableFuture<UpdateChapterPayload> {
|
||||||
val (clientMutationId, id, patch) = input
|
val (clientMutationId, id, patch) = input
|
||||||
|
|
||||||
updateChapters(listOf(id), patch)
|
updateChapters(listOf(id), patch)
|
||||||
@@ -76,7 +82,10 @@ class ChapterMutation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateChapters(dataFetchingEnvironment: DataFetchingEnvironment, input: UpdateChaptersInput): CompletableFuture<UpdateChaptersPayload> {
|
fun updateChapters(
|
||||||
|
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||||
|
input: UpdateChaptersInput
|
||||||
|
): CompletableFuture<UpdateChaptersPayload> {
|
||||||
val (clientMutationId, ids, patch) = input
|
val (clientMutationId, ids, patch) = input
|
||||||
|
|
||||||
updateChapters(ids, patch)
|
updateChapters(ids, patch)
|
||||||
@@ -88,4 +97,31 @@ class ChapterMutation {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class FetchChaptersInput(
|
||||||
|
val clientMutationId: String? = null,
|
||||||
|
val mangaId: Int
|
||||||
|
)
|
||||||
|
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 = ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||||
|
.orderBy(ChapterTable.sourceOrder)
|
||||||
|
.map { ChapterType(it) }
|
||||||
|
FetchChaptersPayload(
|
||||||
|
clientMutationId = clientMutationId,
|
||||||
|
chapters = chapters
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ package suwayomi.tachidesk.graphql.mutations
|
|||||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||||
import com.expediagroup.graphql.server.extensions.getValuesFromDataLoader
|
import com.expediagroup.graphql.server.extensions.getValuesFromDataLoader
|
||||||
import graphql.schema.DataFetchingEnvironment
|
import graphql.schema.DataFetchingEnvironment
|
||||||
|
import org.jetbrains.exposed.sql.select
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.tachidesk.graphql.queries.MangaQuery
|
||||||
import suwayomi.tachidesk.graphql.types.MangaType
|
import suwayomi.tachidesk.graphql.types.MangaType
|
||||||
|
import suwayomi.tachidesk.manga.impl.Manga
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,4 +85,31 @@ class MangaMutation {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class FetchMangaInput(
|
||||||
|
val clientMutationId: String? = null,
|
||||||
|
val id: Int
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
FetchMangaPayload(
|
||||||
|
clientMutationId = clientMutationId,
|
||||||
|
manga = MangaType(manga)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ package suwayomi.tachidesk.manga.impl
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||||
@@ -31,7 +32,6 @@ import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
|
|||||||
import suwayomi.tachidesk.manga.model.dataclass.paginatedFrom
|
import suwayomi.tachidesk.manga.model.dataclass.paginatedFrom
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
|
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable.scanlator
|
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.PageTable
|
import suwayomi.tachidesk.manga.model.table.PageTable
|
||||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||||
@@ -56,6 +56,48 @@ object Chapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getSourceChapters(mangaId: Int): List<ChapterDataClass> {
|
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 chapterIds = chapterList.map { dbChapterMap.getValue(it.url)[ChapterTable.id] }
|
||||||
|
val chapterMetas = getChaptersMetaMaps(chapterIds)
|
||||||
|
|
||||||
|
return chapterList.mapIndexed { index, it ->
|
||||||
|
|
||||||
|
val dbChapter = dbChapterMap.getValue(it.url)
|
||||||
|
|
||||||
|
ChapterDataClass(
|
||||||
|
id = dbChapter[ChapterTable.id].value,
|
||||||
|
url = it.url,
|
||||||
|
name = it.name,
|
||||||
|
uploadDate = it.date_upload,
|
||||||
|
chapterNumber = it.chapter_number,
|
||||||
|
scanlator = it.scanlator,
|
||||||
|
mangaId = mangaId,
|
||||||
|
|
||||||
|
read = dbChapter[ChapterTable.isRead],
|
||||||
|
bookmarked = dbChapter[ChapterTable.isBookmarked],
|
||||||
|
lastPageRead = dbChapter[ChapterTable.lastPageRead],
|
||||||
|
lastReadAt = dbChapter[ChapterTable.lastReadAt],
|
||||||
|
|
||||||
|
index = chapterList.size - index,
|
||||||
|
fetchedAt = dbChapter[ChapterTable.fetchedAt],
|
||||||
|
realUrl = dbChapter[ChapterTable.realUrl],
|
||||||
|
downloaded = dbChapter[ChapterTable.isDownloaded],
|
||||||
|
|
||||||
|
pageCount = dbChapter[ChapterTable.pageCount],
|
||||||
|
|
||||||
|
chapterCount = chapterList.size,
|
||||||
|
meta = chapterMetas.getValue(dbChapter[ChapterTable.id])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun fetchChapterList(mangaId: Int): List<SChapter> {
|
||||||
val manga = getManga(mangaId)
|
val manga = getManga(mangaId)
|
||||||
val source = getCatalogueSourceOrStub(manga.sourceId.toLong())
|
val source = getCatalogueSourceOrStub(manga.sourceId.toLong())
|
||||||
|
|
||||||
@@ -72,7 +114,6 @@ object Chapter {
|
|||||||
ChapterRecognition.parseChapterNumber(it, sManga)
|
ChapterRecognition.parseChapterNumber(it, sManga)
|
||||||
}
|
}
|
||||||
|
|
||||||
val chapterCount = chapterList.count()
|
|
||||||
var now = Instant.now().epochSecond
|
var now = Instant.now().epochSecond
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
@@ -118,9 +159,10 @@ object Chapter {
|
|||||||
|
|
||||||
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
|
// clear any orphaned/duplicate chapters that are in the db but not in `chapterList`
|
||||||
val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
|
val dbChapterCount = transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count() }
|
||||||
if (dbChapterCount > chapterCount) { // we got some clean up due
|
if (dbChapterCount > chapterList.size) { // we got some clean up due
|
||||||
val dbChapterList = transaction {
|
val dbChapterList = transaction {
|
||||||
ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.url to ASC).toList()
|
ChapterTable.select { ChapterTable.manga eq mangaId }
|
||||||
|
.orderBy(ChapterTable.url to ASC).toList()
|
||||||
}
|
}
|
||||||
val chapterUrls = chapterList.map { it.url }.toSet()
|
val chapterUrls = chapterList.map { it.url }.toSet()
|
||||||
|
|
||||||
@@ -137,43 +179,7 @@ object Chapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val dbChapterMap = transaction {
|
return chapterList
|
||||||
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)
|
|
||||||
|
|
||||||
return chapterList.mapIndexed { index, it ->
|
|
||||||
|
|
||||||
val dbChapter = dbChapterMap.getValue(it.url)
|
|
||||||
|
|
||||||
ChapterDataClass(
|
|
||||||
id = dbChapter[ChapterTable.id].value,
|
|
||||||
url = it.url,
|
|
||||||
name = it.name,
|
|
||||||
uploadDate = it.date_upload,
|
|
||||||
chapterNumber = it.chapter_number,
|
|
||||||
scanlator = it.scanlator,
|
|
||||||
mangaId = mangaId,
|
|
||||||
|
|
||||||
read = dbChapter[ChapterTable.isRead],
|
|
||||||
bookmarked = dbChapter[ChapterTable.isBookmarked],
|
|
||||||
lastPageRead = dbChapter[ChapterTable.lastPageRead],
|
|
||||||
lastReadAt = dbChapter[ChapterTable.lastReadAt],
|
|
||||||
|
|
||||||
index = chapterCount - 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])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun modifyChapter(
|
fun modifyChapter(
|
||||||
|
|||||||
@@ -60,49 +60,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() }
|
var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
|
|
||||||
return if (mangaEntry[MangaTable.initialized] && !onlineFetch) {
|
return if (!onlineFetch && mangaEntry[MangaTable.initialized]) {
|
||||||
getMangaDataClass(mangaId, mangaEntry)
|
getMangaDataClass(mangaId, mangaEntry)
|
||||||
} else { // initialize manga
|
} else { // initialize manga
|
||||||
val source = getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
val sManga = fetchManga(mangaId) ?: return getMangaDataClass(mangaId, mangaEntry)
|
||||||
?: return getMangaDataClass(mangaId, mangaEntry)
|
|
||||||
val sManga = SManga.create().apply {
|
|
||||||
url = mangaEntry[MangaTable.url]
|
|
||||||
title = mangaEntry[MangaTable.title]
|
|
||||||
}
|
|
||||||
val networkManga = source.fetchMangaDetails(sManga).awaitSingle()
|
|
||||||
sManga.copyFrom(networkManga)
|
|
||||||
|
|
||||||
transaction {
|
|
||||||
MangaTable.update({ MangaTable.id eq mangaId }) {
|
|
||||||
if (sManga.title != mangaEntry[MangaTable.title]) {
|
|
||||||
val canUpdateTitle = updateMangaDownloadDir(mangaId, sManga.title)
|
|
||||||
|
|
||||||
if (canUpdateTitle) {
|
|
||||||
it[MangaTable.title] = sManga.title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
it[MangaTable.initialized] = true
|
|
||||||
|
|
||||||
it[MangaTable.artist] = sManga.artist
|
|
||||||
it[MangaTable.author] = sManga.author
|
|
||||||
it[MangaTable.description] = truncate(sManga.description, 4096)
|
|
||||||
it[MangaTable.genre] = sManga.genre
|
|
||||||
it[MangaTable.status] = sManga.status
|
|
||||||
if (!sManga.thumbnail_url.isNullOrEmpty() && sManga.thumbnail_url != mangaEntry[MangaTable.thumbnail_url]) {
|
|
||||||
it[MangaTable.thumbnail_url] = sManga.thumbnail_url
|
|
||||||
it[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
|
|
||||||
clearMangaThumbnailCache(mangaId)
|
|
||||||
}
|
|
||||||
|
|
||||||
it[MangaTable.realUrl] = runCatching {
|
|
||||||
(source as? HttpSource)?.getMangaUrl(sManga)
|
|
||||||
}.getOrNull()
|
|
||||||
|
|
||||||
it[MangaTable.lastFetchedAt] = Instant.now().epochSecond
|
|
||||||
|
|
||||||
it[MangaTable.updateStrategy] = sManga.update_strategy.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||||
|
|
||||||
@@ -135,6 +96,53 @@ 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 networkManga = source.fetchMangaDetails(sManga).awaitSingle()
|
||||||
|
sManga.copyFrom(networkManga)
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
MangaTable.update({ MangaTable.id eq mangaId }) {
|
||||||
|
if (sManga.title != mangaEntry[MangaTable.title]) {
|
||||||
|
val canUpdateTitle = updateMangaDownloadDir(mangaId, sManga.title)
|
||||||
|
|
||||||
|
if (canUpdateTitle) {
|
||||||
|
it[MangaTable.title] = sManga.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it[MangaTable.initialized] = true
|
||||||
|
|
||||||
|
it[MangaTable.artist] = sManga.artist
|
||||||
|
it[MangaTable.author] = sManga.author
|
||||||
|
it[MangaTable.description] = truncate(sManga.description, 4096)
|
||||||
|
it[MangaTable.genre] = sManga.genre
|
||||||
|
it[MangaTable.status] = sManga.status
|
||||||
|
if (!sManga.thumbnail_url.isNullOrEmpty() && sManga.thumbnail_url != mangaEntry[MangaTable.thumbnail_url]) {
|
||||||
|
it[MangaTable.thumbnail_url] = sManga.thumbnail_url
|
||||||
|
it[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
|
||||||
|
clearMangaThumbnailCache(mangaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
it[MangaTable.realUrl] = runCatching {
|
||||||
|
(source as? HttpSource)?.getMangaUrl(sManga)
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
|
it[MangaTable.lastFetchedAt] = Instant.now().epochSecond
|
||||||
|
|
||||||
|
it[MangaTable.updateStrategy] = sManga.update_strategy.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
val mangaDaaClass = getManga(mangaId, onlineFetch)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user