mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-04 11:24:35 -05:00
Shikimori tracker (#1839)
* Shikimori tracker * Add authUrl and callback * Add OAuth id and secret
This commit is contained in:
@@ -5,6 +5,7 @@ import suwayomi.tachidesk.manga.impl.track.tracker.bangumi.Bangumi
|
|||||||
import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.Kitsu
|
import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.Kitsu
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.MangaUpdates
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.MangaUpdates
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.MyAnimeList
|
import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.MyAnimeList
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.Shikimori
|
||||||
|
|
||||||
object TrackerManager {
|
object TrackerManager {
|
||||||
const val MYANIMELIST = 1
|
const val MYANIMELIST = 1
|
||||||
@@ -22,7 +23,7 @@ object TrackerManager {
|
|||||||
|
|
||||||
val kitsu = Kitsu(KITSU)
|
val kitsu = Kitsu(KITSU)
|
||||||
|
|
||||||
// val shikimori = Shikimori(SHIKIMORI)
|
val shikimori = Shikimori(SHIKIMORI)
|
||||||
val bangumi = Bangumi(BANGUMI)
|
val bangumi = Bangumi(BANGUMI)
|
||||||
|
|
||||||
// val komga = Komga(KOMGA)
|
// val komga = Komga(KOMGA)
|
||||||
@@ -30,7 +31,7 @@ object TrackerManager {
|
|||||||
// val kavita = Kavita(context, KAVITA)
|
// val kavita = Kavita(context, KAVITA)
|
||||||
// val suwayomi = Suwayomi(SUWAYOMI)
|
// val suwayomi = Suwayomi(SUWAYOMI)
|
||||||
|
|
||||||
val services: List<Tracker> = listOf(myAnimeList, aniList, kitsu, mangaUpdates, bangumi)
|
val services: List<Tracker> = listOf(myAnimeList, aniList, kitsu, mangaUpdates, shikimori, bangumi)
|
||||||
|
|
||||||
fun getTracker(id: Int) = services.find { it.id == id }
|
fun getTracker(id: Int) = services.find { it.id == id }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTracker
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.extractToken
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMOAuth
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class Shikimori(
|
||||||
|
id: Int,
|
||||||
|
) : Tracker(id, "Shikimori"),
|
||||||
|
DeletableTracker {
|
||||||
|
companion object {
|
||||||
|
const val READING = 1
|
||||||
|
const val COMPLETED = 2
|
||||||
|
const val ON_HOLD = 3
|
||||||
|
const val DROPPED = 4
|
||||||
|
const val PLAN_TO_READ = 5
|
||||||
|
const val REREADING = 6
|
||||||
|
|
||||||
|
private val SCORE_LIST =
|
||||||
|
IntRange(0, 10)
|
||||||
|
.map(Int::toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val interceptor by lazy { ShikimoriInterceptor(this) }
|
||||||
|
|
||||||
|
private val api by lazy { ShikimoriApi(id, client, interceptor) }
|
||||||
|
|
||||||
|
override fun getScoreList(): List<String> = SCORE_LIST
|
||||||
|
|
||||||
|
override fun displayScore(track: Track): String = track.score.toInt().toString()
|
||||||
|
|
||||||
|
private suspend fun add(track: Track): Track = api.addLibManga(track, getUsername())
|
||||||
|
|
||||||
|
override suspend fun update(
|
||||||
|
track: Track,
|
||||||
|
didReadChapter: Boolean,
|
||||||
|
): Track {
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
if (didReadChapter) {
|
||||||
|
if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) {
|
||||||
|
track.status = COMPLETED
|
||||||
|
} else if (track.status != REREADING) {
|
||||||
|
track.status = READING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.updateLibManga(track, getUsername())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun delete(track: Track) {
|
||||||
|
api.deleteLibManga(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun bind(
|
||||||
|
track: Track,
|
||||||
|
hasReadChapters: Boolean,
|
||||||
|
): Track {
|
||||||
|
val remoteTrack = api.findLibManga(track, getUsername())
|
||||||
|
return if (remoteTrack != null) {
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.library_id = remoteTrack.library_id
|
||||||
|
|
||||||
|
if (track.status != COMPLETED) {
|
||||||
|
val isRereading = track.status == REREADING
|
||||||
|
track.status = if (!isRereading && hasReadChapters) READING else track.status
|
||||||
|
}
|
||||||
|
|
||||||
|
update(track)
|
||||||
|
} else {
|
||||||
|
// Set default fields if it's not found in the list
|
||||||
|
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||||
|
track.score = 0.0
|
||||||
|
add(track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<TrackSearch> = api.search(query)
|
||||||
|
|
||||||
|
override suspend fun refresh(track: Track): Track {
|
||||||
|
api.findLibManga(track, getUsername())?.let { remoteTrack ->
|
||||||
|
track.library_id = remoteTrack.library_id
|
||||||
|
track.copyPersonalFrom(remoteTrack)
|
||||||
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
|
} ?: throw Exception("Could not find manga")
|
||||||
|
return track
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLogo(): String = "/static/tracker/shikimori.png"
|
||||||
|
|
||||||
|
override fun getStatusList(): List<Int> = listOf(READING, COMPLETED, ON_HOLD, DROPPED, PLAN_TO_READ, REREADING)
|
||||||
|
|
||||||
|
override fun getStatus(status: Int): String? =
|
||||||
|
when (status) {
|
||||||
|
READING -> "Reading"
|
||||||
|
PLAN_TO_READ -> "Plan to read"
|
||||||
|
COMPLETED -> "Completed"
|
||||||
|
ON_HOLD -> "On hold"
|
||||||
|
DROPPED -> "Dropped"
|
||||||
|
REREADING -> "Rereading"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getReadingStatus(): Int = READING
|
||||||
|
|
||||||
|
override fun getRereadingStatus(): Int = REREADING
|
||||||
|
|
||||||
|
override fun getCompletionStatus(): Int = COMPLETED
|
||||||
|
|
||||||
|
override fun authUrl(): String = ShikimoriApi.authUrl().toString()
|
||||||
|
|
||||||
|
override suspend fun authCallback(url: String) {
|
||||||
|
val token = url.extractToken("code") ?: throw IOException("cannot find token")
|
||||||
|
login(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun login(
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
) = login(password)
|
||||||
|
|
||||||
|
suspend fun login(code: String) {
|
||||||
|
try {
|
||||||
|
val oauth = api.accessToken(code)
|
||||||
|
interceptor.newAuth(oauth)
|
||||||
|
val user = api.getCurrentUser()
|
||||||
|
saveCredentials(user.toString(), oauth.accessToken)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveToken(oauth: SMOAuth?) {
|
||||||
|
trackPreferences.setTrackToken(this, json.encodeToString(oauth))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreToken(): SMOAuth? =
|
||||||
|
try {
|
||||||
|
trackPreferences.getTrackToken(this)?.let { json.decodeFromString<SMOAuth>(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun logout() {
|
||||||
|
super.logout()
|
||||||
|
trackPreferences.setTrackToken(this, null)
|
||||||
|
interceptor.newAuth(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import eu.kanade.tachiyomi.network.DELETE
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlinx.serialization.json.putJsonObject
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMAddMangaResponse
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMManga
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMOAuth
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMUser
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMUserListEntry
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class ShikimoriApi(
|
||||||
|
private val trackId: Int,
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
interceptor: ShikimoriInterceptor,
|
||||||
|
) {
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
|
suspend fun addLibManga(
|
||||||
|
track: Track,
|
||||||
|
userId: String,
|
||||||
|
): Track =
|
||||||
|
withIOContext {
|
||||||
|
with(json) {
|
||||||
|
val payload =
|
||||||
|
buildJsonObject {
|
||||||
|
putJsonObject("user_rate") {
|
||||||
|
put("user_id", userId)
|
||||||
|
put("target_id", track.remote_id)
|
||||||
|
put("target_type", "Manga")
|
||||||
|
put("chapters", track.last_chapter_read.toInt())
|
||||||
|
put("score", track.score.toInt())
|
||||||
|
put("status", track.toShikimoriStatus())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authClient
|
||||||
|
.newCall(
|
||||||
|
POST(
|
||||||
|
"$API_URL/v2/user_rates",
|
||||||
|
body = payload.toString().toRequestBody(jsonMime),
|
||||||
|
),
|
||||||
|
).awaitSuccess()
|
||||||
|
.parseAs<SMAddMangaResponse>()
|
||||||
|
.let {
|
||||||
|
// save id of the entry for possible future delete request
|
||||||
|
track.library_id = it.id
|
||||||
|
}
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateLibManga(
|
||||||
|
track: Track,
|
||||||
|
userId: String,
|
||||||
|
): Track = addLibManga(track, userId)
|
||||||
|
|
||||||
|
suspend fun deleteLibManga(track: Track) {
|
||||||
|
withIOContext {
|
||||||
|
authClient
|
||||||
|
.newCall(DELETE("$API_URL/v2/user_rates/${track.library_id}"))
|
||||||
|
.awaitSuccess()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun search(search: String): List<TrackSearch> =
|
||||||
|
withIOContext {
|
||||||
|
val url =
|
||||||
|
"$API_URL/mangas"
|
||||||
|
.toUri()
|
||||||
|
.buildUpon()
|
||||||
|
.appendQueryParameter("order", "popularity")
|
||||||
|
.appendQueryParameter("search", search)
|
||||||
|
.appendQueryParameter("limit", "20")
|
||||||
|
.build()
|
||||||
|
with(json) {
|
||||||
|
authClient
|
||||||
|
.newCall(GET(url.toString()))
|
||||||
|
.awaitSuccess()
|
||||||
|
.parseAs<List<SMManga>>()
|
||||||
|
.map { it.toTrack(trackId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findLibManga(
|
||||||
|
track: Track,
|
||||||
|
userId: String,
|
||||||
|
): Track? =
|
||||||
|
withIOContext {
|
||||||
|
val urlMangas =
|
||||||
|
"$API_URL/mangas"
|
||||||
|
.toUri()
|
||||||
|
.buildUpon()
|
||||||
|
.appendPath(track.remote_id.toString())
|
||||||
|
.build()
|
||||||
|
val manga =
|
||||||
|
with(json) {
|
||||||
|
authClient
|
||||||
|
.newCall(GET(urlMangas.toString()))
|
||||||
|
.awaitSuccess()
|
||||||
|
.parseAs<SMManga>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val url =
|
||||||
|
"$API_URL/v2/user_rates"
|
||||||
|
.toUri()
|
||||||
|
.buildUpon()
|
||||||
|
.appendQueryParameter("user_id", userId)
|
||||||
|
.appendQueryParameter("target_id", track.remote_id.toString())
|
||||||
|
.appendQueryParameter("target_type", "Manga")
|
||||||
|
.build()
|
||||||
|
with(json) {
|
||||||
|
authClient
|
||||||
|
.newCall(GET(url.toString()))
|
||||||
|
.awaitSuccess()
|
||||||
|
.parseAs<List<SMUserListEntry>>()
|
||||||
|
.let { entries ->
|
||||||
|
if (entries.size > 1) {
|
||||||
|
throw Exception("Too many manga in response")
|
||||||
|
}
|
||||||
|
entries
|
||||||
|
.map { it.toTrack(trackId, manga) }
|
||||||
|
.firstOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getCurrentUser(): Int =
|
||||||
|
with(json) {
|
||||||
|
authClient
|
||||||
|
.newCall(GET("$API_URL/users/whoami"))
|
||||||
|
.awaitSuccess()
|
||||||
|
.parseAs<SMUser>()
|
||||||
|
.id
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun accessToken(code: String): SMOAuth =
|
||||||
|
withIOContext {
|
||||||
|
with(json) {
|
||||||
|
client
|
||||||
|
.newCall(accessTokenRequest(code))
|
||||||
|
.awaitSuccess()
|
||||||
|
.parseAs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun accessTokenRequest(code: String) =
|
||||||
|
POST(
|
||||||
|
OAUTH_URL,
|
||||||
|
body =
|
||||||
|
FormBody
|
||||||
|
.Builder()
|
||||||
|
.add("grant_type", "authorization_code")
|
||||||
|
.add("client_id", CLIENT_ID)
|
||||||
|
.add("client_secret", CLIENT_SECRET)
|
||||||
|
.add("code", code)
|
||||||
|
.add("redirect_uri", REDIRECT_URL)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BASE_URL = "https://shikimori.one"
|
||||||
|
private const val API_URL = "$BASE_URL/api"
|
||||||
|
private const val OAUTH_URL = "$BASE_URL/oauth/token"
|
||||||
|
private const val LOGIN_URL = "$BASE_URL/oauth/authorize"
|
||||||
|
|
||||||
|
private const val REDIRECT_URL = "https://suwayomi.org/tracker-oauth"
|
||||||
|
|
||||||
|
private const val CLIENT_ID = "qTrMBF5HtM_33Pv2Vm2fFmEaBUI_c3LvohyJ0beQ9pA"
|
||||||
|
private const val CLIENT_SECRET = "MN_XHQK_aeSqduW_rB64cARi2fFoLGl-AgZ0iMD9zq0"
|
||||||
|
|
||||||
|
fun authUrl(): Uri =
|
||||||
|
LOGIN_URL
|
||||||
|
.toUri()
|
||||||
|
.buildUpon()
|
||||||
|
.appendQueryParameter("client_id", CLIENT_ID)
|
||||||
|
.appendQueryParameter("redirect_uri", REDIRECT_URL)
|
||||||
|
.appendQueryParameter("response_type", "code")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun refreshTokenRequest(token: String) =
|
||||||
|
POST(
|
||||||
|
OAUTH_URL,
|
||||||
|
body =
|
||||||
|
FormBody
|
||||||
|
.Builder()
|
||||||
|
.add("grant_type", "refresh_token")
|
||||||
|
.add("client_id", CLIENT_ID)
|
||||||
|
.add("client_secret", CLIENT_SECRET)
|
||||||
|
.add("refresh_token", token)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.AppInfo
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.SMOAuth
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto.isExpired
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class ShikimoriInterceptor(
|
||||||
|
private val shikimori: Shikimori,
|
||||||
|
) : Interceptor {
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth object used for authenticated requests.
|
||||||
|
*/
|
||||||
|
private var oauth: SMOAuth? = shikimori.restoreToken()
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
|
val currAuth = oauth ?: throw Exception("Not authenticated with Shikimori")
|
||||||
|
|
||||||
|
val refreshToken = currAuth.refreshToken!!
|
||||||
|
|
||||||
|
// Refresh access token if expired.
|
||||||
|
if (currAuth.isExpired()) {
|
||||||
|
val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken))
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
newAuth(json.decodeFromString<SMOAuth>(response.body.string()))
|
||||||
|
} else {
|
||||||
|
response.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the authorization header to the original request.
|
||||||
|
val authRequest =
|
||||||
|
originalRequest
|
||||||
|
.newBuilder()
|
||||||
|
.addHeader("Authorization", "Bearer ${oauth!!.accessToken}")
|
||||||
|
.header("User-Agent", "Suwayomi v${AppInfo.getVersionName()})")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return chain.proceed(authRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newAuth(oauth: SMOAuth?) {
|
||||||
|
this.oauth = oauth
|
||||||
|
shikimori.saveToken(oauth)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
|
|
||||||
|
fun Track.toShikimoriStatus() =
|
||||||
|
when (status) {
|
||||||
|
Shikimori.READING -> "watching"
|
||||||
|
Shikimori.COMPLETED -> "completed"
|
||||||
|
Shikimori.ON_HOLD -> "on_hold"
|
||||||
|
Shikimori.DROPPED -> "dropped"
|
||||||
|
Shikimori.PLAN_TO_READ -> "planned"
|
||||||
|
Shikimori.REREADING -> "rewatching"
|
||||||
|
else -> throw NotImplementedError("Unknown status: $status")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toTrackStatus(status: String) =
|
||||||
|
when (status) {
|
||||||
|
"watching" -> Shikimori.READING
|
||||||
|
"completed" -> Shikimori.COMPLETED
|
||||||
|
"on_hold" -> Shikimori.ON_HOLD
|
||||||
|
"dropped" -> Shikimori.DROPPED
|
||||||
|
"planned" -> Shikimori.PLAN_TO_READ
|
||||||
|
"rewatching" -> Shikimori.REREADING
|
||||||
|
else -> throw NotImplementedError("Unknown status: $status")
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SMAddMangaResponse(
|
||||||
|
val id: Long,
|
||||||
|
)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.ShikimoriApi
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SMManga(
|
||||||
|
val id: Long,
|
||||||
|
val name: String,
|
||||||
|
val chapters: Int,
|
||||||
|
val image: SUMangaCover,
|
||||||
|
val score: Double,
|
||||||
|
val url: String,
|
||||||
|
val status: String,
|
||||||
|
val kind: String,
|
||||||
|
@SerialName("aired_on")
|
||||||
|
val airedOn: String?,
|
||||||
|
) {
|
||||||
|
fun toTrack(trackId: Int): TrackSearch =
|
||||||
|
TrackSearch.create(trackId).apply {
|
||||||
|
remote_id = this@SMManga.id
|
||||||
|
title = name
|
||||||
|
total_chapters = chapters
|
||||||
|
cover_url = ShikimoriApi.BASE_URL + image.preview
|
||||||
|
summary = ""
|
||||||
|
score = this@SMManga.score
|
||||||
|
tracking_url = ShikimoriApi.BASE_URL + url
|
||||||
|
publishing_status = this@SMManga.status
|
||||||
|
publishing_type = kind
|
||||||
|
start_date = airedOn ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SUMangaCover(
|
||||||
|
val preview: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SMOAuth(
|
||||||
|
@SerialName("access_token")
|
||||||
|
val accessToken: String,
|
||||||
|
@SerialName("token_type")
|
||||||
|
val tokenType: String,
|
||||||
|
@SerialName("created_at")
|
||||||
|
val createdAt: Long,
|
||||||
|
@SerialName("expires_in")
|
||||||
|
val expiresIn: Long,
|
||||||
|
@SerialName("refresh_token")
|
||||||
|
val refreshToken: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Access token lives 1 day
|
||||||
|
fun SMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SMUser(
|
||||||
|
val id: Int,
|
||||||
|
)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.shikimori.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.ShikimoriApi
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.shikimori.toTrackStatus
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SMUserListEntry(
|
||||||
|
val id: Long,
|
||||||
|
val chapters: Double,
|
||||||
|
val score: Int,
|
||||||
|
val status: String,
|
||||||
|
) {
|
||||||
|
fun toTrack(
|
||||||
|
trackId: Int,
|
||||||
|
manga: SMManga,
|
||||||
|
): Track =
|
||||||
|
Track.create(trackId).apply {
|
||||||
|
title = manga.name
|
||||||
|
remote_id = this@SMUserListEntry.id
|
||||||
|
total_chapters = manga.chapters
|
||||||
|
library_id = this@SMUserListEntry.id
|
||||||
|
last_chapter_read = this@SMUserListEntry.chapters
|
||||||
|
score = this@SMUserListEntry.score.toDouble()
|
||||||
|
status = toTrackStatus(this@SMUserListEntry.status)
|
||||||
|
tracking_url = ShikimoriApi.BASE_URL + manga.url
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
server/src/main/resources/static/tracker/shikimori.png
Normal file
BIN
server/src/main/resources/static/tracker/shikimori.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
Reference in New Issue
Block a user