diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt index 808f41881..718aec584 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt @@ -3,10 +3,13 @@ package suwayomi.tachidesk.graphql.mutations import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import com.expediagroup.graphql.server.extensions.getValuesFromDataLoader import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import suwayomi.tachidesk.graphql.types.ChapterType +import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.server.JavalinSetup.future import java.time.Instant import java.util.concurrent.CompletableFuture @@ -63,7 +66,10 @@ class ChapterMutation { } } - fun updateChapter(dataFetchingEnvironment: DataFetchingEnvironment, input: UpdateChapterInput): CompletableFuture { + fun updateChapter( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateChapterInput + ): CompletableFuture { val (clientMutationId, id, patch) = input updateChapters(listOf(id), patch) @@ -76,7 +82,10 @@ class ChapterMutation { } } - fun updateChapters(dataFetchingEnvironment: DataFetchingEnvironment, input: UpdateChaptersInput): CompletableFuture { + fun updateChapters( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateChaptersInput + ): CompletableFuture { val (clientMutationId, ids, patch) = input 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 + ) + + fun fetchChapters( + input: FetchChaptersInput + ): CompletableFuture { + 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 + ) + } + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt index 0adee4225..fb2dc5315 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt @@ -3,10 +3,14 @@ package suwayomi.tachidesk.graphql.mutations import com.expediagroup.graphql.server.extensions.getValueFromDataLoader import com.expediagroup.graphql.server.extensions.getValuesFromDataLoader import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update +import suwayomi.tachidesk.graphql.queries.MangaQuery import suwayomi.tachidesk.graphql.types.MangaType +import suwayomi.tachidesk.manga.impl.Manga import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.server.JavalinSetup.future 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 { + 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) + ) + } + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt index c64ab5654..37b5fc719 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt @@ -7,6 +7,7 @@ package suwayomi.tachidesk.manga.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 eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource 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.table.ChapterMetaTable 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.PageTable import suwayomi.tachidesk.manga.model.table.toDataClass @@ -56,6 +56,48 @@ object Chapter { } private suspend fun getSourceChapters(mangaId: Int): List { + 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 { val manga = getManga(mangaId) val source = getCatalogueSourceOrStub(manga.sourceId.toLong()) @@ -72,7 +114,6 @@ object Chapter { ChapterRecognition.parseChapterNumber(it, sManga) } - val chapterCount = chapterList.count() var now = Instant.now().epochSecond transaction { @@ -118,9 +159,10 @@ object Chapter { // clear any orphaned/duplicate chapters that are in the db but not in `chapterList` 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 { - 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() @@ -137,43 +179,7 @@ object Chapter { } } - 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 = 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]) - ) - } + return chapterList } fun modifyChapter( diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Manga.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Manga.kt index d4d66e853..f09b728a7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Manga.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Manga.kt @@ -60,49 +60,10 @@ object Manga { suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass { 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) } else { // initialize manga - val source = getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference]) - ?: 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 - } - } + val sManga = fetchManga(mangaId) ?: return getMangaDataClass(mangaId, mangaEntry) 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 { val mangaDaaClass = getManga(mangaId, onlineFetch)