Update MAL codebase

This commit is contained in:
Zakhar Timoshenko
2023-01-30 01:27:45 +03:00
parent ee2538ba7f
commit 80be0e403d
11 changed files with 74 additions and 134 deletions

View File

@@ -20,7 +20,6 @@ import org.koitharu.kotatsu.scrobbling.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.mal.data.MALAuthenticator
import org.koitharu.kotatsu.scrobbling.mal.data.MALInterceptor
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
import org.koitharu.kotatsu.scrobbling.mal.data.MALStorage
import org.koitharu.kotatsu.scrobbling.mal.domain.MALScrobbler
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerType
@@ -54,13 +53,16 @@ object ScrobblingModule {
@Provides
@Singleton
fun provideMALRepository(
storage: MALStorage,
@ScrobblerType(ScrobblerService.MAL) storage: ScrobblerStorage,
database: MangaDatabase,
authenticator: MALAuthenticator,
): MALRepository {
val okHttp = OkHttpClient.Builder().apply {
authenticator(authenticator)
addInterceptor(MALInterceptor(storage))
if (BuildConfig.DEBUG) {
addInterceptor(CurlLoggingInterceptor())
}
}.build()
return MALRepository(okHttp, storage, database)
}
@@ -96,6 +98,13 @@ object ScrobblingModule {
@ApplicationContext context: Context,
): ScrobblerStorage = ScrobblerStorage(context, ScrobblerService.SHIKIMORI)
@Provides
@Singleton
@ScrobblerType(ScrobblerService.MAL)
fun provideMALStorage(
@ApplicationContext context: Context,
): ScrobblerStorage = ScrobblerStorage(context, ScrobblerService.MAL)
@Provides
@ElementsIntoSet
fun provideScrobblers(

View File

@@ -7,11 +7,14 @@ import okhttp3.Response
import okhttp3.Route
import org.koitharu.kotatsu.BuildConfig
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 MALAuthenticator @Inject constructor(
private val storage: MALStorage,
@ScrobblerType(ScrobblerService.MAL) private val storage: ScrobblerStorage,
private val repositoryProvider: Provider<MALRepository>,
) : Authenticator {

View File

@@ -3,23 +3,23 @@ package org.koitharu.kotatsu.scrobbling.mal.data
import okhttp3.Interceptor
import okhttp3.Response
import org.koitharu.kotatsu.core.network.CommonHeaders
import java.io.IOException
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
class MALInterceptor(private val storage: MALStorage) : Interceptor {
private const val JSON = "application/json"
class MALInterceptor(private val storage: ScrobblerStorage) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val sourceRequest = chain.request()
val request = sourceRequest.newBuilder()
if (!sourceRequest.url.pathSegments.contains("oauth2")) {
request.header(CommonHeaders.CONTENT_TYPE, JSON)
request.header(CommonHeaders.ACCEPT, JSON)
if (!sourceRequest.url.pathSegments.contains("oauth")) {
storage.accessToken?.let {
request.header(CommonHeaders.AUTHORIZATION, "Bearer $it")
}
}
val response = chain.proceed(request.build())
if (!response.isSuccessful && !response.isRedirect) {
throw IOException("${response.code} ${response.message}")
}
return response
return chain.proceed(request.build())
}
}

View File

@@ -3,11 +3,17 @@ package org.koitharu.kotatsu.scrobbling.mal.data
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.scrobbling.data.ScrobblerRepository
import org.koitharu.kotatsu.scrobbling.data.ScrobblerStorage
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.mal.data.model.MALUser
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
import org.koitharu.kotatsu.utils.PKCEGenerator
private const val REDIRECT_URI = "kotatsu://mal-auth"
@@ -19,13 +25,13 @@ private const val MANGA_PAGE_SIZE = 250
class MALRepository(
private val okHttp: OkHttpClient,
private val storage: MALStorage,
private val storage: ScrobblerStorage,
private val db: MangaDatabase,
) {
) : ScrobblerRepository {
private var codeVerifier: String = ""
val oauthUrl: String
override val oauthUrl: String
get() = "${BASE_OAUTH_URL}/v1/oauth2/authorize?" +
"response_type=code" +
"&client_id=af16954886b040673378423f5d62cccd" +
@@ -33,10 +39,12 @@ class MALRepository(
"&code_challenge=${getPKCEChallengeCode()}" +
"&code_challenge_method=plain"
val isAuthorized: Boolean
override val isAuthorized: Boolean
get() = storage.accessToken != null
override val cachedUser: ScrobblerUser?
get() = TODO("Not yet implemented")
suspend fun authorize(code: String?) {
override suspend fun authorize(code: String?) {
val body = FormBody.Builder()
if (code != null) {
body.add("client_id", "af16954886b040673378423f5d62cccd")
@@ -53,7 +61,7 @@ class MALRepository(
storage.refreshToken = response.getString("refresh_token")
}
suspend fun loadUser(): MALUser {
override suspend fun loadUser(): ScrobblerUser {
val request = Request.Builder()
.get()
.url("${BASE_API_URL}/users")
@@ -61,15 +69,31 @@ class MALRepository(
return MALUser(response).also { storage.user = it }
}
fun getCachedUser(): MALUser? {
return storage.user
}
suspend fun unregister(mangaId: Long) {
override suspend fun unregister(mangaId: Long) {
return db.scrobblingDao.delete(ScrobblerService.MAL.id, mangaId)
}
fun logout() {
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
TODO("Not yet implemented")
}
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
TODO("Not yet implemented")
}
override suspend fun createRate(mangaId: Long, scrobblerMangaId: Long) {
TODO("Not yet implemented")
}
override suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter) {
TODO("Not yet implemented")
}
override suspend fun updateRate(rateId: Int, mangaId: Long, rating: Float, status: String?, comment: String?) {
TODO("Not yet implemented")
}
override fun logout() {
storage.clear()
}
@@ -78,4 +102,11 @@ class MALRepository(
return codeVerifier
}
private fun MALUser(json: JSONObject) = ScrobblerUser(
id = json.getLong("id"),
nickname = json.getString("nickname"),
avatar = json.getString("avatar"),
service = ScrobblerService.SHIKIMORI,
)
}

View File

@@ -1,41 +0,0 @@
package org.koitharu.kotatsu.scrobbling.mal.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.mal.data.model.MALUser
import javax.inject.Inject
import javax.inject.Singleton
private const val PREF_NAME = "myanimelist"
private const val KEY_ACCESS_TOKEN = "access_token"
private const val KEY_REFRESH_TOKEN = "refresh_token"
private const val KEY_USER = "user"
@Singleton
class MALStorage @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: MALUser?
get() = prefs.getString(KEY_USER, null)?.let {
MALUser(JSONObject(it))
}
set(value) = prefs.edit {
putString(KEY_USER, value?.toJson()?.toString())
}
fun clear() = prefs.edit {
clear()
}
}

View File

@@ -1,37 +0,0 @@
package org.koitharu.kotatsu.scrobbling.mal.data.model
import org.json.JSONObject
class MALUser(
val id: Long,
val nickname: String,
) {
constructor(json: JSONObject) : this(
id = json.getLong("id"),
nickname = json.getString("name"),
)
fun toJson() = JSONObject().apply {
put("id", id)
put("nickname", nickname)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MALUser
if (id != other.id) return false
if (nickname != other.nickname) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + nickname.hashCode()
return result
}
}

View File

@@ -17,7 +17,7 @@ private const val RATING_MAX = 10f
class MALScrobbler @Inject constructor(
private val repository: MALRepository,
db: MangaDatabase,
) : Scrobbler(db, ScrobblerService.MAL) {
) : Scrobbler(db, ScrobblerService.MAL, repository) {
init {
statuses[ScrobblingStatus.PLANNED] = "plan_to_read"
@@ -27,21 +27,6 @@ class MALScrobbler @Inject constructor(
statuses[ScrobblingStatus.DROPPED] = "dropped"
}
override val isAvailable: Boolean
get() = repository.isAuthorized
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
TODO()
}
override suspend fun linkManga(mangaId: Long, targetId: Long) {
TODO()
}
override suspend fun scrobble(mangaId: Long, chapter: MangaChapter) {
TODO()
}
override suspend fun updateScrobblingInfo(
mangaId: Long,
rating: Float,
@@ -51,11 +36,4 @@ class MALScrobbler @Inject constructor(
TODO()
}
override suspend fun unregisterScrobbling(mangaId: Long) {
repository.unregister(mangaId)
}
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
TODO()
}
}

View File

@@ -8,7 +8,7 @@ import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.scrobbling.mal.data.model.MALUser
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject
@@ -43,7 +43,7 @@ class MALSettingsFragment : BasePreferenceFragment(R.string.mal) {
}
}
private fun onUserChanged(user: MALUser?) {
private fun onUserChanged(user: ScrobblerUser?) {
val pref = findPreference<Preference>(KEY_USER) ?: return
pref.isSelectable = user == null
pref.title = user?.nickname ?: getString(R.string.sign_in)

View File

@@ -6,10 +6,8 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository
import org.koitharu.kotatsu.scrobbling.mal.data.model.MALUser
import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository
import org.koitharu.kotatsu.scrobbling.shikimori.data.model.ShikimoriUser
class MALSettingsViewModel @AssistedInject constructor(
private val repository: MALRepository,
@@ -19,7 +17,7 @@ class MALSettingsViewModel @AssistedInject constructor(
val authorizationUrl: String
get() = repository.oauthUrl
val user = MutableLiveData<MALUser?>()
val user = MutableLiveData<ScrobblerUser?>()
init {
if (authCode != null) {
@@ -38,7 +36,7 @@ class MALSettingsViewModel @AssistedInject constructor(
private fun loadUser() = launchJob(Dispatchers.Default) {
val userModel = if (repository.isAuthorized) {
repository.getCachedUser()?.let(user::postValue)
repository.cachedUser?.let(user::postValue)
repository.loadUser()
} else {
null

View File

@@ -241,7 +241,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
private fun bindMALSummary() {
findPreference<Preference>(AppSettings.KEY_MAL)?.summary = if (malRepository.isAuthorized) {
getString(R.string.logged_in_as, malRepository.getCachedUser()?.nickname)
getString(R.string.logged_in_as, malRepository.cachedUser?.nickname)
} else {
getString(R.string.disabled)
}

View File

@@ -25,7 +25,6 @@
android:fragment="org.koitharu.kotatsu.scrobbling.shikimori.ui.ShikimoriSettingsFragment"
android:icon="@drawable/ic_shikimori"
android:key="shikimori"
android:icon="@drawable/ic_shikimori"
android:title="@string/shikimori" />
<PreferenceScreen