Files
Suwayomi-Server/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Source.kt
2026-06-23 17:07:25 -04:00

235 lines
9.3 KiB
Kotlin

package suwayomi.tachidesk.manga.impl
/*
* 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 androidx.preference.Preference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.sourcePreferences
import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.json.JsonMapper
import io.javalin.json.fromJsonString
import org.jetbrains.exposed.v1.core.and
import org.jetbrains.exposed.v1.core.dao.id.EntityID
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.inList
import org.jetbrains.exposed.v1.core.statements.BatchUpdateStatement
import org.jetbrains.exposed.v1.jdbc.batchInsert
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.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
import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
import suwayomi.tachidesk.manga.model.table.SourceTable
import uy.kohesive.injekt.injectLazy
import xyz.nulldev.androidcompat.androidimpl.CustomContext
object Source {
private val logger = KotlinLogging.logger {}
fun getSourceList(): List<SourceDataClass> {
return transaction {
SourceTable.selectAll().mapNotNull {
val catalogueSource = getCatalogueSourceOrNull(it[SourceTable.id].value) ?: return@mapNotNull null
val sourceExtension = ExtensionTable.selectAll().where { ExtensionTable.id eq it[SourceTable.extension] }.first()
SourceDataClass(
id = it[SourceTable.id].value.toString(),
name = it[SourceTable.name],
lang = it[SourceTable.lang],
iconUrl = proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
supportsLatest = catalogueSource.supportsLatest,
isConfigurable = catalogueSource is ConfigurableSource,
isNsfw = it[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
displayName = catalogueSource.toString(),
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
)
}
}
}
fun getSource(sourceId: Long): SourceDataClass? { // all the data extracted fresh form the source instance
return transaction {
val source = SourceTable.selectAll().where { SourceTable.id eq sourceId }.firstOrNull() ?: return@transaction null
val catalogueSource = getCatalogueSourceOrNull(sourceId) ?: return@transaction null
val extension = ExtensionTable.selectAll().where { ExtensionTable.id eq source[SourceTable.extension] }.first()
SourceDataClass(
id = sourceId.toString(),
name = source[SourceTable.name],
lang = source[SourceTable.lang],
iconUrl = proxyExtensionIconUrl(extension[ExtensionTable.pkgName]),
supportsLatest = catalogueSource.supportsLatest,
isConfigurable = catalogueSource is ConfigurableSource,
isNsfw = source[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
displayName = catalogueSource.toString(),
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
)
}
}
private val context: CustomContext by injectLazy()
/**
* (2021-11) Clients should support these types for extensions to work properly
* - EditTextPreference
* - SwitchPreferenceCompat
* - ListPreference
* - CheckBoxPreference
* - MultiSelectListPreference
*/
data class PreferenceObject(
val type: String,
val props: Any,
)
var preferenceScreenMap: MutableMap<Long, PreferenceScreen> = mutableMapOf()
/**
* Gets a source's PreferenceScreen, puts the result into [preferenceScreenMap]
*/
fun getSourcePreferences(sourceId: Long): List<PreferenceObject> =
getSourcePreferencesRaw(sourceId).map {
PreferenceObject(it::class.java.simpleName, it)
}
fun getSourcePreferencesRaw(sourceId: Long): List<Preference> {
val source = getCatalogueSourceOrStub(sourceId)
if (source is ConfigurableSource) {
val sourceShardPreferences = source.sourcePreferences()
val screen = PreferenceScreen(context)
screen.sharedPreferences = sourceShardPreferences
source.setupPreferenceScreen(screen)
preferenceScreenMap[sourceId] = screen
return screen.preferences
}
return emptyList()
}
data class SourcePreferenceChange(
val position: Int,
val value: String,
)
private val jsonMapper: JsonMapper by injectLazy()
fun setSourcePreference(
sourceId: Long,
position: Int,
value: String,
getValue: (Preference) -> Any = { pref ->
when (pref.defaultValueType) {
"String" -> value
"Boolean" -> value.toBoolean()
"Set<String>" -> jsonMapper.fromJsonString<List<String>>(value).toSet()
else -> throw RuntimeException("Unsupported type conversion")
}
},
) {
val screen = preferenceScreenMap[sourceId]!!
val pref = screen.preferences[position]
if (!pref.isEnabled) {
return
}
val newValue = getValue(pref)
pref.saveNewValue(newValue)
pref.callChangeListener(newValue)
// must reload the source because a preference was changed
unregisterCatalogueSource(sourceId)
}
fun getSourcesMetaMaps(ids: List<Long>): Map<Long, Map<String, String>> =
transaction {
SourceMetaTable
.selectAll()
.where { SourceMetaTable.ref inList ids }
.groupBy { it[SourceMetaTable.ref] }
.mapValues { it.value.associate { it[SourceMetaTable.key] to it[SourceMetaTable.value] } }
.withDefault { emptyMap() }
}
fun modifyMeta(
sourceId: Long,
key: String,
value: String,
) {
modifySourceMetas(mapOf(sourceId to mapOf(key to value)))
}
fun modifySourceMetas(metaBySourceIds: Map<Long, Map<String, String>>) {
transaction {
val sourceIds = metaBySourceIds.keys
val metaKeys = metaBySourceIds.flatMap { it.value.keys }
val dbMetaBySourceId =
SourceMetaTable
.selectAll()
.where { (SourceMetaTable.ref inList sourceIds) and (SourceMetaTable.key inList metaKeys) }
.groupBy { it[SourceMetaTable.ref] }
val existingMetaByMetaId =
sourceIds.flatMap { sourceId ->
val metaByKey = dbMetaBySourceId[sourceId].orEmpty().associateBy { it[SourceMetaTable.key] }
val existingMetas = metaBySourceIds[sourceId].orEmpty().filter { (key) -> key in metaByKey.keys }
existingMetas.map { entry ->
val metaId = metaByKey[entry.key]!![SourceMetaTable.id].value
metaId to entry
}
}
val newMetaBySourceId =
sourceIds.flatMap { sourceId ->
val metaByKey = dbMetaBySourceId[sourceId].orEmpty().associateBy { it[SourceMetaTable.key] }
metaBySourceIds[sourceId]
.orEmpty()
.filter { entry -> entry.key !in metaByKey.keys }
.map { entry -> sourceId to entry }
}
if (existingMetaByMetaId.isNotEmpty()) {
BatchUpdateStatement(SourceMetaTable)
.apply {
existingMetaByMetaId.forEach { (metaId, entry) ->
addBatch(EntityID(metaId, SourceMetaTable))
this[SourceMetaTable.value] = entry.value
}
}.toExecutable()
.execute(this@transaction)
}
if (newMetaBySourceId.isNotEmpty()) {
SourceMetaTable.batchInsert(newMetaBySourceId) { (sourceId, entry) ->
this[SourceMetaTable.ref] = sourceId
this[SourceMetaTable.key] = entry.key
this[SourceMetaTable.value] = entry.value
}
}
}
}
}