mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-22 12:13:34 -05:00
Implement extension store
This commit is contained in:
@@ -34,7 +34,7 @@ object MangaAPI {
|
||||
get("update/{pkgName}", ExtensionController.update)
|
||||
get("uninstall/{pkgName}", ExtensionController.uninstall)
|
||||
|
||||
get("icon/{apkName}", ExtensionController.icon)
|
||||
get("icon/{pkgName}", ExtensionController.icon)
|
||||
}
|
||||
|
||||
path("source") {
|
||||
|
||||
@@ -165,17 +165,17 @@ object ExtensionController {
|
||||
/** icon for extension named `apkName` */
|
||||
val icon =
|
||||
handler(
|
||||
pathParam<String>("apkName"),
|
||||
pathParam<String>("pkgName"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
summary("Extension icon")
|
||||
description("Icon for extension named `apkName`")
|
||||
}
|
||||
},
|
||||
behaviorOf = { ctx, apkName ->
|
||||
behaviorOf = { ctx, pkgName ->
|
||||
ctx.getAttribute(Attribute.TachideskUser).requireUser()
|
||||
ctx.future {
|
||||
future { Extension.getExtensionIcon(apkName) }
|
||||
future { Extension.getExtensionIcon(pkgName) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
val httpCacheSeconds = 365.days.inWholeSeconds
|
||||
|
||||
@@ -18,6 +18,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
@@ -114,17 +115,20 @@ object Chapter {
|
||||
val mutex = Manga.mangaInfoMutex.get(mangaId) { Mutex() }
|
||||
val chapterList =
|
||||
mutex.withLock {
|
||||
val mangaEntry = transaction {
|
||||
MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||
}
|
||||
val mangaEntry =
|
||||
transaction {
|
||||
MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||
}
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
|
||||
val chapters = Manga.fetchMangaAndChapters(
|
||||
mangaEntry = mangaEntry,
|
||||
source = source,
|
||||
fetchDetails = false,
|
||||
fetchChapters = true,
|
||||
).chapters
|
||||
val chapters =
|
||||
Manga
|
||||
.fetchMangaAndChapters(
|
||||
mangaEntry = mangaEntry,
|
||||
source = source,
|
||||
fetchDetails = false,
|
||||
fetchChapters = true,
|
||||
).chapters
|
||||
|
||||
updateChapterListDatabase(mangaEntry, chapters, source)
|
||||
}
|
||||
@@ -150,22 +154,28 @@ object Chapter {
|
||||
}
|
||||
|
||||
// Recognize number for new chapters.
|
||||
val sManga = SManga.create().apply {
|
||||
url = mangaEntry[MangaTable.url]
|
||||
title = mangaEntry[MangaTable.title]
|
||||
thumbnail_url = mangaEntry[MangaTable.thumbnail_url]
|
||||
artist = mangaEntry[MangaTable.artist]
|
||||
author = mangaEntry[MangaTable.author]
|
||||
description = mangaEntry[MangaTable.description]
|
||||
genre = mangaEntry[MangaTable.genre]
|
||||
status = mangaEntry[MangaTable.status]
|
||||
update_strategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy])
|
||||
memo = mangaEntry[MangaTable.memo]
|
||||
initialized = mangaEntry[MangaTable.initialized]
|
||||
}
|
||||
val sManga =
|
||||
SManga.create().apply {
|
||||
url = mangaEntry[MangaTable.url]
|
||||
title = mangaEntry[MangaTable.title]
|
||||
thumbnail_url = mangaEntry[MangaTable.thumbnail_url]
|
||||
artist = mangaEntry[MangaTable.artist]
|
||||
author = mangaEntry[MangaTable.author]
|
||||
description = mangaEntry[MangaTable.description]
|
||||
genre = mangaEntry[MangaTable.genre]
|
||||
status = mangaEntry[MangaTable.status]
|
||||
update_strategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy])
|
||||
memo = Json.decodeFromString(mangaEntry[MangaTable.memo])
|
||||
initialized = mangaEntry[MangaTable.initialized]
|
||||
}
|
||||
uniqueChapters.forEach { chapter ->
|
||||
(source as? HttpSource)?.prepareNewChapter(chapter, sManga)
|
||||
val chapterNumber = ChapterRecognition.parseChapterNumber(mangaEntry[MangaTable.title], chapter.name, chapter.chapter_number.toDouble())
|
||||
val chapterNumber =
|
||||
ChapterRecognition.parseChapterNumber(
|
||||
mangaEntry[MangaTable.title],
|
||||
chapter.name,
|
||||
chapter.chapter_number.toDouble(),
|
||||
)
|
||||
chapter.chapter_number = chapterNumber.toFloat()
|
||||
chapter.name = chapter.name.sanitize(mangaEntry[MangaTable.title])
|
||||
chapter.scanlator = chapter.scanlator?.ifBlank { null }?.trim()
|
||||
@@ -270,7 +280,7 @@ object Chapter {
|
||||
this[ChapterTable.fetchedAt] = chapter.fetchedAt
|
||||
this[ChapterTable.manga] = chapter.mangaId
|
||||
this[ChapterTable.realUrl] = chapter.realUrl
|
||||
this[ChapterTable.memo] = chapter.memo
|
||||
this[ChapterTable.memo] = Json.encodeToString(chapter.memo)
|
||||
this[ChapterTable.isRead] = false
|
||||
this[ChapterTable.isBookmarked] = false
|
||||
this[ChapterTable.isDownloaded] = false
|
||||
@@ -321,7 +331,7 @@ object Chapter {
|
||||
this[ChapterTable.realUrl] = it.realUrl
|
||||
this[ChapterTable.lastModifiedAt] = it.lastModifiedAt
|
||||
this[ChapterTable.version] = it.version
|
||||
this[ChapterTable.memo] = it.memo
|
||||
this[ChapterTable.memo] = Json.encodeToString(it.memo)
|
||||
this[ChapterTable.isDownloaded] = currentChapter.downloaded
|
||||
this[ChapterTable.pageCount] = currentChapter.pageCount
|
||||
|
||||
@@ -359,7 +369,7 @@ object Chapter {
|
||||
mangaEntry[MangaTable.id].value,
|
||||
currentLatestChapterNumber,
|
||||
numberOfCurrentChapters,
|
||||
insertedChapters
|
||||
insertedChapters,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import io.github.reactivecircus.cache4k.Cache
|
||||
import io.javalin.http.HttpStatus
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Response
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
@@ -105,24 +106,26 @@ object Manga {
|
||||
genre = mangaEntry[MangaTable.genre]
|
||||
status = mangaEntry[MangaTable.status]
|
||||
update_strategy = UpdateStrategy.valueOf(mangaEntry[MangaTable.updateStrategy])
|
||||
memo = mangaEntry[MangaTable.memo]
|
||||
memo = Json.decodeFromString(mangaEntry[MangaTable.memo])
|
||||
initialized = mangaEntry[MangaTable.initialized]
|
||||
}
|
||||
val sChapters = transaction {
|
||||
ChapterTable.selectAll()
|
||||
.where { ChapterTable.manga eq mangaEntry[MangaTable.id] }
|
||||
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
|
||||
.map {
|
||||
SChapter.create().apply {
|
||||
url = it[ChapterTable.url]
|
||||
name = it[ChapterTable.name]
|
||||
chapter_number = it[ChapterTable.chapter_number]
|
||||
scanlator = it[ChapterTable.scanlator]
|
||||
date_upload = it[ChapterTable.date_upload]
|
||||
memo = it[ChapterTable.memo]
|
||||
val sChapters =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.manga eq mangaEntry[MangaTable.id] }
|
||||
.orderBy(ChapterTable.sourceOrder to SortOrder.DESC)
|
||||
.map {
|
||||
SChapter.create().apply {
|
||||
url = it[ChapterTable.url]
|
||||
name = it[ChapterTable.name]
|
||||
chapter_number = it[ChapterTable.chapter_number]
|
||||
scanlator = it[ChapterTable.scanlator]
|
||||
date_upload = it[ChapterTable.date_upload]
|
||||
memo = Json.decodeFromString(it[ChapterTable.memo])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.getMangaUpdate(
|
||||
sManga,
|
||||
@@ -137,12 +140,13 @@ object Manga {
|
||||
val mangaEntry =
|
||||
transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
val source = getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference]) ?: return null
|
||||
val sManga = fetchMangaAndChapters(
|
||||
mangaEntry,
|
||||
source,
|
||||
fetchDetails = true,
|
||||
fetchChapters = false
|
||||
).manga
|
||||
val sManga =
|
||||
fetchMangaAndChapters(
|
||||
mangaEntry,
|
||||
source,
|
||||
fetchDetails = true,
|
||||
fetchChapters = false,
|
||||
).manga
|
||||
|
||||
updateMangaDatabase(mangaEntry, source, sManga)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ package suwayomi.tachidesk.manga.impl
|
||||
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
@@ -75,6 +76,7 @@ object MangaList {
|
||||
this[MangaTable.status] = it.status
|
||||
this[MangaTable.thumbnail_url] = it.thumbnail_url
|
||||
this[MangaTable.updateStrategy] = it.update_strategy.name
|
||||
this[MangaTable.memo] = Json.encodeToString(it.memo)
|
||||
|
||||
this[MangaTable.sourceReference] = sourceId
|
||||
}.associate { Pair(it[MangaTable.url], it[MangaTable.id].value) }
|
||||
@@ -103,6 +105,7 @@ object MangaList {
|
||||
this[MangaTable.status] = sManga.status
|
||||
this[MangaTable.thumbnail_url] = sManga.thumbnail_url ?: manga[MangaTable.thumbnail_url]
|
||||
this[MangaTable.updateStrategy] = sManga.update_strategy.name
|
||||
this[MangaTable.memo] = Json.encodeToString(sManga.memo)
|
||||
if (!sManga.thumbnail_url.isNullOrEmpty() && manga[MangaTable.thumbnail_url] != sManga.thumbnail_url) {
|
||||
this[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
|
||||
Manga.clearThumbnail(manga[MangaTable.id].value)
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.manga.impl.Source.preferenceScreenMap
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
|
||||
@@ -49,10 +49,10 @@ object Source {
|
||||
id = it[SourceTable.id].value.toString(),
|
||||
name = it[SourceTable.name],
|
||||
lang = it[SourceTable.lang],
|
||||
iconUrl = getExtensionIconUrl(sourceExtension[ExtensionTable.apkName]),
|
||||
iconUrl = proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
||||
supportsLatest = catalogueSource.supportsLatest,
|
||||
isConfigurable = catalogueSource is ConfigurableSource,
|
||||
isNsfw = it[SourceTable.isNsfw],
|
||||
isNsfw = it[SourceTable.contentRating] >= 3,
|
||||
displayName = catalogueSource.toString(),
|
||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||
)
|
||||
@@ -70,13 +70,10 @@ object Source {
|
||||
id = sourceId.toString(),
|
||||
name = source[SourceTable.name],
|
||||
lang = source[SourceTable.lang],
|
||||
iconUrl =
|
||||
getExtensionIconUrl(
|
||||
extension[ExtensionTable.apkName],
|
||||
),
|
||||
iconUrl = proxyExtensionIconUrl(extension[ExtensionTable.pkgName]),
|
||||
supportsLatest = catalogueSource.supportsLatest,
|
||||
isConfigurable = catalogueSource is ConfigurableSource,
|
||||
isNsfw = source[SourceTable.isNsfw],
|
||||
isNsfw = source[SourceTable.contentRating] >= 3,
|
||||
displayName = catalogueSource.toString(),
|
||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||
)
|
||||
|
||||
@@ -9,7 +9,6 @@ package suwayomi.tachidesk.manga.impl.backup.proto.handlers
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
@@ -79,7 +78,7 @@ object BackupMangaHandler {
|
||||
lastModifiedAt = mangaRow[MangaTable.lastModifiedAt],
|
||||
version = mangaRow[MangaTable.version],
|
||||
initialized = mangaRow[MangaTable.initialized],
|
||||
memo = Json.encodeToString(mangaRow[MangaTable.memo]).encodeToByteArray()
|
||||
memo = Json.encodeToString(mangaRow[MangaTable.memo]).encodeToByteArray(),
|
||||
)
|
||||
|
||||
val mangaId = mangaRow[MangaTable.id].value
|
||||
@@ -242,7 +241,7 @@ object BackupMangaHandler {
|
||||
|
||||
it[lastModifiedAt] = manga.lastModifiedAt
|
||||
it[version] = manga.version
|
||||
it[memo] = Json.decodeFromString<JsonObject>(manga.memo.decodeToString())
|
||||
it[memo] = manga.memo.decodeToString()
|
||||
}.value
|
||||
} else {
|
||||
val dbMangaId = dbManga[MangaTable.id].value
|
||||
@@ -265,7 +264,7 @@ object BackupMangaHandler {
|
||||
|
||||
it[lastModifiedAt] = manga.lastModifiedAt
|
||||
it[version] = manga.version
|
||||
it[memo] = Json.decodeFromString<JsonObject>(manga.memo.decodeToString())
|
||||
it[memo] = manga.memo.decodeToString()
|
||||
}
|
||||
|
||||
dbMangaId
|
||||
@@ -357,7 +356,7 @@ object BackupMangaHandler {
|
||||
|
||||
this[ChapterTable.lastModifiedAt] = chapter.lastModifiedAt
|
||||
this[ChapterTable.version] = chapter.version
|
||||
this[ChapterTable.memo] = Json.decodeFromString<JsonObject>(chapter.memo.decodeToString())
|
||||
this[ChapterTable.memo] = chapter.memo.decodeToString()
|
||||
}.map { it[ChapterTable.id].value }
|
||||
} else {
|
||||
emptyList()
|
||||
|
||||
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import net.dongliu.apk.parser.ApkFile
|
||||
import net.dongliu.apk.parser.bean.Icon
|
||||
@@ -23,11 +24,10 @@ import okio.source
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.select
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.extensionTableAsDataClass
|
||||
import suwayomi.tachidesk.manga.impl.extension.github.ExtensionGithubApi
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.EXTENSION_FEATURE
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
|
||||
@@ -62,18 +62,20 @@ object Extension {
|
||||
|
||||
suspend fun installExtension(pkgName: String): Int {
|
||||
logger.debug { "Installing $pkgName" }
|
||||
val extensionRecord = extensionTableAsDataClass().first { it.pkgName == pkgName }
|
||||
val apkUrl =
|
||||
transaction {
|
||||
ExtensionTable
|
||||
.select(ExtensionTable.apkUrl)
|
||||
.where { ExtensionTable.pkgName eq pkgName }
|
||||
.firstOrNull()
|
||||
?.get(ExtensionTable.apkUrl)
|
||||
} ?: throw NullPointerException("Could not find extension $pkgName")
|
||||
|
||||
return installAPK {
|
||||
val apkURL =
|
||||
ExtensionGithubApi.getApkUrl(
|
||||
extensionRecord.repo ?: throw NullPointerException("Could not find extension repo"),
|
||||
extensionRecord.apkName,
|
||||
)
|
||||
val apkName = Uri.parse(apkURL).lastPathSegment!!
|
||||
val apkName = Uri.parse(apkUrl).lastPathSegment!!
|
||||
val apkSavePath = "${applicationDirs.extensionsRoot}/$apkName"
|
||||
// download apk file
|
||||
downloadAPKFile(apkURL, apkSavePath)
|
||||
downloadAPKFile(apkUrl, apkSavePath)
|
||||
|
||||
apkSavePath
|
||||
}
|
||||
@@ -193,9 +195,9 @@ object Extension {
|
||||
it[name] = extensionName
|
||||
it[this.pkgName] = packageInfo.packageName
|
||||
it[versionName] = packageInfo.versionName
|
||||
it[versionCode] = packageInfo.versionCode
|
||||
it[versionCode] = packageInfo.versionCode.toLong()
|
||||
it[lang] = extensionLang
|
||||
it[this.isNsfw] = isNsfw
|
||||
it[contentRating] = if (isNsfw) 3 else 0 // todo will change
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +206,7 @@ object Extension {
|
||||
it[this.isInstalled] = true
|
||||
it[this.classFQName] = className
|
||||
it[versionName] = packageInfo.versionName
|
||||
it[versionCode] = packageInfo.versionCode
|
||||
it[versionCode] = packageInfo.versionCode.toLong()
|
||||
}
|
||||
|
||||
val extensionId =
|
||||
@@ -220,7 +222,7 @@ object Extension {
|
||||
it[name] = httpSource.name
|
||||
it[lang] = httpSource.lang
|
||||
it[extension] = extensionId
|
||||
it[SourceTable.isNsfw] = isNsfw
|
||||
it[contentRating] = if (isNsfw) 3 else 0
|
||||
}
|
||||
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
||||
}
|
||||
@@ -343,7 +345,9 @@ object Extension {
|
||||
logger.debug { "Uninstalling $pkgName" }
|
||||
|
||||
val extensionRecord = transaction { ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.first() }
|
||||
val fileNameWithoutType = extensionRecord[ExtensionTable.apkName].substringBefore(".apk")
|
||||
val fileNameWithoutType =
|
||||
extensionRecord[ExtensionTable.apkName]?.substringBefore(".apk")
|
||||
?: throw NullPointerException("Missing $pkgName apkName")
|
||||
val jarPath = "${applicationDirs.extensionsRoot}/$fileNameWithoutType.jar"
|
||||
val sources =
|
||||
transaction {
|
||||
@@ -359,6 +363,7 @@ object Extension {
|
||||
ExtensionTable.update({ ExtensionTable.pkgName eq pkgName }) {
|
||||
it[isInstalled] = false
|
||||
it[hasUpdate] = false
|
||||
it[apkName] = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,8 +390,7 @@ object Extension {
|
||||
it[versionName] = targetExtension.versionName
|
||||
it[versionCode] = targetExtension.versionCode
|
||||
it[lang] = targetExtension.lang
|
||||
it[isNsfw] = targetExtension.isNsfw
|
||||
it[apkName] = targetExtension.apkName
|
||||
it[contentRating] = targetExtension.contentRating.ordinal
|
||||
it[iconUrl] = targetExtension.iconUrl
|
||||
it[hasUpdate] = false
|
||||
}
|
||||
@@ -394,17 +398,21 @@ object Extension {
|
||||
return installExtension(pkgName)
|
||||
}
|
||||
|
||||
suspend fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
|
||||
val iconUrl =
|
||||
if (apkName == "localSource") {
|
||||
""
|
||||
} else {
|
||||
transaction { ExtensionTable.selectAll().where { ExtensionTable.apkName eq apkName }.first() }[ExtensionTable.iconUrl]
|
||||
}
|
||||
|
||||
suspend fun getExtensionIcon(pkgName: String): Pair<InputStream, String> {
|
||||
val cacheSaveDir = "${applicationDirs.extensionsRoot}/icon"
|
||||
|
||||
return getImageResponse(cacheSaveDir, apkName) {
|
||||
if (pkgName == LocalSource::class.java.`package`.name) {
|
||||
return getImageResponse(cacheSaveDir, "localSource") {
|
||||
network.client
|
||||
.newCall(GET("", cache = CacheControl.FORCE_NETWORK))
|
||||
.await()
|
||||
}
|
||||
}
|
||||
|
||||
val iconUrl =
|
||||
transaction { ExtensionTable.selectAll().where { ExtensionTable.pkgName eq pkgName }.first() }[ExtensionTable.iconUrl]
|
||||
|
||||
return getImageResponse(cacheSaveDir, pkgName) {
|
||||
network.client
|
||||
.newCall(
|
||||
GET(iconUrl, cache = CacheControl.FORCE_NETWORK),
|
||||
@@ -412,5 +420,5 @@ object Extension {
|
||||
}
|
||||
}
|
||||
|
||||
fun getExtensionIconUrl(apkName: String): String = "/api/v1/extension/icon/$apkName"
|
||||
fun proxyExtensionIconUrl(pkgName: String): String = "/api/v1/extension/icon/$pkgName"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
package suwayomi.tachidesk.manga.impl.extension
|
||||
|
||||
/*
|
||||
* 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 eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
||||
import org.jetbrains.exposed.v1.jdbc.insert
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import suwayomi.tachidesk.manga.impl.extension.github.NetworkExtensionStore
|
||||
import suwayomi.tachidesk.manga.impl.extension.github.NetworkLegacyExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.github.NetworkLegacyExtensionRepo
|
||||
import suwayomi.tachidesk.manga.impl.extension.github.toExtensionInfo
|
||||
import suwayomi.tachidesk.manga.impl.extension.github.toExtensionInfos
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
object ExtensionStoreService {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
val network: NetworkHelper by injectLazy()
|
||||
val protoBuf: ProtoBuf by injectLazy()
|
||||
val json: Json by injectLazy()
|
||||
|
||||
suspend fun fetch(indexUrl: String): ExtensionStore = fetch(indexUrl, forceV2 = false)
|
||||
|
||||
private suspend fun fetch(
|
||||
indexUrl: String,
|
||||
forceV2: Boolean,
|
||||
): ExtensionStore {
|
||||
var updatedIndexUrl = indexUrl
|
||||
return try {
|
||||
val response = network.client.newCall(GET(indexUrl)).awaitSuccess()
|
||||
response.body
|
||||
.source()
|
||||
.use { source ->
|
||||
try {
|
||||
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
logger.debug { "Failed to decode as protobuf, trying JSON" }
|
||||
if (forceV2) throw e
|
||||
try {
|
||||
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
|
||||
} catch (_: IllegalArgumentException) {
|
||||
logger.debug { "Failed to decode as NetworkExtensionStore, trying LegacyExtensionRepo" }
|
||||
val legacyIndex =
|
||||
try {
|
||||
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(source.peek())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
if (!indexUrl.endsWith("/index.min.json")) {
|
||||
throw e
|
||||
}
|
||||
logger.debug { "Retrying with /index.min.json" }
|
||||
updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json")
|
||||
network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().body.source().use {
|
||||
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (legacyIndex.indexV2 != null) {
|
||||
return fetch(legacyIndex.indexV2, forceV2 = true)
|
||||
} else {
|
||||
legacyIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toExtensionStore(updatedIndexUrl)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
logger.debug(e) { "Failed to fetch extension store '$indexUrl'" }
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
fun upsert(store: ExtensionStore) {
|
||||
transaction {
|
||||
val existing =
|
||||
ExtensionStoreTable
|
||||
.selectAll()
|
||||
.where { ExtensionStoreTable.indexUrl eq store.indexUrl }
|
||||
.firstOrNull()
|
||||
|
||||
if (existing == null) {
|
||||
ExtensionStoreTable.insert {
|
||||
it[name] = store.name
|
||||
it[badgeLabel] = store.badgeLabel
|
||||
it[signingKey] = store.signingKey
|
||||
it[contactWebsite] = store.contact.website
|
||||
it[contactDiscord] = store.contact.discord
|
||||
it[indexUrl] = store.indexUrl
|
||||
it[isLegacy] = store.isLegacy
|
||||
}
|
||||
} else {
|
||||
ExtensionStoreTable.update({ ExtensionStoreTable.indexUrl eq store.indexUrl }) {
|
||||
it[name] = store.name
|
||||
it[badgeLabel] = store.badgeLabel
|
||||
it[signingKey] = store.signingKey
|
||||
it[contactWebsite] = store.contact.website
|
||||
it[contactDiscord] = store.contact.discord
|
||||
it[isLegacy] = store.isLegacy
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAndRefresh(): List<ExtensionStore> {
|
||||
val stores =
|
||||
transaction {
|
||||
ExtensionStoreTable.selectAll().toList()
|
||||
}
|
||||
return stores.mapNotNull { storeRow ->
|
||||
val oldIndexUrl = storeRow[ExtensionStoreTable.indexUrl]
|
||||
val oldName = storeRow[ExtensionStoreTable.name]
|
||||
try {
|
||||
val store = fetch(oldIndexUrl)
|
||||
upsert(store)
|
||||
if (store.indexUrl != oldIndexUrl) {
|
||||
transaction {
|
||||
ExtensionStoreTable.deleteWhere { ExtensionStoreTable.indexUrl eq oldIndexUrl }
|
||||
}
|
||||
syncDbToPrefs()
|
||||
}
|
||||
store
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Failed to fetch extension store '$oldName ($oldIndexUrl)'" }
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun syncDbToPrefs() {
|
||||
val dbStores =
|
||||
transaction {
|
||||
ExtensionStoreTable
|
||||
.selectAll()
|
||||
.map { it[ExtensionStoreTable.indexUrl] }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
val currentPrefs = serverConfig.extensionStores.value.toSet()
|
||||
val toAdd = dbStores - currentPrefs
|
||||
val toRemove = currentPrefs - dbStores
|
||||
|
||||
if (toAdd.isNotEmpty()) {
|
||||
serverConfig.extensionStores.value = (serverConfig.extensionStores.value + toAdd).distinct()
|
||||
}
|
||||
|
||||
if (toRemove.isNotEmpty()) {
|
||||
serverConfig.extensionStores.value = serverConfig.extensionStores.value.filterNot { it in toRemove }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun syncPrefsToDb() {
|
||||
val prefUrls = serverConfig.extensionStores.value.toSet()
|
||||
|
||||
val dbStores =
|
||||
transaction {
|
||||
ExtensionStoreTable.selectAll().associateBy { it[ExtensionStoreTable.indexUrl] }
|
||||
}
|
||||
|
||||
val toAdd = prefUrls - dbStores.keys
|
||||
|
||||
toAdd.forEach { url ->
|
||||
try {
|
||||
val store = fetch(url)
|
||||
upsert(store)
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Failed to sync preference store '$url' to database" }
|
||||
}
|
||||
}
|
||||
|
||||
val toRemove = dbStores.keys - prefUrls
|
||||
if (toRemove.isNotEmpty()) {
|
||||
transaction {
|
||||
ExtensionStoreTable.deleteWhere { ExtensionStoreTable.indexUrl inList toRemove.toList() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getExtensions(store: ExtensionStore): List<ExtensionInfo> {
|
||||
val extensions =
|
||||
if (!store.isLegacy) {
|
||||
val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess()
|
||||
response.body
|
||||
.source()
|
||||
.use { source ->
|
||||
try {
|
||||
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
||||
} catch (_: IllegalArgumentException) {
|
||||
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
|
||||
}
|
||||
}.toExtensionInfos(store)
|
||||
} else {
|
||||
val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json")
|
||||
val response = network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess()
|
||||
response.body.source().use { source ->
|
||||
json
|
||||
.decodeFromBufferedSource<List<NetworkLegacyExtension>>(source)
|
||||
.map { it.toExtensionInfo(store, storeBaseUrl) }
|
||||
}
|
||||
}
|
||||
return extensions
|
||||
}
|
||||
}
|
||||
@@ -21,12 +21,10 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.jdbc.update
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIconUrl
|
||||
import suwayomi.tachidesk.manga.impl.extension.github.ExtensionGithubApi
|
||||
import suwayomi.tachidesk.manga.impl.extension.github.OnlineExtension
|
||||
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -34,23 +32,23 @@ object ExtensionsList {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
var lastUpdateCheck: Long = 0
|
||||
var updateMap = ConcurrentHashMap<String, OnlineExtension>()
|
||||
var updateMap = ConcurrentHashMap<String, ExtensionInfo>()
|
||||
|
||||
suspend fun fetchExtensions() {
|
||||
// update if 60 seconds has passed or requested offline and database is empty
|
||||
val extensions =
|
||||
serverConfig.extensionRepos.value.map { repo ->
|
||||
kotlin
|
||||
.runCatching {
|
||||
ExtensionGithubApi.findExtensions(repo.repoUrlReplace())
|
||||
}.onFailure {
|
||||
logger.warn(it) {
|
||||
"Failed to fetch extensions for repo: $repo"
|
||||
}
|
||||
}
|
||||
val allExtensions = mutableListOf<ExtensionInfo>()
|
||||
|
||||
ExtensionStoreService.getAndRefresh().forEach { store ->
|
||||
try {
|
||||
val extensions = ExtensionStoreService.getExtensions(store)
|
||||
allExtensions.addAll(extensions)
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) {
|
||||
"Failed to fetch extensions for store: ${store.indexUrl}"
|
||||
}
|
||||
}
|
||||
val foundExtensions = extensions.mapNotNull { it.getOrNull() }.flatten()
|
||||
updateExtensionDatabase(foundExtensions)
|
||||
}
|
||||
|
||||
updateExtensionDatabase(allExtensions)
|
||||
}
|
||||
|
||||
suspend fun fetchExtensionsCached() {
|
||||
@@ -74,25 +72,25 @@ object ExtensionsList {
|
||||
transaction {
|
||||
ExtensionTable.selectAll().filter { it[ExtensionTable.name] != LocalSource.EXTENSION_NAME }.map {
|
||||
ExtensionDataClass(
|
||||
it[ExtensionTable.repo],
|
||||
it[ExtensionTable.apkName],
|
||||
getExtensionIconUrl(it[ExtensionTable.apkName]),
|
||||
it[ExtensionTable.name],
|
||||
it[ExtensionTable.pkgName],
|
||||
it[ExtensionTable.versionName],
|
||||
it[ExtensionTable.versionCode],
|
||||
it[ExtensionTable.lang],
|
||||
it[ExtensionTable.isNsfw],
|
||||
it[ExtensionTable.isInstalled],
|
||||
it[ExtensionTable.hasUpdate],
|
||||
it[ExtensionTable.isObsolete],
|
||||
repo = it[ExtensionTable.storeIndexUrl],
|
||||
apkName = it[ExtensionTable.apkName].orEmpty(),
|
||||
iconUrl = proxyExtensionIconUrl(it[ExtensionTable.pkgName]),
|
||||
name = it[ExtensionTable.name],
|
||||
pkgName = it[ExtensionTable.pkgName],
|
||||
versionName = it[ExtensionTable.versionName],
|
||||
versionCode = it[ExtensionTable.versionCode].toInt(),
|
||||
lang = it[ExtensionTable.lang],
|
||||
isNsfw = it[ExtensionTable.contentRating] == 3,
|
||||
installed = it[ExtensionTable.isInstalled],
|
||||
hasUpdate = it[ExtensionTable.hasUpdate],
|
||||
obsolete = it[ExtensionTable.isObsolete],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val updateExtensionDatabaseMutex = Mutex()
|
||||
|
||||
private suspend fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
|
||||
private suspend fun updateExtensionDatabase(foundExtensions: List<ExtensionInfo>) {
|
||||
updateExtensionDatabaseMutex.withLock {
|
||||
transaction {
|
||||
val uniqueExtensions =
|
||||
@@ -106,10 +104,10 @@ object ExtensionsList {
|
||||
.selectAll()
|
||||
.toList()
|
||||
.associateBy { it[ExtensionTable.pkgName] }
|
||||
val extensionsToUpdate = mutableListOf<Pair<OnlineExtension, ResultRow>>()
|
||||
val extensionsToInsert = mutableListOf<OnlineExtension>()
|
||||
val extensionsToUpdate = mutableListOf<Pair<ExtensionInfo, ResultRow>>()
|
||||
val extensionsToInsert = mutableListOf<ExtensionInfo>()
|
||||
val extensionsToDelete =
|
||||
installedExtensions.filter { it.value[ExtensionTable.repo] != null }.mapNotNull { (pkgName, extension) ->
|
||||
installedExtensions.filter { it.value[ExtensionTable.storeIndexUrl] != null }.mapNotNull { (pkgName, extension) ->
|
||||
extension.takeUnless { uniqueExtensions.any { it.pkgName == pkgName } }
|
||||
}
|
||||
uniqueExtensions.forEach {
|
||||
@@ -132,7 +130,7 @@ object ExtensionsList {
|
||||
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
||||
// Always update icon url and repo
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
this[ExtensionTable.storeIndexUrl] = foundExtension.storeIndexUrl
|
||||
|
||||
// add these because batch updates need matching columns
|
||||
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
|
||||
@@ -168,13 +166,14 @@ object ExtensionsList {
|
||||
extensionsToFullyUpdate.forEach { (foundExtension, extensionRecord) ->
|
||||
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
|
||||
// extension is not installed, so we can overwrite the data without a care
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
this[ExtensionTable.storeIndexUrl] = foundExtension.storeIndexUrl
|
||||
this[ExtensionTable.name] = foundExtension.name
|
||||
this[ExtensionTable.extensionLib] = foundExtension.extensionLib
|
||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||
this[ExtensionTable.lang] = foundExtension.lang
|
||||
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
|
||||
this[ExtensionTable.apkName] = foundExtension.apkName
|
||||
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal
|
||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
}
|
||||
}.toExecutable()
|
||||
@@ -183,14 +182,15 @@ object ExtensionsList {
|
||||
}
|
||||
if (extensionsToInsert.isNotEmpty()) {
|
||||
ExtensionTable.batchInsert(extensionsToInsert) { foundExtension ->
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
this[ExtensionTable.storeIndexUrl] = foundExtension.storeIndexUrl
|
||||
this[ExtensionTable.name] = foundExtension.name
|
||||
this[ExtensionTable.pkgName] = foundExtension.pkgName
|
||||
this[ExtensionTable.extensionLib] = foundExtension.extensionLib
|
||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||
this[ExtensionTable.lang] = foundExtension.lang
|
||||
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
|
||||
this[ExtensionTable.apkName] = foundExtension.apkName
|
||||
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal
|
||||
this[ExtensionTable.apkUrl] = foundExtension.apkUrl
|
||||
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
|
||||
}
|
||||
}
|
||||
@@ -215,16 +215,4 @@ object ExtensionsList {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.repoUrlReplace(): String =
|
||||
if (contains("github")) {
|
||||
replace(repoMatchRegex) {
|
||||
"https://raw.githubusercontent.com/${it.groupValues[2]}/${it.groupValues[3]}/" +
|
||||
(it.groupValues.getOrNull(4)?.ifBlank { null } ?: "repo") +
|
||||
"/" +
|
||||
(it.groupValues.getOrNull(5)?.ifBlank { null } ?: "index.min.json")
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package suwayomi.tachidesk.manga.impl.extension.github
|
||||
|
||||
/*
|
||||
* 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 suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
||||
|
||||
interface BaseNetworkExtensionStore {
|
||||
fun toExtensionStore(indexUrl: String): ExtensionStore
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package suwayomi.tachidesk.manga.impl.extension.github
|
||||
|
||||
/*
|
||||
* 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 eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MIN
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
object ExtensionGithubApi {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
@Serializable
|
||||
private data class ExtensionJsonObject(
|
||||
val name: String,
|
||||
val pkg: String,
|
||||
val apk: String,
|
||||
val lang: String,
|
||||
val code: Int,
|
||||
val version: String,
|
||||
val nsfw: Int,
|
||||
val hasReadme: Int = 0,
|
||||
val hasChangelog: Int = 0,
|
||||
val sources: List<ExtensionSourceJsonObject>?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class ExtensionSourceJsonObject(
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val id: Long,
|
||||
val baseUrl: String,
|
||||
)
|
||||
|
||||
suspend fun findExtensions(repo: String): List<OnlineExtension> {
|
||||
val response =
|
||||
client.newCall(GET(repo)).awaitSuccess()
|
||||
|
||||
return with(json) {
|
||||
response
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions(repo.substringBeforeLast('/') + '/')
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(
|
||||
repo: String,
|
||||
apkName: String,
|
||||
): String = "${repo}apk/$apkName"
|
||||
|
||||
private val client by lazy {
|
||||
val network: NetworkHelper by injectLazy()
|
||||
network.client
|
||||
.newBuilder()
|
||||
.addNetworkInterceptor { chain ->
|
||||
val originalResponse = chain.proceed(chain.request())
|
||||
originalResponse
|
||||
.newBuilder()
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun List<ExtensionJsonObject>.toExtensions(repo: String): List<OnlineExtension> =
|
||||
this
|
||||
.filter {
|
||||
val libVersion = it.version.substringBeforeLast('.').toDouble()
|
||||
libVersion in LIB_VERSION_MIN..LIB_VERSION_MAX
|
||||
}.map {
|
||||
OnlineExtension(
|
||||
repo = repo,
|
||||
name = it.name.substringAfter("Tachiyomi: "),
|
||||
pkgName = it.pkg,
|
||||
versionName = it.version,
|
||||
versionCode = it.code,
|
||||
lang = it.lang,
|
||||
isNsfw = it.nsfw == 1,
|
||||
hasReadme = it.hasReadme == 1,
|
||||
hasChangelog = it.hasChangelog == 1,
|
||||
sources = it.sources?.toExtensionSources() ?: emptyList(),
|
||||
apkName = it.apk,
|
||||
iconUrl = "${repo}icon/${it.pkg}.png",
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<ExtensionSourceJsonObject>.toExtensionSources(): List<OnlineExtensionSource> =
|
||||
this.map {
|
||||
OnlineExtensionSource(
|
||||
name = it.name,
|
||||
lang = it.lang,
|
||||
id = it.id,
|
||||
baseUrl = it.baseUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package suwayomi.tachidesk.manga.impl.extension.github
|
||||
|
||||
/*
|
||||
* 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 kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class NetworkExtensionStore(
|
||||
@ProtoNumber(1) val name: String,
|
||||
@ProtoNumber(2) val badgeLabel: String,
|
||||
@ProtoNumber(3) val signingKey: String,
|
||||
@ProtoNumber(4) val contact: Contact,
|
||||
@ProtoNumber(5) val extensions: List<Extension>,
|
||||
) : BaseNetworkExtensionStore {
|
||||
@Serializable
|
||||
data class Contact(
|
||||
@ProtoNumber(1) val website: String,
|
||||
@ProtoNumber(2) val discord: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Extension(
|
||||
@ProtoNumber(1) val name: String,
|
||||
@ProtoNumber(2) val packageName: String,
|
||||
@ProtoNumber(3) val resources: Resources,
|
||||
@ProtoNumber(4) val extensionLib: String,
|
||||
@ProtoNumber(5) val versionCode: Long,
|
||||
@ProtoNumber(6) val versionName: String,
|
||||
@ProtoNumber(7) val sources: List<Source>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Resources(
|
||||
@ProtoNumber(1) val apkUrl: String,
|
||||
@ProtoNumber(2) val iconUrl: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Source(
|
||||
@ProtoNumber(1) val id: Long,
|
||||
@ProtoNumber(2) val name: String,
|
||||
@ProtoNumber(3) val language: String,
|
||||
@ProtoNumber(4) val homeUrl: String = "",
|
||||
@ProtoNumber(5) val mirrorUrls: List<String> = emptyList(),
|
||||
@ProtoNumber(6) val contentRating: ContentRating = ContentRating.SAFE,
|
||||
@ProtoNumber(7) val message: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class ContentRating {
|
||||
@ProtoNumber(0)
|
||||
@JsonNames("CONTENT_RATING_SAFE")
|
||||
SAFE,
|
||||
|
||||
@ProtoNumber(1)
|
||||
@JsonNames("CONTENT_RATING_SUGGESTIVE")
|
||||
SUGGESTIVE,
|
||||
|
||||
@ProtoNumber(2)
|
||||
@JsonNames("CONTENT_RATING_EROTICA")
|
||||
EROTICA,
|
||||
|
||||
@ProtoNumber(3)
|
||||
@JsonNames("CONTENT_RATING_PORNOGRAPHIC")
|
||||
PORNOGRAPHIC,
|
||||
}
|
||||
|
||||
override fun toExtensionStore(indexUrl: String): ExtensionStore =
|
||||
ExtensionStore(
|
||||
indexUrl = indexUrl,
|
||||
name = name,
|
||||
badgeLabel = badgeLabel,
|
||||
signingKey = signingKey,
|
||||
contact =
|
||||
ExtensionStore.Contact(
|
||||
website = contact.website,
|
||||
discord = contact.discord,
|
||||
),
|
||||
isLegacy = false,
|
||||
)
|
||||
}
|
||||
|
||||
fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<ExtensionInfo> =
|
||||
extensions.map { extension ->
|
||||
val lang = extension.sources.map { it.language }.toSet()
|
||||
ExtensionInfo(
|
||||
storeIndexUrl = store.indexUrl,
|
||||
name = extension.name,
|
||||
pkgName = extension.packageName,
|
||||
apkUrl = extension.resources.apkUrl,
|
||||
iconUrl = extension.resources.iconUrl,
|
||||
extensionLib = extension.extensionLib,
|
||||
versionCode = extension.versionCode,
|
||||
versionName = extension.versionName,
|
||||
lang = if (lang.size == 1) lang.first() else "all",
|
||||
contentRating =
|
||||
when (extension.sources.maxOfOrNull { it.contentRating }) {
|
||||
NetworkExtensionStore.ContentRating.SAFE -> ContentRating.SAFE
|
||||
NetworkExtensionStore.ContentRating.SUGGESTIVE -> ContentRating.SUGGESTIVE
|
||||
NetworkExtensionStore.ContentRating.EROTICA -> ContentRating.EROTICA
|
||||
NetworkExtensionStore.ContentRating.PORNOGRAPHIC -> ContentRating.PORNOGRAPHIC
|
||||
null -> ContentRating.SAFE
|
||||
},
|
||||
sources =
|
||||
extension.sources.map { source ->
|
||||
ExtensionSource(
|
||||
id = source.id,
|
||||
name = source.name,
|
||||
lang = source.language,
|
||||
homeUrl = source.homeUrl,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package suwayomi.tachidesk.manga.impl.extension.github
|
||||
|
||||
/*
|
||||
* 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 kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Serializable
|
||||
data class NetworkLegacyExtension(
|
||||
val name: String,
|
||||
val pkg: String,
|
||||
val apk: String,
|
||||
val lang: String,
|
||||
val version: String,
|
||||
val code: Long,
|
||||
val nsfw: Int,
|
||||
val sources: List<Source>? = null,
|
||||
) {
|
||||
@Serializable
|
||||
data class Source(
|
||||
val id: Long,
|
||||
val lang: String,
|
||||
val name: String,
|
||||
val baseUrl: String,
|
||||
)
|
||||
}
|
||||
|
||||
fun NetworkLegacyExtension.toExtensionInfo(
|
||||
store: ExtensionStore,
|
||||
storeBaseUrl: String,
|
||||
): ExtensionInfo =
|
||||
ExtensionInfo(
|
||||
storeIndexUrl = store.indexUrl,
|
||||
name = name.substringAfter("Tachiyomi: "),
|
||||
pkgName = pkg,
|
||||
apkUrl = "$storeBaseUrl/apk/$apk",
|
||||
iconUrl = "$storeBaseUrl/icon/$pkg.png",
|
||||
extensionLib = version.substringBeforeLast('.'),
|
||||
versionCode = code,
|
||||
versionName = version,
|
||||
lang = lang,
|
||||
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
|
||||
sources =
|
||||
if (sources.isNullOrEmpty()) {
|
||||
listOf(
|
||||
ExtensionSource(
|
||||
id = 0,
|
||||
name = name,
|
||||
lang = lang,
|
||||
homeUrl = "",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
sources.map { source ->
|
||||
ExtensionSource(
|
||||
id = source.id,
|
||||
name = source.name,
|
||||
lang = source.lang,
|
||||
homeUrl = source.baseUrl,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
package suwayomi.tachidesk.manga.impl.extension.github
|
||||
|
||||
/*
|
||||
* 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 kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
|
||||
|
||||
@Serializable
|
||||
data class NetworkLegacyExtensionRepo(
|
||||
@SerialName("index_v2") val indexV2: String?,
|
||||
val meta: Meta,
|
||||
) : BaseNetworkExtensionStore {
|
||||
@Serializable
|
||||
data class Meta(
|
||||
val name: String,
|
||||
val shortName: String?,
|
||||
val website: String,
|
||||
val signingKeyFingerprint: String,
|
||||
)
|
||||
|
||||
override fun toExtensionStore(indexUrl: String): ExtensionStore =
|
||||
ExtensionStore(
|
||||
indexUrl = indexUrl,
|
||||
name = meta.name,
|
||||
badgeLabel = meta.shortName ?: meta.name,
|
||||
signingKey = meta.signingKeyFingerprint,
|
||||
contact =
|
||||
ExtensionStore.Contact(
|
||||
website = meta.website,
|
||||
discord = null,
|
||||
),
|
||||
isLegacy = true,
|
||||
)
|
||||
}
|
||||
@@ -41,7 +41,7 @@ object PackageTools {
|
||||
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
|
||||
const val METADATA_NSFW = "tachiyomi.extension.nsfw"
|
||||
const val LIB_VERSION_MIN = 1.3
|
||||
const val LIB_VERSION_MAX = 1.5
|
||||
const val LIB_VERSION_MAX = 1.6
|
||||
|
||||
/**
|
||||
* Convert dex to jar, a wrapper for the dex2jar library
|
||||
|
||||
@@ -45,7 +45,9 @@ object GetCatalogueSource {
|
||||
ExtensionTable.selectAll().where { ExtensionTable.id eq extensionId }.first()
|
||||
}
|
||||
|
||||
val apkName = extensionRecord[ExtensionTable.apkName]
|
||||
val apkName =
|
||||
extensionRecord[ExtensionTable.apkName]
|
||||
?: throw NullPointerException("Missing apkName")
|
||||
val className = extensionRecord[ExtensionTable.classFQName]
|
||||
val jarName = apkName.substringBefore(".apk") + ".jar"
|
||||
val jarPath = "${applicationDirs.extensionsRoot}/$jarName"
|
||||
|
||||
@@ -29,7 +29,11 @@ open class StubSource(
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularManga"))
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> = Observable.error(getSourceNotInstalledException())
|
||||
|
||||
override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage = throw getSourceNotInstalledException()
|
||||
override suspend fun getSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): MangasPage = throw getSourceNotInstalledException()
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
|
||||
override fun fetchSearchManga(
|
||||
@@ -45,7 +49,12 @@ open class StubSource(
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList()
|
||||
|
||||
override suspend fun getMangaUpdate(manga: SManga, chapters: List<SChapter>, fetchDetails: Boolean, fetchChapters: Boolean): SMangaUpdate = throw getSourceNotInstalledException()
|
||||
override suspend fun getMangaUpdate(
|
||||
manga: SManga,
|
||||
chapters: List<SChapter>,
|
||||
fetchDetails: Boolean,
|
||||
fetchChapters: Boolean,
|
||||
): SMangaUpdate = throw getSourceNotInstalledException()
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> = Observable.error(getSourceNotInstalledException())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package suwayomi.tachidesk.manga.impl.extension.github
|
||||
package suwayomi.tachidesk.manga.model.dataclass
|
||||
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
@@ -7,24 +7,30 @@ package suwayomi.tachidesk.manga.impl.extension.github
|
||||
* 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/. */
|
||||
|
||||
data class OnlineExtensionSource(
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val id: Long,
|
||||
val baseUrl: String,
|
||||
)
|
||||
|
||||
data class OnlineExtension(
|
||||
val repo: String,
|
||||
data class ExtensionInfo(
|
||||
val storeIndexUrl: String,
|
||||
val name: String,
|
||||
val pkgName: String,
|
||||
val apkName: String,
|
||||
val lang: String,
|
||||
val versionCode: Int,
|
||||
val versionName: String,
|
||||
val isNsfw: Boolean,
|
||||
val hasReadme: Boolean,
|
||||
val hasChangelog: Boolean,
|
||||
val sources: List<OnlineExtensionSource>,
|
||||
val apkUrl: String,
|
||||
val iconUrl: String,
|
||||
val extensionLib: String,
|
||||
val versionCode: Long,
|
||||
val versionName: String,
|
||||
val lang: String,
|
||||
val contentRating: ContentRating,
|
||||
val sources: List<ExtensionSource>,
|
||||
)
|
||||
|
||||
data class ExtensionSource(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val homeUrl: String,
|
||||
)
|
||||
|
||||
enum class ContentRating {
|
||||
SAFE,
|
||||
SUGGESTIVE,
|
||||
EROTICA,
|
||||
PORNOGRAPHIC,
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package suwayomi.tachidesk.manga.model.dataclass
|
||||
|
||||
/*
|
||||
* 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/. */
|
||||
|
||||
data class ExtensionStore(
|
||||
val indexUrl: String,
|
||||
val name: String,
|
||||
val badgeLabel: String,
|
||||
val signingKey: String,
|
||||
val contact: Contact,
|
||||
val isLegacy: Boolean,
|
||||
) {
|
||||
data class Contact(
|
||||
val website: String,
|
||||
val discord: String?,
|
||||
)
|
||||
}
|
||||
@@ -7,14 +7,13 @@ package suwayomi.tachidesk.manga.model.table
|
||||
* 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 kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.exposed.v1.core.ReferenceOption
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.json.json
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.columns.truncatingVarchar
|
||||
import suwayomi.tachidesk.server.database.DBManager
|
||||
import suwayomi.tachidesk.manga.model.table.columns.unlimitedVarchar
|
||||
|
||||
object ChapterTable : IntIdTable() {
|
||||
val url = varchar("url", 2048)
|
||||
@@ -46,7 +45,7 @@ object ChapterTable : IntIdTable() {
|
||||
val version = long("version").default(0)
|
||||
val isSyncing = bool("is_syncing").default(false)
|
||||
|
||||
val memo = json<JsonObject>("memo", DBManager.format)
|
||||
val memo = unlimitedVarchar("memo")
|
||||
}
|
||||
|
||||
fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
|
||||
@@ -69,5 +68,5 @@ fun ChapterTable.toDataClass(chapterEntry: ResultRow) =
|
||||
pageCount = chapterEntry[pageCount],
|
||||
lastModifiedAt = chapterEntry[lastModifiedAt],
|
||||
version = chapterEntry[version],
|
||||
memo = chapterEntry[memo],
|
||||
memo = Json.decodeFromString(chapterEntry[memo]),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package suwayomi.tachidesk.manga.model.table
|
||||
|
||||
/*
|
||||
* 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 org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
|
||||
object ExtensionStoreTable : IntIdTable() {
|
||||
val indexUrl = varchar("index_url", 2048)
|
||||
val name = varchar("name", 256)
|
||||
val badgeLabel = varchar("badge_label", 32)
|
||||
val signingKey = varchar("signing_key", 512)
|
||||
val contactWebsite = varchar("contact_website", 2048)
|
||||
val contactDiscord = varchar("contact_discord", 2048).nullable()
|
||||
val isLegacy = bool("is_legacy").default(false)
|
||||
}
|
||||
@@ -10,8 +10,8 @@ package suwayomi.tachidesk.manga.model.table
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
|
||||
object ExtensionTable : IntIdTable() {
|
||||
val apkName = varchar("apk_name", 1024)
|
||||
val repo = varchar("repo", 1024).nullable()
|
||||
val apkName = varchar("apk_name", 1024).nullable()
|
||||
val storeIndexUrl = varchar("store_index_url", 2048).nullable()
|
||||
|
||||
// default is the local source icon from tachiyomi
|
||||
@Suppress("ktlint:standard:max-line-length")
|
||||
@@ -23,10 +23,12 @@ object ExtensionTable : IntIdTable() {
|
||||
|
||||
val name = varchar("name", 128)
|
||||
val pkgName = varchar("pkg_name", 128)
|
||||
val apkUrl = varchar("apk_url", 2048)
|
||||
val extensionLib = varchar("extension_lib", 16).nullable()
|
||||
val versionName = varchar("version_name", 16)
|
||||
val versionCode = integer("version_code")
|
||||
val versionCode = long("version_code")
|
||||
val lang = varchar("lang", 32)
|
||||
val isNsfw = bool("is_nsfw")
|
||||
val contentRating = integer("content_rating")
|
||||
|
||||
val isInstalled = bool("is_installed").default(false)
|
||||
val hasUpdate = bool("has_update").default(false)
|
||||
|
||||
@@ -9,16 +9,14 @@ package suwayomi.tachidesk.manga.model.table
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.json.json
|
||||
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
||||
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.toGenreList
|
||||
import suwayomi.tachidesk.manga.model.table.columns.truncatingVarchar
|
||||
import suwayomi.tachidesk.manga.model.table.columns.unlimitedVarchar
|
||||
import suwayomi.tachidesk.server.database.DBManager
|
||||
|
||||
object MangaTable : IntIdTable() {
|
||||
val url = varchar("url", 2048)
|
||||
@@ -51,7 +49,7 @@ object MangaTable : IntIdTable() {
|
||||
val lastModifiedAt = long("last_modified_at").default(0)
|
||||
val version = long("version").default(0)
|
||||
val isSyncing = bool("is_syncing").default(false)
|
||||
val memo = json<JsonObject>("memo", DBManager.format)
|
||||
val memo = unlimitedVarchar("memo")
|
||||
}
|
||||
|
||||
fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||
@@ -76,7 +74,7 @@ fun MangaTable.toDataClass(mangaEntry: ResultRow) =
|
||||
updateStrategy = UpdateStrategy.valueOf(mangaEntry[updateStrategy]),
|
||||
lastModifiedAt = mangaEntry[lastModifiedAt],
|
||||
version = mangaEntry[version],
|
||||
memo = mangaEntry[memo],
|
||||
memo = Json.decodeFromString(mangaEntry[memo]),
|
||||
)
|
||||
|
||||
enum class MangaStatus(
|
||||
|
||||
@@ -8,11 +8,13 @@ package suwayomi.tachidesk.manga.model.table
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IdTable
|
||||
import suwayomi.tachidesk.manga.model.table.columns.unlimitedVarchar
|
||||
|
||||
object SourceTable : IdTable<Long>() {
|
||||
override val id = long("id").entityId()
|
||||
val name = varchar("name", 128)
|
||||
val lang = varchar("lang", 32)
|
||||
val extension = reference("extension", ExtensionTable)
|
||||
val isNsfw = bool("is_nsfw").default(false)
|
||||
val message = unlimitedVarchar("message").nullable()
|
||||
val contentRating = integer("content_rating").default(0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user