mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-11 06:44:34 -05:00
Compare commits
18 Commits
3a78453a02
...
396cfa734a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
396cfa734a | ||
|
|
a1fdf6d77a | ||
|
|
07ae17105b | ||
|
|
b8772f60bf | ||
|
|
733b9c9919 | ||
|
|
0b0c056bcb | ||
|
|
aff95bfc37 | ||
|
|
00bc3e39b6 | ||
|
|
e9c2cc49a6 | ||
|
|
ea310ba54b | ||
|
|
72347f45cc | ||
|
|
1e73e526c6 | ||
|
|
8fd0fdba08 | ||
|
|
4b61d375ed | ||
|
|
3cf4cf6cf8 | ||
|
|
74ade8a3a3 | ||
|
|
3bb2e4329e | ||
|
|
33ec15c136 |
@@ -172,7 +172,7 @@ server.maxLogFolderSize = "100mb"
|
||||
server.extensionStores = []
|
||||
server.maxSourcesInParallel = 6
|
||||
```
|
||||
- `server.extensionStores` is a list of extension stores(previously called repositories) for custom sources. Uses the same format as Mihon; each entry is expected to be a string URL pointing to a JSON or PROTOBUF file representing the repository.
|
||||
- `server.extensionStores` is a list of extension stores (previously called repositories) for custom sources. Uses the same format as Mihon; each entry is expected to be a string URL pointing to a JSON or PROTOBUF file representing the repository.
|
||||
- `server.maxSourcesInParallel = 6` sets how many sources can do requests (updates, downloads) in parallel. Updates/downloads are grouped by source and all mangas of a source are updated/downloaded synchronously. Range: 1 <= n <= 20.
|
||||
|
||||
### Backup
|
||||
|
||||
@@ -287,9 +287,10 @@ class ServerConfig(
|
||||
replaceWith = "extensionStores",
|
||||
message = "Replaced with extensionStores",
|
||||
migrateConfigValue = {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(it.unwrapped() as? List<String>)
|
||||
?.map {
|
||||
if (it.contains("github")) {
|
||||
if (it.contains("github.com")) {
|
||||
it.replace(repoMatchRegex) {
|
||||
"https://raw.githubusercontent.com/${it.groupValues[2]}/${it.groupValues[3]}/" +
|
||||
(it.groupValues.getOrNull(4)?.ifBlank { null } ?: "repo") +
|
||||
@@ -303,7 +304,7 @@ class ServerConfig(
|
||||
},
|
||||
),
|
||||
readMigrated = { extensionStores.value },
|
||||
setMigrated = { extensionStores.value = it },
|
||||
setMigrated = { extensionStores.value = (extensionStores.value + it).distinct() },
|
||||
typeInfo =
|
||||
SettingsRegistry.PartialTypeInfo(
|
||||
specificType = "List<String>",
|
||||
@@ -1117,7 +1118,7 @@ class ServerConfig(
|
||||
val extensionStores: MutableStateFlow<List<String>> by ListSetting<String>(
|
||||
protoNumber = 97,
|
||||
group = SettingGroup.EXTENSION,
|
||||
privacySafe = false,
|
||||
privacySafe = true,
|
||||
defaultValue = emptyList(),
|
||||
itemValidator = { url ->
|
||||
if (url.isNotEmpty()) {
|
||||
@@ -1127,11 +1128,7 @@ class ServerConfig(
|
||||
}
|
||||
},
|
||||
itemToValidValue = { url ->
|
||||
if (url.isNotEmpty()) {
|
||||
url
|
||||
} else {
|
||||
null
|
||||
}
|
||||
url.ifEmpty { null }
|
||||
},
|
||||
typeInfo =
|
||||
SettingsRegistry.PartialTypeInfo(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.EMPTY
|
||||
|
||||
class SChapterImpl : SChapter {
|
||||
override lateinit var url: String
|
||||
@@ -15,5 +16,5 @@ class SChapterImpl : SChapter {
|
||||
|
||||
override var date_upload: Long = 0
|
||||
|
||||
override var memo: JsonObject = JsonObject(emptyMap())
|
||||
override var memo: JsonObject = JsonObject.EMPTY
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package eu.kanade.tachiyomi.source.model
|
||||
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.EMPTY
|
||||
|
||||
class SMangaImpl : SManga {
|
||||
override lateinit var url: String
|
||||
@@ -25,5 +26,5 @@ class SMangaImpl : SManga {
|
||||
|
||||
override var initialized: Boolean = false
|
||||
|
||||
override var memo: JsonObject = JsonObject(emptyMap())
|
||||
override var memo: JsonObject = JsonObject.EMPTY
|
||||
}
|
||||
|
||||
@@ -16,6 +16,26 @@ import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
|
||||
class ExtensionStoreDataLoader : KotlinDataLoader<String, ExtensionStoreType> {
|
||||
override val dataLoaderName = "ExtensionStoreDataLoader"
|
||||
|
||||
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<String, ExtensionStoreType> =
|
||||
DataLoaderFactory.newDataLoader { ids ->
|
||||
future {
|
||||
transaction {
|
||||
addLogger(Slf4jSqlDebugLogger)
|
||||
val manga =
|
||||
ExtensionStoreTable
|
||||
.selectAll()
|
||||
.where { ExtensionStoreTable.indexUrl inList ids }
|
||||
.map { ExtensionStoreType(it) }
|
||||
.associateBy { it.indexUrl }
|
||||
ids.map { manga[it] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionStoreForExtension : KotlinDataLoader<String, ExtensionStoreType> {
|
||||
override val dataLoaderName = "ExtensionStoreForExtension"
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import suwayomi.tachidesk.graphql.types.ChapterType
|
||||
import suwayomi.tachidesk.graphql.types.MetaInput
|
||||
import suwayomi.tachidesk.graphql.types.SyncConflictInfoType
|
||||
import suwayomi.tachidesk.manga.impl.Chapter
|
||||
import suwayomi.tachidesk.manga.impl.Manga
|
||||
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReadyById
|
||||
import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
|
||||
@@ -173,7 +174,7 @@ class ChapterMutation {
|
||||
val (clientMutationId, mangaId) = input
|
||||
|
||||
return future {
|
||||
Chapter.fetchChapterList(mangaId)
|
||||
Manga.updateMangaAndChapters(mangaId, updateManga = false)
|
||||
|
||||
val chapters =
|
||||
transaction {
|
||||
|
||||
@@ -19,11 +19,9 @@ import suwayomi.tachidesk.graphql.types.ChapterType
|
||||
import suwayomi.tachidesk.graphql.types.MangaMetaType
|
||||
import suwayomi.tachidesk.graphql.types.MangaType
|
||||
import suwayomi.tachidesk.graphql.types.MetaInput
|
||||
import suwayomi.tachidesk.manga.impl.Chapter
|
||||
import suwayomi.tachidesk.manga.impl.Library
|
||||
import suwayomi.tachidesk.manga.impl.Manga
|
||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
@@ -156,7 +154,7 @@ class MangaMutation {
|
||||
val (clientMutationId, id) = input
|
||||
|
||||
return future {
|
||||
Manga.fetchManga(id)
|
||||
Manga.updateMangaAndChapters(id, updateChapters = false)
|
||||
|
||||
val manga =
|
||||
transaction {
|
||||
@@ -187,20 +185,11 @@ class MangaMutation {
|
||||
val (clientMutationId, id, fetchManga, fetchChapters) = input
|
||||
|
||||
return future {
|
||||
var mangaEntry =
|
||||
transaction { MangaTable.selectAll().where { MangaTable.id eq id }.first() }
|
||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||
val sMangaUpdate =
|
||||
Manga.fetchMangaAndChapters(
|
||||
mangaEntry = mangaEntry,
|
||||
source = source,
|
||||
fetchDetails = fetchManga,
|
||||
fetchChapters = fetchChapters,
|
||||
)
|
||||
|
||||
Manga.updateMangaDatabase(mangaEntry, source, sMangaUpdate.manga)
|
||||
mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq id }.first() }
|
||||
Chapter.updateChapterListDatabase(mangaEntry, sMangaUpdate.chapters, source)
|
||||
Manga.updateMangaAndChapters(
|
||||
mangaId = id,
|
||||
updateManga = fetchManga,
|
||||
updateChapters = fetchChapters,
|
||||
)
|
||||
|
||||
val (manga, chapters) =
|
||||
transaction {
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.ContentRatingFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
||||
import suwayomi.tachidesk.graphql.queries.filter.IntFilter
|
||||
@@ -28,6 +29,7 @@ import suwayomi.tachidesk.graphql.queries.filter.LongFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
|
||||
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEnum
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
@@ -126,7 +128,10 @@ class ExtensionQuery {
|
||||
opAnd.eq(versionCode?.toLong(), ExtensionTable.versionCode)
|
||||
opAnd.eq(versionCodeLong, ExtensionTable.versionCode)
|
||||
opAnd.eq(lang, ExtensionTable.lang)
|
||||
opAnd.eq(isNsfw?.let { if (it) 3 else 0 }, ExtensionTable.contentRating)
|
||||
opAnd.eq(
|
||||
isNsfw?.let { if (it) ContentRating.PORNOGRAPHIC.ordinal else ContentRating.SAFE.ordinal },
|
||||
ExtensionTable.contentRating,
|
||||
)
|
||||
opAnd.eq(contentRating?.ordinal, ExtensionTable.contentRating)
|
||||
opAnd.eq(isInstalled, ExtensionTable.isInstalled)
|
||||
opAnd.eq(hasUpdate, ExtensionTable.hasUpdate)
|
||||
@@ -153,7 +158,7 @@ class ExtensionQuery {
|
||||
val lang: StringFilter? = null,
|
||||
@GraphQLDeprecated("", ReplaceWith("storeIndexUrl"))
|
||||
val isNsfw: BooleanFilter? = null,
|
||||
// val contentRating: EnumFilter<ContentRating>? = null,
|
||||
val contentRating: ContentRatingFilter? = null,
|
||||
val isInstalled: BooleanFilter? = null,
|
||||
val hasUpdate: BooleanFilter? = null,
|
||||
val isObsolete: BooleanFilter? = null,
|
||||
@@ -174,7 +179,7 @@ class ExtensionQuery {
|
||||
andFilterWithCompareString(ExtensionTable.versionName, versionName),
|
||||
andFilterWithCompare(ExtensionTable.versionCode, versionCodeLong),
|
||||
andFilterWithCompareString(ExtensionTable.lang, lang),
|
||||
// andFilterWithCompareEnum(ExtensionTable.contentRating, contentRating),
|
||||
andFilterWithCompareEnum(ExtensionTable.contentRating, contentRating),
|
||||
andFilterWithCompare(ExtensionTable.isInstalled, isInstalled),
|
||||
andFilterWithCompare(ExtensionTable.hasUpdate, hasUpdate),
|
||||
andFilterWithCompare(ExtensionTable.isObsolete, isObsolete),
|
||||
|
||||
@@ -7,33 +7,185 @@ package suwayomi.tachidesk.graphql.queries
|
||||
* 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.eq
|
||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.Column
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
||||
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
|
||||
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Order
|
||||
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
|
||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||
import suwayomi.tachidesk.graphql.server.primitives.QueryResults
|
||||
import suwayomi.tachidesk.graphql.server.primitives.applyBeforeAfter
|
||||
import suwayomi.tachidesk.graphql.server.primitives.greaterNotUnique
|
||||
import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
|
||||
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
|
||||
import suwayomi.tachidesk.graphql.types.ExtensionStoreNodeList
|
||||
import suwayomi.tachidesk.graphql.types.ExtensionStoreType
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class ExtensionStoreQuery {
|
||||
@RequireAuth
|
||||
fun extensionStore(indexUrl: String): CompletableFuture<ExtensionStoreType?> =
|
||||
CompletableFuture.supplyAsync {
|
||||
transaction {
|
||||
ExtensionStoreTable
|
||||
.selectAll()
|
||||
.where { ExtensionStoreTable.indexUrl eq indexUrl }
|
||||
.firstOrNull()
|
||||
?.let { ExtensionStoreType(it) }
|
||||
fun extensionStore(
|
||||
dataFetchingEnvironment: DataFetchingEnvironment,
|
||||
indexUrl: String,
|
||||
): CompletableFuture<ExtensionStoreType> = dataFetchingEnvironment.getValueFromDataLoader("ExtensionStoreDataLoader", indexUrl)
|
||||
|
||||
enum class ExtensionStoreOrderBy(
|
||||
override val column: Column<*>,
|
||||
) : OrderBy<ExtensionStoreType> {
|
||||
NAME(ExtensionStoreTable.name),
|
||||
INDEX_URL(ExtensionStoreTable.indexUrl),
|
||||
;
|
||||
|
||||
override fun greater(cursor: Cursor): Op<Boolean> =
|
||||
when (this) {
|
||||
NAME -> greaterNotUnique(ExtensionStoreTable.name, ExtensionStoreTable.id, cursor, String::toString)
|
||||
INDEX_URL -> greaterNotUnique(ExtensionStoreTable.indexUrl, ExtensionStoreTable.id, cursor, String::toString)
|
||||
}
|
||||
|
||||
override fun less(cursor: Cursor): Op<Boolean> =
|
||||
when (this) {
|
||||
NAME -> lessNotUnique(ExtensionStoreTable.name, ExtensionStoreTable.id, cursor, String::toString)
|
||||
INDEX_URL -> lessNotUnique(ExtensionStoreTable.indexUrl, ExtensionStoreTable.id, cursor, String::toString)
|
||||
}
|
||||
|
||||
override fun asCursor(type: ExtensionStoreType): Cursor {
|
||||
val value =
|
||||
when (this) {
|
||||
INDEX_URL -> type.indexUrl
|
||||
NAME -> type.indexUrl + "-" + type.name
|
||||
}
|
||||
return Cursor(value)
|
||||
}
|
||||
}
|
||||
|
||||
data class ExtensionStoreOrder(
|
||||
override val by: ExtensionStoreOrderBy,
|
||||
override val byType: SortOrder? = null,
|
||||
) : Order<ExtensionStoreOrderBy>
|
||||
|
||||
data class ExtensionStoreCondition(
|
||||
val id: Int? = null,
|
||||
val indexUrl: String? = null,
|
||||
val name: String? = null,
|
||||
) : HasGetOp {
|
||||
override fun getOp(): Op<Boolean>? {
|
||||
val opAnd = OpAnd()
|
||||
opAnd.eq(id, ExtensionStoreTable.id)
|
||||
opAnd.eq(indexUrl, ExtensionStoreTable.indexUrl)
|
||||
opAnd.eq(name, ExtensionStoreTable.name)
|
||||
|
||||
return opAnd.op
|
||||
}
|
||||
}
|
||||
|
||||
data class ExtensionStoreFilter(
|
||||
val indexUrl: StringFilter? = null,
|
||||
val name: StringFilter? = null,
|
||||
override val and: List<ExtensionStoreFilter>? = null,
|
||||
override val or: List<ExtensionStoreFilter>? = null,
|
||||
override val not: ExtensionStoreFilter? = null,
|
||||
) : Filter<ExtensionStoreFilter> {
|
||||
override fun getOpList(): List<Op<Boolean>> =
|
||||
listOfNotNull(
|
||||
andFilterWithCompareString(ExtensionStoreTable.indexUrl, indexUrl),
|
||||
andFilterWithCompareString(ExtensionStoreTable.name, name),
|
||||
)
|
||||
}
|
||||
|
||||
@RequireAuth
|
||||
fun extensionStores(): List<ExtensionStoreType> =
|
||||
transaction {
|
||||
ExtensionStoreTable
|
||||
.selectAll()
|
||||
.toList()
|
||||
.map { ExtensionStoreType(it) }
|
||||
}
|
||||
fun extensionStores(
|
||||
condition: ExtensionStoreCondition? = null,
|
||||
filter: ExtensionStoreFilter? = null,
|
||||
order: List<ExtensionStoreOrder>? = null,
|
||||
before: Cursor? = null,
|
||||
after: Cursor? = null,
|
||||
first: Int? = null,
|
||||
last: Int? = null,
|
||||
offset: Int? = null,
|
||||
): ExtensionStoreNodeList {
|
||||
val queryResults =
|
||||
transaction {
|
||||
val res = ExtensionStoreTable.selectAll()
|
||||
|
||||
res.applyOps(condition, filter)
|
||||
|
||||
if (order != null || (last != null || before != null)) {
|
||||
val baseSort = listOf(ExtensionStoreOrder(ExtensionStoreOrderBy.INDEX_URL, SortOrder.ASC))
|
||||
val actualSort = (order.orEmpty() + baseSort)
|
||||
actualSort.forEach { (orderBy, orderByType) ->
|
||||
val orderByColumn = orderBy.column
|
||||
val orderType = orderByType.maybeSwap(last ?: before)
|
||||
|
||||
res.orderBy(orderByColumn to orderType)
|
||||
}
|
||||
}
|
||||
|
||||
val total = res.count()
|
||||
val firstResult = res.firstOrNull()?.get(ExtensionStoreTable.indexUrl)
|
||||
val lastResult = res.lastOrNull()?.get(ExtensionStoreTable.indexUrl)
|
||||
|
||||
res.applyBeforeAfter(
|
||||
before = before,
|
||||
after = after,
|
||||
orderBy = order?.firstOrNull()?.by ?: ExtensionStoreOrderBy.INDEX_URL,
|
||||
orderByType = order?.firstOrNull()?.byType,
|
||||
)
|
||||
|
||||
if (first != null) {
|
||||
res.limit(first).offset(offset?.toLong() ?: 0)
|
||||
} else if (last != null) {
|
||||
res.limit(last)
|
||||
}
|
||||
|
||||
QueryResults(total, firstResult, lastResult, res.toList())
|
||||
}
|
||||
|
||||
val getAsCursor: (ExtensionStoreType) -> Cursor = (order?.firstOrNull()?.by ?: ExtensionStoreOrderBy.INDEX_URL)::asCursor
|
||||
|
||||
val resultsAsType = queryResults.results.map { ExtensionStoreType(it) }
|
||||
|
||||
return ExtensionStoreNodeList(
|
||||
resultsAsType,
|
||||
if (resultsAsType.isEmpty()) {
|
||||
emptyList()
|
||||
} else {
|
||||
listOfNotNull(
|
||||
resultsAsType.firstOrNull()?.let {
|
||||
ExtensionStoreNodeList.ExtensionStoreEdge(
|
||||
getAsCursor(it),
|
||||
it,
|
||||
)
|
||||
},
|
||||
resultsAsType.lastOrNull()?.let {
|
||||
ExtensionStoreNodeList.ExtensionStoreEdge(
|
||||
getAsCursor(it),
|
||||
it,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
pageInfo =
|
||||
PageInfo(
|
||||
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.indexUrl,
|
||||
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.indexUrl,
|
||||
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
|
||||
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) },
|
||||
),
|
||||
totalCount = queryResults.total.toInt(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,18 +15,20 @@ import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
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.neq
|
||||
import org.jetbrains.exposed.v1.jdbc.selectAll
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
||||
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.ContentRatingFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
||||
import suwayomi.tachidesk.graphql.queries.filter.LongFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.OpAnd
|
||||
import suwayomi.tachidesk.graphql.queries.filter.StringFilter
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEnum
|
||||
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
|
||||
import suwayomi.tachidesk.graphql.queries.filter.applyOps
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
@@ -93,7 +95,7 @@ class SourceQuery {
|
||||
val id: Long? = null,
|
||||
val name: String? = null,
|
||||
val lang: String? = null,
|
||||
@GraphQLDeprecated("replace with contentRating == 3", ReplaceWith("contentRating"))
|
||||
@GraphQLDeprecated("replace with contentRating == ContentRating.PORNOGRAPHIC", ReplaceWith("contentRating"))
|
||||
val isNsfw: Boolean? = null,
|
||||
val contentRating: ContentRating? = null,
|
||||
) : HasGetOp {
|
||||
@@ -102,7 +104,14 @@ class SourceQuery {
|
||||
opAnd.eq(id, SourceTable.id)
|
||||
opAnd.eq(name, SourceTable.name)
|
||||
opAnd.eq(lang, SourceTable.lang)
|
||||
opAnd.andWhere(isNsfw) { if (it) SourceTable.contentRating greaterEq 3 else SourceTable.contentRating less 3 }
|
||||
opAnd.andWhere(isNsfw) {
|
||||
if (it) {
|
||||
SourceTable.contentRating eq ContentRating.PORNOGRAPHIC.ordinal
|
||||
} else {
|
||||
SourceTable.contentRating neq
|
||||
ContentRating.PORNOGRAPHIC.ordinal
|
||||
}
|
||||
}
|
||||
opAnd.andWhere(contentRating) { SourceTable.contentRating eq it.getValue() }
|
||||
|
||||
return opAnd.op
|
||||
@@ -113,9 +122,9 @@ class SourceQuery {
|
||||
val id: LongFilter? = null,
|
||||
val name: StringFilter? = null,
|
||||
val lang: StringFilter? = null,
|
||||
@GraphQLDeprecated("replace with contentRating == 3", ReplaceWith("contentRating"))
|
||||
@GraphQLDeprecated("replace with contentRating == ContentRating.PORNOGRAPHIC", ReplaceWith("contentRating"))
|
||||
val isNsfw: BooleanFilter? = null,
|
||||
// val contentRating: EnumFilter<ContentRating>? = null,
|
||||
val contentRating: ContentRatingFilter? = null,
|
||||
override val and: List<SourceFilter>? = null,
|
||||
override val or: List<SourceFilter>? = null,
|
||||
override val not: SourceFilter? = null,
|
||||
@@ -125,7 +134,7 @@ class SourceQuery {
|
||||
andFilterWithCompareEntity(SourceTable.id, id),
|
||||
andFilterWithCompareString(SourceTable.name, name),
|
||||
andFilterWithCompareString(SourceTable.lang, lang),
|
||||
// andFilterWithCompareEnum(SourceTable.contentRating, contentRating)
|
||||
andFilterWithCompareEnum(SourceTable.contentRating, contentRating),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.jetbrains.exposed.v1.core.upperCase
|
||||
import org.jetbrains.exposed.v1.core.wrap
|
||||
import org.jetbrains.exposed.v1.jdbc.Query
|
||||
import org.jetbrains.exposed.v1.jdbc.andWhere
|
||||
import suwayomi.tachidesk.graphql.types.ContentRating
|
||||
|
||||
class ILikeEscapeOp(
|
||||
expr1: Expression<*>,
|
||||
@@ -329,23 +330,23 @@ data class DoubleFilter(
|
||||
)
|
||||
}
|
||||
|
||||
data class EnumFilter<T : Enum<T>>(
|
||||
data class ContentRatingFilter(
|
||||
override val isNull: Boolean? = null,
|
||||
override val equalTo: T? = null,
|
||||
override val notEqualTo: T? = null,
|
||||
override val notEqualToAll: List<T>? = null,
|
||||
override val notEqualToAny: List<T>? = null,
|
||||
override val distinctFrom: T? = null,
|
||||
override val distinctFromAll: List<T>? = null,
|
||||
override val distinctFromAny: List<T>? = null,
|
||||
override val notDistinctFrom: T? = null,
|
||||
override val `in`: List<T>? = null,
|
||||
override val notIn: List<T>? = null,
|
||||
override val lessThan: T? = null,
|
||||
override val lessThanOrEqualTo: T? = null,
|
||||
override val greaterThan: T? = null,
|
||||
override val greaterThanOrEqualTo: T? = null,
|
||||
) : ComparableScalarFilter<T>
|
||||
override val equalTo: ContentRating? = null,
|
||||
override val notEqualTo: ContentRating? = null,
|
||||
override val notEqualToAll: List<ContentRating>? = null,
|
||||
override val notEqualToAny: List<ContentRating>? = null,
|
||||
override val distinctFrom: ContentRating? = null,
|
||||
override val distinctFromAll: List<ContentRating>? = null,
|
||||
override val distinctFromAny: List<ContentRating>? = null,
|
||||
override val notDistinctFrom: ContentRating? = null,
|
||||
override val `in`: List<ContentRating>? = null,
|
||||
override val notIn: List<ContentRating>? = null,
|
||||
override val lessThan: ContentRating? = null,
|
||||
override val lessThanOrEqualTo: ContentRating? = null,
|
||||
override val greaterThan: ContentRating? = null,
|
||||
override val greaterThanOrEqualTo: ContentRating? = null,
|
||||
) : ComparableScalarFilter<ContentRating>
|
||||
|
||||
data class StringFilter(
|
||||
override val isNull: Boolean? = null,
|
||||
|
||||
@@ -22,6 +22,7 @@ import suwayomi.tachidesk.graphql.dataLoaders.DownloadedChapterCountForMangaData
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForExtensionStore
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionStoreForExtension
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.FirstUnreadChapterForMangaDataLoader
|
||||
import suwayomi.tachidesk.graphql.dataLoaders.GlobalMetaDataLoader
|
||||
@@ -81,6 +82,7 @@ class TachideskDataLoaderRegistryFactory {
|
||||
ExtensionDataLoader(),
|
||||
ExtensionForSourceDataLoader(),
|
||||
ExtensionForExtensionStore(),
|
||||
ExtensionStoreDataLoader(),
|
||||
ExtensionStoreForExtension(),
|
||||
TrackerDataLoader(),
|
||||
TrackerStatusesDataLoader(),
|
||||
|
||||
@@ -10,7 +10,11 @@ package suwayomi.tachidesk.graphql.types
|
||||
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
||||
import graphql.schema.DataFetchingEnvironment
|
||||
import org.jetbrains.exposed.v1.core.ResultRow
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Edge
|
||||
import suwayomi.tachidesk.graphql.server.primitives.Node
|
||||
import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionStoreTable
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
@@ -36,3 +40,45 @@ class ExtensionStoreType(
|
||||
fun extension(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<ExtensionNodeList> =
|
||||
dataFetchingEnvironment.getValueFromDataLoader<String, ExtensionNodeList>("ExtensionForExtensionStore", indexUrl)
|
||||
}
|
||||
|
||||
data class ExtensionStoreNodeList(
|
||||
override val nodes: List<ExtensionStoreType>,
|
||||
override val edges: List<ExtensionStoreEdge>,
|
||||
override val pageInfo: PageInfo,
|
||||
override val totalCount: Int,
|
||||
) : NodeList() {
|
||||
data class ExtensionStoreEdge(
|
||||
override val cursor: Cursor,
|
||||
override val node: ExtensionStoreType,
|
||||
) : Edge()
|
||||
|
||||
companion object {
|
||||
fun List<ExtensionStoreType>.toNodeList(): ExtensionStoreNodeList =
|
||||
ExtensionStoreNodeList(
|
||||
nodes = this,
|
||||
edges = getEdges(),
|
||||
pageInfo =
|
||||
PageInfo(
|
||||
hasNextPage = false,
|
||||
hasPreviousPage = false,
|
||||
startCursor = Cursor(0.toString()),
|
||||
endCursor = Cursor(lastIndex.toString()),
|
||||
),
|
||||
totalCount = size,
|
||||
)
|
||||
|
||||
private fun List<ExtensionStoreType>.getEdges(): List<ExtensionStoreEdge> {
|
||||
if (isEmpty()) return emptyList()
|
||||
return listOf(
|
||||
ExtensionStoreEdge(
|
||||
cursor = Cursor("0"),
|
||||
node = first(),
|
||||
),
|
||||
ExtensionStoreEdge(
|
||||
cursor = Cursor(lastIndex.toString()),
|
||||
node = last(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class ExtensionType(
|
||||
versionCode = row[ExtensionTable.versionCode].toInt(),
|
||||
versionCodeLong = row[ExtensionTable.versionCode],
|
||||
lang = row[ExtensionTable.lang],
|
||||
isNsfw = row[ExtensionTable.contentRating] == 3,
|
||||
isNsfw = row[ExtensionTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
||||
contentRating = ContentRating.valueOf(row[ExtensionTable.contentRating]),
|
||||
isInstalled = row[ExtensionTable.isInstalled],
|
||||
hasUpdate = row[ExtensionTable.hasUpdate],
|
||||
|
||||
@@ -63,7 +63,7 @@ class SourceType(
|
||||
iconUrl = Extension.proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
||||
supportsLatest = catalogueSource.supportsLatest,
|
||||
isConfigurable = catalogueSource is ConfigurableSource,
|
||||
isNsfw = row[SourceTable.contentRating] >= 3,
|
||||
isNsfw = row[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
||||
displayName = catalogueSource.toString(),
|
||||
homeUrl = runCatching { (catalogueSource as? HttpSource)?.getHomeUrl() }.getOrNull(),
|
||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||
|
||||
@@ -55,7 +55,7 @@ object UpdateController {
|
||||
)
|
||||
|
||||
/**
|
||||
* Class made for handling return type in the documentation for [recentChapters],
|
||||
* Class made for handling return type in the documentation for [UpdateController.recentChapters],
|
||||
* since OpenApi cannot handle runtime generics.
|
||||
*/
|
||||
private class PagedMangaChapterListDataClass : PaginatedList<MangaChapterDataClass>(emptyList(), false)
|
||||
|
||||
@@ -81,7 +81,7 @@ object Manga {
|
||||
return if (!onlineFetch && mangaEntry[MangaTable.initialized]) {
|
||||
MangaTable.toDataClass(mangaEntry)
|
||||
} else { // initialize manga
|
||||
fetchManga(mangaId) ?: return MangaTable.toDataClass(mangaEntry)
|
||||
updateMangaAndChapters(mangaId, updateChapters = false)
|
||||
|
||||
mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
|
||||
@@ -152,6 +152,38 @@ object Manga {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateMangaAndChapters(
|
||||
mangaId: Int,
|
||||
updateManga: Boolean = true,
|
||||
updateChapters: Boolean = true,
|
||||
) {
|
||||
mangaInfoMutex.get(mangaId) { Mutex() }.withLock {
|
||||
var mangaEntry =
|
||||
transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
val source =
|
||||
getCatalogueSourceOrNull(mangaEntry[MangaTable.sourceReference])
|
||||
?: throw NullPointerException("Missing source ${mangaEntry[MangaTable.sourceReference]}")
|
||||
val mangaUpdate =
|
||||
fetchMangaAndChapters(
|
||||
mangaEntry,
|
||||
source,
|
||||
fetchDetails = updateManga,
|
||||
fetchChapters = updateChapters,
|
||||
)
|
||||
|
||||
if (updateManga) {
|
||||
updateMangaDatabase(mangaEntry, source, mangaUpdate.manga)
|
||||
mangaEntry =
|
||||
transaction {
|
||||
MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||
}
|
||||
}
|
||||
if (updateChapters) {
|
||||
Chapter.updateChapterListDatabase(mangaEntry, mangaUpdate.chapters, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMangaDatabase(
|
||||
mangaEntry: ResultRow,
|
||||
source: CatalogueSource,
|
||||
|
||||
@@ -29,6 +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.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
|
||||
@@ -52,7 +53,7 @@ object Source {
|
||||
iconUrl = proxyExtensionIconUrl(sourceExtension[ExtensionTable.pkgName]),
|
||||
supportsLatest = catalogueSource.supportsLatest,
|
||||
isConfigurable = catalogueSource is ConfigurableSource,
|
||||
isNsfw = it[SourceTable.contentRating] >= 3,
|
||||
isNsfw = it[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
||||
displayName = catalogueSource.toString(),
|
||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||
)
|
||||
@@ -73,7 +74,7 @@ object Source {
|
||||
iconUrl = proxyExtensionIconUrl(extension[ExtensionTable.pkgName]),
|
||||
supportsLatest = catalogueSource.supportsLatest,
|
||||
isConfigurable = catalogueSource is ConfigurableSource,
|
||||
isNsfw = source[SourceTable.contentRating] >= 3,
|
||||
isNsfw = source[SourceTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
||||
displayName = catalogueSource.toString(),
|
||||
baseUrl = runCatching { (catalogueSource as? HttpSource)?.baseUrl }.getOrNull(),
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ 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.graphql.types.ContentRating
|
||||
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
|
||||
@@ -197,7 +198,13 @@ object Extension {
|
||||
it[versionName] = packageInfo.versionName
|
||||
it[versionCode] = packageInfo.versionCode.toLong()
|
||||
it[lang] = extensionLang
|
||||
it[contentRating] = if (isNsfw) 3 else 0 // todo will change
|
||||
// todo will change
|
||||
it[contentRating] =
|
||||
if (isNsfw) {
|
||||
ContentRating.PORNOGRAPHIC.ordinal
|
||||
} else {
|
||||
ContentRating.SAFE.ordinal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +229,7 @@ object Extension {
|
||||
it[name] = httpSource.name
|
||||
it[lang] = httpSource.lang
|
||||
it[extension] = extensionId
|
||||
it[contentRating] = if (isNsfw) 3 else 0
|
||||
it[contentRating] = if (isNsfw) ContentRating.PORNOGRAPHIC.ordinal else ContentRating.SAFE.ordinal
|
||||
}
|
||||
logger.debug { "Installed source ${httpSource.name} (${httpSource.lang}) with id:${httpSource.id}" }
|
||||
}
|
||||
|
||||
@@ -49,44 +49,41 @@ object ExtensionStoreService {
|
||||
): ExtensionStore {
|
||||
var updatedIndexUrl = indexUrl
|
||||
return try {
|
||||
val response = network.client.newCall(GET(indexUrl)).awaitSuccess()
|
||||
response.body
|
||||
.source()
|
||||
.use { source ->
|
||||
network.client.newCall(GET(indexUrl)).awaitSuccess().body.source().use { source ->
|
||||
try {
|
||||
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
logger.debug { "Failed to decode as protobuf, trying JSON" }
|
||||
try {
|
||||
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
||||
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
|
||||
} 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)
|
||||
}
|
||||
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 { "Checking for new repo.json format" }
|
||||
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
|
||||
}
|
||||
|
||||
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'" }
|
||||
logger.error(e) { "Failed to fetch extension store '$indexUrl'" }
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -127,24 +124,27 @@ object ExtensionStoreService {
|
||||
transaction {
|
||||
ExtensionStoreTable.selectAll().toList()
|
||||
}
|
||||
return stores.mapNotNull { storeRow ->
|
||||
var needsPrefUpdate = false
|
||||
val updateStores = 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()
|
||||
needsPrefUpdate = true
|
||||
}
|
||||
upsert(store)
|
||||
store
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Failed to fetch extension store '$oldName ($oldIndexUrl)'" }
|
||||
null
|
||||
}
|
||||
}
|
||||
if (needsPrefUpdate) syncDbToPrefs()
|
||||
return updateStores
|
||||
}
|
||||
|
||||
fun syncDbToPrefs() {
|
||||
@@ -178,22 +178,33 @@ object ExtensionStoreService {
|
||||
}
|
||||
|
||||
val toAdd = prefUrls - dbStores.keys
|
||||
val toRemove = (dbStores.keys - prefUrls).toMutableSet()
|
||||
var needsPrefUpdate = toRemove.isNotEmpty()
|
||||
|
||||
toAdd.forEach { url ->
|
||||
try {
|
||||
val store = fetch(url)
|
||||
if (store.indexUrl != url) {
|
||||
transaction {
|
||||
ExtensionStoreTable.deleteWhere { ExtensionStoreTable.indexUrl eq url }
|
||||
}
|
||||
needsPrefUpdate = true
|
||||
toRemove -= store.indexUrl
|
||||
}
|
||||
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() }
|
||||
}
|
||||
}
|
||||
if (needsPrefUpdate) {
|
||||
syncDbToPrefs()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getExtensions(store: ExtensionStore): List<ExtensionInfo> {
|
||||
|
||||
@@ -22,6 +22,7 @@ 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.proxyExtensionIconUrl
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ContentRating
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.ExtensionInfo
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
@@ -80,7 +81,7 @@ object ExtensionsList {
|
||||
versionName = it[ExtensionTable.versionName],
|
||||
versionCode = it[ExtensionTable.versionCode].toInt(),
|
||||
lang = it[ExtensionTable.lang],
|
||||
isNsfw = it[ExtensionTable.contentRating] == 3,
|
||||
isNsfw = it[ExtensionTable.contentRating] == ContentRating.PORNOGRAPHIC.ordinal,
|
||||
installed = it[ExtensionTable.isInstalled],
|
||||
hasUpdate = it[ExtensionTable.hasUpdate],
|
||||
obsolete = it[ExtensionTable.isObsolete],
|
||||
|
||||
@@ -121,6 +121,14 @@ fun NetworkExtensionStore.toExtensionInfos(store: ExtensionStore): List<Extensio
|
||||
name = source.name,
|
||||
lang = source.language,
|
||||
homeUrl = source.homeUrl,
|
||||
message = source.message,
|
||||
contentRating =
|
||||
when (source.contentRating) {
|
||||
NetworkExtensionStore.ContentRating.SAFE -> ContentRating.SAFE
|
||||
NetworkExtensionStore.ContentRating.SUGGESTIVE -> ContentRating.SUGGESTIVE
|
||||
NetworkExtensionStore.ContentRating.EROTICA -> ContentRating.EROTICA
|
||||
NetworkExtensionStore.ContentRating.PORNOGRAPHIC -> ContentRating.PORNOGRAPHIC
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -58,6 +58,8 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
||||
name = name,
|
||||
lang = lang,
|
||||
homeUrl = "",
|
||||
message = null,
|
||||
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
@@ -67,6 +69,8 @@ fun NetworkLegacyExtension.toExtensionInfo(
|
||||
name = source.name,
|
||||
lang = source.lang,
|
||||
homeUrl = source.baseUrl,
|
||||
message = null,
|
||||
contentRating = if (nsfw == 1) ContentRating.PORNOGRAPHIC else ContentRating.SAFE,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -31,7 +31,6 @@ import kotlinx.coroutines.sync.withPermit
|
||||
import suwayomi.tachidesk.global.impl.sync.SyncManager
|
||||
import suwayomi.tachidesk.manga.impl.Category
|
||||
import suwayomi.tachidesk.manga.impl.CategoryManga
|
||||
import suwayomi.tachidesk.manga.impl.Chapter
|
||||
import suwayomi.tachidesk.manga.impl.Manga
|
||||
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
|
||||
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude
|
||||
@@ -311,10 +310,10 @@ class Updater : IUpdater {
|
||||
tracker[job.manga.id] =
|
||||
try {
|
||||
logger.info { "Updating ${job.manga}" }
|
||||
if (serverConfig.updateMangas.value || !job.manga.initialized) {
|
||||
Manga.getManga(job.manga.id, true)
|
||||
}
|
||||
Chapter.getChapterList(job.manga.id, true)
|
||||
Manga.updateMangaAndChapters(
|
||||
job.manga.id,
|
||||
updateManga = serverConfig.updateMangas.value || !job.manga.initialized,
|
||||
)
|
||||
job.copy(status = JobStatus.COMPLETE)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Error while updating ${job.manga}" }
|
||||
|
||||
@@ -46,7 +46,7 @@ object ImageResponse {
|
||||
/**
|
||||
* Get a cached image response
|
||||
*
|
||||
* Note: The caller should also call [clearCachedImage] when appropriate
|
||||
* Note: The caller should also call [ImageResponse.clearCachedImage] when appropriate
|
||||
*
|
||||
* @param cacheSavePath where to save the cached image. Caller should decide to use perma cache or temp cache (OS temp dir)
|
||||
* @param fileName what the saved cache file should be named
|
||||
|
||||
@@ -26,6 +26,8 @@ data class ExtensionSource(
|
||||
val name: String,
|
||||
val lang: String,
|
||||
val homeUrl: String,
|
||||
val message: String?,
|
||||
val contentRating: ContentRating,
|
||||
)
|
||||
|
||||
enum class ContentRating {
|
||||
|
||||
@@ -3,6 +3,7 @@ package suwayomi.tachidesk.opds.impl
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.jetbrains.exposed.v1.core.SortOrder
|
||||
import suwayomi.tachidesk.i18n.MR
|
||||
import suwayomi.tachidesk.manga.impl.Manga
|
||||
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.opds.constants.OpdsConstants
|
||||
@@ -17,7 +18,6 @@ import suwayomi.tachidesk.opds.repository.ChapterRepository
|
||||
import suwayomi.tachidesk.opds.repository.MangaRepository
|
||||
import suwayomi.tachidesk.opds.repository.NavigationRepository
|
||||
import suwayomi.tachidesk.opds.util.OpdsDateUtil
|
||||
import suwayomi.tachidesk.opds.util.OpdsStringUtil
|
||||
import suwayomi.tachidesk.opds.util.OpdsXmlUtil
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import java.util.Locale
|
||||
@@ -647,8 +647,7 @@ object OpdsFeedBuilder {
|
||||
// If no chapters are found in the database, attempt to fetch them from the source.
|
||||
if (chapterEntries.isEmpty() && totalChapters == 0L) {
|
||||
try {
|
||||
suwayomi.tachidesk.manga.impl.Chapter
|
||||
.fetchChapterList(mangaId)
|
||||
Manga.updateMangaAndChapters(mangaId, updateManga = false)
|
||||
|
||||
// Re-query after fetching.
|
||||
val (refetchedChapters, refetchedTotal) =
|
||||
|
||||
@@ -8,6 +8,7 @@ package suwayomi.tachidesk.server.database.migration
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||
import suwayomi.tachidesk.graphql.types.ContentRating
|
||||
import suwayomi.tachidesk.graphql.types.DatabaseType
|
||||
import suwayomi.tachidesk.server.database.migration.helpers.MAYBE_TYPE_PREFIX
|
||||
import suwayomi.tachidesk.server.database.migration.helpers.UNLIMITED_TEXT
|
||||
@@ -43,11 +44,11 @@ class M0057_AddNewExtensionApiFields : SQLMigration() {
|
||||
}
|
||||
ALTER TABLE EXTENSION ADD COLUMN apk_url VARCHAR(2048);
|
||||
ALTER TABLE EXTENSION ADD COLUMN content_rating INTEGER DEFAULT 0;
|
||||
UPDATE EXTENSION SET content_rating = 3 WHERE is_nsfw = TRUE;
|
||||
UPDATE EXTENSION SET content_rating = ${ContentRating.PORNOGRAPHIC.ordinal} WHERE is_nsfw = TRUE;
|
||||
ALTER TABLE EXTENSION DROP COLUMN is_nsfw;
|
||||
ALTER TABLE SOURCE ADD COLUMN message $UNLIMITED_TEXT;
|
||||
ALTER TABLE SOURCE ADD COLUMN content_rating INTEGER DEFAULT 0;
|
||||
UPDATE SOURCE SET content_rating = 3 WHERE is_nsfw = TRUE;
|
||||
UPDATE SOURCE SET content_rating = ${ContentRating.PORNOGRAPHIC.ordinal} WHERE is_nsfw = TRUE;
|
||||
ALTER TABLE SOURCE DROP COLUMN is_nsfw;
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import rx.Observable
|
||||
import suwayomi.tachidesk.manga.impl.Search.FilterChange
|
||||
import suwayomi.tachidesk.manga.impl.Search.FilterObject
|
||||
import suwayomi.tachidesk.manga.impl.Search.SerializableGroup
|
||||
@@ -41,12 +40,11 @@ class SearchTest : ApplicationTest() {
|
||||
) : StubSource(id) {
|
||||
var mangas: List<SManga> = emptyList()
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchManga"))
|
||||
override fun fetchSearchManga(
|
||||
override suspend fun getSearchManga(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList,
|
||||
): Observable<MangasPage> = Observable.just(MangasPage(mangas, false))
|
||||
): MangasPage = MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
private val sourceId = 1L
|
||||
|
||||
@@ -66,6 +66,7 @@ fun createChapters(
|
||||
this[ChapterTable.sourceOrder] = it
|
||||
this[ChapterTable.isRead] = read
|
||||
this[ChapterTable.manga] = mangaId
|
||||
this[ChapterTable.memo] = "{}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user