mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-04 03:14:40 -05:00
* add trackers support * Cleanup Tracker Code * Add GraphQL support for Tracking * Fix lint and deprecation errors * remove password from logs * Fixes after merge * Disable tracking for now * More disabled --------- Co-authored-by: Syer10 <syer10@users.noreply.github.com>
472 lines
20 KiB
Kotlin
472 lines
20 KiB
Kotlin
package suwayomi.tachidesk.graphql.queries
|
|
|
|
import com.expediagroup.graphql.server.extensions.getValueFromDataLoader
|
|
import graphql.schema.DataFetchingEnvironment
|
|
import org.jetbrains.exposed.sql.Column
|
|
import org.jetbrains.exposed.sql.Op
|
|
import org.jetbrains.exposed.sql.SortOrder
|
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
|
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.less
|
|
import org.jetbrains.exposed.sql.selectAll
|
|
import org.jetbrains.exposed.sql.transactions.transaction
|
|
import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter
|
|
import suwayomi.tachidesk.graphql.queries.filter.DoubleFilter
|
|
import suwayomi.tachidesk.graphql.queries.filter.Filter
|
|
import suwayomi.tachidesk.graphql.queries.filter.HasGetOp
|
|
import suwayomi.tachidesk.graphql.queries.filter.IntFilter
|
|
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.andFilterWithCompareEntity
|
|
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.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.TrackRecordNodeList
|
|
import suwayomi.tachidesk.graphql.types.TrackRecordType
|
|
import suwayomi.tachidesk.graphql.types.TrackSearchType
|
|
import suwayomi.tachidesk.graphql.types.TrackerNodeList
|
|
import suwayomi.tachidesk.graphql.types.TrackerType
|
|
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
|
import java.util.concurrent.CompletableFuture
|
|
|
|
class TrackQuery {
|
|
fun tracker(
|
|
dataFetchingEnvironment: DataFetchingEnvironment,
|
|
id: Int,
|
|
): CompletableFuture<TrackerType> {
|
|
return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackerType>("TrackerDataLoader", id)
|
|
}
|
|
|
|
enum class TrackerOrderBy {
|
|
ID,
|
|
NAME,
|
|
IS_LOGGED_IN,
|
|
;
|
|
|
|
fun greater(
|
|
tracker: TrackerType,
|
|
cursor: Cursor,
|
|
): Boolean {
|
|
return when (this) {
|
|
ID -> tracker.id > cursor.value.toInt()
|
|
NAME -> tracker.name > cursor.value
|
|
IS_LOGGED_IN -> {
|
|
val value = cursor.value.substringAfter('-').toBooleanStrict()
|
|
!value || tracker.isLoggedIn
|
|
}
|
|
}
|
|
}
|
|
|
|
fun less(
|
|
tracker: TrackerType,
|
|
cursor: Cursor,
|
|
): Boolean {
|
|
return when (this) {
|
|
ID -> tracker.id < cursor.value.toInt()
|
|
NAME -> tracker.name < cursor.value
|
|
IS_LOGGED_IN -> {
|
|
val value = cursor.value.substringAfter('-').toBooleanStrict()
|
|
value || !tracker.isLoggedIn
|
|
}
|
|
}
|
|
}
|
|
|
|
fun asCursor(type: TrackerType): Cursor {
|
|
val value =
|
|
when (this) {
|
|
ID -> type.id.toString()
|
|
NAME -> type.name
|
|
IS_LOGGED_IN -> type.id.toString() + "-" + type.isLoggedIn
|
|
}
|
|
return Cursor(value)
|
|
}
|
|
}
|
|
|
|
data class TrackerCondition(
|
|
val id: Int? = null,
|
|
val name: String? = null,
|
|
val icon: String? = null,
|
|
val isLoggedIn: Boolean? = null,
|
|
)
|
|
|
|
data class TrackerFilter(
|
|
val id: IntFilter? = null,
|
|
val name: StringFilter? = null,
|
|
val icon: StringFilter? = null,
|
|
val isLoggedIn: BooleanFilter? = null,
|
|
val authUrl: StringFilter? = null,
|
|
val and: List<TrackerFilter>? = null,
|
|
val or: List<TrackerFilter>? = null,
|
|
val not: TrackerFilter? = null,
|
|
)
|
|
|
|
fun trackers(
|
|
condition: TrackerCondition? = null,
|
|
orderBy: TrackerOrderBy? = null,
|
|
orderByType: SortOrder? = null,
|
|
before: Cursor? = null,
|
|
after: Cursor? = null,
|
|
first: Int? = null,
|
|
last: Int? = null,
|
|
offset: Int? = null,
|
|
): TrackerNodeList {
|
|
val (queryResults, resultsAsType) =
|
|
run {
|
|
var res = TrackerManager.services.map { TrackerType(it) }
|
|
|
|
if (condition != null) {
|
|
res =
|
|
res.filter { tracker ->
|
|
(condition.id == null || (condition.id == tracker.id)) &&
|
|
(condition.name == null || (condition.name == tracker.name)) &&
|
|
(condition.icon == null || (condition.icon == tracker.icon)) &&
|
|
(condition.isLoggedIn == null || (condition.isLoggedIn == tracker.isLoggedIn))
|
|
}
|
|
}
|
|
|
|
if (orderBy != null || (last != null || before != null)) {
|
|
val orderType = orderByType.maybeSwap(last ?: before)
|
|
|
|
res =
|
|
when (orderType) {
|
|
SortOrder.DESC, SortOrder.DESC_NULLS_FIRST, SortOrder.DESC_NULLS_LAST ->
|
|
when (orderBy) {
|
|
TrackerOrderBy.ID, null -> res.sortedByDescending { it.id }
|
|
TrackerOrderBy.NAME -> res.sortedByDescending { it.name }
|
|
TrackerOrderBy.IS_LOGGED_IN -> res.sortedByDescending { it.isLoggedIn }
|
|
}
|
|
SortOrder.ASC, SortOrder.ASC_NULLS_FIRST, SortOrder.ASC_NULLS_LAST ->
|
|
when (orderBy) {
|
|
TrackerOrderBy.ID, null -> res.sortedBy { it.id }
|
|
TrackerOrderBy.NAME -> res.sortedBy { it.name }
|
|
TrackerOrderBy.IS_LOGGED_IN -> res.sortedBy { it.isLoggedIn }
|
|
}
|
|
}
|
|
}
|
|
|
|
val total = res.size
|
|
val firstResult = res.firstOrNull()
|
|
val lastResult = res.lastOrNull()
|
|
|
|
val realOrderBy = orderBy ?: TrackerOrderBy.ID
|
|
if (after != null) {
|
|
res =
|
|
res.filter {
|
|
when (orderByType) {
|
|
SortOrder.DESC, SortOrder.DESC_NULLS_FIRST, SortOrder.DESC_NULLS_LAST -> realOrderBy.less(it, after)
|
|
null, SortOrder.ASC, SortOrder.ASC_NULLS_FIRST, SortOrder.ASC_NULLS_LAST -> realOrderBy.greater(it, after)
|
|
}
|
|
}
|
|
} else if (before != null) {
|
|
res =
|
|
res.filter {
|
|
when (orderByType) {
|
|
SortOrder.DESC, SortOrder.DESC_NULLS_FIRST, SortOrder.DESC_NULLS_LAST -> realOrderBy.greater(it, before)
|
|
null, SortOrder.ASC, SortOrder.ASC_NULLS_FIRST, SortOrder.ASC_NULLS_LAST -> realOrderBy.less(it, before)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (first != null) {
|
|
res = res.drop(offset ?: 0).take(first)
|
|
} else if (last != null) {
|
|
res = res.take(last)
|
|
}
|
|
|
|
QueryResults(total.toLong(), firstResult, lastResult, emptyList()) to res
|
|
}
|
|
|
|
val getAsCursor: (TrackerType) -> Cursor = (orderBy ?: TrackerOrderBy.ID)::asCursor
|
|
|
|
return TrackerNodeList(
|
|
resultsAsType,
|
|
if (resultsAsType.isEmpty()) {
|
|
emptyList()
|
|
} else {
|
|
listOfNotNull(
|
|
resultsAsType.firstOrNull()?.let {
|
|
TrackerNodeList.TrackerEdge(
|
|
getAsCursor(it),
|
|
it,
|
|
)
|
|
},
|
|
resultsAsType.lastOrNull()?.let {
|
|
TrackerNodeList.TrackerEdge(
|
|
getAsCursor(it),
|
|
it,
|
|
)
|
|
},
|
|
)
|
|
},
|
|
pageInfo =
|
|
PageInfo(
|
|
hasNextPage = queryResults.lastKey?.id != resultsAsType.lastOrNull()?.id,
|
|
hasPreviousPage = queryResults.firstKey?.id != resultsAsType.firstOrNull()?.id,
|
|
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
|
|
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) },
|
|
),
|
|
totalCount = queryResults.total.toInt(),
|
|
)
|
|
}
|
|
|
|
fun trackRecord(
|
|
dataFetchingEnvironment: DataFetchingEnvironment,
|
|
id: Int,
|
|
): CompletableFuture<TrackRecordType> {
|
|
return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackRecordType>("TrackRecordDataLoader", id)
|
|
}
|
|
|
|
enum class TrackRecordOrderBy(override val column: Column<out Comparable<*>>) : OrderBy<TrackRecordType> {
|
|
ID(TrackRecordTable.id),
|
|
MANGA_ID(TrackRecordTable.mangaId),
|
|
SYNC_ID(TrackRecordTable.syncId),
|
|
REMOTE_ID(TrackRecordTable.remoteId),
|
|
TITLE(TrackRecordTable.title),
|
|
LAST_CHAPTER_READ(TrackRecordTable.lastChapterRead),
|
|
TOTAL_CHAPTERS(TrackRecordTable.lastChapterRead),
|
|
SCORE(TrackRecordTable.score),
|
|
START_DATE(TrackRecordTable.startDate),
|
|
FINISH_DATE(TrackRecordTable.finishDate),
|
|
;
|
|
|
|
override fun greater(cursor: Cursor): Op<Boolean> {
|
|
return when (this) {
|
|
ID -> TrackRecordTable.id greater cursor.value.toInt()
|
|
MANGA_ID -> greaterNotUnique(TrackRecordTable.mangaId, TrackRecordTable.id, cursor)
|
|
SYNC_ID -> greaterNotUnique(TrackRecordTable.syncId, TrackRecordTable.id, cursor, String::toInt)
|
|
REMOTE_ID -> greaterNotUnique(TrackRecordTable.remoteId, TrackRecordTable.id, cursor, String::toLong)
|
|
TITLE -> greaterNotUnique(TrackRecordTable.title, TrackRecordTable.id, cursor, String::toString)
|
|
LAST_CHAPTER_READ -> greaterNotUnique(TrackRecordTable.lastChapterRead, TrackRecordTable.id, cursor, String::toDouble)
|
|
TOTAL_CHAPTERS -> greaterNotUnique(TrackRecordTable.totalChapters, TrackRecordTable.id, cursor, String::toInt)
|
|
SCORE -> greaterNotUnique(TrackRecordTable.score, TrackRecordTable.id, cursor, String::toDouble)
|
|
START_DATE -> greaterNotUnique(TrackRecordTable.startDate, TrackRecordTable.id, cursor, String::toLong)
|
|
FINISH_DATE -> greaterNotUnique(TrackRecordTable.finishDate, TrackRecordTable.id, cursor, String::toLong)
|
|
}
|
|
}
|
|
|
|
override fun less(cursor: Cursor): Op<Boolean> {
|
|
return when (this) {
|
|
ID -> TrackRecordTable.id less cursor.value.toInt()
|
|
MANGA_ID -> lessNotUnique(TrackRecordTable.mangaId, TrackRecordTable.id, cursor)
|
|
SYNC_ID -> lessNotUnique(TrackRecordTable.syncId, TrackRecordTable.id, cursor, String::toInt)
|
|
REMOTE_ID -> lessNotUnique(TrackRecordTable.remoteId, TrackRecordTable.id, cursor, String::toLong)
|
|
TITLE -> lessNotUnique(TrackRecordTable.title, TrackRecordTable.id, cursor, String::toString)
|
|
LAST_CHAPTER_READ -> lessNotUnique(TrackRecordTable.lastChapterRead, TrackRecordTable.id, cursor, String::toDouble)
|
|
TOTAL_CHAPTERS -> lessNotUnique(TrackRecordTable.totalChapters, TrackRecordTable.id, cursor, String::toInt)
|
|
SCORE -> lessNotUnique(TrackRecordTable.score, TrackRecordTable.id, cursor, String::toDouble)
|
|
START_DATE -> lessNotUnique(TrackRecordTable.startDate, TrackRecordTable.id, cursor, String::toLong)
|
|
FINISH_DATE -> lessNotUnique(TrackRecordTable.finishDate, TrackRecordTable.id, cursor, String::toLong)
|
|
}
|
|
}
|
|
|
|
override fun asCursor(type: TrackRecordType): Cursor {
|
|
val value =
|
|
when (this) {
|
|
ID -> type.id.toString()
|
|
MANGA_ID -> type.id.toString() + "-" + type.mangaId
|
|
SYNC_ID -> type.id.toString() + "-" + type.syncId
|
|
REMOTE_ID -> type.id.toString() + "-" + type.remoteId
|
|
TITLE -> type.id.toString() + "-" + type.title
|
|
LAST_CHAPTER_READ -> type.id.toString() + "-" + type.lastChapterRead
|
|
TOTAL_CHAPTERS -> type.id.toString() + "-" + type.totalChapters
|
|
SCORE -> type.id.toString() + "-" + type.score
|
|
START_DATE -> type.id.toString() + "-" + type.startDate
|
|
FINISH_DATE -> type.id.toString() + "-" + type.finishDate
|
|
}
|
|
return Cursor(value)
|
|
}
|
|
}
|
|
|
|
data class TrackRecordCondition(
|
|
val id: Int? = null,
|
|
val mangaId: Int? = null,
|
|
val syncId: Int? = null,
|
|
val remoteId: Long? = null,
|
|
val libraryId: Long? = null,
|
|
val title: String? = null,
|
|
val lastChapterRead: Double? = null,
|
|
val totalChapters: Int? = null,
|
|
val status: Int? = null,
|
|
val score: Double? = null,
|
|
val remoteUrl: String? = null,
|
|
val startDate: Long? = null,
|
|
val finishDate: Long? = null,
|
|
) : HasGetOp {
|
|
override fun getOp(): Op<Boolean>? {
|
|
val opAnd = OpAnd()
|
|
opAnd.eq(id, TrackRecordTable.id)
|
|
opAnd.eq(mangaId, TrackRecordTable.mangaId)
|
|
opAnd.eq(syncId, TrackRecordTable.syncId)
|
|
opAnd.eq(remoteId, TrackRecordTable.remoteId)
|
|
opAnd.eq(libraryId, TrackRecordTable.libraryId)
|
|
opAnd.eq(title, TrackRecordTable.title)
|
|
opAnd.eq(lastChapterRead, TrackRecordTable.lastChapterRead)
|
|
opAnd.eq(totalChapters, TrackRecordTable.totalChapters)
|
|
opAnd.eq(status, TrackRecordTable.status)
|
|
opAnd.eq(score, TrackRecordTable.score)
|
|
opAnd.eq(remoteUrl, TrackRecordTable.remoteUrl)
|
|
opAnd.eq(startDate, TrackRecordTable.startDate)
|
|
opAnd.eq(finishDate, TrackRecordTable.finishDate)
|
|
|
|
return opAnd.op
|
|
}
|
|
}
|
|
|
|
data class TrackRecordFilter(
|
|
val id: IntFilter? = null,
|
|
val mangaId: IntFilter? = null,
|
|
val syncId: IntFilter? = null,
|
|
val remoteId: LongFilter? = null,
|
|
val libraryId: LongFilter? = null,
|
|
val title: StringFilter? = null,
|
|
val lastChapterRead: DoubleFilter? = null,
|
|
val totalChapters: IntFilter? = null,
|
|
val status: IntFilter? = null,
|
|
val score: DoubleFilter? = null,
|
|
val remoteUrl: StringFilter? = null,
|
|
val startDate: LongFilter? = null,
|
|
val finishDate: LongFilter? = null,
|
|
override val and: List<TrackRecordFilter>? = null,
|
|
override val or: List<TrackRecordFilter>? = null,
|
|
override val not: TrackRecordFilter? = null,
|
|
) : Filter<TrackRecordFilter> {
|
|
override fun getOpList(): List<Op<Boolean>> {
|
|
return listOfNotNull(
|
|
andFilterWithCompareEntity(TrackRecordTable.id, id),
|
|
andFilterWithCompareEntity(TrackRecordTable.mangaId, mangaId),
|
|
andFilterWithCompare(TrackRecordTable.syncId, syncId),
|
|
andFilterWithCompare(TrackRecordTable.remoteId, remoteId),
|
|
andFilterWithCompare(TrackRecordTable.libraryId, libraryId),
|
|
andFilterWithCompareString(TrackRecordTable.title, title),
|
|
andFilterWithCompare(TrackRecordTable.lastChapterRead, lastChapterRead),
|
|
andFilterWithCompare(TrackRecordTable.totalChapters, totalChapters),
|
|
andFilterWithCompare(TrackRecordTable.status, status),
|
|
andFilterWithCompare(TrackRecordTable.score, score),
|
|
andFilterWithCompareString(TrackRecordTable.remoteUrl, remoteUrl),
|
|
andFilterWithCompare(TrackRecordTable.startDate, startDate),
|
|
andFilterWithCompare(TrackRecordTable.finishDate, finishDate),
|
|
)
|
|
}
|
|
}
|
|
|
|
fun trackRecords(
|
|
condition: TrackRecordCondition? = null,
|
|
filter: TrackRecordFilter? = null,
|
|
orderBy: TrackRecordOrderBy? = null,
|
|
orderByType: SortOrder? = null,
|
|
before: Cursor? = null,
|
|
after: Cursor? = null,
|
|
first: Int? = null,
|
|
last: Int? = null,
|
|
offset: Int? = null,
|
|
): TrackRecordNodeList {
|
|
val queryResults =
|
|
transaction {
|
|
val res = TrackRecordTable.selectAll()
|
|
|
|
res.applyOps(condition, filter)
|
|
|
|
if (orderBy != null || (last != null || before != null)) {
|
|
val orderByColumn = orderBy?.column ?: TrackRecordTable.id
|
|
val orderType = orderByType.maybeSwap(last ?: before)
|
|
|
|
if (orderBy == TrackRecordOrderBy.ID || orderBy == null) {
|
|
res.orderBy(orderByColumn to orderType)
|
|
} else {
|
|
res.orderBy(
|
|
orderByColumn to orderType,
|
|
TrackRecordTable.id to SortOrder.ASC,
|
|
)
|
|
}
|
|
}
|
|
|
|
val total = res.count()
|
|
val firstResult = res.firstOrNull()?.get(TrackRecordTable.id)?.value
|
|
val lastResult = res.lastOrNull()?.get(TrackRecordTable.id)?.value
|
|
|
|
res.applyBeforeAfter(
|
|
before = before,
|
|
after = after,
|
|
orderBy = orderBy ?: TrackRecordOrderBy.ID,
|
|
orderByType = orderByType,
|
|
)
|
|
|
|
if (first != null) {
|
|
res.limit(first, offset?.toLong() ?: 0)
|
|
} else if (last != null) {
|
|
res.limit(last)
|
|
}
|
|
|
|
QueryResults(total, firstResult, lastResult, res.toList())
|
|
}
|
|
|
|
val getAsCursor: (TrackRecordType) -> Cursor = (orderBy ?: TrackRecordOrderBy.ID)::asCursor
|
|
|
|
val resultsAsType = queryResults.results.map { TrackRecordType(it) }
|
|
|
|
return TrackRecordNodeList(
|
|
resultsAsType,
|
|
if (resultsAsType.isEmpty()) {
|
|
emptyList()
|
|
} else {
|
|
listOfNotNull(
|
|
resultsAsType.firstOrNull()?.let {
|
|
TrackRecordNodeList.TrackRecordEdge(
|
|
getAsCursor(it),
|
|
it,
|
|
)
|
|
},
|
|
resultsAsType.lastOrNull()?.let {
|
|
TrackRecordNodeList.TrackRecordEdge(
|
|
getAsCursor(it),
|
|
it,
|
|
)
|
|
},
|
|
)
|
|
},
|
|
pageInfo =
|
|
PageInfo(
|
|
hasNextPage = queryResults.lastKey != resultsAsType.lastOrNull()?.id,
|
|
hasPreviousPage = queryResults.firstKey != resultsAsType.firstOrNull()?.id,
|
|
startCursor = resultsAsType.firstOrNull()?.let { getAsCursor(it) },
|
|
endCursor = resultsAsType.lastOrNull()?.let { getAsCursor(it) },
|
|
),
|
|
totalCount = queryResults.total.toInt(),
|
|
)
|
|
}
|
|
|
|
data class SearchTrackerInput(
|
|
val trackerId: Int,
|
|
val query: String,
|
|
)
|
|
|
|
data class SearchTrackerPayload(val trackSearches: List<TrackSearchType>)
|
|
|
|
fun searchTracker(input: SearchTrackerInput): CompletableFuture<SearchTrackerPayload> {
|
|
return future {
|
|
val tracker =
|
|
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
|
|
"Tracker not found"
|
|
}
|
|
require(tracker.isLoggedIn) {
|
|
"Tracker needs to be logged-in to search"
|
|
}
|
|
SearchTrackerPayload(
|
|
tracker.search(input.query).map {
|
|
TrackSearchType(it)
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|