From 68d24dff27ff6faeb8f99e53c5e789a7522772d4 Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Thu, 17 Oct 2024 17:01:07 +0300 Subject: [PATCH 01/17] feat: add Hikka logo to resources --- app/src/main/res/drawable/ic_tracker_hikka.webp | Bin 0 -> 37730 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/src/main/res/drawable/ic_tracker_hikka.webp diff --git a/app/src/main/res/drawable/ic_tracker_hikka.webp b/app/src/main/res/drawable/ic_tracker_hikka.webp new file mode 100644 index 0000000000000000000000000000000000000000..6ff0e77f37259f7e4b30c737bd0152f14e627343 GIT binary patch literal 37730 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 Date: Thu, 17 Oct 2024 17:21:53 +0300 Subject: [PATCH 02/17] feat: add core Hikka tracker classes and DTOs for API requests --- .../tachiyomi/data/track/hikka/Hikka.kt | 177 ++++++++++++++++++ .../tachiyomi/data/track/hikka/HikkaApi.kt | 177 ++++++++++++++++++ .../data/track/hikka/HikkaInterceptor.kt | 88 +++++++++ .../tachiyomi/data/track/hikka/HikkaUtils.kt | 33 ++++ .../data/track/hikka/dto/HKAuthTokenInfo.kt | 13 ++ .../data/track/hikka/dto/HKClient.kt | 14 ++ .../tachiyomi/data/track/hikka/dto/HKManga.kt | 40 ++++ .../data/track/hikka/dto/HKMangaPagination.kt | 9 + .../tachiyomi/data/track/hikka/dto/HKOAuth.kt | 17 ++ .../data/track/hikka/dto/HKPagination.kt | 10 + .../tachiyomi/data/track/hikka/dto/HKRead.kt | 34 ++++ .../tachiyomi/data/track/hikka/dto/HKUser.kt | 16 ++ 12 files changed, 628 insertions(+) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKAuthTokenInfo.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKClient.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKMangaPagination.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKPagination.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKUser.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt new file mode 100644 index 000000000..efaaaa390 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt @@ -0,0 +1,177 @@ +package eu.kanade.tachiyomi.data.track.hikka + +import android.graphics.Color +import dev.icerock.moko.resources.StringResource +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.track.BaseTracker +import eu.kanade.tachiyomi.data.track.DeletableTracker +import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import tachiyomi.domain.track.model.Track +import tachiyomi.i18n.MR +import uy.kohesive.injekt.injectLazy + +class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker { + + companion object { + const val READING = 0L + const val COMPLETED = 1L + const val ON_HOLD = 2L + const val DROPPED = 3L + const val PLAN_TO_READ = 4L + const val REREADING = 5L + + private val SCORE_LIST = IntRange(0, 10) + .map(Int::toString) + .toImmutableList() + } + + private val json: Json by injectLazy() + + private val interceptor by lazy { HikkaInterceptor(this) } + private val api by lazy { HikkaApi(id, client, interceptor) } + + override fun getLogoColor(): Int { + return Color.rgb(0, 0, 0) + } + + override fun getLogo(): Int { + return R.drawable.ic_tracker_hikka + } + + override fun getStatusList(): List { + return listOf( + READING, + COMPLETED, + ON_HOLD, + DROPPED, + PLAN_TO_READ, + REREADING + ) + } + + override fun getStatus(status: Long): StringResource? = when (status) { + READING -> MR.strings.reading + PLAN_TO_READ -> MR.strings.plan_to_read + COMPLETED -> MR.strings.completed + ON_HOLD -> MR.strings.on_hold + DROPPED -> MR.strings.dropped + REREADING -> MR.strings.repeating + else -> null + } + + override fun getReadingStatus(): Long { + return READING + } + + override fun getRereadingStatus(): Long { + return REREADING + } + + override fun getCompletionStatus(): Long { + return COMPLETED + } + + override fun getScoreList(): ImmutableList { + return SCORE_LIST + } + + override fun displayScore(track: Track): String { + return track.score.toInt().toString() + } + + override suspend fun update( + track: eu.kanade.tachiyomi.data.database.models.Track, + didReadChapter: Boolean, + ): eu.kanade.tachiyomi.data.database.models.Track { + if (track.status != COMPLETED) { + if (didReadChapter) { + if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) { + track.status = COMPLETED + } else if (track.status != REREADING) { + track.status = READING + } + } + } + return api.updateUserManga(track) + } + + override suspend fun bind( + track: eu.kanade.tachiyomi.data.database.models.Track, + hasReadChapters: Boolean, + ): eu.kanade.tachiyomi.data.database.models.Track { + val remoteTrack = api.getManga(track) + + 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 + } + + return update(track) + } + + private suspend fun add(track: eu.kanade.tachiyomi.data.database.models.Track): eu.kanade.tachiyomi.data.database.models.Track { + return api.addUserManga(track) + } + + override suspend fun search(query: String): List { + return api.searchManga(query) + } + + override suspend fun refresh(track: eu.kanade.tachiyomi.data.database.models.Track): eu.kanade.tachiyomi.data.database.models.Track { + val remoteTrack = api.updateUserManga(track) + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + return track + } + + override suspend fun login(username: String, password: String) = login(password) + + suspend fun login(code: String) { + try { + val oauth = HKOAuth(code, System.currentTimeMillis() / 1000 + 30 * 60) + interceptor.setAuth(oauth) + val reference = api.getCurrentUser().reference + saveCredentials(reference, oauth.accessToken) + } catch (e: Throwable) { + logout() + } + } + + override suspend fun delete(track: Track) { + api.deleteManga(track) + } + + override fun logout() { + super.logout() + trackPreferences.trackToken(this).delete() + interceptor.setAuth(null) + } + + fun getIfAuthExpired(): Boolean { + return trackPreferences.trackAuthExpired(this).get() + } + + fun setAuthExpired() { + trackPreferences.trackAuthExpired(this).set(true) + } + + fun saveOAuth(oAuth: HKOAuth?) { + trackPreferences.trackToken(this).set(json.encodeToString(oAuth)) + } + + fun loadOAuth(): HKOAuth? { + return try { + json.decodeFromString(trackPreferences.trackToken(this).get()) + } catch (e: Exception) { + null + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt new file mode 100644 index 000000000..f3bcc5874 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt @@ -0,0 +1,177 @@ +package eu.kanade.tachiyomi.data.track.hikka + +import android.net.Uri +import androidx.core.net.toUri +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.hikka.dto.HKAuthTokenInfo +import eu.kanade.tachiyomi.data.track.hikka.dto.HKMangaPagination +import eu.kanade.tachiyomi.data.track.hikka.dto.HKManga +import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth +import eu.kanade.tachiyomi.data.track.hikka.dto.HKRead +import eu.kanade.tachiyomi.data.track.hikka.dto.HKUser +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.network.DELETE +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.PUT +import eu.kanade.tachiyomi.network.awaitSuccess +import eu.kanade.tachiyomi.network.jsonMime +import eu.kanade.tachiyomi.network.parseAs +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import okhttp3.Headers +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import tachiyomi.core.common.util.lang.withIOContext +import uy.kohesive.injekt.injectLazy + +class HikkaApi( + private val trackId: Long, + private val client: OkHttpClient, + interceptor: HikkaInterceptor, +) { + suspend fun getCurrentUser(): HKUser { + return withIOContext { + val request = Request.Builder() + .url("${BASE_API_URL}/user/me") + .get() + .build() + with(json) { + authClient.newCall(request) + .awaitSuccess() + .parseAs() + } + } + } + + suspend fun getTokenInfo(): HKAuthTokenInfo { + return withIOContext { + val request = Request.Builder() + .url("${BASE_API_URL}/auth/token/info") + .get() + .build() + with(json) { + authClient.newCall(request) + .awaitSuccess() + .parseAs() + } + } + } + + suspend fun searchManga(query: String): List { + return withIOContext { + val url = "$BASE_API_URL/manga".toUri().buildUpon() + .appendQueryParameter("page", "1") + .appendQueryParameter("size", "50") + .build() + + val payload = buildJsonObject { + put("media_type", buildJsonArray { }) + put("status", buildJsonArray { }) + put("only_translated", false) + put("magazines", buildJsonArray { }) + put("genres", buildJsonArray { }) + put("score", buildJsonArray { + add(0) + add(10) + }) + put("query", query) + put("sort", buildJsonArray { + add("score:asc") + add("scored_by:asc") + }) + } + + with(json) { + authClient.newCall(POST(url.toString(), body=payload.toString().toRequestBody(jsonMime))) + .awaitSuccess() + .parseAs() + .list + .map { it.toTrack(trackId) } + } + } + } + + suspend fun getManga(track: Track): TrackSearch { + return withIOContext { + val slug = track.tracking_url.split("/")[4] + + val url = "$BASE_API_URL/manga/${slug}".toUri().buildUpon() + .build() + + with(json) { + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + .toTrack(trackId) + } + } + } + + suspend fun deleteManga(track: tachiyomi.domain.track.model.Track) { + return withIOContext { + val slug = track.remoteUrl.split("/")[4] + + val url = "$BASE_API_URL/read/manga/${slug}".toUri().buildUpon() + .build() + + authClient.newCall(DELETE(url.toString())) + .awaitSuccess() + } + } + + suspend fun addUserManga(track: Track): Track { + return withIOContext { + val slug = track.tracking_url.split("/")[4] + + val url = "$BASE_API_URL/read/manga/${slug}".toUri().buildUpon() + .build() + + val payload = buildJsonObject { + put("note", "") + put("chapters", track.last_chapter_read.toInt()) + put("volumes", 0) + put("rereads", 0) + put("score", track.score.toInt()) + put("status", track.toApiStatus()) + } + + with(json) { + authClient.newCall(PUT(url.toString(), body=payload.toString().toRequestBody(jsonMime))) + .awaitSuccess() + .parseAs() + .toTrack(trackId) + } + } + } + + suspend fun updateUserManga(track: Track): Track = addUserManga(track) + + private val json: Json by injectLazy() + private val authClient = client.newBuilder().addInterceptor(interceptor).build() + + companion object { + const val BASE_API_URL = "https://hikka.io/api" + const val BASE_URL = "https://hikka.io" + private const val SCOPE = "readlist,read:user-details" + private const val REFERENCE = "49eda83d-baa6-45f8-9936-b2a41d944da4" + + fun authUrl(): Uri = "$BASE_URL/oauth".toUri().buildUpon() + .appendQueryParameter("reference", REFERENCE) + .appendQueryParameter("scope", SCOPE) + .build() + + fun refreshTokenRequest(oauth: HKOAuth): Request { + val headers = Headers.Builder() + .add("auth", oauth.accessToken) + .add("Cookie", "auth=${oauth.accessToken}") + .build() + + return GET("$BASE_API_URL/auth/token/info", headers = headers) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt new file mode 100644 index 000000000..220b30224 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt @@ -0,0 +1,88 @@ +package eu.kanade.tachiyomi.data.track.hikka + +import android.util.Log +import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth +import kotlinx.serialization.json.Json +import okhttp3.Interceptor +import okhttp3.Response +import org.json.JSONObject +import uy.kohesive.injekt.injectLazy +import java.io.IOException + +class HikkaInterceptor(private val hikka: Hikka) : Interceptor { + + private val json: Json by injectLazy() + private var oauth: HKOAuth? = hikka.loadOAuth() + private val tokenExpired get() = hikka.getIfAuthExpired() + + override fun intercept(chain: Interceptor.Chain): Response { + if (tokenExpired) { + throw HKTokenExpired() + } + val originalRequest = chain.request() + + if (oauth?.isExpired() == true) { + refreshToken(chain) + } + + if (oauth == null) { + throw IOException("Hikka.io: User is not authenticated") + } + + val authRequest = originalRequest.newBuilder() + .addHeader("auth", oauth!!.accessToken) + .addHeader("Cookie", "auth=${oauth!!.accessToken}") + .addHeader("accept", "application/json") + .build() + + Log.println(Log.WARN, "interceptor", "Set Auth Request Headers: " + authRequest.headers) + + return chain.proceed(authRequest) + } + + /** + * Called when the user authenticates with MyAnimeList for the first time. Sets the refresh token + * and the oauth object. + */ + fun setAuth(oauth: HKOAuth?) { + this.oauth = oauth + hikka.saveOAuth(oauth) + } + + private fun refreshToken(chain: Interceptor.Chain): HKOAuth = synchronized(this) { + if (tokenExpired) throw HKTokenExpired() + oauth?.takeUnless { it.isExpired() }?.let { return@synchronized it } + + val response = try { + chain.proceed(HikkaApi.refreshTokenRequest(oauth!!)) + } catch (_: Throwable) { + throw HKTokenRefreshFailed() + } + + if (response.code == 401) { + hikka.setAuthExpired() + throw HKTokenExpired() + } + + return runCatching { + if (response.isSuccessful && oauth != null) { + val responseBody = response.body?.string() ?: return@runCatching null + val jsonObject = JSONObject(responseBody) + + val secret = oauth!!.accessToken + val expiration = jsonObject.getLong("expiration") + + HKOAuth(secret, expiration) + } else { + response.close() + null + } + }.getOrNull()?.also { + this.oauth = it + hikka.saveOAuth(it) + } ?: throw HKTokenRefreshFailed() + } +} + +class HKTokenRefreshFailed : IOException("Hikka.io: Failed to refresh account token") +class HKTokenExpired : IOException("Hikka.io: Login has expired") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt new file mode 100644 index 000000000..42a3b0f16 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt @@ -0,0 +1,33 @@ +package eu.kanade.tachiyomi.data.track.hikka + +import eu.kanade.tachiyomi.data.database.models.Track +import java.security.MessageDigest + +fun Track.toApiStatus() = when (status) { + Hikka.READING -> "reading" + Hikka.COMPLETED -> "completed" + Hikka.ON_HOLD -> "on_hold" + Hikka.DROPPED -> "dropped" + Hikka.PLAN_TO_READ -> "planned" + Hikka.REREADING -> "completed" + else -> throw NotImplementedError("To Api: Unknown status: $status") +} + +fun toTrackStatus(status: String) = when (status) { + "reading" -> Hikka.READING + "completed" -> Hikka.COMPLETED + "on_hold" -> Hikka.ON_HOLD + "dropped" -> Hikka.DROPPED + "planned" -> Hikka.PLAN_TO_READ + "rewatching" -> Hikka.REREADING + else -> throw NotImplementedError("To Track: Unknown status: $status") +} + +fun stringToNumber(input: String): Long { + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(input.toByteArray()) + + return hash.copyOfRange(0, 8).fold(0L) { acc, byte -> + acc shl 8 or (byte.toLong() and 0xff) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKAuthTokenInfo.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKAuthTokenInfo.kt new file mode 100644 index 000000000..13f8233d3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKAuthTokenInfo.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.data.track.hikka.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class HKAuthTokenInfo( + val reference: String, + val created: Long, + val client: HKClient, + val scope: List, + val expiration: Long, + val used: Long +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKClient.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKClient.kt new file mode 100644 index 000000000..98af1651a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKClient.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.data.track.hikka.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class HKClient( + val reference: String, + val name: String, + val description: String, + val verified: Boolean, + val user: HKUser, + val created: Long, + val updated: Long +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt new file mode 100644 index 000000000..8f7990ba6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt @@ -0,0 +1,40 @@ +package eu.kanade.tachiyomi.data.track.hikka.dto + +import eu.kanade.tachiyomi.data.track.hikka.HikkaApi +import eu.kanade.tachiyomi.data.track.hikka.stringToNumber +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class HKManga( + @SerialName("data_type") val dataType: String, + @SerialName("title_original") val titleOriginal: String, + @SerialName("media_type") val mediaType: String, + @SerialName("title_ua") val titleUa: String? = null, + @SerialName("title_en") val titleEn: String? = null, + val chapters: Int? = null, + val volumes: Int? = null, + @SerialName("translated_ua") val translatedUa: Boolean, + val status: String, + val image: String, + val year: Int, + @SerialName("scored_by") val scoredBy: Int, + val score: Double, + val slug: String +) { + fun toTrack(trackId: Long): TrackSearch { + return TrackSearch.create(trackId).apply { + remote_id = stringToNumber(this@HKManga.slug) + title = this@HKManga.titleUa ?: this@HKManga.titleEn ?: this@HKManga.titleOriginal + total_chapters = this@HKManga.chapters?.toLong() ?: 0 + cover_url = this@HKManga.image + summary = "" + score = this@HKManga.score + tracking_url = HikkaApi.BASE_URL + "/manga/${this@HKManga.slug}" + publishing_status = this@HKManga.status + publishing_type = "manga" + start_date = "" + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKMangaPagination.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKMangaPagination.kt new file mode 100644 index 000000000..77641cbc2 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKMangaPagination.kt @@ -0,0 +1,9 @@ +package eu.kanade.tachiyomi.data.track.hikka.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class HKMangaPagination( + val pagination: HKPagination, + val list: List +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt new file mode 100644 index 000000000..7b9caec5d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.data.track.hikka.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class HKOAuth( + @SerialName("secret") + val accessToken: String, + + @SerialName("expiration") + val expiration: Long, +) { + fun isExpired(): Boolean { + return (expiration - 1000) < System.currentTimeMillis() / 1000 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKPagination.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKPagination.kt new file mode 100644 index 000000000..d5b056175 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKPagination.kt @@ -0,0 +1,10 @@ +package eu.kanade.tachiyomi.data.track.hikka.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class HKPagination( + val total: Int, + val pages: Int, + val page: Int +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt new file mode 100644 index 000000000..80093a278 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt @@ -0,0 +1,34 @@ +package eu.kanade.tachiyomi.data.track.hikka.dto + +import eu.kanade.tachiyomi.data.track.hikka.HikkaApi +import eu.kanade.tachiyomi.data.track.hikka.stringToNumber +import eu.kanade.tachiyomi.data.track.hikka.toTrackStatus +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import kotlinx.serialization.Serializable + +@Serializable +data class HKRead( + val reference: String, + val note: String, + val updated: Long, + val created: Long, + val status: String, + val chapters: Int, + val volumes: Int, + val rereads: Int, + val score: Int, + val content: HKManga +) { + fun toTrack(trackId: Long): TrackSearch { + return TrackSearch.create(trackId).apply { + title = this@HKRead.content.titleUa ?: this@HKRead.content.titleEn ?: this@HKRead.content.titleOriginal + remote_id = stringToNumber(this@HKRead.content.slug) + total_chapters = this@HKRead.content.chapters?.toLong() ?: 0 + library_id = stringToNumber(this@HKRead.content.slug) + last_chapter_read = this@HKRead.chapters.toDouble() + score = this@HKRead.score.toDouble() + status = toTrackStatus(this@HKRead.status) + tracking_url = HikkaApi.BASE_URL + "/manga/${this@HKRead.content.slug}" + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKUser.kt new file mode 100644 index 000000000..5541eea70 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKUser.kt @@ -0,0 +1,16 @@ +package eu.kanade.tachiyomi.data.track.hikka.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class HKUser( + val reference: String, + val updated: Long, + val created: Long, + val description: String, + val username: String, + val cover: String, + val active: Boolean, + val avatar: String, + val role: String +) From 06f01de4b58e7ed72b4b6f55110e7222bf0fdc0d Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Thu, 17 Oct 2024 17:55:18 +0300 Subject: [PATCH 03/17] feat: update AndroidManifest for auth support, modify auth-related classes, and add Hikka to tracker list --- app/src/main/AndroidManifest.xml | 1 + .../more/settings/screen/SettingsTrackingScreen.kt | 7 +++++++ .../kanade/tachiyomi/data/track/TrackerManager.kt | 4 +++- .../ui/setting/track/TrackLoginActivity.kt | 14 ++++++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d11579ed8..d6a848e13 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -180,6 +180,7 @@ + diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt index 021f0ceb2..4a45a566c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt @@ -47,6 +47,7 @@ import eu.kanade.tachiyomi.data.track.Tracker import eu.kanade.tachiyomi.data.track.TrackerManager import eu.kanade.tachiyomi.data.track.anilist.AnilistApi import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi +import eu.kanade.tachiyomi.data.track.hikka.HikkaApi import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi import eu.kanade.tachiyomi.util.system.openInBrowser @@ -164,6 +165,12 @@ object SettingsTrackingScreen : SearchableSettings { login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) }, logout = { dialog = LogoutDialog(trackerManager.bangumi) }, ), + Preference.PreferenceItem.TrackerPreference( + title = trackerManager.hikka.name, + tracker = trackerManager.hikka, + login = { context.openInBrowser(HikkaApi.authUrl(), forceDefaultBrowser = true) }, + logout = { dialog = LogoutDialog(trackerManager.hikka) }, + ), Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.tracking_info)), ), ), diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt index 1071fa7ee..1935a6b1e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackerManager.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.bangumi.Bangumi +import eu.kanade.tachiyomi.data.track.hikka.Hikka import eu.kanade.tachiyomi.data.track.kavita.Kavita import eu.kanade.tachiyomi.data.track.kitsu.Kitsu import eu.kanade.tachiyomi.data.track.komga.Komga @@ -28,8 +29,9 @@ class TrackerManager { val mangaUpdates = MangaUpdates(7L) val kavita = Kavita(KAVITA) val suwayomi = Suwayomi(9L) + val hikka = Hikka(10L) - val trackers = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi) + val trackers = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi, hikka) fun loggedInTrackers() = trackers.filter { it.isLoggedIn } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt index 3f742cfe0..5f1ef2c69 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt @@ -12,6 +12,7 @@ class TrackLoginActivity : BaseOAuthLoginActivity() { "bangumi-auth" -> handleBangumi(data) "myanimelist-auth" -> handleMyAnimeList(data) "shikimori-auth" -> handleShikimori(data) + "hikka-auth" -> handleHikka(data) } } @@ -67,4 +68,17 @@ class TrackLoginActivity : BaseOAuthLoginActivity() { returnToSettings() } } + + private fun handleHikka(data: Uri) { + val code = data.getQueryParameter("code") + if (code != null) { + lifecycleScope.launchIO { + trackerManager.hikka.login(code) + returnToSettings() + } + } else { + trackerManager.hikka.logout() + returnToSettings() + } + } } From 03f9ae8bb232e0353466aea42d6721f692c8a1fe Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Thu, 17 Oct 2024 20:32:47 +0300 Subject: [PATCH 04/17] feat: improve Hikka authorization --- .../tachiyomi/data/track/hikka/Hikka.kt | 10 +++------- .../tachiyomi/data/track/hikka/HikkaApi.kt | 20 +++++++++++++------ .../data/track/hikka/HikkaInterceptor.kt | 15 ++++---------- .../tachiyomi/data/track/hikka/dto/HKOAuth.kt | 2 +- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt index efaaaa390..4cdfa384a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt @@ -117,10 +117,6 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker { return update(track) } - private suspend fun add(track: eu.kanade.tachiyomi.data.database.models.Track): eu.kanade.tachiyomi.data.database.models.Track { - return api.addUserManga(track) - } - override suspend fun search(query: String): List { return api.searchManga(query) } @@ -136,10 +132,10 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker { suspend fun login(code: String) { try { - val oauth = HKOAuth(code, System.currentTimeMillis() / 1000 + 30 * 60) + val oauth = api.accessToken(code) interceptor.setAuth(oauth) - val reference = api.getCurrentUser().reference - saveCredentials(reference, oauth.accessToken) + val user = api.getCurrentUser() + saveCredentials(user.reference, oauth.accessToken) } catch (e: Throwable) { logout() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt index f3bcc5874..c44d1e2cd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt @@ -48,20 +48,29 @@ class HikkaApi( } } - suspend fun getTokenInfo(): HKAuthTokenInfo { + suspend fun getTokenInfo(accessToken: String): HKAuthTokenInfo { return withIOContext { val request = Request.Builder() .url("${BASE_API_URL}/auth/token/info") + .header("auth", accessToken) .get() .build() with(json) { - authClient.newCall(request) + client.newCall(request) .awaitSuccess() .parseAs() } } } + suspend fun accessToken(code: String): HKOAuth { + return withIOContext { + val tokenInfo = getTokenInfo(code) + val oauth = HKOAuth(code, tokenInfo.expiration) + oauth + } + } + suspend fun searchManga(query: String): List { return withIOContext { val url = "$BASE_API_URL/manga".toUri().buildUpon() @@ -165,13 +174,12 @@ class HikkaApi( .appendQueryParameter("scope", SCOPE) .build() - fun refreshTokenRequest(oauth: HKOAuth): Request { + fun refreshTokenRequest(hkOAuth: HKOAuth): Request { val headers = Headers.Builder() - .add("auth", oauth.accessToken) - .add("Cookie", "auth=${oauth.accessToken}") + .add("auth", hkOAuth.accessToken) .build() - return GET("$BASE_API_URL/auth/token/info", headers = headers) + return GET("$BASE_API_URL/user/me", headers = headers) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt index 220b30224..8116bd7f0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt @@ -26,24 +26,17 @@ class HikkaInterceptor(private val hikka: Hikka) : Interceptor { } if (oauth == null) { - throw IOException("Hikka.io: User is not authenticated") + throw IOException("User is not authenticated") } val authRequest = originalRequest.newBuilder() .addHeader("auth", oauth!!.accessToken) - .addHeader("Cookie", "auth=${oauth!!.accessToken}") .addHeader("accept", "application/json") .build() - Log.println(Log.WARN, "interceptor", "Set Auth Request Headers: " + authRequest.headers) - return chain.proceed(authRequest) } - /** - * Called when the user authenticates with MyAnimeList for the first time. Sets the refresh token - * and the oauth object. - */ fun setAuth(oauth: HKOAuth?) { this.oauth = oauth hikka.saveOAuth(oauth) @@ -59,7 +52,7 @@ class HikkaInterceptor(private val hikka: Hikka) : Interceptor { throw HKTokenRefreshFailed() } - if (response.code == 401) { + if (response.code != 200) { hikka.setAuthExpired() throw HKTokenExpired() } @@ -84,5 +77,5 @@ class HikkaInterceptor(private val hikka: Hikka) : Interceptor { } } -class HKTokenRefreshFailed : IOException("Hikka.io: Failed to refresh account token") -class HKTokenExpired : IOException("Hikka.io: Login has expired") +class HKTokenRefreshFailed : IOException("Hikka: Failed to refresh account token") +class HKTokenExpired : IOException("Hikka: Login has expired") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt index 7b9caec5d..f4822537e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt @@ -12,6 +12,6 @@ data class HKOAuth( val expiration: Long, ) { fun isExpired(): Boolean { - return (expiration - 1000) < System.currentTimeMillis() / 1000 + return (expiration - 7200) < (System.currentTimeMillis() / 1000) } } From 9eeed9f0503ecedc05e6b1f707060235545e3ea4 Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Thu, 17 Oct 2024 20:33:09 +0300 Subject: [PATCH 05/17] feat: improve Hikka authorization --- .../eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt index 8116bd7f0..87e53c8fe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.data.track.hikka -import android.util.Log import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth import kotlinx.serialization.json.Json import okhttp3.Interceptor From f39e947ece13a36dc05056b19b3a514e05cbe74d Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Thu, 17 Oct 2024 22:25:48 +0300 Subject: [PATCH 06/17] feat: more optimization Hikka authorization --- .../tachiyomi/data/track/hikka/Hikka.kt | 8 --- .../tachiyomi/data/track/hikka/HikkaApi.kt | 14 ++++- .../data/track/hikka/HikkaInterceptor.kt | 61 ++++--------------- .../tachiyomi/data/track/hikka/dto/HKOAuth.kt | 6 +- 4 files changed, 25 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt index 4cdfa384a..7512728f5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt @@ -151,14 +151,6 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker { interceptor.setAuth(null) } - fun getIfAuthExpired(): Boolean { - return trackPreferences.trackAuthExpired(this).get() - } - - fun setAuthExpired() { - trackPreferences.trackAuthExpired(this).set(true) - } - fun saveOAuth(oAuth: HKOAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oAuth)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt index c44d1e2cd..10104cead 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt @@ -174,12 +174,20 @@ class HikkaApi( .appendQueryParameter("scope", SCOPE) .build() - fun refreshTokenRequest(hkOAuth: HKOAuth): Request { + fun refreshTokenRequest(accessToken: String): Request { val headers = Headers.Builder() - .add("auth", hkOAuth.accessToken) + .add("auth", accessToken) .build() - return GET("$BASE_API_URL/user/me", headers = headers) + return GET("$BASE_API_URL/user/me", headers = headers) // Any request with auth + } + + fun authTokenInfo(accessToken: String): Request { + val headers = Headers.Builder() + .add("auth", accessToken) + .build() + + return GET("$BASE_API_URL/auth/token/info", headers = headers) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt index 87e53c8fe..6b9e92d29 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt @@ -1,31 +1,33 @@ package eu.kanade.tachiyomi.data.track.hikka +import eu.kanade.tachiyomi.data.track.hikka.dto.HKAuthTokenInfo import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth import kotlinx.serialization.json.Json import okhttp3.Interceptor import okhttp3.Response -import org.json.JSONObject import uy.kohesive.injekt.injectLazy -import java.io.IOException class HikkaInterceptor(private val hikka: Hikka) : Interceptor { private val json: Json by injectLazy() private var oauth: HKOAuth? = hikka.loadOAuth() - private val tokenExpired get() = hikka.getIfAuthExpired() override fun intercept(chain: Interceptor.Chain): Response { - if (tokenExpired) { - throw HKTokenExpired() - } val originalRequest = chain.request() - if (oauth?.isExpired() == true) { - refreshToken(chain) - } + val currAuth = oauth ?: throw Exception("Not authenticated with Hikka") - if (oauth == null) { - throw IOException("User is not authenticated") + if (currAuth.isExpired()) { + val refreshTokenResponse = chain.proceed(HikkaApi.refreshTokenRequest(currAuth.accessToken)) + if (!refreshTokenResponse.isSuccessful) + refreshTokenResponse.close() + + val authTokenInfoResponse = chain.proceed(HikkaApi.authTokenInfo(currAuth.accessToken)) + if (!authTokenInfoResponse.isSuccessful) + authTokenInfoResponse.close() + + val authTokenInfo = json.decodeFromString(authTokenInfoResponse.body.string()) + setAuth(HKOAuth(oauth!!.accessToken, authTokenInfo.expiration)) } val authRequest = originalRequest.newBuilder() @@ -40,41 +42,4 @@ class HikkaInterceptor(private val hikka: Hikka) : Interceptor { this.oauth = oauth hikka.saveOAuth(oauth) } - - private fun refreshToken(chain: Interceptor.Chain): HKOAuth = synchronized(this) { - if (tokenExpired) throw HKTokenExpired() - oauth?.takeUnless { it.isExpired() }?.let { return@synchronized it } - - val response = try { - chain.proceed(HikkaApi.refreshTokenRequest(oauth!!)) - } catch (_: Throwable) { - throw HKTokenRefreshFailed() - } - - if (response.code != 200) { - hikka.setAuthExpired() - throw HKTokenExpired() - } - - return runCatching { - if (response.isSuccessful && oauth != null) { - val responseBody = response.body?.string() ?: return@runCatching null - val jsonObject = JSONObject(responseBody) - - val secret = oauth!!.accessToken - val expiration = jsonObject.getLong("expiration") - - HKOAuth(secret, expiration) - } else { - response.close() - null - } - }.getOrNull()?.also { - this.oauth = it - hikka.saveOAuth(it) - } ?: throw HKTokenRefreshFailed() - } } - -class HKTokenRefreshFailed : IOException("Hikka: Failed to refresh account token") -class HKTokenExpired : IOException("Hikka: Login has expired") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt index f4822537e..69f34a74d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt @@ -1,17 +1,13 @@ package eu.kanade.tachiyomi.data.track.hikka.dto -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class HKOAuth( - @SerialName("secret") val accessToken: String, - - @SerialName("expiration") val expiration: Long, ) { fun isExpired(): Boolean { - return (expiration - 7200) < (System.currentTimeMillis() / 1000) + return (expiration - 43200) < (System.currentTimeMillis() / 1000) } } From 41db4a5865155b7df0ac3348735dafd720665ae4 Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Thu, 17 Oct 2024 22:46:26 +0300 Subject: [PATCH 07/17] ref: added more specificity and removed unnecessary code in Hikka Api --- .../eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt | 9 ++------- .../tachiyomi/data/track/hikka/HikkaInterceptor.kt | 3 +-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt index 10104cead..72fd8c755 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt @@ -48,15 +48,10 @@ class HikkaApi( } } - suspend fun getTokenInfo(accessToken: String): HKAuthTokenInfo { + private suspend fun getTokenInfo(accessToken: String): HKAuthTokenInfo { return withIOContext { - val request = Request.Builder() - .url("${BASE_API_URL}/auth/token/info") - .header("auth", accessToken) - .get() - .build() with(json) { - client.newCall(request) + client.newCall(authTokenInfo(accessToken)) .awaitSuccess() .parseAs() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt index 6b9e92d29..886b73ac8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt @@ -8,14 +8,13 @@ import okhttp3.Response import uy.kohesive.injekt.injectLazy class HikkaInterceptor(private val hikka: Hikka) : Interceptor { - private val json: Json by injectLazy() private var oauth: HKOAuth? = hikka.loadOAuth() override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() - val currAuth = oauth ?: throw Exception("Not authenticated with Hikka") + val currAuth = oauth ?: throw Exception("Hikka: You are not authorized") if (currAuth.isExpired()) { val refreshTokenResponse = chain.proceed(HikkaApi.refreshTokenRequest(currAuth.accessToken)) From e332a5018e5222852e20df7e8ceec59f223c1f1f Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Tue, 22 Oct 2024 18:59:57 +0300 Subject: [PATCH 08/17] fix: corrected function names and added reread logic --- .../java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt | 2 +- .../eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt | 9 +++++++-- .../tachiyomi/data/track/hikka/HikkaInterceptor.kt | 1 + .../eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt | 7 +++---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt index 7512728f5..ad93c5846 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt @@ -142,7 +142,7 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker { } override suspend fun delete(track: Track) { - api.deleteManga(track) + api.deleteUserManga(track) } override fun logout() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt index 72fd8c755..a537a4711 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt @@ -116,7 +116,7 @@ class HikkaApi( } } - suspend fun deleteManga(track: tachiyomi.domain.track.model.Track) { + suspend fun deleteUserManga(track: tachiyomi.domain.track.model.Track) { return withIOContext { val slug = track.remoteUrl.split("/")[4] @@ -135,11 +135,16 @@ class HikkaApi( val url = "$BASE_API_URL/read/manga/${slug}".toUri().buildUpon() .build() + var rereads = 0 + + if (track.status == Hikka.REREADING) + rereads = 1 + val payload = buildJsonObject { put("note", "") put("chapters", track.last_chapter_read.toInt()) put("volumes", 0) - put("rereads", 0) + put("rereads", rereads) put("score", track.score.toInt()) put("status", track.toApiStatus()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt index 886b73ac8..db19db4ea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt @@ -19,6 +19,7 @@ class HikkaInterceptor(private val hikka: Hikka) : Interceptor { if (currAuth.isExpired()) { val refreshTokenResponse = chain.proceed(HikkaApi.refreshTokenRequest(currAuth.accessToken)) if (!refreshTokenResponse.isSuccessful) + throw Exception("Hikka: The token is expired") refreshTokenResponse.close() val authTokenInfoResponse = chain.proceed(HikkaApi.authTokenInfo(currAuth.accessToken)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt index 42a3b0f16..a4363c128 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt @@ -9,8 +9,8 @@ fun Track.toApiStatus() = when (status) { Hikka.ON_HOLD -> "on_hold" Hikka.DROPPED -> "dropped" Hikka.PLAN_TO_READ -> "planned" - Hikka.REREADING -> "completed" - else -> throw NotImplementedError("To Api: Unknown status: $status") + Hikka.REREADING -> "reading" + else -> throw NotImplementedError("Hikka: Unknown status: $status") } fun toTrackStatus(status: String) = when (status) { @@ -19,8 +19,7 @@ fun toTrackStatus(status: String) = when (status) { "on_hold" -> Hikka.ON_HOLD "dropped" -> Hikka.DROPPED "planned" -> Hikka.PLAN_TO_READ - "rewatching" -> Hikka.REREADING - else -> throw NotImplementedError("To Track: Unknown status: $status") + else -> throw NotImplementedError("Hikka: Unknown status: $status") } fun stringToNumber(input: String): Long { From 23a34cd3b705b6e466bd250b3beadf23433c45ee Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Sun, 27 Oct 2024 01:02:16 +0300 Subject: [PATCH 09/17] fix: slightly optimised the function of string to number conversion and fixed the check --- .../tachiyomi/data/track/hikka/HikkaInterceptor.kt | 9 ++++++--- .../eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt | 10 +++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt index db19db4ea..e555ab3cc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt @@ -18,13 +18,16 @@ class HikkaInterceptor(private val hikka: Hikka) : Interceptor { if (currAuth.isExpired()) { val refreshTokenResponse = chain.proceed(HikkaApi.refreshTokenRequest(currAuth.accessToken)) - if (!refreshTokenResponse.isSuccessful) - throw Exception("Hikka: The token is expired") + if (!refreshTokenResponse.isSuccessful) { refreshTokenResponse.close() + hikka.logout() + throw Exception("Hikka: The token is expired") + } val authTokenInfoResponse = chain.proceed(HikkaApi.authTokenInfo(currAuth.accessToken)) - if (!authTokenInfoResponse.isSuccessful) + if (!authTokenInfoResponse.isSuccessful) { authTokenInfoResponse.close() + } val authTokenInfo = json.decodeFromString(authTokenInfoResponse.body.string()) setAuth(HKOAuth(oauth!!.accessToken, authTokenInfo.expiration)) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt index a4363c128..a31b692c9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaUtils.kt @@ -1,7 +1,7 @@ package eu.kanade.tachiyomi.data.track.hikka import eu.kanade.tachiyomi.data.database.models.Track -import java.security.MessageDigest +import java.util.UUID fun Track.toApiStatus() = when (status) { Hikka.READING -> "reading" @@ -23,10 +23,6 @@ fun toTrackStatus(status: String) = when (status) { } fun stringToNumber(input: String): Long { - val digest = MessageDigest.getInstance("SHA-256") - val hash = digest.digest(input.toByteArray()) - - return hash.copyOfRange(0, 8).fold(0L) { acc, byte -> - acc shl 8 or (byte.toLong() and 0xff) - } + val uuid = UUID.nameUUIDFromBytes(input.toByteArray()) + return uuid.mostSignificantBits and Long.MAX_VALUE } From 6b8c2dcdc3ae6f4aab96d5f662fa492f7a59a0e9 Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Sun, 27 Oct 2024 02:14:17 +0300 Subject: [PATCH 10/17] fix: recode the rereading logic in HikkaApi --- .../tachiyomi/data/track/hikka/HikkaApi.kt | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt index a537a4711..e0a6306cf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt @@ -100,6 +100,21 @@ class HikkaApi( } } + private suspend fun getRead(track: Track): HKRead { + return withIOContext { + val slug = track.tracking_url.split("/")[4] + + val url = "$BASE_API_URL/read/manga/${slug}".toUri().buildUpon() + .build() + + with(json) { + authClient.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + } + } + } + suspend fun getManga(track: Track): TrackSearch { return withIOContext { val slug = track.tracking_url.split("/")[4] @@ -135,10 +150,10 @@ class HikkaApi( val url = "$BASE_API_URL/read/manga/${slug}".toUri().buildUpon() .build() - var rereads = 0 - - if (track.status == Hikka.REREADING) + var rereads = getRead(track).rereads + if (track.status == Hikka.REREADING && rereads == 0) { rereads = 1 + } val payload = buildJsonObject { put("note", "") From 45cd945f7c5d0b97b8cefdb94df8551c7d29c3a0 Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Sun, 27 Oct 2024 02:25:24 +0300 Subject: [PATCH 11/17] fix: fixed a bug where the server returned a 404 error due to receiving data in HikkaApi --- .../tachiyomi/data/track/hikka/HikkaApi.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt index e0a6306cf..4852fdd2e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt @@ -4,8 +4,8 @@ import android.net.Uri import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.hikka.dto.HKAuthTokenInfo -import eu.kanade.tachiyomi.data.track.hikka.dto.HKMangaPagination import eu.kanade.tachiyomi.data.track.hikka.dto.HKManga +import eu.kanade.tachiyomi.data.track.hikka.dto.HKMangaPagination import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth import eu.kanade.tachiyomi.data.track.hikka.dto.HKRead import eu.kanade.tachiyomi.data.track.hikka.dto.HKUser @@ -100,17 +100,18 @@ class HikkaApi( } } - private suspend fun getRead(track: Track): HKRead { + private suspend fun getRead(track: Track): HKRead? { return withIOContext { val slug = track.tracking_url.split("/")[4] - - val url = "$BASE_API_URL/read/manga/${slug}".toUri().buildUpon() - .build() - + val url = "$BASE_API_URL/read/manga/${slug}".toUri().buildUpon().build() with(json) { - authClient.newCall(GET(url.toString())) - .awaitSuccess() - .parseAs() + val response = authClient.newCall(GET(url.toString())).execute() + if (response.code == 404) { + return@withIOContext null + } + response.use { + it.parseAs() + } } } } @@ -150,7 +151,7 @@ class HikkaApi( val url = "$BASE_API_URL/read/manga/${slug}".toUri().buildUpon() .build() - var rereads = getRead(track).rereads + var rereads = getRead(track)?.rereads ?: 0 if (track.status == Hikka.REREADING && rereads == 0) { rereads = 1 } From a8f4e63d54478d5b3f16561b8e396ed6ca2b578e Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Sun, 27 Oct 2024 03:00:28 +0300 Subject: [PATCH 12/17] ref: optimized imports and reduced the length of lines --- .../kanade/tachiyomi/data/track/hikka/Hikka.kt | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt index ad93c5846..48d5e5926 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.track.hikka import android.graphics.Color import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.BaseTracker import eu.kanade.tachiyomi.data.track.DeletableTracker import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth @@ -11,7 +12,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import tachiyomi.domain.track.model.Track +import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.i18n.MR import uy.kohesive.injekt.injectLazy @@ -80,14 +81,14 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker { return SCORE_LIST } - override fun displayScore(track: Track): String { + override fun displayScore(track: DomainTrack): String { return track.score.toInt().toString() } override suspend fun update( - track: eu.kanade.tachiyomi.data.database.models.Track, + track: Track, didReadChapter: Boolean, - ): eu.kanade.tachiyomi.data.database.models.Track { + ): Track { if (track.status != COMPLETED) { if (didReadChapter) { if (track.last_chapter_read.toLong() == track.total_chapters && track.total_chapters > 0) { @@ -100,10 +101,7 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker { return api.updateUserManga(track) } - override suspend fun bind( - track: eu.kanade.tachiyomi.data.database.models.Track, - hasReadChapters: Boolean, - ): eu.kanade.tachiyomi.data.database.models.Track { + override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { val remoteTrack = api.getManga(track) track.copyPersonalFrom(remoteTrack) @@ -121,7 +119,7 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker { return api.searchManga(query) } - override suspend fun refresh(track: eu.kanade.tachiyomi.data.database.models.Track): eu.kanade.tachiyomi.data.database.models.Track { + override suspend fun refresh(track: Track): Track { val remoteTrack = api.updateUserManga(track) track.copyPersonalFrom(remoteTrack) track.total_chapters = remoteTrack.total_chapters @@ -141,7 +139,7 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker { } } - override suspend fun delete(track: Track) { + override suspend fun delete(track: DomainTrack) { api.deleteUserManga(track) } From f480d4cb38981ca92d202b83338f7a012d5c954d Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Sun, 27 Oct 2024 15:31:12 +0300 Subject: [PATCH 13/17] ref: optimized imports and refactor code --- .../tachiyomi/data/track/hikka/Hikka.kt | 6 ++-- .../tachiyomi/data/track/hikka/HikkaApi.kt | 34 +++++++++++-------- .../data/track/hikka/dto/HKAuthTokenInfo.kt | 2 +- .../data/track/hikka/dto/HKClient.kt | 2 +- .../tachiyomi/data/track/hikka/dto/HKManga.kt | 2 +- .../data/track/hikka/dto/HKMangaPagination.kt | 2 +- .../data/track/hikka/dto/HKPagination.kt | 2 +- .../tachiyomi/data/track/hikka/dto/HKRead.kt | 2 +- .../tachiyomi/data/track/hikka/dto/HKUser.kt | 2 +- 9 files changed, 30 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt index 48d5e5926..12524e716 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt @@ -12,9 +12,9 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json -import tachiyomi.domain.track.model.Track as DomainTrack import tachiyomi.i18n.MR import uy.kohesive.injekt.injectLazy +import tachiyomi.domain.track.model.Track as DomainTrack class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker { @@ -51,7 +51,7 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker { ON_HOLD, DROPPED, PLAN_TO_READ, - REREADING + REREADING, ) } @@ -132,7 +132,7 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker { try { val oauth = api.accessToken(code) interceptor.setAuth(oauth) - val user = api.getCurrentUser() + val user = api.getCurrentUser() saveCredentials(user.reference, oauth.accessToken) } catch (e: Throwable) { logout() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt index 4852fdd2e..18108ddb6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt @@ -79,19 +79,25 @@ class HikkaApi( put("only_translated", false) put("magazines", buildJsonArray { }) put("genres", buildJsonArray { }) - put("score", buildJsonArray { - add(0) - add(10) - }) + put( + "score", + buildJsonArray { + add(0) + add(10) + }, + ) put("query", query) - put("sort", buildJsonArray { - add("score:asc") - add("scored_by:asc") - }) + put( + "sort", + buildJsonArray { + add("score:asc") + add("scored_by:asc") + }, + ) } with(json) { - authClient.newCall(POST(url.toString(), body=payload.toString().toRequestBody(jsonMime))) + authClient.newCall(POST(url.toString(), body = payload.toString().toRequestBody(jsonMime))) .awaitSuccess() .parseAs() .list @@ -103,7 +109,7 @@ class HikkaApi( private suspend fun getRead(track: Track): HKRead? { return withIOContext { val slug = track.tracking_url.split("/")[4] - val url = "$BASE_API_URL/read/manga/${slug}".toUri().buildUpon().build() + val url = "$BASE_API_URL/read/manga/$slug".toUri().buildUpon().build() with(json) { val response = authClient.newCall(GET(url.toString())).execute() if (response.code == 404) { @@ -120,7 +126,7 @@ class HikkaApi( return withIOContext { val slug = track.tracking_url.split("/")[4] - val url = "$BASE_API_URL/manga/${slug}".toUri().buildUpon() + val url = "$BASE_API_URL/manga/$slug".toUri().buildUpon() .build() with(json) { @@ -136,7 +142,7 @@ class HikkaApi( return withIOContext { val slug = track.remoteUrl.split("/")[4] - val url = "$BASE_API_URL/read/manga/${slug}".toUri().buildUpon() + val url = "$BASE_API_URL/read/manga/$slug".toUri().buildUpon() .build() authClient.newCall(DELETE(url.toString())) @@ -148,7 +154,7 @@ class HikkaApi( return withIOContext { val slug = track.tracking_url.split("/")[4] - val url = "$BASE_API_URL/read/manga/${slug}".toUri().buildUpon() + val url = "$BASE_API_URL/read/manga/$slug".toUri().buildUpon() .build() var rereads = getRead(track)?.rereads ?: 0 @@ -166,7 +172,7 @@ class HikkaApi( } with(json) { - authClient.newCall(PUT(url.toString(), body=payload.toString().toRequestBody(jsonMime))) + authClient.newCall(PUT(url.toString(), body = payload.toString().toRequestBody(jsonMime))) .awaitSuccess() .parseAs() .toTrack(trackId) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKAuthTokenInfo.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKAuthTokenInfo.kt index 13f8233d3..7f0e096a6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKAuthTokenInfo.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKAuthTokenInfo.kt @@ -9,5 +9,5 @@ data class HKAuthTokenInfo( val client: HKClient, val scope: List, val expiration: Long, - val used: Long + val used: Long, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKClient.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKClient.kt index 98af1651a..824cffbb9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKClient.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKClient.kt @@ -10,5 +10,5 @@ data class HKClient( val verified: Boolean, val user: HKUser, val created: Long, - val updated: Long + val updated: Long, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt index 8f7990ba6..ff35a5b13 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt @@ -21,7 +21,7 @@ data class HKManga( val year: Int, @SerialName("scored_by") val scoredBy: Int, val score: Double, - val slug: String + val slug: String, ) { fun toTrack(trackId: Long): TrackSearch { return TrackSearch.create(trackId).apply { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKMangaPagination.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKMangaPagination.kt index 77641cbc2..a8245fe65 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKMangaPagination.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKMangaPagination.kt @@ -5,5 +5,5 @@ import kotlinx.serialization.Serializable @Serializable data class HKMangaPagination( val pagination: HKPagination, - val list: List + val list: List, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKPagination.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKPagination.kt index d5b056175..3c7e5f2c7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKPagination.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKPagination.kt @@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable data class HKPagination( val total: Int, val pages: Int, - val page: Int + val page: Int, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt index 80093a278..7d95ec242 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt @@ -17,7 +17,7 @@ data class HKRead( val volumes: Int, val rereads: Int, val score: Int, - val content: HKManga + val content: HKManga, ) { fun toTrack(trackId: Long): TrackSearch { return TrackSearch.create(trackId).apply { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKUser.kt index 5541eea70..4210c6b16 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKUser.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKUser.kt @@ -12,5 +12,5 @@ data class HKUser( val cover: String, val active: Boolean, val avatar: String, - val role: String + val role: String, ) From e44666673fe4b2725d270874bb60309c7129b925 Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Sun, 27 Oct 2024 19:44:47 +0300 Subject: [PATCH 14/17] feat: changed authorization principle, removed dependence on third-party server --- .../tachiyomi/data/track/hikka/Hikka.kt | 4 +-- .../tachiyomi/data/track/hikka/HikkaApi.kt | 30 ++++++++++--------- .../data/track/hikka/HikkaInterceptor.kt | 2 +- .../tachiyomi/data/track/hikka/dto/HKOAuth.kt | 5 +++- .../ui/setting/track/TrackLoginActivity.kt | 6 ++-- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt index 12524e716..3bd43a574 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/Hikka.kt @@ -128,9 +128,9 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker { override suspend fun login(username: String, password: String) = login(password) - suspend fun login(code: String) { + suspend fun login(reference: String) { try { - val oauth = api.accessToken(code) + val oauth = api.accessToken(reference) interceptor.setAuth(oauth) val user = api.getCurrentUser() saveCredentials(user.reference, oauth.accessToken) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt index 18108ddb6..6cb9fd9d1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaApi.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.track.hikka import android.net.Uri import androidx.core.net.toUri import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.hikka.dto.HKAuthTokenInfo import eu.kanade.tachiyomi.data.track.hikka.dto.HKManga import eu.kanade.tachiyomi.data.track.hikka.dto.HKMangaPagination import eu.kanade.tachiyomi.data.track.hikka.dto.HKOAuth @@ -48,24 +47,16 @@ class HikkaApi( } } - private suspend fun getTokenInfo(accessToken: String): HKAuthTokenInfo { + suspend fun accessToken(reference: String): HKOAuth { return withIOContext { with(json) { - client.newCall(authTokenInfo(accessToken)) + client.newCall(authTokenCreate(reference)) .awaitSuccess() - .parseAs() + .parseAs() } } } - suspend fun accessToken(code: String): HKOAuth { - return withIOContext { - val tokenInfo = getTokenInfo(code) - val oauth = HKOAuth(code, tokenInfo.expiration) - oauth - } - } - suspend fun searchManga(query: String): List { return withIOContext { val url = "$BASE_API_URL/manga".toUri().buildUpon() @@ -189,10 +180,13 @@ class HikkaApi( const val BASE_API_URL = "https://hikka.io/api" const val BASE_URL = "https://hikka.io" private const val SCOPE = "readlist,read:user-details" - private const val REFERENCE = "49eda83d-baa6-45f8-9936-b2a41d944da4" + private const val CLIENT_REFERENCE = "49eda83d-baa6-45f8-9936-b2a41d944da4" + private const val CLIENT_SECRET = "8Zxzs13Pvikx6m_4rwjF7t2BxxnEb0wWtXIRQ_68HyCvmdhGE9hdfz" + + "SL1Pas4h927LaV2ocjVoc--S_vmorHEWWh42Z_z70j-wSFYsraQQ98" + + "hiOeTH2BaDf77ZcA9W5Z" fun authUrl(): Uri = "$BASE_URL/oauth".toUri().buildUpon() - .appendQueryParameter("reference", REFERENCE) + .appendQueryParameter("reference", CLIENT_REFERENCE) .appendQueryParameter("scope", SCOPE) .build() @@ -204,6 +198,14 @@ class HikkaApi( return GET("$BASE_API_URL/user/me", headers = headers) // Any request with auth } + fun authTokenCreate(reference: String): Request { + val payload = buildJsonObject { + put("request_reference", reference) + put("client_secret", CLIENT_SECRET) + } + return POST("$BASE_API_URL/auth/token", body = payload.toString().toRequestBody(jsonMime)) + } + fun authTokenInfo(accessToken: String): Request { val headers = Headers.Builder() .add("auth", accessToken) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt index e555ab3cc..062b88861 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/HikkaInterceptor.kt @@ -30,7 +30,7 @@ class HikkaInterceptor(private val hikka: Hikka) : Interceptor { } val authTokenInfo = json.decodeFromString(authTokenInfoResponse.body.string()) - setAuth(HKOAuth(oauth!!.accessToken, authTokenInfo.expiration)) + setAuth(HKOAuth(oauth!!.accessToken, authTokenInfo.expiration, authTokenInfo.created)) } val authRequest = originalRequest.newBuilder() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt index 69f34a74d..1c72733e0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt @@ -1,11 +1,14 @@ package eu.kanade.tachiyomi.data.track.hikka.dto +import android.util.Log +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class HKOAuth( - val accessToken: String, + @SerialName("secret") val accessToken: String, val expiration: Long, + val created: Long, ) { fun isExpired(): Boolean { return (expiration - 43200) < (System.currentTimeMillis() / 1000) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt index 5f1ef2c69..9487e57f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/track/TrackLoginActivity.kt @@ -70,10 +70,10 @@ class TrackLoginActivity : BaseOAuthLoginActivity() { } private fun handleHikka(data: Uri) { - val code = data.getQueryParameter("code") - if (code != null) { + val reference = data.getQueryParameter("reference") + if (reference != null) { lifecycleScope.launchIO { - trackerManager.hikka.login(code) + trackerManager.hikka.login(reference) returnToSettings() } } else { From 4a3209267d29785600775b24beec91ac357d31e1 Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Sun, 27 Oct 2024 19:49:18 +0300 Subject: [PATCH 15/17] fix: remove unused import --- .../java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt index 1c72733e0..fbccb8d22 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKOAuth.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.data.track.hikka.dto -import android.util.Log import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable From ef3369be262cd45ff0642fa961efd9db28bcba34 Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Mon, 28 Oct 2024 03:11:31 +0300 Subject: [PATCH 16/17] fix: fixed a bug in DTO that caused a data parsing error --- .../java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt index 7d95ec242..213ac2951 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKRead.kt @@ -9,7 +9,7 @@ import kotlinx.serialization.Serializable @Serializable data class HKRead( val reference: String, - val note: String, + val note: String?, val updated: Long, val created: Long, val status: String, From e9b41975daaa50386f0f07a9c9b35f0cd45e66fd Mon Sep 17 00:00:00 2001 From: Lorg0n Date: Wed, 30 Oct 2024 21:05:06 +0300 Subject: [PATCH 17/17] fix: fixed a bug in DTO that caused a data parsing error in manga search --- .../java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt index ff35a5b13..40928aa6e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/hikka/dto/HKManga.kt @@ -18,7 +18,7 @@ data class HKManga( @SerialName("translated_ua") val translatedUa: Boolean, val status: String, val image: String, - val year: Int, + val year: Int? = null, @SerialName("scored_by") val scoredBy: Int, val score: Double, val slug: String,