mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-13 15:54:35 -05:00
Support Custom Repos (#803)
* Support custom repos * Fix migration * Make extension after update optional
This commit is contained in:
@@ -143,7 +143,7 @@ object Chapter {
|
||||
val chapterNumber = ChapterRecognition.parseChapterNumber(manga.title, chapter.name, chapter.chapter_number.toDouble())
|
||||
chapter.chapter_number = chapterNumber.toFloat()
|
||||
chapter.name = chapter.name.sanitize(manga.title)
|
||||
chapter.scanlator = chapter.scanlator?.ifBlank { null }
|
||||
chapter.scanlator = chapter.scanlator?.ifBlank { null }?.trim()
|
||||
}
|
||||
|
||||
val now = Instant.now().epochSecond
|
||||
|
||||
@@ -23,6 +23,7 @@ import suwayomi.tachidesk.manga.impl.extension.github.ExtensionGithubApi
|
||||
import suwayomi.tachidesk.manga.impl.extension.github.OnlineExtension
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -38,7 +39,17 @@ object ExtensionsList {
|
||||
logger.debug("Getting extensions list from the internet")
|
||||
lastUpdateCheck = System.currentTimeMillis()
|
||||
|
||||
val foundExtensions = ExtensionGithubApi.findExtensions()
|
||||
val extensions =
|
||||
(listOf(ExtensionGithubApi.REPO_URL_PREFIX) + serverConfig.extensionRepos.value).map { repo ->
|
||||
kotlin.runCatching {
|
||||
ExtensionGithubApi.findExtensions(repo)
|
||||
}.onFailure {
|
||||
logger.warn(it) {
|
||||
"Failed to fetch extensions for repo: $repo"
|
||||
}
|
||||
}
|
||||
}
|
||||
val foundExtensions = extensions.mapNotNull { it.getOrNull() }.flatten()
|
||||
updateExtensionDatabase(foundExtensions)
|
||||
} else {
|
||||
logger.debug("used cached extension list")
|
||||
@@ -54,6 +65,7 @@ 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],
|
||||
@@ -77,7 +89,7 @@ object ExtensionsList {
|
||||
val extensionsToUpdate = mutableListOf<Pair<OnlineExtension, ResultRow>>()
|
||||
val extensionsToInsert = mutableListOf<OnlineExtension>()
|
||||
val extensionsToDelete =
|
||||
installedExtensions.mapNotNull { (pkgName, extension) ->
|
||||
installedExtensions.filter { it.value[ExtensionTable.repo] != null }.mapNotNull { (pkgName, extension) ->
|
||||
extension.takeUnless { foundExtensions.any { it.pkgName == pkgName } }
|
||||
}
|
||||
foundExtensions.forEach {
|
||||
@@ -124,6 +136,7 @@ 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.name] = foundExtension.name
|
||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||
this[ExtensionTable.versionCode] = foundExtension.versionCode
|
||||
@@ -138,6 +151,7 @@ object ExtensionsList {
|
||||
}
|
||||
if (extensionsToInsert.isNotEmpty()) {
|
||||
ExtensionTable.batchInsert(extensionsToInsert) { foundExtension ->
|
||||
this[ExtensionTable.repo] = foundExtension.repo
|
||||
this[ExtensionTable.name] = foundExtension.name
|
||||
this[ExtensionTable.pkgName] = foundExtension.pkgName
|
||||
this[ExtensionTable.versionName] = foundExtension.versionName
|
||||
|
||||
@@ -20,7 +20,7 @@ import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
object ExtensionGithubApi {
|
||||
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
|
||||
const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
|
||||
private const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/tachiyomiorg/tachiyomi-extensions@repo/"
|
||||
private val logger = KotlinLogging.logger {}
|
||||
private val json: Json by injectLazy()
|
||||
@@ -49,13 +49,13 @@ object ExtensionGithubApi {
|
||||
|
||||
private var requiresFallbackSource = false
|
||||
|
||||
suspend fun findExtensions(): List<OnlineExtension> {
|
||||
suspend fun findExtensions(repo: String): List<OnlineExtension> {
|
||||
val githubResponse =
|
||||
if (requiresFallbackSource) {
|
||||
null
|
||||
} else {
|
||||
try {
|
||||
client.newCall(GET("${REPO_URL_PREFIX}index.min.json")).awaitSuccess()
|
||||
client.newCall(GET("${repo.repoUrlReplace()}index.min.json")).awaitSuccess()
|
||||
} catch (e: Throwable) {
|
||||
logger.error(e) { "Failed to get extensions from GitHub" }
|
||||
requiresFallbackSource = true
|
||||
@@ -65,18 +65,18 @@ object ExtensionGithubApi {
|
||||
|
||||
val response =
|
||||
githubResponse ?: run {
|
||||
client.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json")).awaitSuccess()
|
||||
client.newCall(GET("${repo.fallbackRepoUrlReplace()}index.min.json")).awaitSuccess()
|
||||
}
|
||||
|
||||
return with(json) {
|
||||
response
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions()
|
||||
.toExtensions(repo.repoUrlReplace())
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: ExtensionDataClass): String {
|
||||
return "$REPO_URL_PREFIX/apk/${extension.apkName}"
|
||||
return "${extension.repo!!.repoUrlReplace()}/apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
private val client by lazy {
|
||||
@@ -91,7 +91,7 @@ object ExtensionGithubApi {
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun List<ExtensionJsonObject>.toExtensions(): List<OnlineExtension> {
|
||||
private fun List<ExtensionJsonObject>.toExtensions(repo: String): List<OnlineExtension> {
|
||||
return this
|
||||
.filter {
|
||||
val libVersion = it.version.substringBeforeLast('.').toDouble()
|
||||
@@ -99,6 +99,7 @@ object ExtensionGithubApi {
|
||||
}
|
||||
.map {
|
||||
OnlineExtension(
|
||||
repo = repo,
|
||||
name = it.name.substringAfter("Tachiyomi: "),
|
||||
pkgName = it.pkg,
|
||||
versionName = it.version,
|
||||
@@ -109,7 +110,7 @@ object ExtensionGithubApi {
|
||||
hasChangelog = it.hasChangelog == 1,
|
||||
sources = it.sources?.toExtensionSources() ?: emptyList(),
|
||||
apkName = it.apk,
|
||||
iconUrl = "${REPO_URL_PREFIX}icon/${it.pkg}.png",
|
||||
iconUrl = "${repo}icon/${it.pkg}.png",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -124,4 +125,22 @@ object ExtensionGithubApi {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.repoUrlReplace() =
|
||||
replace(repoMatchRegex) {
|
||||
"https://raw.githubusercontent.com/${it.groupValues[1]}/${it.groupValues[2]}/" +
|
||||
"${it.groupValues.getOrNull(3)?.ifBlank { null } ?: "repo"}/"
|
||||
}
|
||||
|
||||
private fun String.fallbackRepoUrlReplace() =
|
||||
replace(repoMatchRegex) {
|
||||
"https://gcore.jsdelivr.net/gh/${it.groupValues[1]}/${it.groupValues[2]}@" +
|
||||
"${it.groupValues.getOrNull(3)?.ifBlank { null } ?: "repo"}/"
|
||||
}
|
||||
|
||||
private val repoMatchRegex =
|
||||
(
|
||||
"https:\\/\\/(?:www|raw)?(?:github|githubusercontent)\\.com" +
|
||||
"\\/([^\\/]+)\\/([^\\/]+)(?:\\/(?:tree|blob)\\/(.*))?\\/?"
|
||||
).toRegex()
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ data class OnlineExtensionSource(
|
||||
)
|
||||
|
||||
data class OnlineExtension(
|
||||
val repo: String,
|
||||
val name: String,
|
||||
val pkgName: String,
|
||||
val apkName: String,
|
||||
|
||||
@@ -44,8 +44,7 @@ object PackageTools {
|
||||
const val LIB_VERSION_MAX = 1.5
|
||||
|
||||
private const val OFFICIAL_SIGNATURE = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23" // inorichi's key
|
||||
private const val UNOFFICIAL_SIGNATURE = "64feb21075ba97ebc9cc981243645b331595c111cef1b0d084236a0403b00581" // ArMor's key
|
||||
val trustedSignatures = setOf(OFFICIAL_SIGNATURE, UNOFFICIAL_SIGNATURE)
|
||||
val trustedSignatures = setOf(OFFICIAL_SIGNATURE)
|
||||
|
||||
/**
|
||||
* Convert dex to jar, a wrapper for the dex2jar library
|
||||
|
||||
@@ -8,6 +8,7 @@ package suwayomi.tachidesk.manga.model.dataclass
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
data class ExtensionDataClass(
|
||||
val repo: String?,
|
||||
val apkName: String,
|
||||
val iconUrl: String,
|
||||
val name: String,
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.jetbrains.exposed.dao.id.IntIdTable
|
||||
|
||||
object ExtensionTable : IntIdTable() {
|
||||
val apkName = varchar("apk_name", 1024)
|
||||
val repo = varchar("repo", 1024).nullable()
|
||||
|
||||
// default is the local source icon from tachiyomi
|
||||
@Suppress("ktlint:standard:max-line-length")
|
||||
|
||||
@@ -80,6 +80,6 @@ enum class MangaStatus(val value: Int) {
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun valueOf(value: Int): MangaStatus = values().find { it.value == value } ?: UNKNOWN
|
||||
fun valueOf(value: Int): MangaStatus = entries.find { it.value == value } ?: UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user