Complete AniList api integration #208
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
import okio.Buffer
|
||||||
|
import org.koitharu.kotatsu.core.network.CommonHeaders.ACCEPT_ENCODING
|
||||||
|
|
||||||
|
class CurlLoggingInterceptor(
|
||||||
|
private val curlOptions: String? = null
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
var isCompressed = false
|
||||||
|
|
||||||
|
val curlCmd = StringBuilder()
|
||||||
|
curlCmd.append("curl")
|
||||||
|
if (curlOptions != null) {
|
||||||
|
curlCmd.append(' ').append(curlOptions)
|
||||||
|
}
|
||||||
|
curlCmd.append(" -X ").append(request.method)
|
||||||
|
|
||||||
|
for ((name, value) in request.headers) {
|
||||||
|
if (name.equals(ACCEPT_ENCODING, ignoreCase = true) && value.equals("gzip", ignoreCase = true)) {
|
||||||
|
isCompressed = true
|
||||||
|
}
|
||||||
|
curlCmd.append(" -H \"").append(name).append(": ").append(value.escape()).append('\"')
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = request.body
|
||||||
|
if (body != null) {
|
||||||
|
val buffer = Buffer()
|
||||||
|
body.writeTo(buffer)
|
||||||
|
val charset = body.contentType()?.charset() ?: Charsets.UTF_8
|
||||||
|
curlCmd.append(" --data-raw '")
|
||||||
|
.append(buffer.readString(charset).replace("\n", "\\n"))
|
||||||
|
.append("'")
|
||||||
|
}
|
||||||
|
if (isCompressed) {
|
||||||
|
curlCmd.append(" --compressed")
|
||||||
|
}
|
||||||
|
curlCmd.append(" \"").append(request.url).append('"')
|
||||||
|
|
||||||
|
log("---cURL (" + request.url + ")")
|
||||||
|
log(curlCmd.toString())
|
||||||
|
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.escape() = replace("\"", "\\\"")
|
||||||
|
|
||||||
|
private fun log(msg: String) {
|
||||||
|
Log.d("CURL", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,6 +99,9 @@ interface AppModule {
|
|||||||
addInterceptor(GZipInterceptor())
|
addInterceptor(GZipInterceptor())
|
||||||
addInterceptor(UserAgentInterceptor())
|
addInterceptor(UserAgentInterceptor())
|
||||||
addInterceptor(CloudFlareInterceptor())
|
addInterceptor(CloudFlareInterceptor())
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
addInterceptor(CurlLoggingInterceptor())
|
||||||
|
}
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ object CommonHeaders {
|
|||||||
const val CONTENT_DISPOSITION = "Content-Disposition"
|
const val CONTENT_DISPOSITION = "Content-Disposition"
|
||||||
const val COOKIE = "Cookie"
|
const val COOKIE = "Cookie"
|
||||||
const val CONTENT_ENCODING = "Content-Encoding"
|
const val CONTENT_ENCODING = "Content-Encoding"
|
||||||
|
const val ACCEPT_ENCODING = "Accept-Encoding"
|
||||||
const val AUTHORIZATION = "Authorization"
|
const val AUTHORIZATION = "Authorization"
|
||||||
|
|
||||||
val CACHE_CONTROL_DISABLED: CacheControl
|
val CACHE_CONTROL_DISABLED: CacheControl
|
||||||
|
|||||||
@@ -257,7 +257,8 @@ class DetailsViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateScrobbling(rating: Float, status: ScrobblingStatus?) {
|
fun updateScrobbling(rating: Float, status: ScrobblingStatus?) {
|
||||||
for (scrobbler in scrobblers) {
|
for (info in scrobblingInfo.value ?: return) {
|
||||||
|
val scrobbler = scrobblers.first { it.scrobblerService == info.scrobbler }
|
||||||
if (!scrobbler.isAvailable) continue
|
if (!scrobbler.isAvailable) continue
|
||||||
launchJob(Dispatchers.Default) {
|
launchJob(Dispatchers.Default) {
|
||||||
scrobbler.updateScrobblingInfo(
|
scrobbler.updateScrobblingInfo(
|
||||||
|
|||||||
@@ -117,12 +117,13 @@ class MainActivity :
|
|||||||
binding.navRail?.headerView?.setOnClickListener(this)
|
binding.navRail?.headerView?.setOnClickListener(this)
|
||||||
binding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null
|
binding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(navigationDelegate)
|
|
||||||
onBackPressedDispatcher.addCallback(ExitCallback(this, binding.container))
|
|
||||||
navigationDelegate = MainNavigationDelegate(checkNotNull(bottomNav ?: binding.navRail), supportFragmentManager)
|
navigationDelegate = MainNavigationDelegate(checkNotNull(bottomNav ?: binding.navRail), supportFragmentManager)
|
||||||
navigationDelegate.addOnFragmentChangedListener(this)
|
navigationDelegate.addOnFragmentChangedListener(this)
|
||||||
navigationDelegate.onCreate(savedInstanceState)
|
navigationDelegate.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
onBackPressedDispatcher.addCallback(navigationDelegate)
|
||||||
|
onBackPressedDispatcher.addCallback(ExitCallback(this, binding.container))
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
onFirstStart()
|
onFirstStart()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import dagger.multibindings.ElementsIntoSet
|
import dagger.multibindings.ElementsIntoSet
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
|
import org.koitharu.kotatsu.core.network.CurlLoggingInterceptor
|
||||||
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListAuthenticator
|
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListAuthenticator
|
||||||
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListInterceptor
|
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListInterceptor
|
||||||
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
|
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
|
||||||
@@ -37,6 +39,9 @@ object ScrobblingModule {
|
|||||||
val okHttp = OkHttpClient.Builder().apply {
|
val okHttp = OkHttpClient.Builder().apply {
|
||||||
authenticator(authenticator)
|
authenticator(authenticator)
|
||||||
addInterceptor(ShikimoriInterceptor(storage))
|
addInterceptor(ShikimoriInterceptor(storage))
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
addInterceptor(CurlLoggingInterceptor())
|
||||||
|
}
|
||||||
}.build()
|
}.build()
|
||||||
return ShikimoriRepository(okHttp, storage, database)
|
return ShikimoriRepository(okHttp, storage, database)
|
||||||
}
|
}
|
||||||
@@ -51,6 +56,9 @@ object ScrobblingModule {
|
|||||||
val okHttp = OkHttpClient.Builder().apply {
|
val okHttp = OkHttpClient.Builder().apply {
|
||||||
authenticator(authenticator)
|
authenticator(authenticator)
|
||||||
addInterceptor(AniListInterceptor(storage))
|
addInterceptor(AniListInterceptor(storage))
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
addInterceptor(CurlLoggingInterceptor())
|
||||||
|
}
|
||||||
}.build()
|
}.build()
|
||||||
return AniListRepository(okHttp, storage, database)
|
return AniListRepository(okHttp, storage, database)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
mediaId
|
||||||
|
status
|
||||||
|
notes
|
||||||
|
score
|
||||||
|
progress
|
||||||
}
|
}
|
||||||
if (status != null) {
|
""",
|
||||||
put("status", status)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
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, " ")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.koitharu.kotatsu.scrobbling.anilist.data
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
|
||||||
|
|
||||||
|
enum class ScoreFormat {
|
||||||
|
|
||||||
|
POINT_100, POINT_10_DECIMAL, POINT_10, POINT_5, POINT_3;
|
||||||
|
|
||||||
|
fun normalize(score: Float): Float = when (this) {
|
||||||
|
POINT_100 -> score / 100f
|
||||||
|
POINT_10_DECIMAL,
|
||||||
|
POINT_10 -> score / 10f
|
||||||
|
|
||||||
|
POINT_5 -> score / 5f
|
||||||
|
POINT_3 -> score / 3f
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun of(rawValue: String?): ScoreFormat {
|
||||||
|
rawValue ?: return POINT_10_DECIMAL
|
||||||
|
return runCatching { valueOf(rawValue) }
|
||||||
|
.onFailure { it.printStackTraceDebug() }
|
||||||
|
.getOrDefault(POINT_10_DECIMAL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,6 @@ import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
private const val RATING_MAX = 10f
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class AniListScrobbler @Inject constructor(
|
class AniListScrobbler @Inject constructor(
|
||||||
private val repository: AniListRepository,
|
private val repository: AniListRepository,
|
||||||
@@ -17,12 +15,12 @@ class AniListScrobbler @Inject constructor(
|
|||||||
) : Scrobbler(db, ScrobblerService.ANILIST, repository) {
|
) : Scrobbler(db, ScrobblerService.ANILIST, repository) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
statuses[ScrobblingStatus.PLANNED] = "planned"
|
statuses[ScrobblingStatus.PLANNED] = "PLANNING"
|
||||||
statuses[ScrobblingStatus.READING] = "watching"
|
statuses[ScrobblingStatus.READING] = "CURRENT"
|
||||||
statuses[ScrobblingStatus.RE_READING] = "rewatching"
|
statuses[ScrobblingStatus.RE_READING] = "REPEATING"
|
||||||
statuses[ScrobblingStatus.COMPLETED] = "completed"
|
statuses[ScrobblingStatus.COMPLETED] = "COMPLETED"
|
||||||
statuses[ScrobblingStatus.ON_HOLD] = "on_hold"
|
statuses[ScrobblingStatus.ON_HOLD] = "PAUSED"
|
||||||
statuses[ScrobblingStatus.DROPPED] = "dropped"
|
statuses[ScrobblingStatus.DROPPED] = "DROPPED"
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateScrobblingInfo(
|
override suspend fun updateScrobblingInfo(
|
||||||
@@ -36,7 +34,7 @@ class AniListScrobbler @Inject constructor(
|
|||||||
repository.updateRate(
|
repository.updateRate(
|
||||||
rateId = entity.id,
|
rateId = entity.id,
|
||||||
mangaId = entity.mangaId,
|
mangaId = entity.mangaId,
|
||||||
rating = rating * RATING_MAX,
|
rating = rating,
|
||||||
status = statuses[status],
|
status = statuses[status],
|
||||||
comment = comment,
|
comment = comment,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ class ScrobblerStorage(context: Context, service: ScrobblerService) {
|
|||||||
putString(KEY_USER, str)
|
putString(KEY_USER, str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
operator fun get(key: String): String? = prefs.getString(key, null)
|
||||||
|
|
||||||
|
operator fun set(key: String, value: String?) = prefs.edit { putString(key, value) }
|
||||||
|
|
||||||
fun clear() = prefs.edit {
|
fun clear() = prefs.edit {
|
||||||
clear()
|
clear()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,8 @@ abstract class ScrobblingDao {
|
|||||||
@Query("SELECT * FROM scrobblings WHERE scrobbler = :scrobbler AND manga_id = :mangaId")
|
@Query("SELECT * FROM scrobblings WHERE scrobbler = :scrobbler AND manga_id = :mangaId")
|
||||||
abstract fun observe(scrobbler: Int, mangaId: Long): Flow<ScrobblingEntity?>
|
abstract fun observe(scrobbler: Int, mangaId: Long): Flow<ScrobblingEntity?>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Upsert
|
||||||
abstract suspend fun insert(entity: ScrobblingEntity)
|
abstract suspend fun upsert(entity: ScrobblingEntity)
|
||||||
|
|
||||||
@Update
|
|
||||||
abstract suspend fun update(entity: ScrobblingEntity)
|
|
||||||
|
|
||||||
@Query("DELETE FROM scrobblings WHERE scrobbler = :scrobbler AND manga_id = :mangaId")
|
@Query("DELETE FROM scrobblings WHERE scrobbler = :scrobbler AND manga_id = :mangaId")
|
||||||
abstract suspend fun delete(scrobbler: Int, mangaId: Long)
|
abstract suspend fun delete(scrobbler: Int, mangaId: Long)
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ class ShikimoriRepository(
|
|||||||
comment = json.getString("text"),
|
comment = json.getString("text"),
|
||||||
rating = json.getDouble("score").toFloat() / 10f,
|
rating = json.getDouble("score").toFloat() / 10f,
|
||||||
)
|
)
|
||||||
db.scrobblingDao.insert(entity)
|
db.scrobblingDao.upsert(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ScrobblerManga(json: JSONObject) = ScrobblerManga(
|
private fun ScrobblerManga(json: JSONObject) = ScrobblerManga(
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class CurlLoggingInterceptor : Interceptor {
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
return chain.proceed(chain.request()) // no-op implementation
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user