Update to latest Mihon extension lib

This commit is contained in:
Syer10
2026-06-23 17:07:25 -04:00
parent cd91b6e6ef
commit 4ace76f508
25 changed files with 206 additions and 181 deletions

View File

@@ -9,7 +9,6 @@ package eu.kanade.tachiyomi.network
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
@@ -84,8 +83,6 @@ class NetworkHelper(
), ),
).addInterceptor(UncaughtExceptionInterceptor()) ).addInterceptor(UncaughtExceptionInterceptor())
.addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider)) .addInterceptor(UserAgentInterceptor(::defaultUserAgentProvider))
.addNetworkInterceptor(IgnoreGzipInterceptor())
.addNetworkInterceptor(BrotliInterceptor)
// if (preferences.verboseLogging().get()) { // if (preferences.verboseLogging().get()) {
val httpLoggingInterceptor = val httpLoggingInterceptor =

View File

@@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.network.interceptor
import okhttp3.Interceptor
import okhttp3.Response
/**
* To use [okhttp3.brotli.BrotliInterceptor] as a network interceptor,
* add [IgnoreGzipInterceptor] right before it.
*
* This nullifies the transparent gzip of [okhttp3.internal.http.BridgeInterceptor]
* so gzip and Brotli are explicitly handled by the [okhttp3.brotli.BrotliInterceptor].
*/
class IgnoreGzipInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (request.header("Accept-Encoding") == "gzip") {
request = request.newBuilder().removeHeader("Accept-Encoding").build()
}
return chain.proceed(request)
}
}

View File

@@ -483,7 +483,7 @@ class LocalSource(
it[versionCode] = 0 it[versionCode] = 0
it[lang] = LANG it[lang] = LANG
it[extensionLib] = "1.2" it[extensionLib] = "1.2"
it[contentRating] = 0 it[contentWarning] = 0
it[isInstalled] = true it[isInstalled] = true
} }

View File

@@ -95,6 +95,7 @@ class ExtensionStoreMutation {
contactDiscord = it.contactDiscord, contactDiscord = it.contactDiscord,
indexUrl = it.indexUrl, indexUrl = it.indexUrl,
isLegacy = it.isLegacy, isLegacy = it.isLegacy,
extensionListUrl = it.extensionListUrl,
) )
}, },
) )

View File

@@ -21,7 +21,7 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import suwayomi.tachidesk.graphql.directives.RequireAuth import suwayomi.tachidesk.graphql.directives.RequireAuth
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
import suwayomi.tachidesk.graphql.queries.filter.ContentRatingFilter import suwayomi.tachidesk.graphql.queries.filter.ContentWarningFilter
import suwayomi.tachidesk.graphql.queries.filter.Filter import suwayomi.tachidesk.graphql.queries.filter.Filter
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
import suwayomi.tachidesk.graphql.queries.filter.IntFilter import suwayomi.tachidesk.graphql.queries.filter.IntFilter
@@ -43,7 +43,7 @@ import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
import suwayomi.tachidesk.graphql.types.ExtensionNodeList import suwayomi.tachidesk.graphql.types.ExtensionNodeList
import suwayomi.tachidesk.graphql.types.ExtensionType import suwayomi.tachidesk.graphql.types.ExtensionType
import suwayomi.tachidesk.manga.model.dataclass.ContentRating import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
@@ -108,9 +108,9 @@ class ExtensionQuery {
val versionCode: Int? = null, val versionCode: Int? = null,
val versionCodeLong: Long? = null, val versionCodeLong: Long? = null,
val lang: String? = null, val lang: String? = null,
@GraphQLDeprecated("", ReplaceWith("contentRating")) @GraphQLDeprecated("", ReplaceWith("contentWarning"))
val isNsfw: Boolean? = null, val isNsfw: Boolean? = null,
val contentRating: ContentRating? = null, val contentWarning: ContentWarning? = null,
val isInstalled: Boolean? = null, val isInstalled: Boolean? = null,
val hasUpdate: Boolean? = null, val hasUpdate: Boolean? = null,
val isObsolete: Boolean? = null, val isObsolete: Boolean? = null,
@@ -129,10 +129,10 @@ class ExtensionQuery {
opAnd.eq(versionCodeLong, ExtensionTable.versionCode) opAnd.eq(versionCodeLong, ExtensionTable.versionCode)
opAnd.eq(lang, ExtensionTable.lang) opAnd.eq(lang, ExtensionTable.lang)
opAnd.eq( opAnd.eq(
isNsfw?.let { if (it) ContentRating.PORNOGRAPHIC.ordinal else ContentRating.SAFE.ordinal }, isNsfw?.let { if (it) ContentWarning.MIXED.ordinal else ContentWarning.SAFE.ordinal },
ExtensionTable.contentRating, ExtensionTable.contentWarning,
) )
opAnd.eq(contentRating?.ordinal, ExtensionTable.contentRating) opAnd.eq(contentWarning?.ordinal, ExtensionTable.contentWarning)
opAnd.eq(isInstalled, ExtensionTable.isInstalled) opAnd.eq(isInstalled, ExtensionTable.isInstalled)
opAnd.eq(hasUpdate, ExtensionTable.hasUpdate) opAnd.eq(hasUpdate, ExtensionTable.hasUpdate)
opAnd.eq(isObsolete, ExtensionTable.isObsolete) opAnd.eq(isObsolete, ExtensionTable.isObsolete)
@@ -156,9 +156,9 @@ class ExtensionQuery {
val versionCode: IntFilter? = null, val versionCode: IntFilter? = null,
val versionCodeLong: LongFilter? = null, val versionCodeLong: LongFilter? = null,
val lang: StringFilter? = null, val lang: StringFilter? = null,
@GraphQLDeprecated("", ReplaceWith("storeIndexUrl")) @GraphQLDeprecated("", ReplaceWith("contentWarning"))
val isNsfw: BooleanFilter? = null, val isNsfw: BooleanFilter? = null,
val contentRating: ContentRatingFilter? = null, val contentWarning: ContentWarningFilter? = null,
val isInstalled: BooleanFilter? = null, val isInstalled: BooleanFilter? = null,
val hasUpdate: BooleanFilter? = null, val hasUpdate: BooleanFilter? = null,
val isObsolete: BooleanFilter? = null, val isObsolete: BooleanFilter? = null,
@@ -179,7 +179,7 @@ class ExtensionQuery {
andFilterWithCompareString(ExtensionTable.versionName, versionName), andFilterWithCompareString(ExtensionTable.versionName, versionName),
andFilterWithCompare(ExtensionTable.versionCode, versionCodeLong), andFilterWithCompare(ExtensionTable.versionCode, versionCodeLong),
andFilterWithCompareString(ExtensionTable.lang, lang), andFilterWithCompareString(ExtensionTable.lang, lang),
andFilterWithCompareEnum(ExtensionTable.contentRating, contentRating), andFilterWithCompareEnum(ExtensionTable.contentWarning, contentWarning),
andFilterWithCompare(ExtensionTable.isInstalled, isInstalled), andFilterWithCompare(ExtensionTable.isInstalled, isInstalled),
andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate), andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate),
andFilterWithCompare(ExtensionTable.isObsolete, isObsolete), andFilterWithCompare(ExtensionTable.isObsolete, isObsolete),

View File

@@ -15,13 +15,14 @@ import org.jetbrains.exposed.v1.core.Op
import org.jetbrains.exposed.v1.core.SortOrder import org.jetbrains.exposed.v1.core.SortOrder
import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.greater import org.jetbrains.exposed.v1.core.greater
import org.jetbrains.exposed.v1.core.greaterEq
import org.jetbrains.exposed.v1.core.less import org.jetbrains.exposed.v1.core.less
import org.jetbrains.exposed.v1.core.neq import org.jetbrains.exposed.v1.core.neq
import org.jetbrains.exposed.v1.jdbc.selectAll import org.jetbrains.exposed.v1.jdbc.selectAll
import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import suwayomi.tachidesk.graphql.directives.RequireAuth import suwayomi.tachidesk.graphql.directives.RequireAuth
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
import suwayomi.tachidesk.graphql.queries.filter.ContentRatingFilter import suwayomi.tachidesk.graphql.queries.filter.ContentWarningFilter
import suwayomi.tachidesk.graphql.queries.filter.Filter import suwayomi.tachidesk.graphql.queries.filter.Filter
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
import suwayomi.tachidesk.graphql.queries.filter.LongFilter import suwayomi.tachidesk.graphql.queries.filter.LongFilter
@@ -42,7 +43,7 @@ import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
import suwayomi.tachidesk.graphql.types.SourceNodeList import suwayomi.tachidesk.graphql.types.SourceNodeList
import suwayomi.tachidesk.graphql.types.SourceType import suwayomi.tachidesk.graphql.types.SourceType
import suwayomi.tachidesk.manga.model.dataclass.ContentRating import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.SourceTable
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
@@ -95,9 +96,9 @@ class SourceQuery {
val id: Long? = null, val id: Long? = null,
val name: String? = null, val name: String? = null,
val lang: String? = null, val lang: String? = null,
@GraphQLDeprecated("replace with contentRating == ContentRating.PORNOGRAPHIC", ReplaceWith("contentRating")) @GraphQLDeprecated("replace with contentWarning == ContentRating.MIXED", ReplaceWith("contentWarning"))
val isNsfw: Boolean? = null, val isNsfw: Boolean? = null,
val contentRating: ContentRating? = null, val contentWarning: ContentWarning? = null,
) : HasGetOp { ) : HasGetOp {
override fun getOp(): Op<Boolean>? { override fun getOp(): Op<Boolean>? {
val opAnd = OpAnd() val opAnd = OpAnd()
@@ -106,13 +107,12 @@ class SourceQuery {
opAnd.eq(lang, SourceTable.lang) opAnd.eq(lang, SourceTable.lang)
opAnd.andWhere(isNsfw) { opAnd.andWhere(isNsfw) {
if (it) { if (it) {
SourceTable.contentRating eq ContentRating.PORNOGRAPHIC.ordinal SourceTable.contentWarning greaterEq ContentWarning.MIXED.ordinal
} else { } else {
SourceTable.contentRating neq SourceTable.contentWarning less ContentWarning.MIXED.ordinal
ContentRating.PORNOGRAPHIC.ordinal
} }
} }
opAnd.andWhere(contentRating) { SourceTable.contentRating eq it.ordinal } opAnd.andWhere(contentWarning) { SourceTable.contentWarning eq it.ordinal }
return opAnd.op return opAnd.op
} }
@@ -122,9 +122,9 @@ class SourceQuery {
val id: LongFilter? = null, val id: LongFilter? = null,
val name: StringFilter? = null, val name: StringFilter? = null,
val lang: StringFilter? = null, val lang: StringFilter? = null,
@GraphQLDeprecated("replace with contentRating == ContentRating.PORNOGRAPHIC", ReplaceWith("contentRating")) @GraphQLDeprecated("replace with contentWarning", ReplaceWith("contentWarning"))
val isNsfw: BooleanFilter? = null, val isNsfw: BooleanFilter? = null,
val contentRating: ContentRatingFilter? = null, val contentWarning: ContentWarningFilter? = null,
override val and: List<SourceFilter>? = null, override val and: List<SourceFilter>? = null,
override val or: List<SourceFilter>? = null, override val or: List<SourceFilter>? = null,
override val not: SourceFilter? = null, override val not: SourceFilter? = null,
@@ -134,7 +134,7 @@ class SourceQuery {
andFilterWithCompareEntity(SourceTable.id, id), andFilterWithCompareEntity(SourceTable.id, id),
andFilterWithCompareString(SourceTable.name, name), andFilterWithCompareString(SourceTable.name, name),
andFilterWithCompareString(SourceTable.lang, lang), andFilterWithCompareString(SourceTable.lang, lang),
andFilterWithCompareEnum(SourceTable.contentRating, contentRating), andFilterWithCompareEnum(SourceTable.contentWarning, contentWarning),
) )
} }

View File

@@ -28,7 +28,7 @@ import org.jetbrains.exposed.v1.core.upperCase
import org.jetbrains.exposed.v1.core.wrap import org.jetbrains.exposed.v1.core.wrap
import org.jetbrains.exposed.v1.jdbc.Query import org.jetbrains.exposed.v1.jdbc.Query
import org.jetbrains.exposed.v1.jdbc.andWhere import org.jetbrains.exposed.v1.jdbc.andWhere
import suwayomi.tachidesk.manga.model.dataclass.ContentRating import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
class ILikeEscapeOp( class ILikeEscapeOp(
expr1: Expression<*>, expr1: Expression<*>,
@@ -330,23 +330,23 @@ data class DoubleFilter(
) )
} }
data class ContentRatingFilter( data class ContentWarningFilter(
override val isNull: Boolean? = null, override val isNull: Boolean? = null,
override val equalTo: ContentRating? = null, override val equalTo: ContentWarning? = null,
override val notEqualTo: ContentRating? = null, override val notEqualTo: ContentWarning? = null,
override val notEqualToAll: List<ContentRating>? = null, override val notEqualToAll: List<ContentWarning>? = null,
override val notEqualToAny: List<ContentRating>? = null, override val notEqualToAny: List<ContentWarning>? = null,
override val distinctFrom: ContentRating? = null, override val distinctFrom: ContentWarning? = null,
override val distinctFromAll: List<ContentRating>? = null, override val distinctFromAll: List<ContentWarning>? = null,
override val distinctFromAny: List<ContentRating>? = null, override val distinctFromAny: List<ContentWarning>? = null,
override val notDistinctFrom: ContentRating? = null, override val notDistinctFrom: ContentWarning? = null,
override val `in`: List<ContentRating>? = null, override val `in`: List<ContentWarning>? = null,
override val notIn: List<ContentRating>? = null, override val notIn: List<ContentWarning>? = null,
override val lessThan: ContentRating? = null, override val lessThan: ContentWarning? = null,
override val lessThanOrEqualTo: ContentRating? = null, override val lessThanOrEqualTo: ContentWarning? = null,
override val greaterThan: ContentRating? = null, override val greaterThan: ContentWarning? = null,
override val greaterThanOrEqualTo: ContentRating? = null, override val greaterThanOrEqualTo: ContentWarning? = null,
) : ComparableScalarFilter<ContentRating> ) : ComparableScalarFilter<ContentWarning>
data class StringFilter( data class StringFilter(
override val isNull: Boolean? = null, override val isNull: Boolean? = null,

View File

@@ -26,6 +26,7 @@ class ExtensionStoreType(
val contactDiscord: String?, val contactDiscord: String?,
val indexUrl: String, val indexUrl: String,
val isLegacy: Boolean, val isLegacy: Boolean,
val extensionListUrl: String?,
) : Node { ) : Node {
constructor(row: ResultRow) : this( constructor(row: ResultRow) : this(
name = row[ExtensionStoreTable.name], name = row[ExtensionStoreTable.name],
@@ -35,6 +36,7 @@ class ExtensionStoreType(
contactDiscord = row[ExtensionStoreTable.contactDiscord], contactDiscord = row[ExtensionStoreTable.contactDiscord],
indexUrl = row[ExtensionStoreTable.indexUrl], indexUrl = row[ExtensionStoreTable.indexUrl],
isLegacy = row[ExtensionStoreTable.isLegacy], isLegacy = row[ExtensionStoreTable.isLegacy],
extensionListUrl = row[ExtensionStoreTable.extensionListUrl],
) )
fun extensions(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionNodeList> = fun extensions(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionNodeList> =

View File

@@ -18,7 +18,7 @@ import suwayomi.tachidesk.graphql.server.primitives.Node
import suwayomi.tachidesk.graphql.server.primitives.NodeList import suwayomi.tachidesk.graphql.server.primitives.NodeList
import suwayomi.tachidesk.graphql.server.primitives.PageInfo import suwayomi.tachidesk.graphql.server.primitives.PageInfo
import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.model.dataclass.ContentRating import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
@@ -41,9 +41,9 @@ class ExtensionType(
val versionCode: Int, val versionCode: Int,
val versionCodeLong: Long, val versionCodeLong: Long,
val lang: String, val lang: String,
@GraphQLDeprecated("Removed in extension api v1.6", ReplaceWith("contentRating")) @GraphQLDeprecated("Removed in extension api v1.6", ReplaceWith("contentWarning"))
val isNsfw: Boolean, val isNsfw: Boolean,
val contentRating: ContentRating, val contentWarning: ContentWarning,
val isInstalled: Boolean, val isInstalled: Boolean,
val hasUpdate: Boolean, val hasUpdate: Boolean,
val isObsolete: Boolean, val isObsolete: Boolean,
@@ -61,8 +61,8 @@ class ExtensionType(
versionCode = row[ExtensionTable.versionCode].toInt(), versionCode = row[ExtensionTable.versionCode].toInt(),
versionCodeLong = row[ExtensionTable.versionCode], versionCodeLong = row[ExtensionTable.versionCode],
lang = row[ExtensionTable.lang], lang = row[ExtensionTable.lang],
isNsfw = row[ExtensionTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal, isNsfw = row[ExtensionTable.contentWarning] >= ContentWarning.MIXED.ordinal,
contentRating = ContentRating.valueOf(row[ExtensionTable.contentRating]), contentWarning = ContentWarning.valueOf(row[ExtensionTable.contentWarning]),
isInstalled = row[ExtensionTable.isInstalled], isInstalled = row[ExtensionTable.isInstalled],
hasUpdate = row[ExtensionTable.hasUpdate], hasUpdate = row[ExtensionTable.hasUpdate],
isObsolete = row[ExtensionTable.isObsolete], isObsolete = row[ExtensionTable.isObsolete],

View File

@@ -26,7 +26,7 @@ import suwayomi.tachidesk.manga.impl.Source.getSourcePreferencesRaw
import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.model.dataclass.ContentRating import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.SourceTable
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
@@ -42,12 +42,11 @@ class SourceType(
val id: Long, val id: Long,
val name: String, val name: String,
val lang: String, val lang: String,
val message: String?, val contentWarning: ContentWarning,
val contentRating: ContentRating,
val iconUrl: String, val iconUrl: String,
val supportsLatest: Boolean, val supportsLatest: Boolean,
val isConfigurable: Boolean, val isConfigurable: Boolean,
@GraphQLDeprecated("", ReplaceWith("contentRating")) @GraphQLDeprecated("", ReplaceWith("contentWarning"))
val isNsfw: Boolean, val isNsfw: Boolean,
val displayName: String, val displayName: String,
val homeUrl: String?, val homeUrl: String?,
@@ -58,12 +57,11 @@ class SourceType(
id = row[SourceTable.id].value, id = row[SourceTable.id].value,
name = row[SourceTable.name], name = row[SourceTable.name],
lang = row[SourceTable.lang], lang = row[SourceTable.lang],
message = row[SourceTable.message], contentWarning = ContentWarning.valueOf(row[SourceTable.contentWarning]),
contentRating = ContentRating.valueOf(row[SourceTable.contentRating]),
iconUrl = Extension.proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]), iconUrl = Extension.proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
supportsLatest = catalogueSource.supportsLatest, supportsLatest = catalogueSource.supportsLatest,
isConfigurable = catalogueSource is ConfigurableSource, isConfigurable = catalogueSource is ConfigurableSource,
isNsfw = row[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal, isNsfw = row[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
displayName = catalogueSource.toString(), displayName = catalogueSource.toString(),
homeUrl = runCatching { (catalogueSource as? HttpSource)?.getHomeUrl() }.getOrNull(), homeUrl = runCatching { (catalogueSource as? HttpSource)?.getHomeUrl() }.getOrNull(),
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(), baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),

View File

@@ -29,7 +29,7 @@ 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.getCatalogueSourceOrNull
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
import suwayomi.tachidesk.manga.model.dataclass.ContentRating import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceMetaTable import suwayomi.tachidesk.manga.model.table.SourceMetaTable
@@ -53,7 +53,7 @@ object Source {
iconUrl = proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]), iconUrl = proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
supportsLatest = catalogueSource.supportsLatest, supportsLatest = catalogueSource.supportsLatest,
isConfigurable = catalogueSource is ConfigurableSource, isConfigurable = catalogueSource is ConfigurableSource,
isNsfw = it[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal, isNsfw = it[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
displayName = catalogueSource.toString(), displayName = catalogueSource.toString(),
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(), baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
) )
@@ -74,7 +74,7 @@ object Source {
iconUrl = proxyExtensionIconUrl(extension[ExtensionTable.pkgName]), iconUrl = proxyExtensionIconUrl(extension[ExtensionTable.pkgName]),
supportsLatest = catalogueSource.supportsLatest, supportsLatest = catalogueSource.supportsLatest,
isConfigurable = catalogueSource is ConfigurableSource, isConfigurable = catalogueSource is ConfigurableSource,
isNsfw = source[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal, isNsfw = source[SourceTable.contentWarning] >= ContentWarning.MIXED.ordinal,
displayName = catalogueSource.toString(), displayName = catalogueSource.toString(),
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(), baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
) )

View File

@@ -32,6 +32,9 @@ import suwayomi.tachidesk.manga.impl.util.PackageTools
import suwayomi.tachidesk.manga.impl.util.PackageTools.EXTENSION_FEATURE import suwayomi.tachidesk.manga.impl.util.PackageTools.EXTENSION_FEATURE
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MAX
import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MIN import suwayomi.tachidesk.manga.impl.util.PackageTools.LIB_VERSION_MIN
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_CONTENT_WARNING
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_EXTENSION_LIB
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_NAME
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_NSFW import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_NSFW
import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_SOURCE_CLASS import suwayomi.tachidesk.manga.impl.util.PackageTools.METADATA_SOURCE_CLASS
import suwayomi.tachidesk.manga.impl.util.PackageTools.dex2jar import suwayomi.tachidesk.manga.impl.util.PackageTools.dex2jar
@@ -42,7 +45,7 @@ import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.saveImage import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.saveImage
import suwayomi.tachidesk.manga.model.dataclass.ContentRating import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
@@ -151,7 +154,10 @@ object Extension {
// throw Exception("This apk is not a signed with the official tachiyomi signature") // throw Exception("This apk is not a signed with the official tachiyomi signature")
// } // }
val isNsfw = packageInfo.applicationInfo.metaData.getString(METADATA_NSFW) == "1" var contentWarning = packageInfo.applicationInfo.metaData.getInt(METADATA_CONTENT_WARNING)
if (contentWarning == 0) {
contentWarning = packageInfo.applicationInfo.metaData.getInt(METADATA_NSFW)
}
val className = val className =
packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS) packageInfo.packageName + packageInfo.applicationInfo.metaData.getString(METADATA_SOURCE_CLASS)
@@ -184,9 +190,16 @@ object Extension {
} }
val extensionName = val extensionName =
packageInfo.applicationInfo.nonLocalizedLabel packageInfo.applicationInfo.metaData.getString(METADATA_NAME)
.toString() ?: packageInfo.applicationInfo.nonLocalizedLabel
.substringAfter("Tachiyomi: ") .toString()
.substringAfter("Tachiyomi: ")
val extensionLibVersion =
packageInfo.applicationInfo.metaData
.getString(METADATA_EXTENSION_LIB)
.takeUnless { it == "0" }
?: packageInfo.versionName.substringBeforeLast('.')
// update extension info // update extension info
transaction { transaction {
@@ -197,14 +210,9 @@ object Extension {
it[this.pkgName] = packageInfo.packageName it[this.pkgName] = packageInfo.packageName
it[versionName] = packageInfo.versionName it[versionName] = packageInfo.versionName
it[versionCode] = packageInfo.versionCode.toLong() it[versionCode] = packageInfo.versionCode.toLong()
it[extensionLib] = extensionLibVersion
it[lang] = extensionLang it[lang] = extensionLang
// todo will change it[this.contentWarning] = contentWarning
it[contentRating] =
if (isNsfw) {
ContentRating.PORNOGRAPHIC.ordinal
} else {
ContentRating.SAFE.ordinal
}
} }
} }
@@ -229,7 +237,7 @@ object Extension {
it[name] = httpSource.name it[name] = httpSource.name
it[lang] = httpSource.lang it[lang] = httpSource.lang
it[extension] = extensionId it[extension] = extensionId
it[contentRating] = if (isNsfw) ContentRating.PORNOGRAPHIC.ordinal else ContentRating.SAFE.ordinal it[this.contentWarning] = contentWarning
} }
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" } logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
} }
@@ -397,7 +405,7 @@ object Extension {
it[versionName] = targetExtension.versionName it[versionName] = targetExtension.versionName
it[versionCode] = targetExtension.versionCode it[versionCode] = targetExtension.versionCode
it[lang] = targetExtension.lang it[lang] = targetExtension.lang
it[contentRating] = targetExtension.contentRating.ordinal it[contentWarning] = targetExtension.contentWarning.ordinal
it[iconUrl] = targetExtension.iconUrl it[iconUrl] = targetExtension.iconUrl
it[hasUpdate] = false it[hasUpdate] = false
} }

View File

@@ -15,6 +15,9 @@ import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.okio.decodeFromBufferedSource import kotlinx.serialization.json.okio.decodeFromBufferedSource
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
import okio.BufferedSource
import okio.buffer
import okio.gzip
import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.inList import org.jetbrains.exposed.v1.core.inList
import org.jetbrains.exposed.v1.jdbc.deleteWhere import org.jetbrains.exposed.v1.jdbc.deleteWhere
@@ -41,45 +44,45 @@ object ExtensionStoreService {
val protoBuf: ProtoBuf by injectLazy() val protoBuf: ProtoBuf by injectLazy()
val json: Json by injectLazy() val json: Json by injectLazy()
suspend fun fetch(indexUrl: String): ExtensionStore = fetch(indexUrl, forceV2 = false) suspend fun fetch(indexUrl: String): ExtensionStore {
var updatedIndexUrl: String = indexUrl
private suspend fun fetch(
indexUrl: String,
forceV2: Boolean,
): ExtensionStore {
var updatedIndexUrl = indexUrl
return try { return try {
network.client.newCall(GET(indexUrl)).awaitSuccess().body.source().use { source -> val response = network.client.newCall(GET(updatedIndexUrl)).awaitSuccess()
try { response.body.source().decompressIfGzipped().use { source ->
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray()) val networkStore =
} catch (e: IllegalArgumentException) { when (source.peek().readByte()) {
logger.debug { "Failed to decode as protobuf, trying JSON" } // "[..."
try { 0x5B.toByte() -> {
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek()) run {
} catch (e: IllegalArgumentException) {
if (forceV2) throw e
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")) { if (!indexUrl.endsWith("/index.min.json")) {
throw e throw IllegalArgumentException("Provided legacy store url is not valid")
} }
logger.debug { "Checking for new repo.json format" }
updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json") updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json")
network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().body.source().use { network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().body.source().use {
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(it) json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(it)
} }
} }
}
if (legacyIndex.indexV2 != null) { // "{..."
return fetch(legacyIndex.indexV2, forceV2 = true) 0x7B.toByte() -> {
} else { try {
legacyIndex json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(source.peek())
} catch (_: IllegalArgumentException) {
json.decodeFromBufferedSource<NetworkExtensionStore>(source)
}
}
else -> {
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.readByteArray())
} }
} }
}.toExtensionStore(updatedIndexUrl)
if (networkStore is NetworkLegacyExtensionRepo && networkStore.indexV2 != null) {
return fetch(networkStore.indexV2)
}
networkStore.toExtensionStore(updatedIndexUrl)
} }
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
@@ -105,6 +108,7 @@ object ExtensionStoreService {
it[contactDiscord] = store.contact.discord it[contactDiscord] = store.contact.discord
it[indexUrl] = store.indexUrl it[indexUrl] = store.indexUrl
it[isLegacy] = store.isLegacy it[isLegacy] = store.isLegacy
it[extensionListUrl] = store.extensionListUrl
} }
} else { } else {
ExtensionStoreTable.update({ ExtensionStoreTable.indexUrl eq store.indexUrl }) { ExtensionStoreTable.update({ ExtensionStoreTable.indexUrl eq store.indexUrl }) {
@@ -114,6 +118,7 @@ object ExtensionStoreService {
it[contactWebsite] = store.contact.website it[contactWebsite] = store.contact.website
it[contactDiscord] = store.contact.discord it[contactDiscord] = store.contact.discord
it[isLegacy] = store.isLegacy it[isLegacy] = store.isLegacy
it[extensionListUrl] = store.extensionListUrl
} }
} }
} }
@@ -210,17 +215,33 @@ object ExtensionStoreService {
suspend fun getExtensions(store: ExtensionStore): List<ExtensionInfo> { suspend fun getExtensions(store: ExtensionStore): List<ExtensionInfo> {
val extensions = val extensions =
if (!store.isLegacy) { if (store.extensionListUrl != null) {
val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess() val response = network.client.newCall(GET(store.extensionListUrl)).awaitSuccess()
response.body response.body.source().decompressIfGzipped().use { source ->
.source() when (source.peek().readByte()) {
.use { source -> // "{..."
try { 0x7B.toByte() -> {
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray()) json.decodeFromBufferedSource<NetworkExtensionStore.ExtensionList>(source)
} catch (_: IllegalArgumentException) { }
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
else -> {
protoBuf.decodeFromByteArray<NetworkExtensionStore.ExtensionList>(
source.readByteArray(),
)
} }
}.toExtensionInfos(store) }.toExtensionInfos(store)
}
} else if (!store.isLegacy) {
val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess()
response.body.source().decompressIfGzipped().use { source ->
when (source.peek().readByte()) {
// "{..."
0x7B.toByte() -> json.decodeFromBufferedSource<NetworkExtensionStore>(source)
else -> protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.readByteArray())
}.extensionList!!
.toExtensionInfos(store)
}
} else { } else {
val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json") val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json")
val response = network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess() val response = network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess()
@@ -232,4 +253,17 @@ object ExtensionStoreService {
} }
return extensions return extensions
} }
private fun BufferedSource.decompressIfGzipped(): BufferedSource {
val isGzip =
peek().use { peeked ->
try {
peeked.readShort().toInt() == 0x1f8b
} catch (_: Exception) {
false
}
}
return if (isGzip) gzip().buffer() else this
}
} }

View File

@@ -22,7 +22,7 @@ import org.jetbrains.exposed.v1.jdbc.statements.toExecutable
import org.jetbrains.exposed.v1.jdbc.transactions.transaction import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.jetbrains.exposed.v1.jdbc.update import org.jetbrains.exposed.v1.jdbc.update
import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl import suwayomi.tachidesk.manga.impl.extension.Extension.proxyExtensionIconUrl
import suwayomi.tachidesk.manga.model.dataclass.ContentRating import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
@@ -81,7 +81,7 @@ object ExtensionsList {
versionName = it[ExtensionTable.versionName], versionName = it[ExtensionTable.versionName],
versionCode = it[ExtensionTable.versionCode].toInt(), versionCode = it[ExtensionTable.versionCode].toInt(),
lang = it[ExtensionTable.lang], lang = it[ExtensionTable.lang],
isNsfw = it[ExtensionTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal, isNsfw = it[ExtensionTable.contentWarning] >= ContentWarning.MIXED.ordinal,
installed = it[ExtensionTable.isInstalled], installed = it[ExtensionTable.isInstalled],
hasUpdate = it[ExtensionTable.hasUpdate], hasUpdate = it[ExtensionTable.hasUpdate],
obsolete = it[ExtensionTable.isObsolete], obsolete = it[ExtensionTable.isObsolete],
@@ -173,7 +173,7 @@ object ExtensionsList {
this[ExtensionTable.versionName] = foundExtension.versionName this[ExtensionTable.versionName] = foundExtension.versionName
this[ExtensionTable.versionCode] = foundExtension.versionCode this[ExtensionTable.versionCode] = foundExtension.versionCode
this[ExtensionTable.lang] = foundExtension.lang this[ExtensionTable.lang] = foundExtension.lang
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
this[ExtensionTable.apkUrl] = foundExtension.apkUrl this[ExtensionTable.apkUrl] = foundExtension.apkUrl
this[ExtensionTable.iconUrl] = foundExtension.iconUrl this[ExtensionTable.iconUrl] = foundExtension.iconUrl
} }
@@ -190,7 +190,7 @@ object ExtensionsList {
this[ExtensionTable.versionName] = foundExtension.versionName this[ExtensionTable.versionName] = foundExtension.versionName
this[ExtensionTable.versionCode] = foundExtension.versionCode this[ExtensionTable.versionCode] = foundExtension.versionCode
this[ExtensionTable.lang] = foundExtension.lang this[ExtensionTable.lang] = foundExtension.lang
this[ExtensionTable.contentRating] = foundExtension.contentRating.ordinal this[ExtensionTable.contentWarning] = foundExtension.contentWarning.ordinal
this[ExtensionTable.apkUrl] = foundExtension.apkUrl this[ExtensionTable.apkUrl] = foundExtension.apkUrl
this[ExtensionTable.iconUrl] = foundExtension.iconUrl this[ExtensionTable.iconUrl] = foundExtension.iconUrl
} }

View File

@@ -11,7 +11,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames import kotlinx.serialization.json.JsonNames
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import suwayomi.tachidesk.manga.model.dataclass.ContentRating import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
@@ -23,7 +23,8 @@ data class NetworkExtensionStore(
@ProtoNumber(2) val badgeLabel: String, @ProtoNumber(2) val badgeLabel: String,
@ProtoNumber(3) val signingKey: String, @ProtoNumber(3) val signingKey: String,
@ProtoNumber(4) val contact: Contact, @ProtoNumber(4) val contact: Contact,
@ProtoNumber(5) val extensions: List<Extension>, @ProtoNumber(101) val extensionList: ExtensionList?,
@ProtoNumber(102) val extensionListUrl: String?,
) : BaseNetworkExtensionStore { ) : BaseNetworkExtensionStore {
@Serializable @Serializable
data class Contact( data class Contact(
@@ -31,6 +32,11 @@ data class NetworkExtensionStore(
@ProtoNumber(2) val discord: String?, @ProtoNumber(2) val discord: String?,
) )
@Serializable
data class ExtensionList(
@ProtoNumber(1) val extensions: List<Extension>,
)
@Serializable @Serializable
data class Extension( data class Extension(
@ProtoNumber(1) val name: String, @ProtoNumber(1) val name: String,
@@ -55,27 +61,23 @@ data class NetworkExtensionStore(
@ProtoNumber(3) val language: String, @ProtoNumber(3) val language: String,
@ProtoNumber(4) val homeUrl: String = "", @ProtoNumber(4) val homeUrl: String = "",
@ProtoNumber(5) val mirrorUrls: List<String> = emptyList(), @ProtoNumber(5) val mirrorUrls: List<String> = emptyList(),
@ProtoNumber(6) val contentRating: ContentRating = ContentRating.SAFE, @ProtoNumber(6) val contentWarning: ContentWarning = ContentWarning.SAFE,
@ProtoNumber(7) val message: String? = null, @ProtoNumber(7) val message: String? = null,
) )
@Serializable @Serializable
enum class ContentRating { enum class ContentWarning {
@ProtoNumber(0) @ProtoNumber(0)
@JsonNames("CONTENT_RATING_SAFE") @JsonNames("CONTENT_WARNING_SAFE")
SAFE, SAFE,
@ProtoNumber(1) @ProtoNumber(1)
@JsonNames("CONTENT_RATING_SUGGESTIVE") @JsonNames("CONTENT_WARNING_MIXED")
SUGGESTIVE, MIXED,
@ProtoNumber(2) @ProtoNumber(2)
@JsonNames("CONTENT_RATING_EROTICA") @JsonNames("CONTENT_WARNING_NSFW")
EROTICA, NSFW,
@ProtoNumber(3)
@JsonNames("CONTENT_RATING_PORNOGRAPHIC")
PORNOGRAPHIC,
} }
override fun toExtensionStore(indexUrl: String): ExtensionStore = override fun toExtensionStore(indexUrl: String): ExtensionStore =
@@ -90,10 +92,11 @@ data class NetworkExtensionStore(
discord = contact.discord, discord = contact.discord,
), ),
isLegacy = false, isLegacy = false,
extensionListUrl = extensionListUrl,
) )
} }
fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<ExtensionInfo> = fun NetworkExtensionStore.ExtensionList.toExtensionInfos(store: ExtensionStore): List<ExtensionInfo> =
extensions.map { extension -> extensions.map { extension ->
val lang = extension.sources.map { it.language }.toSet() val lang = extension.sources.map { it.language }.toSet()
ExtensionInfo( ExtensionInfo(
@@ -106,13 +109,12 @@ fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<Extensio
versionCode = extension.versionCode, versionCode = extension.versionCode,
versionName = extension.versionName, versionName = extension.versionName,
lang = if (lang.size == 1) lang.first() else "all", lang = if (lang.size == 1) lang.first() else "all",
contentRating = contentWarning =
when (extension.sources.maxOfOrNull { it.contentRating }) { when (extension.sources.maxOfOrNull { it.contentWarning }) {
NetworkExtensionStore.ContentRating.SAFE -> ContentRating.SAFE NetworkExtensionStore.ContentWarning.SAFE -> ContentWarning.SAFE
NetworkExtensionStore.ContentRating.SUGGESTIVE -> ContentRating.SUGGESTIVE NetworkExtensionStore.ContentWarning.MIXED -> ContentWarning.MIXED
NetworkExtensionStore.ContentRating.EROTICA -> ContentRating.EROTICA NetworkExtensionStore.ContentWarning.NSFW -> ContentWarning.NSFW
NetworkExtensionStore.ContentRating.PORNOGRAPHIC -> ContentRating.PORNOGRAPHIC null -> ContentWarning.SAFE
null -> ContentRating.SAFE
}, },
sources = sources =
extension.sources.map { source -> extension.sources.map { source ->
@@ -122,12 +124,11 @@ fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<Extensio
lang = source.language, lang = source.language,
homeUrl = source.homeUrl, homeUrl = source.homeUrl,
message = source.message, message = source.message,
contentRating = contentWarning =
when (source.contentRating) { when (source.contentWarning) {
NetworkExtensionStore.ContentRating.SAFE -> ContentRating.SAFE NetworkExtensionStore.ContentWarning.SAFE -> ContentWarning.SAFE
NetworkExtensionStore.ContentRating.SUGGESTIVE -> ContentRating.SUGGESTIVE NetworkExtensionStore.ContentWarning.MIXED -> ContentWarning.MIXED
NetworkExtensionStore.ContentRating.EROTICA -> ContentRating.EROTICA NetworkExtensionStore.ContentWarning.NSFW -> ContentWarning.NSFW
NetworkExtensionStore.ContentRating.PORNOGRAPHIC -> ContentRating.PORNOGRAPHIC
}, },
) )
}, },

View File

@@ -9,7 +9,7 @@ package suwayomi.tachidesk.manga.impl.extension.github
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import suwayomi.tachidesk.manga.model.dataclass.ContentRating import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource import suwayomi.tachidesk.manga.model.dataclass.ExtensionSource
import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore import suwayomi.tachidesk.manga.model.dataclass.ExtensionStore
@@ -49,7 +49,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
versionCode = code, versionCode = code,
versionName = version, versionName = version,
lang = lang, lang = lang,
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE, contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
sources = sources =
if (sources.isNullOrEmpty()) { if (sources.isNullOrEmpty()) {
listOf( listOf(
@@ -59,7 +59,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
lang = lang, lang = lang,
homeUrl = "", homeUrl = "",
message = null, message = null,
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE, contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
), ),
) )
} else { } else {
@@ -70,7 +70,7 @@ fun NetworkLegacyExtension.toExtensionInfo(
lang = source.lang, lang = source.lang,
homeUrl = source.baseUrl, homeUrl = source.baseUrl,
message = null, message = null,
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE, contentWarning = if (nsfw == 1) ContentWarning.MIXED else ContentWarning.SAFE,
) )
} }
}, },

View File

@@ -36,5 +36,6 @@ data class NetworkLegacyExtensionRepo(
discord = null, discord = null,
), ),
isLegacy = true, isLegacy = true,
extensionListUrl = null,
) )
} }

View File

@@ -40,6 +40,11 @@ object PackageTools {
const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class" const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory" const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
const val METADATA_NSFW = "tachiyomi.extension.nsfw" const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val METADATA_NAME = "tachiyomix.name"
const val METADATA_EXTENSION_LIB = "tachiyomix.extensionLib"
const val METADATA_CONTENT_WARNING = "tachiyomix.contentWarning"
const val LIB_VERSION_MIN = 1.3 const val LIB_VERSION_MIN = 1.3
const val LIB_VERSION_MAX = 1.6 const val LIB_VERSION_MAX = 1.6

View File

@@ -17,7 +17,7 @@ data class ExtensionInfo(
val versionCode: Long, val versionCode: Long,
val versionName: String, val versionName: String,
val lang: String, val lang: String,
val contentRating: ContentRating, val contentWarning: ContentWarning,
val sources: List<ExtensionSource>, val sources: List<ExtensionSource>,
) )
@@ -27,17 +27,16 @@ data class ExtensionSource(
val lang: String, val lang: String,
val homeUrl: String, val homeUrl: String,
val message: String?, val message: String?,
val contentRating: ContentRating, val contentWarning: ContentWarning,
) )
enum class ContentRating { enum class ContentWarning {
SAFE, SAFE,
SUGGESTIVE, MIXED,
EROTICA, NSFW,
PORNOGRAPHIC,
; ;
companion object { companion object {
fun valueOf(contentRating: Int) = entries.find { it.ordinal == contentRating } ?: SAFE fun valueOf(contentWarning: Int) = entries.find { it.ordinal == contentWarning } ?: SAFE
} }
} }

View File

@@ -14,6 +14,7 @@ data class ExtensionStore(
val signingKey: String, val signingKey: String,
val contact: Contact, val contact: Contact,
val isLegacy: Boolean, val isLegacy: Boolean,
val extensionListUrl: String?,
) { ) {
data class Contact( data class Contact(
val website: String, val website: String,

View File

@@ -17,4 +17,5 @@ object ExtensionStoreTable : IntIdTable() {
val contactWebsite = varchar("contact_website", 2048) val contactWebsite = varchar("contact_website", 2048)
val contactDiscord = varchar("contact_discord", 2048).nullable() val contactDiscord = varchar("contact_discord", 2048).nullable()
val isLegacy = bool("is_legacy").default(false) val isLegacy = bool("is_legacy").default(false)
val extensionListUrl = varchar("extension_list_url", 2048).nullable()
} }

View File

@@ -28,7 +28,7 @@ object ExtensionTable : IntIdTable() {
val versionName = varchar("version_name", 16) val versionName = varchar("version_name", 16)
val versionCode = long("version_code") val versionCode = long("version_code")
val lang = varchar("lang", 32) val lang = varchar("lang", 32)
val contentRating = integer("content_rating") val contentWarning = integer("content_warning")
val isInstalled = bool("is_installed").default(false) val isInstalled = bool("is_installed").default(false)
val hasUpdate = bool("has_update").default(false) val hasUpdate = bool("has_update").default(false)

View File

@@ -8,13 +8,11 @@ package suwayomi.tachidesk.manga.model.table
* 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 org.jetbrains.exposed.v1.core.dao.id.IdTable import org.jetbrains.exposed.v1.core.dao.id.IdTable
import suwayomi.tachidesk.manga.model.table.columns.unlimitedVarchar
object SourceTable : IdTable<Long>() { object SourceTable : IdTable<Long>() {
override val id = long("id").entityId() override val id = long("id").entityId()
val name = varchar("name", 128) val name = varchar("name", 128)
val lang = varchar("lang", 32) val lang = varchar("lang", 32)
val extension = reference("extension", ExtensionTable) val extension = reference("extension", ExtensionTable)
val message = unlimitedVarchar("message").nullable() val contentWarning = integer("content_warning").default(0)
val contentRating = integer("content_rating").default(0)
} }

View File

@@ -9,7 +9,7 @@ package suwayomi.tachidesk.server.database.migration
import de.neonew.exposed.migrations.helpers.SQLMigration import de.neonew.exposed.migrations.helpers.SQLMigration
import suwayomi.tachidesk.graphql.types.DatabaseType import suwayomi.tachidesk.graphql.types.DatabaseType
import suwayomi.tachidesk.manga.model.dataclass.ContentRating import suwayomi.tachidesk.manga.model.dataclass.ContentWarning
import suwayomi.tachidesk.server.database.migration.helpers.MAYBE_TYPE_PREFIX import suwayomi.tachidesk.server.database.migration.helpers.MAYBE_TYPE_PREFIX
import suwayomi.tachidesk.server.database.migration.helpers.UNLIMITED_TEXT import suwayomi.tachidesk.server.database.migration.helpers.UNLIMITED_TEXT
import suwayomi.tachidesk.server.database.migration.helpers.toSqlName import suwayomi.tachidesk.server.database.migration.helpers.toSqlName
@@ -43,12 +43,11 @@ class M0057_AddNewExtensionApiFields : SQLMigration() {
} }
} }
ALTER TABLE EXTENSION ADD COLUMN apk_url VARCHAR(2048); ALTER TABLE EXTENSION ADD COLUMN apk_url VARCHAR(2048);
ALTER TABLE EXTENSION ADD COLUMN content_rating INTEGER DEFAULT 0; ALTER TABLE EXTENSION ADD COLUMN content_warning INTEGER DEFAULT 0;
UPDATE EXTENSION SET content_rating = ${ContentRating.PORNOGRAPHIC.ordinal} WHERE is_nsfw = TRUE; UPDATE EXTENSION SET content_warning = ${ContentWarning.MIXED.ordinal} WHERE is_nsfw = TRUE;
ALTER TABLE EXTENSION DROP COLUMN is_nsfw; ALTER TABLE EXTENSION DROP COLUMN is_nsfw;
ALTER TABLE SOURCE ADD COLUMN message $UNLIMITED_TEXT; ALTER TABLE SOURCE ADD COLUMN content_warning INTEGER DEFAULT 0;
ALTER TABLE SOURCE ADD COLUMN content_rating INTEGER DEFAULT 0; UPDATE SOURCE SET content_warning = ${ContentWarning.MIXED.ordinal} WHERE is_nsfw = TRUE;
UPDATE SOURCE SET content_rating = ${ContentRating.PORNOGRAPHIC.ordinal} WHERE is_nsfw = TRUE;
ALTER TABLE SOURCE DROP COLUMN is_nsfw; ALTER TABLE SOURCE DROP COLUMN is_nsfw;

View File

@@ -21,6 +21,7 @@ class M0058_AddExtensionStore : AddTableMigration() {
val contactWebsite = varchar("contact_website", 2048) val contactWebsite = varchar("contact_website", 2048)
val contactDiscord = varchar("contact_discord", 2048).nullable() val contactDiscord = varchar("contact_discord", 2048).nullable()
val isLegacy = bool("is_legacy").default(false) val isLegacy = bool("is_legacy").default(false)
val extensionListUrl = varchar("extension_list_url", 2048).nullable()
} }
override val tables: Array<Table> override val tables: Array<Table>