Minor tracking refactors (#3900)
* Make tracker Api classes take their tracker's ID Also made the name consistently `trackerId` since `trackId` can be confusing in the context of Tracks that also carry several IDs. Also made the public constants in TrackerManager entirely redundant, so trackers are all equally getting their ID defined in their constructor call now. * Explicitly mark unused Exceptions as such * Make isExpired method of BGMOAuth data class As opposed to an extension defined in the same file * Kavita: thumbnail_url -> thumbnailUrl With a SerialName annotation of course * Suwayomi: Remove redundant with(json) Both of these requests don't use parseAs so this not required. * Bangumi: Don't recreate OAuth object for storing * Remove unused attribute from tracker OAuth classes Mostly `token_type`. Also removed `user_id` from BGMOAuth and `scope` & `expiresIn` from MangaBakaOAuth (which already provides `expiresAt`). * Use kotlin.time durations for token expiry math Something like `Clock.System.now().plus(1.hours)` is much easier to read than `System.getEpochMillis() + 3600`. Also lets us use the `epochSeconds` attribute where the given timestamps are seconds-resolution. * Bangumi: Remove redundant apply block * Kavita: Misc simplifications * Use parseAs in tracker interceptors * MAL: Use existing setAuth to store refresh token
This commit is contained in:
@@ -20,24 +20,17 @@ import kotlinx.coroutines.flow.combine
|
|||||||
@SingleIn(AppScope::class)
|
@SingleIn(AppScope::class)
|
||||||
class TrackerManager {
|
class TrackerManager {
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val ANILIST = 2L
|
|
||||||
const val KITSU = 3L
|
|
||||||
const val KAVITA = 8L
|
|
||||||
const val MANGABAKA = 11L
|
|
||||||
}
|
|
||||||
|
|
||||||
val myAnimeList = MyAnimeList(1L)
|
val myAnimeList = MyAnimeList(1L)
|
||||||
val aniList = Anilist(ANILIST)
|
val aniList = Anilist(2L)
|
||||||
val kitsu = Kitsu(KITSU)
|
val kitsu = Kitsu(3L)
|
||||||
val shikimori = Shikimori(4L)
|
val shikimori = Shikimori(4L)
|
||||||
val bangumi = Bangumi(5L)
|
val bangumi = Bangumi(5L)
|
||||||
val komga = Komga(6L)
|
val komga = Komga(6L)
|
||||||
val mangaUpdates = MangaUpdates(7L)
|
val mangaUpdates = MangaUpdates(7L)
|
||||||
val kavita = Kavita(KAVITA)
|
val kavita = Kavita(8L)
|
||||||
val suwayomi = Suwayomi(9L)
|
val suwayomi = Suwayomi(9L)
|
||||||
val hikka = Hikka(10L)
|
val hikka = Hikka(10L)
|
||||||
val mangaBaka = MangaBaka(MANGABAKA)
|
val mangaBaka = MangaBaka(11L)
|
||||||
|
|
||||||
val trackers = listOf(
|
val trackers = listOf(
|
||||||
myAnimeList,
|
myAnimeList,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||||||
|
|
||||||
private val interceptor by lazy { AnilistInterceptor(this, getPassword()) }
|
private val interceptor by lazy { AnilistInterceptor(this, getPassword()) }
|
||||||
|
|
||||||
private val api by lazy { AnilistApi(client, interceptor) }
|
private val api by lazy { AnilistApi(id, client, interceptor) }
|
||||||
|
|
||||||
override val supportsReadingDates: Boolean = true
|
override val supportsReadingDates: Boolean = true
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||||||
// If the preference is an int from APIv1, logout user to force using APIv2
|
// If the preference is an int from APIv1, logout user to force using APIv2
|
||||||
try {
|
try {
|
||||||
scorePreference.get()
|
scorePreference.get()
|
||||||
} catch (e: ClassCastException) {
|
} catch (_: ClassCastException) {
|
||||||
logout()
|
logout()
|
||||||
scorePreference.delete()
|
scorePreference.delete()
|
||||||
}
|
}
|
||||||
@@ -224,7 +224,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||||||
scorePreference.set(currentUser.mediaListOptions.scoreFormat)
|
scorePreference.set(currentUser.mediaListOptions.scoreFormat)
|
||||||
saveDisplayUsername(currentUser.name)
|
saveDisplayUsername(currentUser.name)
|
||||||
saveCredentials(currentUser.id.toString(), oauth.accessToken)
|
saveCredentials(currentUser.id.toString(), oauth.accessToken)
|
||||||
} catch (e: Throwable) {
|
} catch (_: Throwable) {
|
||||||
logout()
|
logout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,7 +242,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||||||
fun loadOAuth(): ALOAuth? {
|
fun loadOAuth(): ALOAuth? {
|
||||||
return try {
|
return try {
|
||||||
json.decodeFromString<ALOAuth>(trackPreferences.trackToken(this).get())
|
json.decodeFromString<ALOAuth>(trackPreferences.trackToken(this).get())
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ import kotlin.time.Duration.Companion.minutes
|
|||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
class AnilistApi(
|
||||||
|
val trackerId: Long,
|
||||||
|
val client: OkHttpClient,
|
||||||
|
interceptor: AnilistInterceptor,
|
||||||
|
) {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
@@ -192,7 +196,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<ALSearchResult>()
|
.parseAs<ALSearchResult>()
|
||||||
.data.page.media
|
.data.page.media
|
||||||
.map { it.toALManga().toTrack() }
|
.map { it.toALManga().toTrack(trackerId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,7 +277,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
.data.page.mediaList
|
.data.page.mediaList
|
||||||
.map { it.toALUserManga() }
|
.map { it.toALUserManga() }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.toTrack()
|
?.toTrack(trackerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,7 +379,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
.data.page.media
|
.data.page.media
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.toALManga()
|
?.toALManga()
|
||||||
?.toTrack()
|
?.toTrack(trackerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist.dto
|
package eu.kanade.tachiyomi.data.track.anilist.dto
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
@@ -21,7 +20,7 @@ data class ALManga(
|
|||||||
val averageScore: Int,
|
val averageScore: Int,
|
||||||
val staff: ALStaff,
|
val staff: ALStaff,
|
||||||
) {
|
) {
|
||||||
fun toTrack() = TrackSearch.create(TrackerManager.ANILIST).apply {
|
fun toTrack(trackerId: Long) = TrackSearch.create(trackerId).apply {
|
||||||
remote_id = remoteId
|
remote_id = remoteId
|
||||||
title = this@ALManga.title
|
title = this@ALManga.title
|
||||||
total_chapters = totalChapters
|
total_chapters = totalChapters
|
||||||
@@ -57,7 +56,7 @@ data class ALUserManga(
|
|||||||
val manga: ALManga,
|
val manga: ALManga,
|
||||||
val private: Boolean,
|
val private: Boolean,
|
||||||
) {
|
) {
|
||||||
fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
|
fun toTrack(trackerId: Long) = Track.create(trackerId).apply {
|
||||||
remote_id = manga.remoteId
|
remote_id = manga.remoteId
|
||||||
title = manga.title
|
title = manga.title
|
||||||
status = toTrackStatus()
|
status = toTrackStatus()
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import tachiyomi.core.common.util.lang.withIOContext
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class BangumiApi(
|
class BangumiApi(
|
||||||
private val trackId: Long,
|
private val trackerId: Long,
|
||||||
private val client: OkHttpClient,
|
private val client: OkHttpClient,
|
||||||
interceptor: BangumiInterceptor,
|
interceptor: BangumiInterceptor,
|
||||||
) {
|
) {
|
||||||
@@ -106,7 +106,7 @@ class BangumiApi(
|
|||||||
.parseAs<BGMSearchResult>()
|
.parseAs<BGMSearchResult>()
|
||||||
.data
|
.data
|
||||||
.filter { it.platform == null || it.platform == "漫画" }
|
.filter { it.platform == null || it.platform == "漫画" }
|
||||||
.map { it.toTrackSearch(trackId) }
|
.map { it.toTrackSearch(trackerId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ class BangumiApi(
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<BGMSubject>()
|
.parseAs<BGMSubject>()
|
||||||
.takeIf { it.platform == null || it.platform == "漫画" }
|
.takeIf { it.platform == null || it.platform == "漫画" }
|
||||||
?.toTrackSearch(trackId)
|
?.toTrackSearch(trackerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.track.bangumi
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.isExpired
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -25,7 +25,9 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
|
|||||||
if (currAuth.isExpired()) {
|
if (currAuth.isExpired()) {
|
||||||
val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refreshToken!!))
|
val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refreshToken!!))
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
currAuth = json.decodeFromString<BGMOAuth>(response.body.string())
|
currAuth = with(json) {
|
||||||
|
response.parseAs<BGMOAuth>()
|
||||||
|
}
|
||||||
newAuth(currAuth)
|
newAuth(currAuth)
|
||||||
} else {
|
} else {
|
||||||
response.close()
|
response.close()
|
||||||
@@ -37,27 +39,13 @@ class BangumiInterceptor(private val bangumi: Bangumi) : Interceptor {
|
|||||||
"User-Agent",
|
"User-Agent",
|
||||||
"antsylich/Mihon/v${BuildConfig.VERSION_NAME} (Android) (http://github.com/mihonapp/mihon)",
|
"antsylich/Mihon/v${BuildConfig.VERSION_NAME} (Android) (http://github.com/mihonapp/mihon)",
|
||||||
)
|
)
|
||||||
.apply {
|
.addHeader("Authorization", "Bearer ${currAuth.accessToken}")
|
||||||
addHeader("Authorization", "Bearer ${currAuth.accessToken}")
|
|
||||||
}
|
|
||||||
.build()
|
.build()
|
||||||
.let(chain::proceed)
|
.let(chain::proceed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newAuth(oauth: BGMOAuth?) {
|
fun newAuth(oauth: BGMOAuth?) {
|
||||||
this.oauth = if (oauth == null) {
|
this.oauth = oauth
|
||||||
null
|
|
||||||
} else {
|
|
||||||
BGMOAuth(
|
|
||||||
oauth.accessToken,
|
|
||||||
oauth.tokenType,
|
|
||||||
System.currentTimeMillis() / 1000,
|
|
||||||
oauth.expiresIn,
|
|
||||||
oauth.refreshToken,
|
|
||||||
this.oauth?.userId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
bangumi.saveToken(oauth)
|
bangumi.saveToken(oauth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,22 @@ package eu.kanade.tachiyomi.data.track.bangumi.dto
|
|||||||
import kotlinx.serialization.EncodeDefault
|
import kotlinx.serialization.EncodeDefault
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
// Incomplete DTO with only our needed attributes
|
||||||
data class BGMOAuth(
|
data class BGMOAuth(
|
||||||
@SerialName("access_token")
|
@SerialName("access_token")
|
||||||
val accessToken: String,
|
val accessToken: String,
|
||||||
@SerialName("token_type")
|
|
||||||
val tokenType: String,
|
|
||||||
@SerialName("created_at")
|
@SerialName("created_at")
|
||||||
@EncodeDefault
|
@EncodeDefault
|
||||||
val createdAt: Long = System.currentTimeMillis() / 1000,
|
val createdAt: Long = Clock.System.now().epochSeconds,
|
||||||
@SerialName("expires_in")
|
@SerialName("expires_in")
|
||||||
val expiresIn: Long,
|
val expiresIn: Long,
|
||||||
@SerialName("refresh_token")
|
@SerialName("refresh_token")
|
||||||
val refreshToken: String?,
|
val refreshToken: String?,
|
||||||
@SerialName("user_id")
|
) {
|
||||||
val userId: Long?,
|
// Access token refresh before expired
|
||||||
)
|
fun isExpired() = Clock.System.now().plus(1.hours).epochSeconds > (createdAt + expiresIn)
|
||||||
|
}
|
||||||
// Access token refresh before expired
|
|
||||||
fun BGMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600)
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ data class BGMSubject(
|
|||||||
val rating: BGMSubjectRating?,
|
val rating: BGMSubjectRating?,
|
||||||
val platform: String?,
|
val platform: String?,
|
||||||
) {
|
) {
|
||||||
fun toTrackSearch(trackId: Long): TrackSearch = TrackSearch.create(trackId).apply {
|
fun toTrackSearch(trackerId: Long): TrackSearch = TrackSearch.create(trackerId).apply {
|
||||||
remote_id = this@BGMSubject.id
|
remote_id = this@BGMSubject.id
|
||||||
title = nameCn.ifBlank { name }
|
title = nameCn.ifBlank { name }
|
||||||
cover_url = images?.common.orEmpty()
|
cover_url = images?.common.orEmpty()
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class HikkaApi(
|
class HikkaApi(
|
||||||
private val trackId: Long,
|
private val trackerId: Long,
|
||||||
private val client: OkHttpClient,
|
private val client: OkHttpClient,
|
||||||
interceptor: HikkaInterceptor,
|
interceptor: HikkaInterceptor,
|
||||||
) {
|
) {
|
||||||
@@ -95,7 +95,7 @@ class HikkaApi(
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<HKMangaPagination>()
|
.parseAs<HKMangaPagination>()
|
||||||
.list
|
.list
|
||||||
.map { it.toTrack(trackId) }
|
.map { it.toTrack(trackerId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ class HikkaApi(
|
|||||||
} else {
|
} else {
|
||||||
response
|
response
|
||||||
.parseAs<HKManga>()
|
.parseAs<HKManga>()
|
||||||
.toTrack(trackId)
|
.toTrack(trackerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ class HikkaApi(
|
|||||||
authClient.newCall(GET(url.toString()))
|
authClient.newCall(GET(url.toString()))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<HKManga>()
|
.parseAs<HKManga>()
|
||||||
.toTrack(trackId)
|
.toTrack(trackerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +193,7 @@ class HikkaApi(
|
|||||||
authClient.newCall(PUT(url.toString(), body = payload.toString().toRequestBody(jsonMime)))
|
authClient.newCall(PUT(url.toString(), body = payload.toString().toRequestBody(jsonMime)))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<HKRead>()
|
.parseAs<HKRead>()
|
||||||
.toTrack(trackId)
|
.toTrack(trackerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.hikka
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.track.hikka.dto.HKAuthTokenInfo
|
import eu.kanade.tachiyomi.data.track.hikka.dto.HKAuthTokenInfo
|
||||||
import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth
|
import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth
|
||||||
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -32,7 +33,9 @@ class HikkaInterceptor(private val hikka: Hikka) : Interceptor {
|
|||||||
throw Exception("Hikka: Auth token info failed")
|
throw Exception("Hikka: Auth token info failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
val authTokenInfo = json.decodeFromString<HKAuthTokenInfo>(authTokenInfoResponse.body.string())
|
val authTokenInfo = with(json) {
|
||||||
|
authTokenInfoResponse.parseAs<HKAuthTokenInfo>()
|
||||||
|
}
|
||||||
setAuth(HKOAuth(currAuth.accessToken, authTokenInfo.expiration, authTokenInfo.created))
|
setAuth(HKOAuth(currAuth.accessToken, authTokenInfo.expiration, authTokenInfo.created))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ data class HKManga(
|
|||||||
val startDate: Long? = null,
|
val startDate: Long? = null,
|
||||||
val read: List<HKRead>? = emptyList(),
|
val read: List<HKRead>? = emptyList(),
|
||||||
) {
|
) {
|
||||||
fun toTrack(trackId: Long): TrackSearch {
|
fun toTrack(trackerId: Long): TrackSearch {
|
||||||
return TrackSearch.create(trackId).apply {
|
return TrackSearch.create(trackerId).apply {
|
||||||
remote_id = stringToNumber(this@HKManga.slug)
|
remote_id = stringToNumber(this@HKManga.slug)
|
||||||
title = this@HKManga.titleUa ?: this@HKManga.titleEn ?: this@HKManga.titleOriginal
|
title = this@HKManga.titleUa ?: this@HKManga.titleEn ?: this@HKManga.titleOriginal
|
||||||
total_chapters = this@HKManga.chapters?.toLong() ?: 0
|
total_chapters = this@HKManga.chapters?.toLong() ?: 0
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.data.track.hikka.dto
|
|||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HKOAuth(
|
data class HKOAuth(
|
||||||
@@ -10,9 +12,5 @@ data class HKOAuth(
|
|||||||
val expiration: Long,
|
val expiration: Long,
|
||||||
val created: Long,
|
val created: Long,
|
||||||
) {
|
) {
|
||||||
fun isExpired(): Boolean {
|
fun isExpired() = Clock.System.now().plus(5.minutes).epochSeconds >= expiration
|
||||||
val currentTime = System.currentTimeMillis() / 1000
|
|
||||||
val buffer = 5 * 60 // safety margin
|
|
||||||
return currentTime >= (expiration - buffer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ data class HKRead(
|
|||||||
val endDate: Long? = null,
|
val endDate: Long? = null,
|
||||||
val content: HKManga? = null,
|
val content: HKManga? = null,
|
||||||
) {
|
) {
|
||||||
fun toTrack(trackId: Long): TrackSearch {
|
fun toTrack(trackerId: Long): TrackSearch {
|
||||||
return TrackSearch.create(trackId).apply {
|
return TrackSearch.create(trackerId).apply {
|
||||||
val mangaContent = this@HKRead.content
|
val mangaContent = this@HKRead.content
|
||||||
if (mangaContent != null) {
|
if (mangaContent != null) {
|
||||||
title = mangaContent.titleUa ?: mangaContent.titleEn ?: mangaContent.titleOriginal
|
title = mangaContent.titleUa ?: mangaContent.titleEn ?: mangaContent.titleOriginal
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
|
|||||||
var authentications: OAuth? = null
|
var authentications: OAuth? = null
|
||||||
|
|
||||||
private val interceptor by lazy { KavitaInterceptor(this) }
|
private val interceptor by lazy { KavitaInterceptor(this) }
|
||||||
val api by lazy { KavitaApi(client, interceptor) }
|
val api by lazy { KavitaApi(id, client, interceptor) }
|
||||||
|
|
||||||
private val sourceManager: SourceManager by lazy { appGraph.sourceManager }
|
private val sourceManager: SourceManager by lazy { appGraph.sourceManager }
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ class Kavita(id: Long) : BaseTracker(id, "Kavita"), EnhancedTracker {
|
|||||||
override suspend fun match(manga: Manga): TrackSearch? =
|
override suspend fun match(manga: Manga): TrackSearch? =
|
||||||
try {
|
try {
|
||||||
api.getTrackSearch(manga.url)
|
api.getTrackSearch(manga.url)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import okhttp3.Dns
|
import okhttp3.Dns
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
@@ -18,7 +18,11 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
|
|
||||||
class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor) {
|
class KavitaApi(
|
||||||
|
private val trackerId: Long,
|
||||||
|
private val client: OkHttpClient,
|
||||||
|
interceptor: KavitaInterceptor,
|
||||||
|
) {
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
@@ -40,7 +44,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||||||
fun getNewToken(apiUrl: String, apiKey: String): String? {
|
fun getNewToken(apiUrl: String, apiKey: String): String? {
|
||||||
val request = POST(
|
val request = POST(
|
||||||
"$apiUrl/Plugin/authenticate?apiKey=$apiKey&pluginName=Tachiyomi-Kavita",
|
"$apiUrl/Plugin/authenticate?apiKey=$apiKey&pluginName=Tachiyomi-Kavita",
|
||||||
body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()),
|
body = EMPTY_JSON_BODY,
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
with(json) {
|
with(json) {
|
||||||
@@ -54,9 +58,9 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||||||
throw IOException("Unauthorized / api key not valid")
|
throw IOException("Unauthorized / api key not valid")
|
||||||
}
|
}
|
||||||
500 -> {
|
500 -> {
|
||||||
logcat(
|
logcat(LogPriority.WARN) {
|
||||||
LogPriority.WARN,
|
"Error fetching JWT token. API URL: $apiUrl, empty API key: ${apiKey.isEmpty()}"
|
||||||
) { "Error fetching JWT token. API URL: $apiUrl, empty API key: ${apiKey.isEmpty()}" }
|
}
|
||||||
throw IOException("Error fetching JWT token")
|
throw IOException("Error fetching JWT token")
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
@@ -64,7 +68,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Not sure which one to catch
|
// Not sure which one to catch
|
||||||
} catch (e: SocketTimeoutException) {
|
} catch (_: SocketTimeoutException) {
|
||||||
logcat(LogPriority.WARN) {
|
logcat(LogPriority.WARN) {
|
||||||
"Could not fetch JWT token. Probably due to connectivity issue or URL '$apiUrl' not available, skipping"
|
"Could not fetch JWT token. Probably due to connectivity issue or URL '$apiUrl' not available, skipping"
|
||||||
}
|
}
|
||||||
@@ -133,10 +137,9 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(
|
logcat(LogPriority.WARN, e) {
|
||||||
LogPriority.WARN,
|
"Exception getting latest chapter read. Could not get itemRequest: $requestUrl"
|
||||||
e,
|
}
|
||||||
) { "Exception getting latest chapter read. Could not get itemRequest: $requestUrl" }
|
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
return 0.0
|
return 0.0
|
||||||
@@ -150,9 +153,9 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||||||
.parseAs()
|
.parseAs()
|
||||||
}
|
}
|
||||||
|
|
||||||
val track = seriesDto.toTrack()
|
val track = seriesDto.toTrack(trackerId)
|
||||||
track.apply {
|
track.apply {
|
||||||
cover_url = seriesDto.thumbnail_url.toString()
|
cover_url = seriesDto.thumbnailUrl.toString()
|
||||||
tracking_url = url
|
tracking_url = url
|
||||||
total_chapters = getTotalChapters(url)
|
total_chapters = getTotalChapters(url)
|
||||||
|
|
||||||
@@ -177,9 +180,13 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
|
|||||||
track.tracking_url,
|
track.tracking_url,
|
||||||
)}&chapterNumber=${track.last_chapter_read}"
|
)}&chapterNumber=${track.last_chapter_read}"
|
||||||
authClient.newCall(
|
authClient.newCall(
|
||||||
POST(requestUrl, body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())),
|
POST(requestUrl, body = EMPTY_JSON_BODY),
|
||||||
)
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
return getTrackSearch(track.tracking_url)
|
return getTrackSearch(track.tracking_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val EMPTY_JSON_BODY = "{}".toRequestBody(jsonMime)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.kavita
|
package eu.kanade.tachiyomi.data.track.kavita
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -9,7 +9,8 @@ data class SeriesDto(
|
|||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val originalName: String = "",
|
val originalName: String = "",
|
||||||
val thumbnail_url: String? = "",
|
@SerialName("thumbnailUrl")
|
||||||
|
val thumbnailUrl: String? = "",
|
||||||
val localizedName: String? = "",
|
val localizedName: String? = "",
|
||||||
val sortName: String? = "",
|
val sortName: String? = "",
|
||||||
val pages: Int,
|
val pages: Int,
|
||||||
@@ -22,7 +23,7 @@ data class SeriesDto(
|
|||||||
val libraryId: Int,
|
val libraryId: Int,
|
||||||
val libraryName: String? = "",
|
val libraryName: String? = "",
|
||||||
) {
|
) {
|
||||||
fun toTrack(): TrackSearch = TrackSearch.create(TrackerManager.KAVITA).also {
|
fun toTrack(trackerId: Long): TrackSearch = TrackSearch.create(trackerId).also {
|
||||||
it.title = name
|
it.title = name
|
||||||
it.summary = ""
|
it.summary = ""
|
||||||
}
|
}
|
||||||
@@ -69,14 +70,7 @@ class OAuth(
|
|||||||
SourceAuth(3),
|
SourceAuth(3),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
fun getToken(apiUrl: String): String? {
|
fun getToken(apiUrl: String): String? = authentications.find { it.apiUrl == apiUrl }?.jwtToken
|
||||||
for (authentication in authentications) {
|
|
||||||
if (authentication.apiUrl == apiUrl) {
|
|
||||||
return authentication.jwtToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SourceAuth(
|
data class SourceAuth(
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import kotlin.time.Instant
|
|||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class KitsuApi(
|
class KitsuApi(
|
||||||
private val trackId: Long,
|
private val trackerId: Long,
|
||||||
private val client: OkHttpClient,
|
private val client: OkHttpClient,
|
||||||
interceptor: KitsuInterceptor,
|
interceptor: KitsuInterceptor,
|
||||||
) {
|
) {
|
||||||
@@ -292,7 +292,7 @@ class KitsuApi(
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<KitsuSearchByTitleResult>()
|
.parseAs<KitsuSearchByTitleResult>()
|
||||||
.data.searchMangaByTitle.nodes
|
.data.searchMangaByTitle.nodes
|
||||||
.map { it.toTrackSearch(trackId) }
|
.map { it.toTrackSearch(trackerId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,7 +334,7 @@ class KitsuApi(
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<KitsuSearchByIdWithLibraryResult>()
|
.parseAs<KitsuSearchByIdWithLibraryResult>()
|
||||||
.data.findMangaById
|
.data.findMangaById
|
||||||
?.toTrackSearch(trackId)
|
?.toTrackSearch(trackerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -436,7 +436,7 @@ class KitsuApi(
|
|||||||
.data.findMangaBySlug
|
.data.findMangaBySlug
|
||||||
}
|
}
|
||||||
|
|
||||||
kitsuManga?.toTrackSearch(trackId)
|
kitsuManga?.toTrackSearch(trackerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.kitsu
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
|
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
|
||||||
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -19,7 +20,7 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor {
|
|||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
val currAuth = oauth ?: throw Exception("Not authenticated with Kitsu")
|
var currAuth = oauth ?: throw Exception("Not authenticated with Kitsu")
|
||||||
|
|
||||||
val refreshToken = currAuth.refreshToken!!
|
val refreshToken = currAuth.refreshToken!!
|
||||||
|
|
||||||
@@ -27,7 +28,10 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor {
|
|||||||
if (currAuth.isExpired()) {
|
if (currAuth.isExpired()) {
|
||||||
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
|
val response = chain.proceed(KitsuApi.refreshTokenRequest(refreshToken))
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
newAuth(json.decodeFromString(response.body.string()))
|
currAuth = with(json) {
|
||||||
|
response.parseAs<KitsuOAuth>()
|
||||||
|
}
|
||||||
|
newAuth(currAuth)
|
||||||
} else {
|
} else {
|
||||||
response.close()
|
response.close()
|
||||||
}
|
}
|
||||||
@@ -35,7 +39,7 @@ class KitsuInterceptor(private val kitsu: Kitsu) : Interceptor {
|
|||||||
|
|
||||||
// Add the authorization header to the original request.
|
// Add the authorization header to the original request.
|
||||||
val authRequest = originalRequest.newBuilder()
|
val authRequest = originalRequest.newBuilder()
|
||||||
.addHeader("Authorization", "Bearer ${oauth!!.accessToken}")
|
.addHeader("Authorization", "Bearer ${currAuth.accessToken}")
|
||||||
.header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
|
.header("User-Agent", "Mihon v${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})")
|
||||||
.header("Accept", "application/vnd.api+json")
|
.header("Accept", "application/vnd.api+json")
|
||||||
.header("Content-Type", "application/vnd.api+json")
|
.header("Content-Type", "application/vnd.api+json")
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ package eu.kanade.tachiyomi.data.track.kitsu.dto
|
|||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class KitsuOAuth(
|
data class KitsuOAuth(
|
||||||
@SerialName("access_token")
|
@SerialName("access_token")
|
||||||
val accessToken: String,
|
val accessToken: String,
|
||||||
@SerialName("token_type")
|
|
||||||
val tokenType: String,
|
|
||||||
@SerialName("created_at")
|
@SerialName("created_at")
|
||||||
val createdAt: Long,
|
val createdAt: Long,
|
||||||
@SerialName("expires_in")
|
@SerialName("expires_in")
|
||||||
@@ -17,5 +16,5 @@ data class KitsuOAuth(
|
|||||||
@SerialName("refresh_token")
|
@SerialName("refresh_token")
|
||||||
val refreshToken: String?,
|
val refreshToken: String?,
|
||||||
) {
|
) {
|
||||||
fun isExpired(): Boolean = (Clock.System.now().toEpochMilliseconds() / 1000) > (createdAt + expiresIn - 3600)
|
fun isExpired() = Clock.System.now().plus(1.hours).epochSeconds > (createdAt + expiresIn)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class Komga(id: Long) : BaseTracker(id, "Komga"), EnhancedTracker {
|
|||||||
override suspend fun match(manga: Manga): TrackSearch? =
|
override suspend fun match(manga: Manga): TrackSearch? =
|
||||||
try {
|
try {
|
||||||
api.getTrackSearch(manga.url)
|
api.getTrackSearch(manga.url)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
private const val READLIST_API = "/api/v1/readlists"
|
private const val READLIST_API = "/api/v1/readlists"
|
||||||
|
|
||||||
class KomgaApi(
|
class KomgaApi(
|
||||||
private val trackId: Long,
|
private val trackerId: Long,
|
||||||
private val client: OkHttpClient,
|
private val client: OkHttpClient,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -97,13 +97,13 @@ class KomgaApi(
|
|||||||
return getTrackSearch(track.tracking_url)
|
return getTrackSearch(track.tracking_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun SeriesDto.toTrack(): TrackSearch = TrackSearch.create(trackId).also {
|
private fun SeriesDto.toTrack(): TrackSearch = TrackSearch.create(trackerId).also {
|
||||||
it.title = metadata.title
|
it.title = metadata.title
|
||||||
it.summary = metadata.summary
|
it.summary = metadata.summary
|
||||||
it.publishing_status = metadata.status
|
it.publishing_status = metadata.status
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ReadListDto.toTrack(): TrackSearch = TrackSearch.create(trackId).also {
|
private fun ReadListDto.toTrack(): TrackSearch = TrackSearch.create(trackerId).also {
|
||||||
it.title = name
|
it.title = name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.net.Uri
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
|
||||||
import eu.kanade.tachiyomi.data.track.mangabaka.dto.MangaBakaItem
|
import eu.kanade.tachiyomi.data.track.mangabaka.dto.MangaBakaItem
|
||||||
import eu.kanade.tachiyomi.data.track.mangabaka.dto.MangaBakaItemResult
|
import eu.kanade.tachiyomi.data.track.mangabaka.dto.MangaBakaItemResult
|
||||||
import eu.kanade.tachiyomi.data.track.mangabaka.dto.MangaBakaListResult
|
import eu.kanade.tachiyomi.data.track.mangabaka.dto.MangaBakaListResult
|
||||||
@@ -41,7 +40,7 @@ import java.util.Locale
|
|||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class MangaBakaApi(
|
class MangaBakaApi(
|
||||||
private val trackId: Long,
|
private val trackerId: Long,
|
||||||
baseClient: OkHttpClient,
|
baseClient: OkHttpClient,
|
||||||
interceptor: MangaBakaInterceptor,
|
interceptor: MangaBakaInterceptor,
|
||||||
) {
|
) {
|
||||||
@@ -120,7 +119,7 @@ class MangaBakaApi(
|
|||||||
.parseAs<MangaBakaItemResult>()
|
.parseAs<MangaBakaItemResult>()
|
||||||
.data
|
.data
|
||||||
|
|
||||||
Track.create(TrackerManager.MANGABAKA).apply {
|
Track.create(trackerId).apply {
|
||||||
remote_id = track.remote_id
|
remote_id = track.remote_id
|
||||||
title = additionalData.chooseBestTitle()
|
title = additionalData.chooseBestTitle()
|
||||||
status = userData.getStatus()
|
status = userData.getStatus()
|
||||||
@@ -196,7 +195,7 @@ class MangaBakaApi(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun parseSearchItem(item: MangaBakaItem): TrackSearch {
|
private fun parseSearchItem(item: MangaBakaItem): TrackSearch {
|
||||||
return TrackSearch.create(trackId).apply {
|
return TrackSearch.create(trackerId).apply {
|
||||||
remote_id = item.id
|
remote_id = item.id
|
||||||
title = item.chooseBestTitle()
|
title = item.chooseBestTitle()
|
||||||
summary = item.description?.trim().orEmpty()
|
summary = item.description?.trim().orEmpty()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.mangabaka
|
package eu.kanade.tachiyomi.data.track.mangabaka
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.track.mangabaka.dto.MangaBakaOAuth
|
import eu.kanade.tachiyomi.data.track.mangabaka.dto.MangaBakaOAuth
|
||||||
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -20,7 +21,9 @@ class MangaBakaInterceptor(private val mangaBaka: MangaBaka) : Interceptor {
|
|||||||
if (currentAuth.isExpired()) {
|
if (currentAuth.isExpired()) {
|
||||||
val response = chain.proceed(MangaBakaApi.refreshTokenRequest(currentAuth.refreshToken))
|
val response = chain.proceed(MangaBakaApi.refreshTokenRequest(currentAuth.refreshToken))
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
currentAuth = json.decodeFromString(response.body.string())
|
currentAuth = with(json) {
|
||||||
|
response.parseAs<MangaBakaOAuth>()
|
||||||
|
}
|
||||||
setAuth(currentAuth)
|
setAuth(currentAuth)
|
||||||
} else {
|
} else {
|
||||||
response.close()
|
response.close()
|
||||||
|
|||||||
@@ -11,13 +11,8 @@ data class MangaBakaOAuth(
|
|||||||
val accessToken: String,
|
val accessToken: String,
|
||||||
@SerialName("refresh_token")
|
@SerialName("refresh_token")
|
||||||
val refreshToken: String,
|
val refreshToken: String,
|
||||||
@SerialName("expires_in")
|
|
||||||
val expiresIn: Long,
|
|
||||||
@SerialName("expires_at")
|
@SerialName("expires_at")
|
||||||
val expiresAt: Long,
|
val expiresAt: Long,
|
||||||
@SerialName("token_type")
|
|
||||||
val tokenType: String,
|
|
||||||
val scope: String,
|
|
||||||
) {
|
) {
|
||||||
fun isExpired(): Boolean = Clock.System.now().plus(1.minutes).epochSeconds > expiresAt
|
fun isExpired(): Boolean = Clock.System.now().plus(1.minutes).epochSeconds > expiresAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
|||||||
|
|
||||||
private val interceptor by lazy { MangaUpdatesInterceptor(this) }
|
private val interceptor by lazy { MangaUpdatesInterceptor(this) }
|
||||||
|
|
||||||
private val api by lazy { MangaUpdatesApi(interceptor, client) }
|
private val api by lazy { MangaUpdatesApi(client, interceptor) }
|
||||||
|
|
||||||
override fun getLogo(): Int = R.drawable.brand_mangaupdates
|
override fun getLogo(): Int = R.drawable.brand_mangaupdates
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
|||||||
return try {
|
return try {
|
||||||
val (series, rating) = api.getSeriesListItem(track)
|
val (series, rating) = api.getSeriesListItem(track)
|
||||||
track.copyFrom(series, rating)
|
track.copyFrom(series, rating)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
track.score = 0.0
|
track.score = 0.0
|
||||||
api.addSeriesToList(track, hasReadChapters)
|
api.addSeriesToList(track, hasReadChapters)
|
||||||
track
|
track
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class MangaUpdatesApi(
|
class MangaUpdatesApi(
|
||||||
interceptor: MangaUpdatesInterceptor,
|
|
||||||
private val client: OkHttpClient,
|
private val client: OkHttpClient,
|
||||||
|
interceptor: MangaUpdatesInterceptor,
|
||||||
) {
|
) {
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ class MangaUpdatesApi(
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<MURating>()
|
.parseAs<MURating>()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
|||||||
val username = api.getCurrentUser()
|
val username = api.getCurrentUser()
|
||||||
saveDisplayUsername(username)
|
saveDisplayUsername(username)
|
||||||
saveCredentials(username, oauth.accessToken)
|
saveCredentials(username, oauth.accessToken)
|
||||||
} catch (e: Throwable) {
|
} catch (_: Throwable) {
|
||||||
logout()
|
logout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ class MyAnimeList(id: Long) : BaseTracker(id, "MyAnimeList"), DeletableTracker {
|
|||||||
fun loadOAuth(): MALOAuth? {
|
fun loadOAuth(): MALOAuth? {
|
||||||
return try {
|
return try {
|
||||||
json.decodeFromString<MALOAuth>(trackPreferences.trackToken(this).get())
|
json.decodeFromString<MALOAuth>(trackPreferences.trackToken(this).get())
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import java.util.Locale
|
|||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class MyAnimeListApi(
|
class MyAnimeListApi(
|
||||||
private val trackId: Long,
|
private val trackerId: Long,
|
||||||
private val client: OkHttpClient,
|
private val client: OkHttpClient,
|
||||||
interceptor: MyAnimeListInterceptor,
|
interceptor: MyAnimeListInterceptor,
|
||||||
) {
|
) {
|
||||||
@@ -223,7 +223,7 @@ class MyAnimeListApi(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun parseSearchItem(searchItem: MALManga): TrackSearch {
|
private fun parseSearchItem(searchItem: MALManga): TrackSearch {
|
||||||
return TrackSearch.create(trackId).apply {
|
return TrackSearch.create(trackerId).apply {
|
||||||
remote_id = searchItem.id
|
remote_id = searchItem.id
|
||||||
title = searchItem.title
|
title = searchItem.title
|
||||||
summary = searchItem.synopsis
|
summary = searchItem.synopsis
|
||||||
|
|||||||
+1
-4
@@ -72,10 +72,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.getOrNull()
|
.getOrNull()
|
||||||
?.also {
|
?.also { setAuth(it) }
|
||||||
this.oauth = it
|
|
||||||
myanimelist.saveOAuth(it)
|
|
||||||
}
|
|
||||||
?: throw MALTokenRefreshFailed()
|
?: throw MALTokenRefreshFailed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package eu.kanade.tachiyomi.data.track.myanimelist.dto
|
|||||||
import kotlinx.serialization.EncodeDefault
|
import kotlinx.serialization.EncodeDefault
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class MALOAuth(
|
data class MALOAuth(
|
||||||
@SerialName("token_type")
|
|
||||||
val tokenType: String,
|
|
||||||
@SerialName("refresh_token")
|
@SerialName("refresh_token")
|
||||||
val refreshToken: String,
|
val refreshToken: String,
|
||||||
@SerialName("access_token")
|
@SerialName("access_token")
|
||||||
@@ -16,10 +16,8 @@ data class MALOAuth(
|
|||||||
val expiresIn: Long,
|
val expiresIn: Long,
|
||||||
@SerialName("created_at")
|
@SerialName("created_at")
|
||||||
@EncodeDefault
|
@EncodeDefault
|
||||||
val createdAt: Long = System.currentTimeMillis() / 1000,
|
val createdAt: Long = Clock.System.now().epochSeconds,
|
||||||
) {
|
) {
|
||||||
// Assumes expired a minute earlier
|
// Assumes expired a minute earlier
|
||||||
private val adjustedExpiresIn: Long = (expiresIn - 60)
|
fun isExpired() = Clock.System.now().plus(1.minutes).epochSeconds <= createdAt + expiresIn
|
||||||
|
|
||||||
fun isExpired() = createdAt + adjustedExpiresIn < System.currentTimeMillis() / 1000
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
|||||||
val user = api.getCurrentUser()
|
val user = api.getCurrentUser()
|
||||||
saveDisplayUsername(user.nickname)
|
saveDisplayUsername(user.nickname)
|
||||||
saveCredentials(user.id, oauth.accessToken)
|
saveCredentials(user.id, oauth.accessToken)
|
||||||
} catch (e: Throwable) {
|
} catch (_: Throwable) {
|
||||||
logout()
|
logout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
|||||||
fun restoreToken(): SMOAuth? {
|
fun restoreToken(): SMOAuth? {
|
||||||
return try {
|
return try {
|
||||||
json.decodeFromString<SMOAuth>(trackPreferences.trackToken(this).get())
|
json.decodeFromString<SMOAuth>(trackPreferences.trackToken(this).get())
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
class ShikimoriApi(
|
class ShikimoriApi(
|
||||||
private val trackId: Long,
|
private val trackerId: Long,
|
||||||
private val client: OkHttpClient,
|
private val client: OkHttpClient,
|
||||||
interceptor: ShikimoriInterceptor,
|
interceptor: ShikimoriInterceptor,
|
||||||
) {
|
) {
|
||||||
@@ -145,7 +145,7 @@ class ShikimoriApi(
|
|||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<SMSearchResult>()
|
.parseAs<SMSearchResult>()
|
||||||
.data.mangas
|
.data.mangas
|
||||||
.map { it.toTrack(trackId) }
|
.map { it.toTrack(trackerId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,7 +196,7 @@ class ShikimoriApi(
|
|||||||
.parseAs<SMSearchResult>()
|
.parseAs<SMSearchResult>()
|
||||||
.data.mangas
|
.data.mangas
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.toTrack(trackId)
|
?.toTrack(trackerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,7 +245,7 @@ class ShikimoriApi(
|
|||||||
if (listResult?.userRate == null) {
|
if (listResult?.userRate == null) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
listResult.toTrack(trackId)
|
listResult.toTrack(trackerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.track.shikimori
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
|
import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.dto.isExpired
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -20,15 +20,16 @@ class ShikimoriInterceptor(private val shikimori: Shikimori) : Interceptor {
|
|||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
val currAuth = oauth ?: throw Exception("Not authenticated with Shikimori")
|
var currAuth = oauth ?: throw Exception("Not authenticated with Shikimori")
|
||||||
|
|
||||||
val refreshToken = currAuth.refreshToken!!
|
|
||||||
|
|
||||||
// Refresh access token if expired.
|
// Refresh access token if expired.
|
||||||
if (currAuth.isExpired()) {
|
if (currAuth.isExpired()) {
|
||||||
val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken))
|
val response = chain.proceed(ShikimoriApi.refreshTokenRequest(currAuth.refreshToken!!))
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
newAuth(json.decodeFromString<SMOAuth>(response.body.string()))
|
currAuth = with(json) {
|
||||||
|
response.parseAs<SMOAuth>()
|
||||||
|
}
|
||||||
|
newAuth(currAuth)
|
||||||
} else {
|
} else {
|
||||||
response.close()
|
response.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ data class SMManga(
|
|||||||
val kind: String?,
|
val kind: String?,
|
||||||
val personRoles: List<SMPersonRole>?,
|
val personRoles: List<SMPersonRole>?,
|
||||||
) {
|
) {
|
||||||
fun toTrack(trackId: Long): TrackSearch {
|
fun toTrack(trackerId: Long): TrackSearch {
|
||||||
return TrackSearch.create(trackId).apply {
|
return TrackSearch.create(trackerId).apply {
|
||||||
remote_id = this@SMManga.id
|
remote_id = this@SMManga.id
|
||||||
title = name
|
title = name
|
||||||
total_chapters = chapters
|
total_chapters = chapters
|
||||||
|
|||||||
@@ -2,20 +2,19 @@ package eu.kanade.tachiyomi.data.track.shikimori.dto
|
|||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SMOAuth(
|
data class SMOAuth(
|
||||||
@SerialName("access_token")
|
@SerialName("access_token")
|
||||||
val accessToken: String,
|
val accessToken: String,
|
||||||
@SerialName("token_type")
|
|
||||||
val tokenType: String,
|
|
||||||
@SerialName("created_at")
|
@SerialName("created_at")
|
||||||
val createdAt: Long,
|
val createdAt: Long,
|
||||||
@SerialName("expires_in")
|
@SerialName("expires_in")
|
||||||
val expiresIn: Long,
|
val expiresIn: Long,
|
||||||
@SerialName("refresh_token")
|
@SerialName("refresh_token")
|
||||||
val refreshToken: String?,
|
val refreshToken: String?,
|
||||||
)
|
) {
|
||||||
|
fun isExpired() = Clock.System.now().plus(1.hours).epochSeconds > (createdAt + expiresIn)
|
||||||
// Access token lives 1 day
|
}
|
||||||
fun SMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600)
|
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ data class SMUserListManga(
|
|||||||
val totalChapters: Long, // the title's total chapters
|
val totalChapters: Long, // the title's total chapters
|
||||||
val userRate: SMUserRate?,
|
val userRate: SMUserRate?,
|
||||||
) {
|
) {
|
||||||
fun toTrack(trackId: Long): Track {
|
fun toTrack(trackerId: Long): Track {
|
||||||
return Track.create(trackId).apply {
|
return Track.create(trackerId).apply {
|
||||||
title = name
|
title = name
|
||||||
total_chapters = totalChapters
|
total_chapters = totalChapters
|
||||||
tracking_url = url
|
tracking_url = url
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker {
|
|||||||
override suspend fun match(manga: DomainManga): TrackSearch? =
|
override suspend fun match(manga: DomainManga): TrackSearch? =
|
||||||
try {
|
try {
|
||||||
api.getTrackSearch(manga.url.getMangaId())
|
api.getTrackSearch(manga.url.getMangaId())
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
class SuwayomiApi(
|
class SuwayomiApi(
|
||||||
private val trackId: Long,
|
private val trackerId: Long,
|
||||||
private val sourceManager: SourceManager,
|
private val sourceManager: SourceManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ class SuwayomiApi(
|
|||||||
.entry
|
.entry
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackSearch.create(trackId).apply {
|
TrackSearch.create(trackerId).apply {
|
||||||
remote_id = mangaId
|
remote_id = mangaId
|
||||||
title = manga.title
|
title = manga.title
|
||||||
cover_url = "$baseUrl/${manga.thumbnailUrl}"
|
cover_url = "$baseUrl/${manga.thumbnailUrl}"
|
||||||
@@ -152,15 +152,13 @@ class SuwayomiApi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
with(json) {
|
client.newCall(
|
||||||
client.newCall(
|
POST(
|
||||||
POST(
|
apiUrl,
|
||||||
apiUrl,
|
body = markPayload.toString().toRequestBody(jsonMime),
|
||||||
body = markPayload.toString().toRequestBody(jsonMime),
|
),
|
||||||
),
|
)
|
||||||
)
|
.awaitSuccess()
|
||||||
.awaitSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
val trackQuery = $$"""
|
val trackQuery = $$"""
|
||||||
|mutation TrackManga($mangaId: Int!) {
|
|mutation TrackManga($mangaId: Int!) {
|
||||||
@@ -175,15 +173,13 @@ class SuwayomiApi(
|
|||||||
put("mangaId", mangaId)
|
put("mangaId", mangaId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
with(json) {
|
client.newCall(
|
||||||
client.newCall(
|
POST(
|
||||||
POST(
|
apiUrl,
|
||||||
apiUrl,
|
body = trackPayload.toString().toRequestBody(jsonMime),
|
||||||
body = trackPayload.toString().toRequestBody(jsonMime),
|
),
|
||||||
),
|
)
|
||||||
)
|
.awaitSuccess()
|
||||||
.awaitSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
return getTrackSearch(track.remote_id)
|
return getTrackSearch(track.remote_id)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user