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
|
2020-12-27 23:46:14 +01:00
|
|
|
import eu.kanade.tachiyomi.network.parseAs
|
2020-12-14 23:57:35 +01:00
|
|
|
import eu.kanade.tachiyomi.util.PkceUtil
|
2021-01-09 00:05:51 +01:00
|
|
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
2020-12-14 23:57:35 +01:00
|
|
|
import kotlinx.coroutines.async
|
|
|
|
import kotlinx.coroutines.awaitAll
|
|
|
|
import kotlinx.serialization.json.JsonObject
|
|
|
|
import kotlinx.serialization.json.boolean
|
2020-12-24 21:46:29 +01:00
|
|
|
import kotlinx.serialization.json.contentOrNull
|
2021-08-28 16:37:45 +02:00
|
|
|
import kotlinx.serialization.json.float
|
2020-12-14 23:57:35 +01:00
|
|
|
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
|
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 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 {
|
2021-01-09 00:05:51 +01:00
|
|
|
return withIOContext {
|
2020-12-14 23:57:35 +01:00
|
|
|
val formBody: RequestBody = FormBody.Builder()
|
|
|
|
.add("client_id", clientId)
|
|
|
|
.add("code", authCode)
|
|
|
|
.add("code_verifier", codeVerifier)
|
|
|
|
.add("grant_type", "authorization_code")
|
|
|
|
.build()
|
2020-12-27 23:46:14 +01:00
|
|
|
client.newCall(POST("$baseOAuthUrl/token", body = formBody))
|
|
|
|
.await()
|
|
|
|
.parseAs()
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
suspend fun getCurrentUser(): String {
|
2021-01-09 00:05:51 +01:00
|
|
|
return withIOContext {
|
2020-12-14 23:57:35 +01:00
|
|
|
val request = Request.Builder()
|
|
|
|
.url("$baseApiUrl/users/@me")
|
|
|
|
.get()
|
|
|
|
.build()
|
2020-12-27 23:46:14 +01:00
|
|
|
authClient.newCall(request)
|
|
|
|
.await()
|
|
|
|
.parseAs<JsonObject>()
|
|
|
|
.let { it["name"]!!.jsonPrimitive.content }
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
suspend fun search(query: String): List<TrackSearch> {
|
2021-01-09 00:05:51 +01:00
|
|
|
return withIOContext {
|
2020-12-14 23:57:35 +01:00
|
|
|
val url = "$baseApiUrl/manga".toUri().buildUpon()
|
|
|
|
.appendQueryParameter("q", query)
|
2021-01-04 21:15:55 +01:00
|
|
|
.appendQueryParameter("nsfw", "true")
|
2020-12-14 23:57:35 +01:00
|
|
|
.build()
|
2020-12-27 23:46:14 +01:00
|
|
|
authClient.newCall(GET(url.toString()))
|
|
|
|
.await()
|
|
|
|
.parseAs<JsonObject>()
|
|
|
|
.let {
|
|
|
|
it["data"]!!.jsonArray
|
|
|
|
.map { data -> data.jsonObject["node"]!!.jsonObject }
|
|
|
|
.map { node ->
|
|
|
|
val id = node["id"]!!.jsonPrimitive.int
|
|
|
|
async { getMangaDetails(id) }
|
|
|
|
}
|
|
|
|
.awaitAll()
|
|
|
|
.filter { trackSearch -> trackSearch.publishing_type != "novel" }
|
|
|
|
}
|
2020-12-14 23:57:35 +01:00
|
|
|
}
|
2016-12-22 21:17:47 +01:00
|
|
|
}
|
|
|
|
|
2020-12-30 21:08:10 +01:00
|
|
|
suspend fun getMangaDetails(id: Int): TrackSearch {
|
2021-01-09 00:05:51 +01:00
|
|
|
return withIOContext {
|
2020-12-14 23:57:35 +01:00
|
|
|
val url = "$baseApiUrl/manga".toUri().buildUpon()
|
|
|
|
.appendPath(id.toString())
|
|
|
|
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
|
|
|
|
.build()
|
2020-12-27 23:46:14 +01:00
|
|
|
authClient.newCall(GET(url.toString()))
|
|
|
|
.await()
|
|
|
|
.parseAs<JsonObject>()
|
|
|
|
.let {
|
|
|
|
val obj = it.jsonObject
|
|
|
|
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
|
|
|
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"
|
|
|
|
publishing_status = obj["status"]!!.jsonPrimitive.content.replace("_", " ")
|
|
|
|
publishing_type = obj["media_type"]!!.jsonPrimitive.content.replace("_", " ")
|
|
|
|
start_date = try {
|
|
|
|
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
|
|
|
outputDf.format(obj["start_date"]!!)
|
|
|
|
} catch (e: Exception) {
|
|
|
|
""
|
|
|
|
}
|
2020-12-14 23:57:35 +01:00
|
|
|
}
|
2016-12-22 21:17:47 +01: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 updateItem(track: Track): Track {
|
2021-01-09 00:05:51 +01:00
|
|
|
return withIOContext {
|
2021-01-14 23:46:31 +01:00
|
|
|
val formBodyBuilder = FormBody.Builder()
|
2020-12-14 23:57:35 +01:00
|
|
|
.add("status", track.toMyAnimeListStatus() ?: "reading")
|
|
|
|
.add("is_rereading", (track.status == MyAnimeList.REREADING).toString())
|
|
|
|
.add("score", track.score.toString())
|
2021-08-28 16:37:45 +02:00
|
|
|
.add("num_chapters_read", track.last_chapter_read.toInt().toString())
|
2021-01-14 23:46:31 +01:00
|
|
|
convertToIsoDate(track.started_reading_date)?.let {
|
|
|
|
formBodyBuilder.add("start_date", it)
|
|
|
|
}
|
|
|
|
convertToIsoDate(track.finished_reading_date)?.let {
|
|
|
|
formBodyBuilder.add("finish_date", it)
|
|
|
|
}
|
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
val request = Request.Builder()
|
|
|
|
.url(mangaUrl(track.media_id).toString())
|
2021-01-14 23:46:31 +01:00
|
|
|
.put(formBodyBuilder.build())
|
2020-12-14 23:57:35 +01:00
|
|
|
.build()
|
2020-12-27 23:46:14 +01:00
|
|
|
authClient.newCall(request)
|
|
|
|
.await()
|
|
|
|
.parseAs<JsonObject>()
|
|
|
|
.let { parseMangaItem(it, track) }
|
2020-04-23 03:23:23 +02:00
|
|
|
}
|
2020-12-14 23:57:35 +01:00
|
|
|
}
|
2020-04-23 03:23:23 +02:00
|
|
|
|
2021-01-14 23:36:22 +01:00
|
|
|
suspend fun findListItem(track: Track): Track? {
|
|
|
|
return withIOContext {
|
|
|
|
val uri = "$baseApiUrl/manga".toUri().buildUpon()
|
|
|
|
.appendPath(track.media_id.toString())
|
2021-01-24 22:58:23 +01:00
|
|
|
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
|
2021-01-14 23:36:22 +01:00
|
|
|
.build()
|
|
|
|
authClient.newCall(GET(uri.toString()))
|
|
|
|
.await()
|
|
|
|
.parseAs<JsonObject>()
|
|
|
|
.let { obj ->
|
2021-01-24 22:58:23 +01:00
|
|
|
track.total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
|
2021-01-24 22:33:47 +01:00
|
|
|
obj.jsonObject["my_list_status"]?.jsonObject?.let {
|
2021-01-14 23:36:22 +01:00
|
|
|
parseMangaItem(it, track)
|
|
|
|
}
|
|
|
|
}
|
2021-01-04 20:30:04 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
suspend fun findListItems(query: String, offset: Int = 0): List<TrackSearch> {
|
2021-01-09 00:05:51 +01:00
|
|
|
return withIOContext {
|
2021-01-04 20:30:04 +01:00
|
|
|
val json = getListPage(offset)
|
|
|
|
val obj = json.jsonObject
|
|
|
|
|
|
|
|
val matches = obj["data"]!!.jsonArray
|
|
|
|
.filter {
|
|
|
|
it.jsonObject["node"]!!.jsonObject["title"]!!.jsonPrimitive.content.contains(
|
|
|
|
query,
|
|
|
|
ignoreCase = true
|
|
|
|
)
|
|
|
|
}
|
|
|
|
.map {
|
|
|
|
val id = it.jsonObject["node"]!!.jsonObject["id"]!!.jsonPrimitive.int
|
|
|
|
async { getMangaDetails(id) }
|
|
|
|
}
|
|
|
|
.awaitAll()
|
|
|
|
|
|
|
|
// Check next page if there's more
|
|
|
|
if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) {
|
|
|
|
matches + findListItems(query, offset + listPaginationAmount)
|
|
|
|
} else {
|
|
|
|
matches
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private suspend fun getListPage(offset: Int): JsonObject {
|
2021-01-09 00:05:51 +01:00
|
|
|
return withIOContext {
|
2020-12-24 21:46:29 +01:00
|
|
|
val urlBuilder = "$baseApiUrl/users/@me/mangalist".toUri().buildUpon()
|
2021-01-14 23:36:22 +01:00
|
|
|
.appendQueryParameter("fields", "list_status{start_date,finish_date}")
|
2020-12-31 16:58:40 +01:00
|
|
|
.appendQueryParameter("limit", listPaginationAmount.toString())
|
2020-12-24 21:46:29 +01:00
|
|
|
if (offset > 0) {
|
|
|
|
urlBuilder.appendQueryParameter("offset", offset.toString())
|
|
|
|
}
|
|
|
|
|
|
|
|
val request = Request.Builder()
|
|
|
|
.url(urlBuilder.build().toString())
|
|
|
|
.get()
|
|
|
|
.build()
|
|
|
|
authClient.newCall(request)
|
|
|
|
.await()
|
2021-01-04 20:30:04 +01:00
|
|
|
.parseAs()
|
2020-12-24 21:46:29 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-27 23:46:14 +01:00
|
|
|
private fun parseMangaItem(response: JsonObject, track: Track): Track {
|
|
|
|
val obj = response.jsonObject
|
2020-12-14 23:57:35 +01:00
|
|
|
return track.apply {
|
|
|
|
val isRereading = obj["is_rereading"]!!.jsonPrimitive.boolean
|
|
|
|
status = if (isRereading) MyAnimeList.REREADING else getStatus(obj["status"]!!.jsonPrimitive.content)
|
2021-08-28 16:37:45 +02:00
|
|
|
last_chapter_read = obj["num_chapters_read"]!!.jsonPrimitive.float
|
2020-12-14 23:57:35 +01:00
|
|
|
score = obj["score"]!!.jsonPrimitive.int.toFloat()
|
2021-01-14 23:46:31 +01:00
|
|
|
obj["start_date"]?.let {
|
|
|
|
started_reading_date = parseDate(it.jsonPrimitive.content)
|
|
|
|
}
|
|
|
|
obj["finish_date"]?.let {
|
|
|
|
finished_reading_date = parseDate(it.jsonPrimitive.content)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun parseDate(isoDate: String): Long {
|
|
|
|
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun convertToIsoDate(epochTime: Long): String? {
|
|
|
|
if (epochTime == 0L) {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return try {
|
|
|
|
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
|
|
|
outputDf.format(epochTime)
|
|
|
|
} catch (e: Exception) {
|
|
|
|
null
|
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"
|
|
|
|
|
2020-12-31 16:58:40 +01:00
|
|
|
private const val listPaginationAmount = 250
|
|
|
|
|
2020-12-14 23:57:35 +01:00
|
|
|
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
|
|
|
}
|