Unify scrobblers implementation
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling
|
package org.koitharu.kotatsu.scrobbling
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
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
|
||||||
@@ -10,13 +12,14 @@ import org.koitharu.kotatsu.core.db.MangaDatabase
|
|||||||
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
|
||||||
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListStorage
|
|
||||||
import org.koitharu.kotatsu.scrobbling.anilist.domain.AniListScrobbler
|
import org.koitharu.kotatsu.scrobbling.anilist.domain.AniListScrobbler
|
||||||
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerType
|
||||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriAuthenticator
|
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriAuthenticator
|
||||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriInterceptor
|
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriInterceptor
|
||||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
|
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
|
||||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriStorage
|
|
||||||
import org.koitharu.kotatsu.scrobbling.shikimori.domain.ShikimoriScrobbler
|
import org.koitharu.kotatsu.scrobbling.shikimori.domain.ShikimoriScrobbler
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@ object ScrobblingModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideShikimoriRepository(
|
fun provideShikimoriRepository(
|
||||||
storage: ShikimoriStorage,
|
@ScrobblerType(ScrobblerService.SHIKIMORI) storage: ScrobblerStorage,
|
||||||
database: MangaDatabase,
|
database: MangaDatabase,
|
||||||
authenticator: ShikimoriAuthenticator,
|
authenticator: ShikimoriAuthenticator,
|
||||||
): ShikimoriRepository {
|
): ShikimoriRepository {
|
||||||
@@ -41,7 +44,7 @@ object ScrobblingModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideAniListRepository(
|
fun provideAniListRepository(
|
||||||
storage: AniListStorage,
|
@ScrobblerType(ScrobblerService.ANILIST) storage: ScrobblerStorage,
|
||||||
database: MangaDatabase,
|
database: MangaDatabase,
|
||||||
authenticator: AniListAuthenticator,
|
authenticator: AniListAuthenticator,
|
||||||
): AniListRepository {
|
): AniListRepository {
|
||||||
@@ -52,6 +55,20 @@ object ScrobblingModule {
|
|||||||
return AniListRepository(okHttp, storage, database)
|
return AniListRepository(okHttp, storage, database)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@ScrobblerType(ScrobblerService.ANILIST)
|
||||||
|
fun provideAniListStorage(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
): ScrobblerStorage = ScrobblerStorage(context, ScrobblerService.ANILIST)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@ScrobblerType(ScrobblerService.SHIKIMORI)
|
||||||
|
fun provideShikimoriStorage(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
): ScrobblerStorage = ScrobblerStorage(context, ScrobblerService.SHIKIMORI)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ElementsIntoSet
|
@ElementsIntoSet
|
||||||
fun provideScrobblers(
|
fun provideScrobblers(
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import okhttp3.Response
|
|||||||
import okhttp3.Route
|
import okhttp3.Route
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerType
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
|
|
||||||
class AniListAuthenticator @Inject constructor(
|
class AniListAuthenticator @Inject constructor(
|
||||||
private val storage: AniListStorage,
|
@ScrobblerType(ScrobblerService.ANILIST) private val storage: ScrobblerStorage,
|
||||||
private val repositoryProvider: Provider<AniListRepository>,
|
private val repositoryProvider: Provider<AniListRepository>,
|
||||||
) : Authenticator {
|
) : Authenticator {
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package org.koitharu.kotatsu.scrobbling.anilist.data
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||||
|
|
||||||
private const val JSON = "application/json"
|
private const val JSON = "application/json"
|
||||||
|
|
||||||
class AniListInterceptor(private val storage: AniListStorage) : Interceptor {
|
class AniListInterceptor(private val storage: ScrobblerStorage) : Interceptor {
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val sourceRequest = chain.request()
|
val sourceRequest = chain.request()
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ 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.parseJsonArray
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
|
||||||
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||||
import org.koitharu.kotatsu.scrobbling.anilist.data.model.AniListUser
|
|
||||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
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.utils.ext.toRequestBody
|
import org.koitharu.kotatsu.utils.ext.toRequestBody
|
||||||
|
|
||||||
private const val REDIRECT_URI = "kotatsu://anilist-auth"
|
private const val REDIRECT_URI = "kotatsu://anilist-auth"
|
||||||
@@ -31,18 +31,18 @@ private const val MANGA_PAGE_SIZE = 10
|
|||||||
|
|
||||||
class AniListRepository(
|
class AniListRepository(
|
||||||
private val okHttp: OkHttpClient,
|
private val okHttp: OkHttpClient,
|
||||||
private val storage: AniListStorage,
|
private val storage: ScrobblerStorage,
|
||||||
private val db: MangaDatabase,
|
private val db: MangaDatabase,
|
||||||
) {
|
) : ScrobblerRepository {
|
||||||
|
|
||||||
val oauthUrl: String
|
override val oauthUrl: String
|
||||||
get() = "${BASE_URL}oauth/authorize?client_id=${BuildConfig.ANILIST_CLIENT_ID}&" +
|
get() = "${BASE_URL}oauth/authorize?client_id=${BuildConfig.ANILIST_CLIENT_ID}&" +
|
||||||
"redirect_uri=${REDIRECT_URI}&response_type=code"
|
"redirect_uri=${REDIRECT_URI}&response_type=code"
|
||||||
|
|
||||||
val isAuthorized: Boolean
|
override val isAuthorized: Boolean
|
||||||
get() = storage.accessToken != null
|
get() = storage.accessToken != null
|
||||||
|
|
||||||
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)
|
||||||
body.add("client_secret", BuildConfig.ANILIST_CLIENT_SECRET)
|
body.add("client_secret", BuildConfig.ANILIST_CLIENT_SECRET)
|
||||||
@@ -62,7 +62,7 @@ class AniListRepository(
|
|||||||
storage.refreshToken = response.getString("refresh_token")
|
storage.refreshToken = response.getString("refresh_token")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadUser(): AniListUser {
|
override suspend fun loadUser(): ScrobblerUser {
|
||||||
val response = query(
|
val response = query(
|
||||||
"""
|
"""
|
||||||
AniChartUser {
|
AniChartUser {
|
||||||
@@ -80,57 +80,61 @@ class AniListRepository(
|
|||||||
return AniListUser(jo).also { storage.user = it }
|
return AniListUser(jo).also { storage.user = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCachedUser(): AniListUser? {
|
override val cachedUser: ScrobblerUser?
|
||||||
return storage.user
|
get() {
|
||||||
}
|
return storage.user
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun unregister(mangaId: Long) {
|
override suspend fun unregister(mangaId: Long) {
|
||||||
return db.scrobblingDao.delete(ScrobblerService.SHIKIMORI.id, mangaId)
|
return db.scrobblingDao.delete(ScrobblerService.SHIKIMORI.id, mangaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun logout() {
|
override fun logout() {
|
||||||
storage.clear()
|
storage.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
val pageOffset = offset % MANGA_PAGE_SIZE
|
val response = query(
|
||||||
val url = BASE_URL.toHttpUrl().newBuilder()
|
"""
|
||||||
.addPathSegment("api")
|
Page(page: $page, perPage: ${MANGA_PAGE_SIZE}) {
|
||||||
.addPathSegment("mangas")
|
media(type: MANGA, isAdult: true, sort: SEARCH_MATCH, search: "${JSONObject.quote(query)}") {
|
||||||
.addEncodedQueryParameter("page", (page + 1).toString())
|
id
|
||||||
.addEncodedQueryParameter("limit", MANGA_PAGE_SIZE.toString())
|
title {
|
||||||
.addEncodedQueryParameter("censored", false.toString())
|
userPreferred
|
||||||
.addQueryParameter("search", query)
|
native
|
||||||
.build()
|
}
|
||||||
val request = Request.Builder().url(url).get().build()
|
coverImage {
|
||||||
val response = okHttp.newCall(request).await().parseJsonArray()
|
medium
|
||||||
val list = response.mapJSON { ScrobblerManga(it) }
|
}
|
||||||
return if (pageOffset != 0) list.drop(pageOffset) else list
|
siteUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
val data = response.getJSONObject("data").getJSONObject("Page").getJSONArray("media")
|
||||||
|
return data.mapJSON { ScrobblerManga(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createRate(mangaId: Long, shikiMangaId: Long) {
|
override suspend fun createRate(mangaId: Long, scrobblerMangaId: Long) {
|
||||||
val user = getCachedUser() ?: loadUser()
|
val response = query(
|
||||||
val payload = JSONObject()
|
"""
|
||||||
payload.put(
|
mutation {
|
||||||
"user_rate",
|
SaveMediaListEntry(mediaId: $scrobblerMangaId) {
|
||||||
JSONObject().apply {
|
id
|
||||||
put("target_id", shikiMangaId)
|
mediaId
|
||||||
put("target_type", "Manga")
|
status
|
||||||
put("user_id", user.id)
|
notes
|
||||||
},
|
scoreRaw
|
||||||
|
progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
val url = BASE_URL.toHttpUrl().newBuilder()
|
|
||||||
.addPathSegment("api")
|
|
||||||
.addPathSegment("v2")
|
|
||||||
.addPathSegment("user_rates")
|
|
||||||
.build()
|
|
||||||
val request = Request.Builder().url(url).post(payload.toRequestBody()).build()
|
|
||||||
val response = okHttp.newCall(request).await().parseJson()
|
|
||||||
saveRate(response, mangaId)
|
saveRate(response, mangaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter) {
|
override suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter) {
|
||||||
val payload = JSONObject()
|
val payload = JSONObject()
|
||||||
payload.put(
|
payload.put(
|
||||||
"user_rate",
|
"user_rate",
|
||||||
@@ -149,7 +153,7 @@ class AniListRepository(
|
|||||||
saveRate(response, mangaId)
|
saveRate(response, mangaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 payload = JSONObject()
|
||||||
payload.put(
|
payload.put(
|
||||||
"user_rate",
|
"user_rate",
|
||||||
@@ -174,12 +178,23 @@ class AniListRepository(
|
|||||||
saveRate(response, mangaId)
|
saveRate(response, mangaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
|
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
|
||||||
val request = Request.Builder()
|
val response = query(
|
||||||
.get()
|
"""
|
||||||
.url("${BASE_URL}api/mangas/$id")
|
Media(id: $id) {
|
||||||
val response = okHttp.newCall(request.build()).await().parseJson()
|
id
|
||||||
return ScrobblerMangaInfo(response)
|
title {
|
||||||
|
userPreferred
|
||||||
|
}
|
||||||
|
coverImage {
|
||||||
|
large
|
||||||
|
}
|
||||||
|
description
|
||||||
|
siteUrl
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
return ScrobblerMangaInfo(response.getJSONObject("data").getJSONObject("Media"))
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveRate(json: JSONObject, mangaId: Long) {
|
private suspend fun saveRate(json: JSONObject, mangaId: Long) {
|
||||||
@@ -187,29 +202,39 @@ class AniListRepository(
|
|||||||
scrobbler = ScrobblerService.SHIKIMORI.id,
|
scrobbler = ScrobblerService.SHIKIMORI.id,
|
||||||
id = json.getInt("id"),
|
id = json.getInt("id"),
|
||||||
mangaId = mangaId,
|
mangaId = mangaId,
|
||||||
targetId = json.getLong("target_id"),
|
targetId = json.getLong("mediaId"),
|
||||||
status = json.getString("status"),
|
status = json.getString("status"),
|
||||||
chapter = json.getInt("chapters"),
|
chapter = json.getInt("progress"),
|
||||||
comment = json.getString("text"),
|
comment = json.getString("notes"),
|
||||||
rating = json.getDouble("score").toFloat() / 10f,
|
rating = json.getDouble("scoreRaw").toFloat() / 100f,
|
||||||
)
|
)
|
||||||
db.scrobblingDao.insert(entity)
|
db.scrobblingDao.insert(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ScrobblerManga(json: JSONObject) = ScrobblerManga(
|
private fun ScrobblerManga(json: JSONObject): ScrobblerManga {
|
||||||
id = json.getLong("id"),
|
val title = json.getJSONObject("title")
|
||||||
name = json.getString("name"),
|
return ScrobblerManga(
|
||||||
altName = json.getStringOrNull("russian"),
|
id = json.getLong("id"),
|
||||||
cover = json.getJSONObject("image").getString("preview").toAbsoluteUrl("shikimori.one"),
|
name = title.getString("userPreferred"),
|
||||||
url = json.getString("url").toAbsoluteUrl("shikimori.one"),
|
altName = title.getStringOrNull("native"),
|
||||||
)
|
cover = json.getJSONObject("coverImage").getString("medium"),
|
||||||
|
url = json.getString("siteUrl"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun ScrobblerMangaInfo(json: JSONObject) = ScrobblerMangaInfo(
|
private fun ScrobblerMangaInfo(json: JSONObject) = ScrobblerMangaInfo(
|
||||||
id = json.getLong("id"),
|
id = json.getLong("id"),
|
||||||
name = json.getString("name"),
|
name = json.getJSONObject("title").getString("userPreferred"),
|
||||||
cover = json.getJSONObject("image").getString("preview").toAbsoluteUrl("shikimori.one"),
|
cover = json.getJSONObject("coverImage").getString("large"),
|
||||||
url = json.getString("url").toAbsoluteUrl("shikimori.one"),
|
url = json.getString("siteUrl"),
|
||||||
descriptionHtml = json.getString("description_html"),
|
descriptionHtml = json.getString("description"),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun AniListUser(json: JSONObject) = ScrobblerUser(
|
||||||
|
id = json.getLong("id"),
|
||||||
|
nickname = json.getString("name"),
|
||||||
|
avatar = json.getJSONObject("avatar").getString("medium"),
|
||||||
|
service = ScrobblerService.ANILIST,
|
||||||
)
|
)
|
||||||
|
|
||||||
private suspend fun query(query: String): JSONObject {
|
private suspend fun query(query: String): JSONObject {
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.anilist.data
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import org.json.JSONObject
|
|
||||||
import org.koitharu.kotatsu.scrobbling.anilist.data.model.AniListUser
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
private const val PREF_NAME = "anilist"
|
|
||||||
private const val KEY_ACCESS_TOKEN = "access_token"
|
|
||||||
private const val KEY_REFRESH_TOKEN = "refresh_token"
|
|
||||||
private const val KEY_USER = "user"
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class AniListStorage @Inject constructor(@ApplicationContext context: Context) {
|
|
||||||
|
|
||||||
private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
var accessToken: String?
|
|
||||||
get() = prefs.getString(KEY_ACCESS_TOKEN, null)
|
|
||||||
set(value) = prefs.edit { putString(KEY_ACCESS_TOKEN, value) }
|
|
||||||
|
|
||||||
var refreshToken: String?
|
|
||||||
get() = prefs.getString(KEY_REFRESH_TOKEN, null)
|
|
||||||
set(value) = prefs.edit { putString(KEY_REFRESH_TOKEN, value) }
|
|
||||||
|
|
||||||
var user: AniListUser?
|
|
||||||
get() = prefs.getString(KEY_USER, null)?.let {
|
|
||||||
AniListUser(JSONObject(it))
|
|
||||||
}
|
|
||||||
set(value) = prefs.edit {
|
|
||||||
putString(KEY_USER, value?.toJson()?.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clear() = prefs.edit {
|
|
||||||
clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.anilist.data.model
|
|
||||||
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
class AniListUser(
|
|
||||||
val id: Long,
|
|
||||||
val nickname: String,
|
|
||||||
val avatar: String,
|
|
||||||
) {
|
|
||||||
|
|
||||||
constructor(json: JSONObject) : this(
|
|
||||||
id = json.getLong("id"),
|
|
||||||
nickname = json.getString("name"),
|
|
||||||
avatar = json.getJSONObject("avatar").getString("medium"),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun toJson() = JSONObject().apply {
|
|
||||||
put("id", id)
|
|
||||||
put("name", nickname)
|
|
||||||
put("avatar", JSONObject().apply { put("medium", avatar) })
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as AniListUser
|
|
||||||
|
|
||||||
if (id != other.id) return false
|
|
||||||
if (nickname != other.nickname) return false
|
|
||||||
if (avatar != other.avatar) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = id.hashCode()
|
|
||||||
result = 31 * result + nickname.hashCode()
|
|
||||||
result = 31 * result + avatar.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.anilist.domain
|
package org.koitharu.kotatsu.scrobbling.anilist.domain
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
||||||
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
|
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
|
||||||
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.ScrobblingStatus
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -17,7 +14,7 @@ private const val RATING_MAX = 10f
|
|||||||
class AniListScrobbler @Inject constructor(
|
class AniListScrobbler @Inject constructor(
|
||||||
private val repository: AniListRepository,
|
private val repository: AniListRepository,
|
||||||
db: MangaDatabase,
|
db: MangaDatabase,
|
||||||
) : Scrobbler(db, ScrobblerService.ANILIST) {
|
) : Scrobbler(db, ScrobblerService.ANILIST, repository) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
statuses[ScrobblingStatus.PLANNED] = "planned"
|
statuses[ScrobblingStatus.PLANNED] = "planned"
|
||||||
@@ -28,22 +25,6 @@ class AniListScrobbler @Inject constructor(
|
|||||||
statuses[ScrobblingStatus.DROPPED] = "dropped"
|
statuses[ScrobblingStatus.DROPPED] = "dropped"
|
||||||
}
|
}
|
||||||
|
|
||||||
override val isAvailable: Boolean
|
|
||||||
get() = repository.isAuthorized
|
|
||||||
|
|
||||||
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
|
|
||||||
return repository.findManga(query, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun linkManga(mangaId: Long, targetId: Long) {
|
|
||||||
repository.createRate(mangaId, targetId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun scrobble(mangaId: Long, chapter: MangaChapter) {
|
|
||||||
val entity = db.scrobblingDao.find(scrobblerService.id, mangaId) ?: return
|
|
||||||
repository.updateRate(entity.id, entity.mangaId, chapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateScrobblingInfo(
|
override suspend fun updateScrobblingInfo(
|
||||||
mangaId: Long,
|
mangaId: Long,
|
||||||
rating: Float,
|
rating: Float,
|
||||||
@@ -60,12 +41,4 @@ class AniListScrobbler @Inject constructor(
|
|||||||
comment = comment,
|
comment = comment,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun unregisterScrobbling(mangaId: Long) {
|
|
||||||
repository.unregister(mangaId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
|
|
||||||
return repository.getMangaInfo(id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import coil.transform.CircleCropTransformation
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||||
import org.koitharu.kotatsu.scrobbling.anilist.data.model.AniListUser
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||||
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
||||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
@@ -52,7 +52,7 @@ class AniListSettingsFragment : BasePreferenceFragment(R.string.anilist) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onUserChanged(user: AniListUser?) {
|
private fun onUserChanged(user: ScrobblerUser?) {
|
||||||
val pref = findPreference<Preference>(KEY_USER) ?: return
|
val pref = findPreference<Preference>(KEY_USER) ?: return
|
||||||
pref.isSelectable = user == null
|
pref.isSelectable = user == null
|
||||||
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import dagger.assisted.AssistedInject
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
|
import org.koitharu.kotatsu.scrobbling.anilist.data.AniListRepository
|
||||||
import org.koitharu.kotatsu.scrobbling.anilist.data.model.AniListUser
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||||
|
|
||||||
class AniListSettingsViewModel @AssistedInject constructor(
|
class AniListSettingsViewModel @AssistedInject constructor(
|
||||||
private val repository: AniListRepository,
|
private val repository: AniListRepository,
|
||||||
@@ -17,7 +17,7 @@ class AniListSettingsViewModel @AssistedInject constructor(
|
|||||||
val authorizationUrl: String
|
val authorizationUrl: String
|
||||||
get() = repository.oauthUrl
|
get() = repository.oauthUrl
|
||||||
|
|
||||||
val user = MutableLiveData<AniListUser?>()
|
val user = MutableLiveData<ScrobblerUser?>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (authCode != null) {
|
if (authCode != null) {
|
||||||
@@ -36,7 +36,7 @@ class AniListSettingsViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun loadUser() = launchJob(Dispatchers.Default) {
|
private fun loadUser() = launchJob(Dispatchers.Default) {
|
||||||
val userModel = if (repository.isAuthorized) {
|
val userModel = if (repository.isAuthorized) {
|
||||||
repository.getCachedUser()?.let(user::postValue)
|
repository.cachedUser?.let(user::postValue)
|
||||||
repository.loadUser()
|
repository.loadUser()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org.koitharu.kotatsu.scrobbling.data
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||||
|
|
||||||
|
interface ScrobblerRepository {
|
||||||
|
|
||||||
|
val oauthUrl: String
|
||||||
|
|
||||||
|
val isAuthorized: Boolean
|
||||||
|
|
||||||
|
val cachedUser: ScrobblerUser?
|
||||||
|
|
||||||
|
suspend fun authorize(code: String?)
|
||||||
|
|
||||||
|
suspend fun loadUser(): ScrobblerUser
|
||||||
|
|
||||||
|
fun logout()
|
||||||
|
|
||||||
|
suspend fun unregister(mangaId: Long)
|
||||||
|
|
||||||
|
suspend fun findManga(query: String, offset: Int): List<ScrobblerManga>
|
||||||
|
|
||||||
|
suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo
|
||||||
|
|
||||||
|
suspend fun createRate(mangaId: Long, scrobblerMangaId: Long)
|
||||||
|
|
||||||
|
suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter)
|
||||||
|
|
||||||
|
suspend fun updateRate(rateId: Int, mangaId: Long, rating: Float, status: String?, comment: String?)
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package org.koitharu.kotatsu.scrobbling.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||||
|
|
||||||
|
private const val KEY_ACCESS_TOKEN = "access_token"
|
||||||
|
private const val KEY_REFRESH_TOKEN = "refresh_token"
|
||||||
|
private const val KEY_USER = "user"
|
||||||
|
|
||||||
|
class ScrobblerStorage(context: Context, service: ScrobblerService) {
|
||||||
|
|
||||||
|
private val prefs = context.getSharedPreferences(service.name, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
var accessToken: String?
|
||||||
|
get() = prefs.getString(KEY_ACCESS_TOKEN, null)
|
||||||
|
set(value) = prefs.edit { putString(KEY_ACCESS_TOKEN, value) }
|
||||||
|
|
||||||
|
var refreshToken: String?
|
||||||
|
get() = prefs.getString(KEY_REFRESH_TOKEN, null)
|
||||||
|
set(value) = prefs.edit { putString(KEY_REFRESH_TOKEN, value) }
|
||||||
|
|
||||||
|
var user: ScrobblerUser?
|
||||||
|
get() = prefs.getString(KEY_USER, null)?.let {
|
||||||
|
val lines = it.lines()
|
||||||
|
if (lines.size != 4) {
|
||||||
|
return@let null
|
||||||
|
}
|
||||||
|
ScrobblerUser(
|
||||||
|
id = lines[0].toLong(),
|
||||||
|
nickname = lines[1],
|
||||||
|
avatar = lines[2],
|
||||||
|
service = ScrobblerService.valueOf(lines[3]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
set(value) = prefs.edit {
|
||||||
|
if (value == null) {
|
||||||
|
remove(KEY_USER)
|
||||||
|
return@edit
|
||||||
|
}
|
||||||
|
val str = buildString {
|
||||||
|
appendLine(value.id)
|
||||||
|
appendLine(value.nickname)
|
||||||
|
appendLine(value.avatar)
|
||||||
|
appendLine(value.service.name)
|
||||||
|
}
|
||||||
|
putString(KEY_USER, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() = prefs.edit {
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
|
||||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
|
||||||
@@ -21,18 +22,27 @@ import java.util.EnumMap
|
|||||||
abstract class Scrobbler(
|
abstract class Scrobbler(
|
||||||
protected val db: MangaDatabase,
|
protected val db: MangaDatabase,
|
||||||
val scrobblerService: ScrobblerService,
|
val scrobblerService: ScrobblerService,
|
||||||
|
private val repository: ScrobblerRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val infoCache = LongSparseArray<ScrobblerMangaInfo>()
|
private val infoCache = LongSparseArray<ScrobblerMangaInfo>()
|
||||||
protected val statuses = EnumMap<ScrobblingStatus, String>(ScrobblingStatus::class.java)
|
protected val statuses = EnumMap<ScrobblingStatus, String>(ScrobblingStatus::class.java)
|
||||||
|
|
||||||
abstract val isAvailable: Boolean
|
val isAvailable: Boolean
|
||||||
|
get() = repository.isAuthorized
|
||||||
|
|
||||||
abstract suspend fun findManga(query: String, offset: Int): List<ScrobblerManga>
|
suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
|
||||||
|
return repository.findManga(query, offset)
|
||||||
|
}
|
||||||
|
|
||||||
abstract suspend fun linkManga(mangaId: Long, targetId: Long)
|
suspend fun linkManga(mangaId: Long, targetId: Long) {
|
||||||
|
repository.createRate(mangaId, targetId)
|
||||||
|
}
|
||||||
|
|
||||||
abstract suspend fun scrobble(mangaId: Long, chapter: MangaChapter)
|
suspend fun scrobble(mangaId: Long, chapter: MangaChapter) {
|
||||||
|
val entity = db.scrobblingDao.find(scrobblerService.id, mangaId) ?: return
|
||||||
|
repository.updateRate(entity.id, entity.mangaId, chapter)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getScrobblingInfoOrNull(mangaId: Long): ScrobblingInfo? {
|
suspend fun getScrobblingInfoOrNull(mangaId: Long): ScrobblingInfo? {
|
||||||
val entity = db.scrobblingDao.find(scrobblerService.id, mangaId) ?: return null
|
val entity = db.scrobblingDao.find(scrobblerService.id, mangaId) ?: return null
|
||||||
@@ -46,9 +56,13 @@ abstract class Scrobbler(
|
|||||||
.map { it?.toScrobblingInfo(mangaId) }
|
.map { it?.toScrobblingInfo(mangaId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract suspend fun unregisterScrobbling(mangaId: Long)
|
suspend fun unregisterScrobbling(mangaId: Long) {
|
||||||
|
repository.unregister(mangaId)
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo
|
protected suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
|
||||||
|
return repository.getMangaInfo(id)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun ScrobblingEntity.toScrobblingInfo(mangaId: Long): ScrobblingInfo? {
|
private suspend fun ScrobblingEntity.toScrobblingInfo(mangaId: Long): ScrobblingInfo? {
|
||||||
val mangaInfo = infoCache.getOrElse(targetId) {
|
val mangaInfo = infoCache.getOrElse(targetId) {
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||||
|
|
||||||
|
import javax.inject.Qualifier
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
annotation class ScrobblerType(
|
||||||
|
val service: ScrobblerService
|
||||||
|
)
|
||||||
@@ -1,34 +1,22 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.shikimori.data.model
|
package org.koitharu.kotatsu.scrobbling.domain.model
|
||||||
|
|
||||||
import org.json.JSONObject
|
class ScrobblerUser(
|
||||||
|
|
||||||
class ShikimoriUser(
|
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val nickname: String,
|
val nickname: String,
|
||||||
val avatar: String,
|
val avatar: String,
|
||||||
|
val service: ScrobblerService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
constructor(json: JSONObject) : this(
|
|
||||||
id = json.getLong("id"),
|
|
||||||
nickname = json.getString("nickname"),
|
|
||||||
avatar = json.getString("avatar"),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun toJson() = JSONObject().apply {
|
|
||||||
put("id", id)
|
|
||||||
put("nickname", nickname)
|
|
||||||
put("avatar", avatar)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
other as ShikimoriUser
|
other as ScrobblerUser
|
||||||
|
|
||||||
if (id != other.id) return false
|
if (id != other.id) return false
|
||||||
if (nickname != other.nickname) return false
|
if (nickname != other.nickname) return false
|
||||||
if (avatar != other.avatar) return false
|
if (avatar != other.avatar) return false
|
||||||
|
if (service != other.service) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -37,6 +25,7 @@ class ShikimoriUser(
|
|||||||
var result = id.hashCode()
|
var result = id.hashCode()
|
||||||
result = 31 * result + nickname.hashCode()
|
result = 31 * result + nickname.hashCode()
|
||||||
result = 31 * result + avatar.hashCode()
|
result = 31 * result + avatar.hashCode()
|
||||||
|
result = 31 * result + service.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.shikimori.data
|
package org.koitharu.kotatsu.scrobbling.shikimori.data
|
||||||
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Provider
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.Authenticator
|
import okhttp3.Authenticator
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@@ -9,9 +7,14 @@ import okhttp3.Response
|
|||||||
import okhttp3.Route
|
import okhttp3.Route
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerType
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Provider
|
||||||
|
|
||||||
class ShikimoriAuthenticator @Inject constructor(
|
class ShikimoriAuthenticator @Inject constructor(
|
||||||
private val storage: ShikimoriStorage,
|
@ScrobblerType(ScrobblerService.SHIKIMORI) private val storage: ScrobblerStorage,
|
||||||
private val repositoryProvider: Provider<ShikimoriRepository>,
|
private val repositoryProvider: Provider<ShikimoriRepository>,
|
||||||
) : Authenticator {
|
) : Authenticator {
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import okhttp3.Interceptor
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||||
|
|
||||||
private const val USER_AGENT_SHIKIMORI = "Kotatsu"
|
private const val USER_AGENT_SHIKIMORI = "Kotatsu"
|
||||||
|
|
||||||
class ShikimoriInterceptor(private val storage: ShikimoriStorage) : Interceptor {
|
class ShikimoriInterceptor(private val storage: ScrobblerStorage) : Interceptor {
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val sourceRequest = chain.request()
|
val sourceRequest = chain.request()
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ 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.parseJsonArray
|
import org.koitharu.kotatsu.parsers.util.parseJsonArray
|
||||||
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
import org.koitharu.kotatsu.parsers.util.toAbsoluteUrl
|
||||||
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
|
||||||
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
|
||||||
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
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.shikimori.data.model.ShikimoriUser
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||||
import org.koitharu.kotatsu.utils.ext.toRequestBody
|
import org.koitharu.kotatsu.utils.ext.toRequestBody
|
||||||
|
|
||||||
private const val REDIRECT_URI = "kotatsu://shikimori-auth"
|
private const val REDIRECT_URI = "kotatsu://shikimori-auth"
|
||||||
@@ -27,18 +29,18 @@ private const val MANGA_PAGE_SIZE = 10
|
|||||||
|
|
||||||
class ShikimoriRepository(
|
class ShikimoriRepository(
|
||||||
private val okHttp: OkHttpClient,
|
private val okHttp: OkHttpClient,
|
||||||
private val storage: ShikimoriStorage,
|
private val storage: ScrobblerStorage,
|
||||||
private val db: MangaDatabase,
|
private val db: MangaDatabase,
|
||||||
) {
|
) : ScrobblerRepository {
|
||||||
|
|
||||||
val oauthUrl: String
|
override val oauthUrl: String
|
||||||
get() = "${BASE_URL}oauth/authorize?client_id=${BuildConfig.SHIKIMORI_CLIENT_ID}&" +
|
get() = "${BASE_URL}oauth/authorize?client_id=${BuildConfig.SHIKIMORI_CLIENT_ID}&" +
|
||||||
"redirect_uri=$REDIRECT_URI&response_type=code&scope="
|
"redirect_uri=$REDIRECT_URI&response_type=code&scope="
|
||||||
|
|
||||||
val isAuthorized: Boolean
|
override val isAuthorized: Boolean
|
||||||
get() = storage.accessToken != null
|
get() = storage.accessToken != null
|
||||||
|
|
||||||
suspend fun authorize(code: String?) {
|
override suspend fun authorize(code: String?) {
|
||||||
val body = FormBody.Builder()
|
val body = FormBody.Builder()
|
||||||
body.add("client_id", BuildConfig.SHIKIMORI_CLIENT_ID)
|
body.add("client_id", BuildConfig.SHIKIMORI_CLIENT_ID)
|
||||||
body.add("client_secret", BuildConfig.SHIKIMORI_CLIENT_SECRET)
|
body.add("client_secret", BuildConfig.SHIKIMORI_CLIENT_SECRET)
|
||||||
@@ -58,7 +60,7 @@ class ShikimoriRepository(
|
|||||||
storage.refreshToken = response.getString("refresh_token")
|
storage.refreshToken = response.getString("refresh_token")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadUser(): ShikimoriUser {
|
override suspend fun loadUser(): ScrobblerUser {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.get()
|
.get()
|
||||||
.url("${BASE_URL}api/users/whoami")
|
.url("${BASE_URL}api/users/whoami")
|
||||||
@@ -66,19 +68,20 @@ class ShikimoriRepository(
|
|||||||
return ShikimoriUser(response).also { storage.user = it }
|
return ShikimoriUser(response).also { storage.user = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCachedUser(): ShikimoriUser? {
|
override val cachedUser: ScrobblerUser?
|
||||||
return storage.user
|
get() {
|
||||||
}
|
return storage.user
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun unregister(mangaId: Long) {
|
override suspend fun unregister(mangaId: Long) {
|
||||||
return db.scrobblingDao.delete(ScrobblerService.SHIKIMORI.id, mangaId)
|
return db.scrobblingDao.delete(ScrobblerService.SHIKIMORI.id, mangaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun logout() {
|
override fun logout() {
|
||||||
storage.clear()
|
storage.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
val pageOffset = offset % MANGA_PAGE_SIZE
|
val pageOffset = offset % MANGA_PAGE_SIZE
|
||||||
val url = BASE_URL.toHttpUrl().newBuilder()
|
val url = BASE_URL.toHttpUrl().newBuilder()
|
||||||
@@ -95,8 +98,8 @@ class ShikimoriRepository(
|
|||||||
return if (pageOffset != 0) list.drop(pageOffset) else list
|
return if (pageOffset != 0) list.drop(pageOffset) else list
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun createRate(mangaId: Long, shikiMangaId: Long) {
|
override suspend fun createRate(mangaId: Long, shikiMangaId: Long) {
|
||||||
val user = getCachedUser() ?: loadUser()
|
val user = cachedUser ?: loadUser()
|
||||||
val payload = JSONObject()
|
val payload = JSONObject()
|
||||||
payload.put(
|
payload.put(
|
||||||
"user_rate",
|
"user_rate",
|
||||||
@@ -116,7 +119,7 @@ class ShikimoriRepository(
|
|||||||
saveRate(response, mangaId)
|
saveRate(response, mangaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter) {
|
override suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter) {
|
||||||
val payload = JSONObject()
|
val payload = JSONObject()
|
||||||
payload.put(
|
payload.put(
|
||||||
"user_rate",
|
"user_rate",
|
||||||
@@ -135,7 +138,7 @@ class ShikimoriRepository(
|
|||||||
saveRate(response, mangaId)
|
saveRate(response, mangaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 payload = JSONObject()
|
||||||
payload.put(
|
payload.put(
|
||||||
"user_rate",
|
"user_rate",
|
||||||
@@ -160,7 +163,7 @@ class ShikimoriRepository(
|
|||||||
saveRate(response, mangaId)
|
saveRate(response, mangaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
|
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.get()
|
.get()
|
||||||
.url("${BASE_URL}api/mangas/$id")
|
.url("${BASE_URL}api/mangas/$id")
|
||||||
@@ -197,4 +200,11 @@ class ShikimoriRepository(
|
|||||||
url = json.getString("url").toAbsoluteUrl("shikimori.one"),
|
url = json.getString("url").toAbsoluteUrl("shikimori.one"),
|
||||||
descriptionHtml = json.getString("description_html"),
|
descriptionHtml = json.getString("description_html"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun ShikimoriUser(json: JSONObject) = ScrobblerUser(
|
||||||
|
id = json.getLong("id"),
|
||||||
|
nickname = json.getString("nickname"),
|
||||||
|
avatar = json.getString("avatar"),
|
||||||
|
service = ScrobblerService.SHIKIMORI,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.shikimori.data
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import org.json.JSONObject
|
|
||||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.model.ShikimoriUser
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
private const val PREF_NAME = "shikimori"
|
|
||||||
private const val KEY_ACCESS_TOKEN = "access_token"
|
|
||||||
private const val KEY_REFRESH_TOKEN = "refresh_token"
|
|
||||||
private const val KEY_USER = "user"
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class ShikimoriStorage @Inject constructor(@ApplicationContext context: Context) {
|
|
||||||
|
|
||||||
private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
var accessToken: String?
|
|
||||||
get() = prefs.getString(KEY_ACCESS_TOKEN, null)
|
|
||||||
set(value) = prefs.edit { putString(KEY_ACCESS_TOKEN, value) }
|
|
||||||
|
|
||||||
var refreshToken: String?
|
|
||||||
get() = prefs.getString(KEY_REFRESH_TOKEN, null)
|
|
||||||
set(value) = prefs.edit { putString(KEY_REFRESH_TOKEN, value) }
|
|
||||||
|
|
||||||
var user: ShikimoriUser?
|
|
||||||
get() = prefs.getString(KEY_USER, null)?.let {
|
|
||||||
ShikimoriUser(JSONObject(it))
|
|
||||||
}
|
|
||||||
set(value) = prefs.edit {
|
|
||||||
putString(KEY_USER, value?.toJson()?.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clear() = prefs.edit {
|
|
||||||
clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
package org.koitharu.kotatsu.scrobbling.shikimori.domain
|
package org.koitharu.kotatsu.scrobbling.shikimori.domain
|
||||||
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
|
||||||
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
|
|
||||||
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.ScrobblingStatus
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus
|
||||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
|
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
private const val RATING_MAX = 10f
|
private const val RATING_MAX = 10f
|
||||||
|
|
||||||
@@ -17,7 +14,7 @@ private const val RATING_MAX = 10f
|
|||||||
class ShikimoriScrobbler @Inject constructor(
|
class ShikimoriScrobbler @Inject constructor(
|
||||||
private val repository: ShikimoriRepository,
|
private val repository: ShikimoriRepository,
|
||||||
db: MangaDatabase,
|
db: MangaDatabase,
|
||||||
) : Scrobbler(db, ScrobblerService.SHIKIMORI) {
|
) : Scrobbler(db, ScrobblerService.SHIKIMORI, repository) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
statuses[ScrobblingStatus.PLANNED] = "planned"
|
statuses[ScrobblingStatus.PLANNED] = "planned"
|
||||||
@@ -28,22 +25,6 @@ class ShikimoriScrobbler @Inject constructor(
|
|||||||
statuses[ScrobblingStatus.DROPPED] = "dropped"
|
statuses[ScrobblingStatus.DROPPED] = "dropped"
|
||||||
}
|
}
|
||||||
|
|
||||||
override val isAvailable: Boolean
|
|
||||||
get() = repository.isAuthorized
|
|
||||||
|
|
||||||
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
|
|
||||||
return repository.findManga(query, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun linkManga(mangaId: Long, targetId: Long) {
|
|
||||||
repository.createRate(mangaId, targetId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun scrobble(mangaId: Long, chapter: MangaChapter) {
|
|
||||||
val entity = db.scrobblingDao.find(scrobblerService.id, mangaId) ?: return
|
|
||||||
repository.updateRate(entity.id, entity.mangaId, chapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateScrobblingInfo(
|
override suspend fun updateScrobblingInfo(
|
||||||
mangaId: Long,
|
mangaId: Long,
|
||||||
rating: Float,
|
rating: Float,
|
||||||
@@ -60,12 +41,4 @@ class ShikimoriScrobbler @Inject constructor(
|
|||||||
comment = comment,
|
comment = comment,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun unregisterScrobbling(mangaId: Long) {
|
|
||||||
repository.unregister(mangaId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
|
|
||||||
return repository.getMangaInfo(id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import coil.ImageLoader
|
|||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import javax.inject.Inject
|
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.model.ShikimoriUser
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||||
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
import org.koitharu.kotatsu.utils.PreferenceIconTarget
|
||||||
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
import org.koitharu.kotatsu.utils.ext.assistedViewModels
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) {
|
class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) {
|
||||||
@@ -47,11 +47,12 @@ class ShikimoriSettingsFragment : BasePreferenceFragment(R.string.shikimori) {
|
|||||||
viewModel.logout()
|
viewModel.logout()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> super.onPreferenceTreeClick(preference)
|
else -> super.onPreferenceTreeClick(preference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onUserChanged(user: ShikimoriUser?) {
|
private fun onUserChanged(user: ScrobblerUser?) {
|
||||||
val pref = findPreference<Preference>(KEY_USER) ?: return
|
val pref = findPreference<Preference>(KEY_USER) ?: return
|
||||||
pref.isSelectable = user == null
|
pref.isSelectable = user == null
|
||||||
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
pref.title = user?.nickname ?: getString(R.string.sign_in)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import dagger.assisted.AssistedFactory
|
|||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
|
||||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
|
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
|
||||||
import org.koitharu.kotatsu.scrobbling.shikimori.data.model.ShikimoriUser
|
|
||||||
|
|
||||||
class ShikimoriSettingsViewModel @AssistedInject constructor(
|
class ShikimoriSettingsViewModel @AssistedInject constructor(
|
||||||
private val repository: ShikimoriRepository,
|
private val repository: ShikimoriRepository,
|
||||||
@@ -17,7 +17,7 @@ class ShikimoriSettingsViewModel @AssistedInject constructor(
|
|||||||
val authorizationUrl: String
|
val authorizationUrl: String
|
||||||
get() = repository.oauthUrl
|
get() = repository.oauthUrl
|
||||||
|
|
||||||
val user = MutableLiveData<ShikimoriUser?>()
|
val user = MutableLiveData<ScrobblerUser?>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (authCode != null) {
|
if (authCode != null) {
|
||||||
@@ -36,7 +36,7 @@ class ShikimoriSettingsViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun loadUser() = launchJob(Dispatchers.Default) {
|
private fun loadUser() = launchJob(Dispatchers.Default) {
|
||||||
val userModel = if (repository.isAuthorized) {
|
val userModel = if (repository.isAuthorized) {
|
||||||
repository.getCachedUser()?.let(user::postValue)
|
repository.cachedUser?.let(user::postValue)
|
||||||
repository.loadUser()
|
repository.loadUser()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
|||||||
|
|
||||||
private fun bindShikimoriSummary() {
|
private fun bindShikimoriSummary() {
|
||||||
findPreference<Preference>(AppSettings.KEY_SHIKIMORI)?.summary = if (shikimoriRepository.isAuthorized) {
|
findPreference<Preference>(AppSettings.KEY_SHIKIMORI)?.summary = if (shikimoriRepository.isAuthorized) {
|
||||||
getString(R.string.logged_in_as, shikimoriRepository.getCachedUser()?.nickname)
|
getString(R.string.logged_in_as, shikimoriRepository.cachedUser?.nickname)
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.disabled)
|
getString(R.string.disabled)
|
||||||
}
|
}
|
||||||
@@ -209,7 +209,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
|||||||
|
|
||||||
private fun bindAniListSummary() {
|
private fun bindAniListSummary() {
|
||||||
findPreference<Preference>(AppSettings.KEY_ANILIST)?.summary = if (aniListRepository.isAuthorized) {
|
findPreference<Preference>(AppSettings.KEY_ANILIST)?.summary = if (aniListRepository.isAuthorized) {
|
||||||
getString(R.string.logged_in_as, aniListRepository.getCachedUser()?.nickname)
|
getString(R.string.logged_in_as, aniListRepository.cachedUser?.nickname)
|
||||||
} else {
|
} else {
|
||||||
getString(R.string.disabled)
|
getString(R.string.disabled)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user