|
|
|
@ -1,7 +1,6 @@
|
|
|
|
package org.koitharu.kotatsu.scrobbling.anilist.data
|
|
|
|
package org.koitharu.kotatsu.scrobbling.anilist.data
|
|
|
|
|
|
|
|
|
|
|
|
import okhttp3.FormBody
|
|
|
|
import okhttp3.FormBody
|
|
|
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
|
|
|
|
|
|
import okhttp3.MediaType.Companion.toMediaType
|
|
|
|
import okhttp3.MediaType.Companion.toMediaType
|
|
|
|
import okhttp3.OkHttpClient
|
|
|
|
import okhttp3.OkHttpClient
|
|
|
|
import okhttp3.Request
|
|
|
|
import okhttp3.Request
|
|
|
|
@ -15,6 +14,7 @@ import org.koitharu.kotatsu.parsers.util.await
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
|
|
|
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
|
|
|
import org.koitharu.kotatsu.parsers.util.parseJson
|
|
|
|
import org.koitharu.kotatsu.parsers.util.parseJson
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.toIntUp
|
|
|
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
|
|
|
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
|
|
|
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
|
|
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
|
|
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
|
|
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
|
|
|
@ -22,12 +22,15 @@ import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
|
|
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
|
|
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
|
|
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
|
|
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
|
|
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
|
|
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
|
|
|
import org.koitharu.kotatsu.utils.ext.toRequestBody
|
|
|
|
import kotlin.math.roundToInt
|
|
|
|
|
|
|
|
|
|
|
|
private const val REDIRECT_URI = "kotatsu://anilist-auth"
|
|
|
|
private const val REDIRECT_URI = "kotatsu://anilist-auth"
|
|
|
|
private const val BASE_URL = "https://anilist.co/api/v2/"
|
|
|
|
private const val BASE_URL = "https://anilist.co/api/v2/"
|
|
|
|
private const val ENDPOINT = "https://graphql.anilist.co"
|
|
|
|
private const val ENDPOINT = "https://graphql.anilist.co"
|
|
|
|
private const val MANGA_PAGE_SIZE = 10
|
|
|
|
private const val MANGA_PAGE_SIZE = 10
|
|
|
|
|
|
|
|
private const val REQUEST_QUERY = "query"
|
|
|
|
|
|
|
|
private const val REQUEST_MUTATION = "mutation"
|
|
|
|
|
|
|
|
private const val KEY_SCORE_FORMAT = "score_format"
|
|
|
|
|
|
|
|
|
|
|
|
class AniListRepository(
|
|
|
|
class AniListRepository(
|
|
|
|
private val okHttp: OkHttpClient,
|
|
|
|
private val okHttp: OkHttpClient,
|
|
|
|
@ -42,6 +45,8 @@ class AniListRepository(
|
|
|
|
override val isAuthorized: Boolean
|
|
|
|
override val isAuthorized: Boolean
|
|
|
|
get() = storage.accessToken != null
|
|
|
|
get() = storage.accessToken != null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private val shrinkRegex = Regex("\\t+")
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun authorize(code: String?) {
|
|
|
|
override suspend fun authorize(code: String?) {
|
|
|
|
val body = FormBody.Builder()
|
|
|
|
val body = FormBody.Builder()
|
|
|
|
body.add("client_id", BuildConfig.ANILIST_CLIENT_ID)
|
|
|
|
body.add("client_id", BuildConfig.ANILIST_CLIENT_ID)
|
|
|
|
@ -63,7 +68,8 @@ class AniListRepository(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun loadUser(): ScrobblerUser {
|
|
|
|
override suspend fun loadUser(): ScrobblerUser {
|
|
|
|
val response = query(
|
|
|
|
val response = doRequest(
|
|
|
|
|
|
|
|
REQUEST_QUERY,
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
AniChartUser {
|
|
|
|
AniChartUser {
|
|
|
|
user {
|
|
|
|
user {
|
|
|
|
@ -72,11 +78,15 @@ class AniListRepository(
|
|
|
|
avatar {
|
|
|
|
avatar {
|
|
|
|
medium
|
|
|
|
medium
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
mediaListOptions {
|
|
|
|
|
|
|
|
scoreFormat
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
""".trimIndent(),
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
val jo = response.getJSONObject("data").getJSONObject("AniChartUser").getJSONObject("user")
|
|
|
|
val jo = response.getJSONObject("data").getJSONObject("AniChartUser").getJSONObject("user")
|
|
|
|
|
|
|
|
storage[KEY_SCORE_FORMAT] = jo.getJSONObject("mediaListOptions").getString("scoreFormat")
|
|
|
|
return AniListUser(jo).also { storage.user = it }
|
|
|
|
return AniListUser(jo).also { storage.user = it }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -86,7 +96,7 @@ class AniListRepository(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun unregister(mangaId: Long) {
|
|
|
|
override suspend fun unregister(mangaId: Long) {
|
|
|
|
return db.scrobblingDao.delete(ScrobblerService.SHIKIMORI.id, mangaId)
|
|
|
|
return db.scrobblingDao.delete(ScrobblerService.ANILIST.id, mangaId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override fun logout() {
|
|
|
|
override fun logout() {
|
|
|
|
@ -94,11 +104,12 @@ class AniListRepository(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
|
|
|
|
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
|
|
|
|
val page = offset / MANGA_PAGE_SIZE
|
|
|
|
val page = (offset / MANGA_PAGE_SIZE.toFloat()).toIntUp() + 1
|
|
|
|
val response = query(
|
|
|
|
val response = doRequest(
|
|
|
|
|
|
|
|
REQUEST_QUERY,
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Page(page: $page, perPage: ${MANGA_PAGE_SIZE}) {
|
|
|
|
Page(page: $page, perPage: ${MANGA_PAGE_SIZE}) {
|
|
|
|
media(type: MANGA, isAdult: true, sort: SEARCH_MATCH, search: "${JSONObject.quote(query)}") {
|
|
|
|
media(type: MANGA, sort: SEARCH_MATCH, search: ${JSONObject.quote(query)}) {
|
|
|
|
id
|
|
|
|
id
|
|
|
|
title {
|
|
|
|
title {
|
|
|
|
userPreferred
|
|
|
|
userPreferred
|
|
|
|
@ -110,76 +121,69 @@ class AniListRepository(
|
|
|
|
siteUrl
|
|
|
|
siteUrl
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
""".trimIndent(),
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
val data = response.getJSONObject("data").getJSONObject("Page").getJSONArray("media")
|
|
|
|
val data = response.getJSONObject("data").getJSONObject("Page").getJSONArray("media")
|
|
|
|
return data.mapJSON { ScrobblerManga(it) }
|
|
|
|
return data.mapJSON { ScrobblerManga(it) }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun createRate(mangaId: Long, scrobblerMangaId: Long) {
|
|
|
|
override suspend fun createRate(mangaId: Long, scrobblerMangaId: Long) {
|
|
|
|
val response = query(
|
|
|
|
val response = doRequest(
|
|
|
|
|
|
|
|
REQUEST_MUTATION,
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
mutation {
|
|
|
|
|
|
|
|
SaveMediaListEntry(mediaId: $scrobblerMangaId) {
|
|
|
|
SaveMediaListEntry(mediaId: $scrobblerMangaId) {
|
|
|
|
id
|
|
|
|
id
|
|
|
|
mediaId
|
|
|
|
mediaId
|
|
|
|
status
|
|
|
|
status
|
|
|
|
notes
|
|
|
|
notes
|
|
|
|
scoreRaw
|
|
|
|
score
|
|
|
|
progress
|
|
|
|
progress
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
""",
|
|
|
|
""".trimIndent(),
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
saveRate(response, mangaId)
|
|
|
|
saveRate(response.getJSONObject("data").getJSONObject("SaveMediaListEntry"), mangaId)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter) {
|
|
|
|
override suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter) {
|
|
|
|
val payload = JSONObject()
|
|
|
|
val response = doRequest(
|
|
|
|
payload.put(
|
|
|
|
REQUEST_MUTATION,
|
|
|
|
"user_rate",
|
|
|
|
"""
|
|
|
|
JSONObject().apply {
|
|
|
|
SaveMediaListEntry(id: $rateId, progress: ${chapter.number}) {
|
|
|
|
put("chapters", chapter.number)
|
|
|
|
id
|
|
|
|
},
|
|
|
|
mediaId
|
|
|
|
|
|
|
|
status
|
|
|
|
|
|
|
|
notes
|
|
|
|
|
|
|
|
score
|
|
|
|
|
|
|
|
progress
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
val url = BASE_URL.toHttpUrl().newBuilder()
|
|
|
|
saveRate(response.getJSONObject("data").getJSONObject("SaveMediaListEntry"), mangaId)
|
|
|
|
.addPathSegment("api")
|
|
|
|
|
|
|
|
.addPathSegment("v2")
|
|
|
|
|
|
|
|
.addPathSegment("user_rates")
|
|
|
|
|
|
|
|
.addPathSegment(rateId.toString())
|
|
|
|
|
|
|
|
.build()
|
|
|
|
|
|
|
|
val request = Request.Builder().url(url).patch(payload.toRequestBody()).build()
|
|
|
|
|
|
|
|
val response = okHttp.newCall(request).await().parseJson()
|
|
|
|
|
|
|
|
saveRate(response, mangaId)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun updateRate(rateId: Int, mangaId: Long, rating: Float, status: String?, comment: String?) {
|
|
|
|
override suspend fun updateRate(rateId: Int, mangaId: Long, rating: Float, status: String?, comment: String?) {
|
|
|
|
val payload = JSONObject()
|
|
|
|
val scoreRaw = (rating * 100f).roundToInt()
|
|
|
|
payload.put(
|
|
|
|
val statusString = status?.let { ", status: $it" }.orEmpty()
|
|
|
|
"user_rate",
|
|
|
|
val notesString = comment?.let { ", notes: ${JSONObject.quote(it)}" }.orEmpty()
|
|
|
|
JSONObject().apply {
|
|
|
|
val response = doRequest(
|
|
|
|
put("score", rating.toString())
|
|
|
|
REQUEST_MUTATION,
|
|
|
|
if (comment != null) {
|
|
|
|
"""
|
|
|
|
put("text", comment)
|
|
|
|
SaveMediaListEntry(id: $rateId, scoreRaw: $scoreRaw$statusString$notesString) {
|
|
|
|
}
|
|
|
|
id
|
|
|
|
if (status != null) {
|
|
|
|
mediaId
|
|
|
|
put("status", status)
|
|
|
|
status
|
|
|
|
|
|
|
|
notes
|
|
|
|
|
|
|
|
score
|
|
|
|
|
|
|
|
progress
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
val url = BASE_URL.toHttpUrl().newBuilder()
|
|
|
|
saveRate(response.getJSONObject("data").getJSONObject("SaveMediaListEntry"), mangaId)
|
|
|
|
.addPathSegment("api")
|
|
|
|
|
|
|
|
.addPathSegment("v2")
|
|
|
|
|
|
|
|
.addPathSegment("user_rates")
|
|
|
|
|
|
|
|
.addPathSegment(rateId.toString())
|
|
|
|
|
|
|
|
.build()
|
|
|
|
|
|
|
|
val request = Request.Builder().url(url).patch(payload.toRequestBody()).build()
|
|
|
|
|
|
|
|
val response = okHttp.newCall(request).await().parseJson()
|
|
|
|
|
|
|
|
saveRate(response, mangaId)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
|
|
|
|
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
|
|
|
|
val response = query(
|
|
|
|
val response = doRequest(
|
|
|
|
|
|
|
|
REQUEST_QUERY,
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Media(id: $id) {
|
|
|
|
Media(id: $id) {
|
|
|
|
id
|
|
|
|
id
|
|
|
|
@ -192,23 +196,24 @@ class AniListRepository(
|
|
|
|
description
|
|
|
|
description
|
|
|
|
siteUrl
|
|
|
|
siteUrl
|
|
|
|
}
|
|
|
|
}
|
|
|
|
""".trimIndent(),
|
|
|
|
""",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return ScrobblerMangaInfo(response.getJSONObject("data").getJSONObject("Media"))
|
|
|
|
return ScrobblerMangaInfo(response.getJSONObject("data").getJSONObject("Media"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun saveRate(json: JSONObject, mangaId: Long) {
|
|
|
|
private suspend fun saveRate(json: JSONObject, mangaId: Long) {
|
|
|
|
|
|
|
|
val scoreFormat = ScoreFormat.of(storage[KEY_SCORE_FORMAT])
|
|
|
|
val entity = ScrobblingEntity(
|
|
|
|
val entity = ScrobblingEntity(
|
|
|
|
scrobbler = ScrobblerService.SHIKIMORI.id,
|
|
|
|
scrobbler = ScrobblerService.ANILIST.id,
|
|
|
|
id = json.getInt("id"),
|
|
|
|
id = json.getInt("id"),
|
|
|
|
mangaId = mangaId,
|
|
|
|
mangaId = mangaId,
|
|
|
|
targetId = json.getLong("mediaId"),
|
|
|
|
targetId = json.getLong("mediaId"),
|
|
|
|
status = json.getString("status"),
|
|
|
|
status = json.getString("status"),
|
|
|
|
chapter = json.getInt("progress"),
|
|
|
|
chapter = json.getInt("progress"),
|
|
|
|
comment = json.getString("notes"),
|
|
|
|
comment = json.getString("notes"),
|
|
|
|
rating = json.getDouble("scoreRaw").toFloat() / 100f,
|
|
|
|
rating = scoreFormat.normalize(json.getDouble("score").toFloat()),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
db.scrobblingDao.insert(entity)
|
|
|
|
db.scrobblingDao.upsert(entity)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun ScrobblerManga(json: JSONObject): ScrobblerManga {
|
|
|
|
private fun ScrobblerManga(json: JSONObject): ScrobblerManga {
|
|
|
|
@ -237,10 +242,9 @@ class AniListRepository(
|
|
|
|
service = ScrobblerService.ANILIST,
|
|
|
|
service = ScrobblerService.ANILIST,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun query(query: String): JSONObject {
|
|
|
|
private suspend fun doRequest(type: String, payload: String): JSONObject {
|
|
|
|
val body = JSONObject()
|
|
|
|
val body = JSONObject()
|
|
|
|
body.put("query", "{$query}")
|
|
|
|
body.put("query", "$type { ${payload.shrink()} }")
|
|
|
|
body.put("variables", null)
|
|
|
|
|
|
|
|
val mediaType = "application/json; charset=utf-8".toMediaType()
|
|
|
|
val mediaType = "application/json; charset=utf-8".toMediaType()
|
|
|
|
val requestBody = body.toString().toRequestBody(mediaType)
|
|
|
|
val requestBody = body.toString().toRequestBody(mediaType)
|
|
|
|
val request = Request.Builder()
|
|
|
|
val request = Request.Builder()
|
|
|
|
@ -254,4 +258,6 @@ class AniListRepository(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return json
|
|
|
|
return json
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private fun String.shrink() = replace(shrinkRegex, " ")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|