Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add trackers support #720

Merged
merged 9 commits into from
Jan 7, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions AndroidCompat/src/main/java/androidx/core/net/Uri.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

@file:Suppress("NOTHING_TO_INLINE") // Aliases to public API.

package androidx.core.net

import android.net.Uri
import java.io.File

/**
* Creates a Uri from the given encoded URI string.
*
* @see Uri.parse
*/
public inline fun String.toUri(): Uri = Uri.parse(this)

/**
* Creates a Uri from the given file.
*
* @see Uri.fromFile
*/
public inline fun File.toUri(): Uri = Uri.fromFile(this)

/**
* Creates a [File] from the given [Uri]. Note that this will throw an
* [IllegalArgumentException] when invoked on a [Uri] that lacks `file` scheme.
*/
public fun Uri.toFile(): File {
require(scheme == "file") { "Uri lacks 'file' scheme: $this" }
return File(requireNotNull(path) { "Uri path is null: $this" })
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import kotlinx.serialization.json.okio.decodeFromBufferedSource
import kotlinx.serialization.serializer
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
Expand All @@ -19,6 +20,8 @@ import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resumeWithException

val jsonMime = "application/json; charset=utf-8".toMediaType()

fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber ->
// Since Call is a one-shot type, clone it for each new subscriber.
Expand Down
14 changes: 14 additions & 0 deletions server/src/main/kotlin/eu/kanade/tachiyomi/util/PkceUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.util

import android.util.Base64
import java.security.SecureRandom

object PkceUtil {
private const val PKCE_BASE64_ENCODE_SETTINGS = Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE

fun generateCodeVerifier(): String {
val codeVerifier = ByteArray(50)
SecureRandom().nextBytes(codeVerifier)
return Base64.encodeToString(codeVerifier, PKCE_BASE64_ENCODE_SETTINGS)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */

package suwayomi.tachidesk.graphql.dataLoaders

import com.expediagroup.graphql.dataloader.KotlinDataLoader
import org.dataloader.DataLoader
import org.dataloader.DataLoaderFactory
import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger
import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.types.TrackRecordNodeList
import suwayomi.tachidesk.graphql.types.TrackRecordNodeList.Companion.toNodeList
import suwayomi.tachidesk.graphql.types.TrackRecordType
import suwayomi.tachidesk.graphql.types.TrackerType
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrack
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
import suwayomi.tachidesk.server.JavalinSetup.future

class TrackerDataLoader : KotlinDataLoader<Int, TrackerType> {
override val dataLoaderName = "TrackerDataLoader"

override fun getDataLoader(): DataLoader<Int, TrackerType> =
DataLoaderFactory.newDataLoader { ids ->
future {
ids.map { id ->
TrackerManager.getTracker(id)?.let { TrackerType(it) }
}
}
}
}

class TrackRecordsForMangaIdDataLoader : KotlinDataLoader<Int, TrackRecordNodeList> {
override val dataLoaderName = "TrackRecordsForMangaIdDataLoader"

override fun getDataLoader(): DataLoader<Int, TrackRecordNodeList> =
DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val trackRecordsByMangaId =
TrackRecordTable.select { TrackRecordTable.mangaId inList ids }
.map { TrackRecordType(it) }
.groupBy { it.mangaId }
ids.map { (trackRecordsByMangaId[it] ?: emptyList()).toNodeList() }
}
}
}
}

class DisplayScoreForTrackRecordDataLoader : KotlinDataLoader<Int, String> {
override val dataLoaderName = "DisplayScoreForTrackRecordDataLoader"

override fun getDataLoader(): DataLoader<Int, String> =
DataLoaderFactory.newDataLoader<Int, String> { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val trackRecords =
TrackRecordTable.select { TrackRecordTable.id inList ids }
.toList()
.map { it.toTrack() }
.associateBy { it.id!! }
.mapValues { TrackerManager.getTracker(it.value.sync_id)?.displayScore(it.value) }

ids.map { trackRecords[it] }
}
}
}
}

class TrackRecordsForTrackerIdDataLoader : KotlinDataLoader<Int, TrackRecordNodeList> {
override val dataLoaderName = "TrackRecordsForTrackerIdDataLoader"

override fun getDataLoader(): DataLoader<Int, TrackRecordNodeList> =
DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val trackRecordsBySyncId =
TrackRecordTable.select { TrackRecordTable.syncId inList ids }
.map { TrackRecordType(it) }
.groupBy { it.mangaId }
ids.map { (trackRecordsBySyncId[it] ?: emptyList()).toNodeList() }
}
}
}
}

class TrackRecordDataLoader : KotlinDataLoader<Int, TrackRecordType> {
override val dataLoaderName = "TrackRecordDataLoader"

override fun getDataLoader(): DataLoader<Int, TrackRecordType> =
DataLoaderFactory.newDataLoader { ids ->
future {
transaction {
addLogger(Slf4jSqlDebugLogger)
val trackRecordsId =
TrackRecordTable.select { TrackRecordTable.id inList ids }
.map { TrackRecordType(it) }
.associateBy { it.id }
ids.map { trackRecordsId[it] }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package suwayomi.tachidesk.graphql.mutations

import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.types.TrackRecordType
import suwayomi.tachidesk.graphql.types.TrackSearchType
import suwayomi.tachidesk.graphql.types.TrackerType
import suwayomi.tachidesk.manga.impl.track.Track
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
import suwayomi.tachidesk.manga.model.dataclass.TrackSearchDataClass
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
import suwayomi.tachidesk.server.JavalinSetup.future
import java.util.concurrent.CompletableFuture

class TrackMutation {
data class LoginTrackerOAuthInput(
val clientMutationId: String? = null,
val trackerId: Int,
val callbackUrl: String,
)

data class LoginTrackerOAuthPayload(
val clientMutationId: String?,
val isLoggedIn: Boolean,
val tracker: TrackerType,
)

fun loginTrackerOAuth(input: LoginTrackerOAuthInput): CompletableFuture<LoginTrackerOAuthPayload> {
val tracker =
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
"Could not find tracker"
}
return future {
tracker.authCallback(input.callbackUrl)
val trackerType = TrackerType(tracker)
LoginTrackerOAuthPayload(
input.clientMutationId,
trackerType.isLoggedIn,
trackerType,
)
}
}

data class LoginTrackerCredentialsInput(
val clientMutationId: String? = null,
val trackerId: Int,
val username: String,
val password: String,
)

data class LoginTrackerCredentialsPayload(
val clientMutationId: String?,
val isLoggedIn: Boolean,
val tracker: TrackerType,
)

fun loginTrackerCredentials(input: LoginTrackerCredentialsInput): CompletableFuture<LoginTrackerCredentialsPayload> {
val tracker =
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
"Could not find tracker"
}
return future {
tracker.login(input.username, input.password)
val trackerType = TrackerType(tracker)
LoginTrackerCredentialsPayload(
input.clientMutationId,
trackerType.isLoggedIn,
trackerType,
)
}
}

data class LogoutTrackerInput(
val clientMutationId: String? = null,
val trackerId: Int,
)

data class LogoutTrackerPayload(
val clientMutationId: String?,
val isLoggedIn: Boolean,
val tracker: TrackerType,
)

fun logoutTracker(input: LogoutTrackerInput): CompletableFuture<LogoutTrackerPayload> {
val tracker =
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
"Could not find tracker"
}
require(tracker.isLoggedIn) {
"Cannot logout of a tracker that is not logged-in"
}
return future {
tracker.logout()
val trackerType = TrackerType(tracker)
LogoutTrackerPayload(
input.clientMutationId,
trackerType.isLoggedIn,
trackerType,
)
}
}

data class BindTrackInput(
val clientMutationId: String? = null,
val mangaId: Int,
val track: TrackSearchType,
)

data class BindTrackPayload(
val clientMutationId: String?,
val trackRecord: TrackRecordType,
)

fun bindTrack(input: BindTrackInput): CompletableFuture<BindTrackPayload> {
val (clientMutationId, mangaId, track) = input

return future {
Track.bind(
mangaId,
TrackSearchDataClass(
syncId = track.syncId,
mediaId = track.mediaId,
title = track.title,
totalChapters = track.totalChapters,
trackingUrl = track.trackingUrl,
coverUrl = track.coverUrl,
summary = track.summary,
publishingStatus = track.publishingStatus,
publishingType = track.publishingType,
startDate = track.startDate,
),
)
val trackRecord =
transaction {
TrackRecordTable.select {
TrackRecordTable.mangaId eq mangaId and (TrackRecordTable.syncId eq track.syncId)
}.first()
}
BindTrackPayload(
clientMutationId,
TrackRecordType(trackRecord),
)
}
}

data class UpdateTrackInput(
val clientMutationId: String? = null,
val recordId: Int,
val status: Int? = null,
val lastChapterRead: Double? = null,
val scoreString: String? = null,
val startDate: Long? = null,
val finishDate: Long? = null,
val unbind: Boolean? = null,
)

data class UpdateTrackPayload(
val clientMutationId: String?,
val trackRecord: TrackRecordType?,
)

fun updateTrack(input: UpdateTrackInput): CompletableFuture<UpdateTrackPayload> {
return future {
Track.update(
Track.UpdateInput(
input.recordId,
input.status,
input.lastChapterRead,
input.scoreString,
input.startDate,
input.finishDate,
input.unbind,
),
)

val trackRecord =
transaction {
TrackRecordTable.select {
TrackRecordTable.id eq input.recordId
}.firstOrNull()
}
UpdateTrackPayload(
input.clientMutationId,
trackRecord?.let { TrackRecordType(it) },
)
}
}
}
Loading