diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt index 83fcc61..26d7cef 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt @@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceFactory import ir.armor.tachidesk.impl.ExtensionsList.extensionTableAsDataClass -import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse import ir.armor.tachidesk.impl.util.PackageTools.EXTENSION_FEATURE import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MAX import ir.armor.tachidesk.impl.util.PackageTools.LIB_VERSION_MIN @@ -27,6 +26,7 @@ import ir.armor.tachidesk.impl.util.PackageTools.getSignatureHash import ir.armor.tachidesk.impl.util.PackageTools.loadExtensionSources import ir.armor.tachidesk.impl.util.PackageTools.trustedSignatures import ir.armor.tachidesk.impl.util.await +import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse import ir.armor.tachidesk.model.database.table.ExtensionTable import ir.armor.tachidesk.model.database.table.SourceTable import ir.armor.tachidesk.server.ApplicationDirs diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt index 88c732e..3d91d5e 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt @@ -11,11 +11,11 @@ import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.SManga import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl import ir.armor.tachidesk.impl.Source.getSource -import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.clearCachedImage -import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import ir.armor.tachidesk.impl.util.await import ir.armor.tachidesk.impl.util.awaitSingle +import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.clearCachedImage +import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse import ir.armor.tachidesk.model.database.table.MangaStatus import ir.armor.tachidesk.model.database.table.MangaTable import ir.armor.tachidesk.model.dataclass.MangaDataClass diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/Page.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Page.kt index 9ffce4f..6d1319e 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Page.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Page.kt @@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import ir.armor.tachidesk.impl.util.awaitSingle import ir.armor.tachidesk.impl.util.storage.CachedImageResponse.getCachedImageResponse -import ir.armor.tachidesk.impl.util.storage.DiskUtil +import ir.armor.tachidesk.impl.util.storage.SafePath import ir.armor.tachidesk.model.database.table.ChapterTable import ir.armor.tachidesk.model.database.table.MangaTable import ir.armor.tachidesk.model.database.table.PageTable @@ -82,8 +82,8 @@ object Page { val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() } val sourceDir = source.toString() - val mangaDir = DiskUtil.buildValidFilename(mangaEntry[MangaTable.title]) - val chapterDir = DiskUtil.buildValidFilename( + val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title]) + val chapterDir = SafePath.buildValidFilename( when { chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}" else -> chapterEntry[ChapterTable.name] diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/CachedImageResponse.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/CachedImageResponse.kt index 3b621f5..8da53c3 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/CachedImageResponse.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/CachedImageResponse.kt @@ -8,13 +8,9 @@ package ir.armor.tachidesk.impl.util.storage * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import okhttp3.Response -import okio.buffer -import okio.sink import java.io.File import java.io.FileInputStream import java.io.InputStream -import java.nio.file.Files -import java.nio.file.Paths object CachedImageResponse { private fun pathToInputStream(path: String): InputStream { @@ -45,18 +41,19 @@ object CachedImageResponse { val response = fetcher() if (response.code == 200) { - val contentType = response.headers["content-type"]!! - val fullPath = filePath + "." + contentType.substringAfter("image/") + val fullPath = "$filePath.tmp" + val saveFile = File(fullPath) + response.body!!.source().saveTo(saveFile) - Files.newOutputStream(Paths.get(fullPath)).use { output -> - response.body!!.source().use { input -> - output.sink().buffer().use { - it.writeAll(input) - it.flush() - } - } - } - return pathToInputStream(fullPath) to contentType + // find image type + val imageType = response.headers["content-type"] + ?: ImageUtil.findImageType { saveFile.inputStream() }?.mime + ?: "image/jpeg" + .substringAfter("image/") + + saveFile.renameTo(File("$filePath.$imageType")) + + return pathToInputStream(fullPath) to imageType } else { throw Exception("request error! ${response.code}") } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/ImageUtil.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/ImageUtil.kt new file mode 100644 index 0000000..cc20a50 --- /dev/null +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/ImageUtil.kt @@ -0,0 +1,69 @@ +package ir.armor.tachidesk.impl.util.storage + +import java.io.InputStream + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt +object ImageUtil { + + fun findImageType(openStream: () -> InputStream): ImageType? { + return openStream().use { findImageType(it) } + } + + fun findImageType(stream: InputStream): ImageType? { + try { + val bytes = ByteArray(8) + + val length = if (stream.markSupported()) { + stream.mark(bytes.size) + stream.read(bytes, 0, bytes.size).also { stream.reset() } + } else { + stream.read(bytes, 0, bytes.size) + } + + if (length == -1) { + return null + } + + if (bytes.compareWith(charByteArrayOf(0xFF, 0xD8, 0xFF))) { + return ImageType.JPG + } + if (bytes.compareWith(charByteArrayOf(0x89, 0x50, 0x4E, 0x47))) { + return ImageType.PNG + } + if (bytes.compareWith("GIF8".toByteArray())) { + return ImageType.GIF + } + if (bytes.compareWith("RIFF".toByteArray())) { + return ImageType.WEBP + } + } catch (e: Exception) { + } + return null + } + + private fun ByteArray.compareWith(magic: ByteArray): Boolean { + return magic.indices.none { this[it] != magic[it] } + } + + private fun charByteArrayOf(vararg bytes: Int): ByteArray { + return ByteArray(bytes.size).apply { + for (i in bytes.indices) { + set(i, bytes[i].toByte()) + } + } + } + + enum class ImageType(val mime: String) { + JPG("image/jpeg"), + PNG("image/png"), + GIF("image/gif"), + WEBP("image/webp") + } +} diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/OkioExtensions.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/OkioExtensions.kt new file mode 100644 index 0000000..d45e75d --- /dev/null +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/OkioExtensions.kt @@ -0,0 +1,48 @@ +package ir.armor.tachidesk.impl.util.storage + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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 okio.BufferedSource +import okio.buffer +import okio.sink +import java.io.File +import java.io.OutputStream + +// adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/ff369010074b058bb734ce24c66508300e6e9ac6/app/src/main/java/eu/kanade/tachiyomi/util/storage/OkioExtensions.kt +/** + * Saves the given source to a file and closes it. Directories will be created if needed. + * + * @param file the file where the source is copied. + */ +fun BufferedSource.saveTo(file: File) { + try { + // Create parent dirs if needed + file.parentFile.mkdirs() + + // Copy to destination + saveTo(file.outputStream()) + } catch (e: Exception) { + close() + file.delete() + throw e + } +} + +/** + * Saves the given source to an output stream and closes both resources. + * + * @param stream the stream where the source is copied. + */ +fun BufferedSource.saveTo(stream: OutputStream) { + use { input -> + stream.sink().buffer().use { + it.writeAll(input) + it.flush() + } + } +} diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/DiskUtil.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/SafePath.kt similarity index 98% rename from server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/DiskUtil.kt rename to server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/SafePath.kt index 0262fbb..b92e6c2 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/DiskUtil.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/storage/SafePath.kt @@ -8,7 +8,7 @@ package ir.armor.tachidesk.impl.util.storage * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ // adopted from: https://github.com/tachiyomiorg/tachiyomi/blob/4cefbce7c34e724b409b6ba127f3c6c5c346ad8d/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt -object DiskUtil { +object SafePath { /** * Mutate the given filename to make it valid for a FAT filesystem, * replacing any invalid characters with "_". This method doesn't allow hidden files (starting @@ -44,4 +44,4 @@ object DiskUtil { else -> true } } -} \ No newline at end of file +} diff --git a/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt index 344ebc3..c5b1976 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt @@ -302,6 +302,16 @@ object JavalinSetup { ) } + // submit a chapter for download + app.put("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx -> + // TODO + } + + // cancel a chapter download + app.delete("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx -> + // TODO + } + // global search, Not implemented yet app.get("/api/v1/search/:searchTerm") { ctx -> val searchTerm = ctx.pathParam("searchTerm") @@ -432,5 +442,19 @@ object JavalinSetup { } ) } + + // Download queue stats + app.ws("/api/v1/downloads") { ws -> + ws.onConnect { ctx -> + // TODO: send current stat + // TODO: add to downlad subscribers + } + ws.onMessage { + // TODO: send current stat + } + ws.onClose { ctx -> + // TODO: remove from subscribers + } + } } }