Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package suwayomi.tachidesk.graphql.mutations

import eu.kanade.tachiyomi.source.local.LocalSource
import eu.kanade.tachiyomi.source.local.image.LocalCoverManager
import eu.kanade.tachiyomi.source.local.io.LocalSourceFileSystem
import eu.kanade.tachiyomi.source.model.SManga
import graphql.execution.DataFetcherResult
import io.javalin.http.UploadedFile
import org.jetbrains.exposed.sql.LikePattern
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
Expand All @@ -19,12 +24,17 @@ import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.graphql.types.MetaInput
import suwayomi.tachidesk.manga.impl.Library
import suwayomi.tachidesk.manga.impl.Manga
import suwayomi.tachidesk.manga.impl.MangaList
import suwayomi.tachidesk.manga.impl.update.IUpdater
import suwayomi.tachidesk.manga.impl.util.getThumbnailDownloadPath
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.toDataClass
import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.JavalinSetup.future
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.time.Instant
import java.util.concurrent.CompletableFuture

Expand All @@ -35,6 +45,7 @@ import java.util.concurrent.CompletableFuture
*/
class MangaMutation {
private val updater: IUpdater by injectLazy()
private val applicationDirs: ApplicationDirs by injectLazy()

data class UpdateMangaPatch(
val inLibrary: Boolean? = null,
Expand Down Expand Up @@ -97,6 +108,70 @@ class MangaMutation {
}
}

// --- Cover Image Upload ---

data class UploadMangaCoverInput(
val clientMutationId: String? = null,
val id: Int,
val cover: UploadedFile,
)

data class UploadMangaCoverPayload(
val clientMutationId: String?,
val manga: MangaType,
)

@RequireAuth
fun uploadMangaCover(input: UploadMangaCoverInput): CompletableFuture<DataFetcherResult<UploadMangaCoverPayload?>> {
val (clientMutationId, id, cover) = input

return future {
asDataFetcherResult {
val imageBytes = cover.content().use { it.readBytes() }
val mimeType = cover.contentType()

val row =
transaction {
MangaTable.selectAll().where { MangaTable.id eq id }.firstOrNull()
?: throw IllegalArgumentException("Manga with id $id not found")
}

Manga.clearThumbnail(id)

val thumbnailDir = applicationDirs.thumbnailDownloadsRoot
File(thumbnailDir).let { if (!it.exists()) it.mkdir() }
val filePath = getThumbnailDownloadPath(id)
ImageResponse.saveImage(filePath, imageBytes.inputStream(), mimeType)

transaction {
MangaTable.update({ MangaTable.id eq id }) { update ->
update[thumbnail_url] = MangaList.proxyThumbnailUrl(id)
update[thumbnailUrlLastFetched] = System.currentTimeMillis()
}
}

val sourceId = row[MangaTable.sourceReference]
if (sourceId == LocalSource.ID) {
val mangaUrl = row[MangaTable.url]
val fileSystem = LocalSourceFileSystem(applicationDirs)
val coverManager = LocalCoverManager(fileSystem)
val sManga = SManga.create().apply { url = mangaUrl }
coverManager.update(sManga, imageBytes.inputStream())
}

val manga =
transaction {
MangaType(MangaTable.selectAll().where { MangaTable.id eq id }.first())
}

UploadMangaCoverPayload(
clientMutationId = clientMutationId,
manga = manga,
)
}
}
}

@RequireAuth
fun updateManga(input: UpdateMangaInput): CompletableFuture<DataFetcherResult<UpdateMangaPayload?>> {
val (clientMutationId, id, patch) = input
Expand Down
10 changes: 10 additions & 0 deletions server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Manga.kt
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,16 @@ object Manga {
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }

// Check thumbnail cache for user-uploaded covers (any source)
val hasUserUploadedCover = mangaEntry[MangaTable.thumbnailUrlLastFetched] > 0
if (hasUserUploadedCover) {
try {
return ThumbnailDownloadHelper.getImage(mangaId)
} catch (_: MissingThumbnailException) {
// fall through to original logic
}
}

if (mangaEntry[MangaTable.inLibrary] && mangaEntry[MangaTable.sourceReference] != LocalSource.ID) {
return try {
ThumbnailDownloadHelper.getImage(mangaId)
Expand Down