Update MAL codebase

pull/302/head
Zakhar Timoshenko 3 years ago
parent ee2538ba7f
commit 80be0e403d

@ -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.MALAuthenticator
import org.koitharu.kotatsu.scrobbling.mal.data.MALInterceptor import org.koitharu.kotatsu.scrobbling.mal.data.MALInterceptor
import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository 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.mal.domain.MALScrobbler
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerType import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerType
@ -54,13 +53,16 @@ object ScrobblingModule {
@Provides @Provides
@Singleton @Singleton
fun provideMALRepository( fun provideMALRepository(
storage: MALStorage, @ScrobblerType(ScrobblerService.MAL) storage: ScrobblerStorage,
database: MangaDatabase, database: MangaDatabase,
authenticator: MALAuthenticator, authenticator: MALAuthenticator,
): MALRepository { ): MALRepository {
val okHttp = OkHttpClient.Builder().apply { val okHttp = OkHttpClient.Builder().apply {
authenticator(authenticator) authenticator(authenticator)
addInterceptor(MALInterceptor(storage)) addInterceptor(MALInterceptor(storage))
if (BuildConfig.DEBUG) {
addInterceptor(CurlLoggingInterceptor())
}
}.build() }.build()
return MALRepository(okHttp, storage, database) return MALRepository(okHttp, storage, database)
} }
@ -96,6 +98,13 @@ object ScrobblingModule {
@ApplicationContext context: Context, @ApplicationContext context: Context,
): ScrobblerStorage = ScrobblerStorage(context, ScrobblerService.SHIKIMORI) ): ScrobblerStorage = ScrobblerStorage(context, ScrobblerService.SHIKIMORI)
@Provides
@Singleton
@ScrobblerType(ScrobblerService.MAL)
fun provideMALStorage(
@ApplicationContext context: Context,
): ScrobblerStorage = ScrobblerStorage(context, ScrobblerService.MAL)
@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 MALAuthenticator @Inject constructor( class MALAuthenticator @Inject constructor(
private val storage: MALStorage, @ScrobblerType(ScrobblerService.MAL) private val storage: ScrobblerStorage,
private val repositoryProvider: Provider<MALRepository>, private val repositoryProvider: Provider<MALRepository>,
) : Authenticator { ) : Authenticator {

@ -3,23 +3,23 @@ package org.koitharu.kotatsu.scrobbling.mal.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 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 { override fun intercept(chain: Interceptor.Chain): Response {
val sourceRequest = chain.request() val sourceRequest = chain.request()
val request = sourceRequest.newBuilder() 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 { storage.accessToken?.let {
request.header(CommonHeaders.AUTHORIZATION, "Bearer $it") request.header(CommonHeaders.AUTHORIZATION, "Bearer $it")
} }
} }
val response = chain.proceed(request.build()) return chain.proceed(request.build())
if (!response.isSuccessful && !response.isRedirect) {
throw IOException("${response.code} ${response.message}")
}
return response
} }
} }

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

@ -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()
}
}

@ -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
}
}

@ -17,7 +17,7 @@ private const val RATING_MAX = 10f
class MALScrobbler @Inject constructor( class MALScrobbler @Inject constructor(
private val repository: MALRepository, private val repository: MALRepository,
db: MangaDatabase, db: MangaDatabase,
) : Scrobbler(db, ScrobblerService.MAL) { ) : Scrobbler(db, ScrobblerService.MAL, repository) {
init { init {
statuses[ScrobblingStatus.PLANNED] = "plan_to_read" statuses[ScrobblingStatus.PLANNED] = "plan_to_read"
@ -27,21 +27,6 @@ class MALScrobbler @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> {
TODO()
}
override suspend fun linkManga(mangaId: Long, targetId: Long) {
TODO()
}
override suspend fun scrobble(mangaId: Long, chapter: MangaChapter) {
TODO()
}
override suspend fun updateScrobblingInfo( override suspend fun updateScrobblingInfo(
mangaId: Long, mangaId: Long,
rating: Float, rating: Float,
@ -51,11 +36,4 @@ class MALScrobbler @Inject constructor(
TODO() TODO()
} }
override suspend fun unregisterScrobbling(mangaId: Long) {
repository.unregister(mangaId)
}
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
TODO()
}
} }

@ -8,7 +8,7 @@ import androidx.preference.Preference
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.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.assistedViewModels
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject 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 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,10 +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.mal.data.MALRepository 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( class MALSettingsViewModel @AssistedInject constructor(
private val repository: MALRepository, private val repository: MALRepository,
@ -19,7 +17,7 @@ class MALSettingsViewModel @AssistedInject constructor(
val authorizationUrl: String val authorizationUrl: String
get() = repository.oauthUrl get() = repository.oauthUrl
val user = MutableLiveData<MALUser?>() val user = MutableLiveData<ScrobblerUser?>()
init { init {
if (authCode != null) { if (authCode != null) {
@ -38,7 +36,7 @@ class MALSettingsViewModel @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

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

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

Loading…
Cancel
Save