2016-12-22 21:17:47 +01:00
|
|
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
import android.net.Uri
|
2020-07-31 16:29:32 +02:00
|
|
|
import androidx.core.net.toUri
|
2016-12-22 21:17:47 +01:00
|
|
|
import eu.kanade.tachiyomi.data.database.models.Track
|
2017-01-20 21:34:15 +01:00
|
|
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
2018-02-17 13:04:49 +01:00
|
|
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
2017-01-20 21:24:31 +01:00
|
|
|
import eu.kanade.tachiyomi.network.GET
|
|
|
|
import eu.kanade.tachiyomi.network.POST
|
2020-12-14 23:57:35 +01:00
|
|
|
import eu.kanade.tachiyomi.network.await
|
|
|
|
import eu.kanade.tachiyomi.util.PkceUtil
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
import kotlinx.coroutines.async
|
|
|
|
import kotlinx.coroutines.awaitAll
|
|
|
|
import kotlinx.coroutines.withContext
|
|
|
|
import kotlinx.serialization.decodeFromString
|
|
|
|
import kotlinx.serialization.json.Json
|
|
|
|
import kotlinx.serialization.json.JsonObject
|
|
|
|
import kotlinx.serialization.json.boolean
|
|
|
|
import kotlinx.serialization.json.int
|
|
|
|
import kotlinx.serialization.json.jsonArray
|
|
|
|
import kotlinx.serialization.json.jsonObject
|
|
|
|
import kotlinx.serialization.json.jsonPrimitive
|
2020-01-05 17:29:27 +01:00
|
|
|
import okhttp3.FormBody
|
|
|
|
import okhttp3.OkHttpClient
|
2020-12-14 23:57:35 +01:00
|
|
|
import okhttp3.Request
|
2020-01-05 17:29:27 +01:00
|
|
|
import okhttp3.RequestBody
|
|
|
|
import okhttp3.Response
|
2020-12-14 23:57:35 +01:00
|
|
|
import uy.kohesive.injekt.injectLazy
|
2020-09-14 00:48:20 +02:00
|
|
|
import java.text.SimpleDateFormat
|
|
|
|
import java.util.Locale
|
2016-12-22 21:17:47 +01:00
|
|
|
|
2020-01-08 01:20:08 +01:00
|
|
|
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
2016-12-22 21:17:47 +01:00
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
private val json: Json by injectLazy()
|
2019-06-09 14:31:19 +02:00
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
2019-06-09 14:31:19 +02:00
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
suspend fun getAccessToken(authCode: String): OAuth {
|
|
|
|
return withContext(Dispatchers.IO) {
|
|
|
|
val formBody: RequestBody = FormBody.Builder()
|
|
|
|
.add("client_id", clientId)
|
|
|
|
.add("code", authCode)
|
|
|
|
.add("code_verifier", codeVerifier)
|
|
|
|
.add("grant_type", "authorization_code")
|
|
|
|
.build()
|
|
|
|
client.newCall(POST("$baseOAuthUrl/token", body = formBody)).await().use {
|
|
|
|
val responseBody = it.body?.string().orEmpty()
|
|
|
|
json.decodeFromString(responseBody)
|
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
suspend fun getCurrentUser(): String {
|
|
|
|
return withContext(Dispatchers.IO) {
|
|
|
|
val request = Request.Builder()
|
|
|
|
.url("$baseApiUrl/users/@me")
|
|
|
|
.get()
|
|
|
|
.build()
|
|
|
|
authClient.newCall(request).await().use {
|
|
|
|
val responseBody = it.body?.string().orEmpty()
|
|
|
|
val response = json.decodeFromString<JsonObject>(responseBody)
|
|
|
|
response["name"]!!.jsonPrimitive.content
|
2020-04-23 03:23:23 +02:00
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
suspend fun search(query: String): List<TrackSearch> {
|
|
|
|
return withContext(Dispatchers.IO) {
|
|
|
|
val url = "$baseApiUrl/manga".toUri().buildUpon()
|
|
|
|
.appendQueryParameter("q", query)
|
|
|
|
.build()
|
|
|
|
authClient.newCall(GET(url.toString())).await().use {
|
|
|
|
val responseBody = it.body?.string().orEmpty()
|
|
|
|
val response = json.decodeFromString<JsonObject>(responseBody)
|
|
|
|
response["data"]!!.jsonArray.map {
|
|
|
|
val node = it.jsonObject["node"]!!.jsonObject
|
|
|
|
val id = node["id"]!!.jsonPrimitive.int
|
|
|
|
async { getMangaDetails(id) }
|
|
|
|
}.awaitAll()
|
2020-04-25 20:24:45 +02:00
|
|
|
}
|
2020-12-14 23:57:35 +01:00
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
private suspend fun getMangaDetails(id: Int): TrackSearch {
|
|
|
|
return withContext(Dispatchers.IO) {
|
|
|
|
val url = "$baseApiUrl/manga".toUri().buildUpon()
|
|
|
|
.appendPath(id.toString())
|
|
|
|
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
|
|
|
|
.build()
|
|
|
|
authClient.newCall(GET(url.toString())).await().use {
|
|
|
|
val responseBody = it.body?.string().orEmpty()
|
|
|
|
val response = json.decodeFromString<JsonObject>(responseBody)
|
|
|
|
val obj = response.jsonObject
|
2020-04-25 20:24:45 +02:00
|
|
|
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
2020-12-14 23:57:35 +01:00
|
|
|
media_id = obj["id"]!!.jsonPrimitive.int
|
|
|
|
title = obj["title"]!!.jsonPrimitive.content
|
|
|
|
summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
|
|
|
|
total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
|
|
|
|
cover_url = obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content ?: ""
|
|
|
|
tracking_url = "https://myanimelist.net/manga/$media_id"
|
2020-12-21 23:24:25 +01:00
|
|
|
publishing_status = obj["status"]!!.jsonPrimitive.content.replace("_", " ")
|
|
|
|
publishing_type = obj["media_type"]!!.jsonPrimitive.content.replace("_", " ")
|
2020-12-14 23:57:35 +01:00
|
|
|
start_date = try {
|
|
|
|
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
|
|
|
outputDf.format(obj["start_date"]!!)
|
|
|
|
} catch (e: Exception) {
|
|
|
|
""
|
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
2020-04-25 20:24:45 +02:00
|
|
|
}
|
2018-11-11 14:00:47 +01:00
|
|
|
}
|
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
suspend fun getListItem(track: Track): Track {
|
|
|
|
return withContext(Dispatchers.IO) {
|
|
|
|
val formBody: RequestBody = FormBody.Builder()
|
|
|
|
.add("status", track.toMyAnimeListStatus() ?: "reading")
|
|
|
|
.build()
|
|
|
|
val request = Request.Builder()
|
|
|
|
.url(mangaUrl(track.media_id).toString())
|
|
|
|
.put(formBody)
|
|
|
|
.build()
|
|
|
|
authClient.newCall(request).await().use {
|
|
|
|
parseMangaItem(it, track)
|
2018-11-11 14:00:47 +01:00
|
|
|
}
|
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
suspend fun addItemToList(track: Track): Track {
|
|
|
|
return withContext(Dispatchers.IO) {
|
|
|
|
val formBody: RequestBody = FormBody.Builder()
|
|
|
|
.add("status", "reading")
|
|
|
|
.add("score", "0")
|
2020-04-25 20:24:45 +02:00
|
|
|
.build()
|
2020-12-14 23:57:35 +01:00
|
|
|
val request = Request.Builder()
|
|
|
|
.url(mangaUrl(track.media_id).toString())
|
|
|
|
.put(formBody)
|
2020-04-25 20:24:45 +02:00
|
|
|
.build()
|
2020-12-14 23:57:35 +01:00
|
|
|
authClient.newCall(request).await().use {
|
|
|
|
parseMangaItem(it, track)
|
2020-04-25 20:24:45 +02:00
|
|
|
}
|
2020-04-23 03:23:23 +02:00
|
|
|
}
|
2020-12-14 23:57:35 +01:00
|
|
|
}
|
2020-04-23 03:23:23 +02:00
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
suspend fun updateItem(track: Track): Track {
|
|
|
|
return withContext(Dispatchers.IO) {
|
|
|
|
val formBody: RequestBody = FormBody.Builder()
|
|
|
|
.add("status", track.toMyAnimeListStatus() ?: "reading")
|
|
|
|
.add("is_rereading", (track.status == MyAnimeList.REREADING).toString())
|
|
|
|
.add("score", track.score.toString())
|
|
|
|
.add("num_chapters_read", track.last_chapter_read.toString())
|
|
|
|
.build()
|
|
|
|
val request = Request.Builder()
|
|
|
|
.url(mangaUrl(track.media_id).toString())
|
|
|
|
.put(formBody)
|
|
|
|
.build()
|
|
|
|
authClient.newCall(request).await().use {
|
|
|
|
parseMangaItem(it, track)
|
2020-04-25 20:24:45 +02:00
|
|
|
}
|
2020-04-23 03:23:23 +02:00
|
|
|
}
|
2020-12-14 23:57:35 +01:00
|
|
|
}
|
2020-04-23 03:23:23 +02:00
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
private fun parseMangaItem(response: Response, track: Track): Track {
|
|
|
|
val responseBody = response.body?.string().orEmpty()
|
|
|
|
val obj = json.decodeFromString<JsonObject>(responseBody).jsonObject
|
|
|
|
return track.apply {
|
|
|
|
val isRereading = obj["is_rereading"]!!.jsonPrimitive.boolean
|
|
|
|
status = if (isRereading) MyAnimeList.REREADING else getStatus(obj["status"]!!.jsonPrimitive.content)
|
|
|
|
last_chapter_read = obj["num_chapters_read"]!!.jsonPrimitive.int
|
|
|
|
score = obj["score"]!!.jsonPrimitive.int.toFloat()
|
2020-02-17 23:23:37 +01:00
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
2020-04-23 03:23:23 +02:00
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
companion object {
|
|
|
|
// Registered under arkon's MAL account
|
|
|
|
private const val clientId = "8fd3313bc138e8b890551aa1de1a2589"
|
|
|
|
|
|
|
|
private const val baseOAuthUrl = "https://myanimelist.net/v1/oauth2"
|
|
|
|
private const val baseApiUrl = "https://api.myanimelist.net/v2"
|
|
|
|
|
|
|
|
private var codeVerifier: String = ""
|
|
|
|
|
|
|
|
fun authUrl(): Uri = "$baseOAuthUrl/authorize".toUri().buildUpon()
|
|
|
|
.appendQueryParameter("client_id", clientId)
|
|
|
|
.appendQueryParameter("code_challenge", getPkceChallengeCode())
|
|
|
|
.appendQueryParameter("response_type", "code")
|
|
|
|
.build()
|
|
|
|
|
|
|
|
fun mangaUrl(id: Int): Uri = "$baseApiUrl/manga".toUri().buildUpon()
|
|
|
|
.appendPath(id.toString())
|
|
|
|
.appendPath("my_list_status")
|
|
|
|
.build()
|
|
|
|
|
|
|
|
fun refreshTokenRequest(refreshToken: String): Request {
|
|
|
|
val formBody: RequestBody = FormBody.Builder()
|
|
|
|
.add("client_id", clientId)
|
|
|
|
.add("refresh_token", refreshToken)
|
|
|
|
.add("grant_type", "refresh_token")
|
|
|
|
.build()
|
|
|
|
return POST("$baseOAuthUrl/token", body = formBody)
|
|
|
|
}
|
2020-04-23 03:23:23 +02:00
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
private fun getPkceChallengeCode(): String {
|
|
|
|
codeVerifier = PkceUtil.generateCodeVerifier()
|
|
|
|
return codeVerifier
|
2020-04-23 03:23:23 +02:00
|
|
|
}
|
|
|
|
}
|
2020-01-05 17:29:27 +01:00
|
|
|
}
|