mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-04 03:14:40 -05:00
cached extension icon
This commit is contained in:
@@ -9,6 +9,7 @@ import io.javalin.Javalin
|
|||||||
import ir.armor.tachidesk.util.applicationSetup
|
import ir.armor.tachidesk.util.applicationSetup
|
||||||
import ir.armor.tachidesk.util.getChapter
|
import ir.armor.tachidesk.util.getChapter
|
||||||
import ir.armor.tachidesk.util.getChapterList
|
import ir.armor.tachidesk.util.getChapterList
|
||||||
|
import ir.armor.tachidesk.util.getExtensionIcon
|
||||||
import ir.armor.tachidesk.util.getExtensionList
|
import ir.armor.tachidesk.util.getExtensionList
|
||||||
import ir.armor.tachidesk.util.getManga
|
import ir.armor.tachidesk.util.getManga
|
||||||
import ir.armor.tachidesk.util.getMangaList
|
import ir.armor.tachidesk.util.getMangaList
|
||||||
@@ -94,6 +95,14 @@ class Main {
|
|||||||
ctx.status(200)
|
ctx.status(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.get("/api/v1/extension/icon/:apkName") { ctx ->
|
||||||
|
val apkName = ctx.pathParam("apkName")
|
||||||
|
val result = getExtensionIcon(apkName)
|
||||||
|
|
||||||
|
ctx.result(result.first)
|
||||||
|
ctx.header("content-type", result.second)
|
||||||
|
}
|
||||||
|
|
||||||
app.get("/api/v1/source/list") { ctx ->
|
app.get("/api/v1/source/list") { ctx ->
|
||||||
ctx.json(getSourceList())
|
ctx.json(getSourceList())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package ir.armor.tachidesk.util
|
|||||||
|
|
||||||
import com.googlecode.dex2jar.tools.Dex2jarCmd
|
import com.googlecode.dex2jar.tools.Dex2jarCmd
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
@@ -24,6 +25,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
|||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
|
|
||||||
@@ -149,3 +151,18 @@ fun removeExtension(pkgName: String) {
|
|||||||
File(jarPath).delete()
|
File(jarPath).delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val network: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
|
fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
||||||
|
val iconUrl = transaction { ExtensionsTable.select { ExtensionsTable.apkName eq apkName }.firstOrNull()!! }[ExtensionsTable.iconUrl]
|
||||||
|
|
||||||
|
val saveDir = "${Config.extensionsRoot}/icon"
|
||||||
|
val fileName = apkName
|
||||||
|
|
||||||
|
return getCachedResponse(saveDir, fileName) {
|
||||||
|
network.client.newCall(
|
||||||
|
GET(iconUrl)
|
||||||
|
).execute()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,8 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
println("used cached extension list")
|
||||||
}
|
}
|
||||||
|
|
||||||
return transaction {
|
return transaction {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package ir.armor.tachidesk.util
|
|||||||
* 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 okhttp3.Response
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
@@ -15,15 +16,15 @@ import java.io.OutputStream
|
|||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
|
||||||
fun writeStream(fileStream: InputStream, path: String) {
|
// fun writeStream(fileStream: InputStream, path: String) {
|
||||||
Files.newOutputStream(Paths.get(path)).use { os ->
|
// Files.newOutputStream(Paths.get(path)).use { os ->
|
||||||
val buffer = ByteArray(128 * 1024)
|
// val buffer = ByteArray(128 * 1024)
|
||||||
var len: Int
|
// var len: Int
|
||||||
while (fileStream.read(buffer).also { len = it } > 0) {
|
// while (fileStream.read(buffer).also { len = it } > 0) {
|
||||||
os.write(buffer, 0, len)
|
// os.write(buffer, 0, len)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
fun pathToInputStream(path: String): InputStream {
|
fun pathToInputStream(path: String): InputStream {
|
||||||
return BufferedInputStream(FileInputStream(path))
|
return BufferedInputStream(FileInputStream(path))
|
||||||
@@ -42,7 +43,7 @@ fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
|
|||||||
*
|
*
|
||||||
* @param stream the stream where the source is copied.
|
* @param stream the stream where the source is copied.
|
||||||
*/
|
*/
|
||||||
fun BufferedSource.saveTo(stream: OutputStream) {
|
private fun BufferedSource.saveTo(stream: OutputStream) {
|
||||||
use { input ->
|
use { input ->
|
||||||
stream.sink().buffer().use {
|
stream.sink().buffer().use {
|
||||||
it.writeAll(input)
|
it.writeAll(input)
|
||||||
@@ -50,3 +51,32 @@ fun BufferedSource.saveTo(stream: OutputStream) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCachedResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair<InputStream, String> {
|
||||||
|
val cachedFile = findFileNameStartingWith(saveDir, fileName)
|
||||||
|
val filePath = "$saveDir/$fileName"
|
||||||
|
if (cachedFile != null) {
|
||||||
|
val fileType = cachedFile.substringAfter(filePath)
|
||||||
|
return Pair(
|
||||||
|
pathToInputStream(cachedFile),
|
||||||
|
"image/$fileType"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = fetcher()
|
||||||
|
|
||||||
|
if (response.code == 200) {
|
||||||
|
val contentType = response.headers["content-type"]!!
|
||||||
|
val fullPath = filePath + "." + contentType.substringAfter("image/")
|
||||||
|
|
||||||
|
Files.newOutputStream(Paths.get(fullPath)).use { os ->
|
||||||
|
response.body!!.source().saveTo(os)
|
||||||
|
}
|
||||||
|
return Pair(
|
||||||
|
pathToInputStream(fullPath),
|
||||||
|
contentType
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw Exception("request error! ${response.code}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,40 +83,19 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
|
|||||||
|
|
||||||
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
|
||||||
var filePath = "${Config.thumbnailsRoot}/$mangaId."
|
val saveDir = Config.thumbnailsRoot
|
||||||
|
val fileName = mangaId.toString()
|
||||||
val potentialCache = findFileNameStartingWith(Config.thumbnailsRoot, mangaId.toString())
|
|
||||||
if (potentialCache != null) {
|
|
||||||
println("using cached thumbnail file")
|
|
||||||
return Pair(
|
|
||||||
pathToInputStream(potentialCache),
|
|
||||||
"image/${potentialCache.substringAfter(filePath)}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return getCachedResponse(saveDir, fileName) {
|
||||||
val sourceId = mangaEntry[MangaTable.sourceReference].value
|
val sourceId = mangaEntry[MangaTable.sourceReference].value
|
||||||
println("getting source for $mangaId")
|
|
||||||
val source = getHttpSource(sourceId)
|
val source = getHttpSource(sourceId)
|
||||||
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
||||||
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
|
||||||
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
|
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
|
||||||
}
|
}
|
||||||
println(thumbnailUrl)
|
|
||||||
val response = source.client.newCall(
|
source.client.newCall(
|
||||||
GET(thumbnailUrl, source.headers)
|
GET(thumbnailUrl, source.headers)
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
if (response.code == 200) {
|
|
||||||
val contentType = response.headers["content-type"]!!
|
|
||||||
filePath += contentType.substringAfter("image/")
|
|
||||||
|
|
||||||
writeStream(response.body!!.byteStream(), filePath)
|
|
||||||
|
|
||||||
return Pair(
|
|
||||||
pathToInputStream(filePath),
|
|
||||||
contentType
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw Exception("request error! ${response.code}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
|||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
fun getTrueImageUrl(page: Page, source: HttpSource): String {
|
||||||
if (page.imageUrl == null) {
|
if (page.imageUrl == null) {
|
||||||
@@ -50,36 +48,10 @@ fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, St
|
|||||||
|
|
||||||
val saveDir = getChapterDir(mangaId, chapterId)
|
val saveDir = getChapterDir(mangaId, chapterId)
|
||||||
File(saveDir).mkdirs()
|
File(saveDir).mkdirs()
|
||||||
var filePath = "$saveDir/$index."
|
val fileName = index.toString()
|
||||||
|
|
||||||
val potentialCache = findFileNameStartingWith(saveDir, index.toString())
|
return getCachedResponse(saveDir, fileName) {
|
||||||
if (potentialCache != null) {
|
source.fetchImage(tachiPage).toBlocking().first()
|
||||||
println("using cached page file for $index")
|
|
||||||
return Pair(
|
|
||||||
pathToInputStream(potentialCache),
|
|
||||||
"image/${potentialCache.substringAfter("$filePath")}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val response = source.fetchImage(tachiPage).toBlocking().first()
|
|
||||||
|
|
||||||
if (response.code == 200) {
|
|
||||||
val contentType = response.headers["content-type"]!!
|
|
||||||
filePath += contentType.substringAfter("image/")
|
|
||||||
|
|
||||||
Files.newOutputStream(Paths.get(filePath)).use { os ->
|
|
||||||
|
|
||||||
response.body!!.source().saveTo(os)
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeStream(response.body!!.source(), filePath)
|
|
||||||
|
|
||||||
return Pair(
|
|
||||||
pathToInputStream(filePath),
|
|
||||||
contentType
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw Exception("request error! ${response.code}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ fun applicationSetup() {
|
|||||||
// make dirs we need
|
// make dirs we need
|
||||||
File(Config.dataRoot).mkdirs()
|
File(Config.dataRoot).mkdirs()
|
||||||
File(Config.extensionsRoot).mkdirs()
|
File(Config.extensionsRoot).mkdirs()
|
||||||
|
File("${Config.extensionsRoot}/icon").mkdirs()
|
||||||
File(Config.thumbnailsRoot).mkdirs()
|
File(Config.thumbnailsRoot).mkdirs()
|
||||||
|
|
||||||
makeDataBaseTables()
|
makeDataBaseTables()
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ interface IProps {
|
|||||||
export default function ExtensionCard(props: IProps) {
|
export default function ExtensionCard(props: IProps) {
|
||||||
const {
|
const {
|
||||||
extension: {
|
extension: {
|
||||||
name, lang, versionName, iconUrl, installed, apkName,
|
name, lang, versionName, installed, apkName,
|
||||||
},
|
},
|
||||||
} = props;
|
} = props;
|
||||||
const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
|
const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
|
||||||
@@ -81,7 +81,7 @@ export default function ExtensionCard(props: IProps) {
|
|||||||
variant="rounded"
|
variant="rounded"
|
||||||
className={classes.icon}
|
className={classes.icon}
|
||||||
alt={name}
|
alt={name}
|
||||||
src={iconUrl}
|
src={`http://127.0.0.1:4567/api/v1/extension/icon/${apkName}`}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<Typography variant="h5" component="h2">
|
<Typography variant="h5" component="h2">
|
||||||
|
|||||||
1
webUI/react/src/typings.d.ts
vendored
1
webUI/react/src/typings.d.ts
vendored
@@ -9,6 +9,7 @@ interface IExtension {
|
|||||||
iconUrl: string
|
iconUrl: string
|
||||||
installed: boolean
|
installed: boolean
|
||||||
apkName: string
|
apkName: string
|
||||||
|
pkgName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISource {
|
interface ISource {
|
||||||
|
|||||||
Reference in New Issue
Block a user