Auth, search

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

@ -1,15 +1,18 @@
package org.koitharu.kotatsu.scrobbling.mal.data package org.koitharu.kotatsu.scrobbling.mal.data
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.json.JSONObject 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.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.await
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.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.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
@ -19,7 +22,8 @@ import org.koitharu.kotatsu.utils.PKCEGenerator
private const val REDIRECT_URI = "kotatsu://mal-auth" private const val REDIRECT_URI = "kotatsu://mal-auth"
private const val BASE_OAUTH_URL = "https://myanimelist.net" private const val BASE_OAUTH_URL = "https://myanimelist.net"
private const val BASE_API_URL = "https://api.myanimelist.net/v2" private const val BASE_API_URL = "https://api.myanimelist.net/v2"
private const val MANGA_PAGE_SIZE = 250 private const val MANGA_PAGE_SIZE = 10
private const val AVATAR_STUB = "https://cdn.myanimelist.net/images/questionmark_50.gif"
// af16954886b040673378423f5d62cccd // af16954886b040673378423f5d62cccd
@ -29,28 +33,32 @@ class MALRepository(
private val db: MangaDatabase, private val db: MangaDatabase,
) : ScrobblerRepository { ) : ScrobblerRepository {
private var codeVerifier: String = "" private var codeVerifier: String = getPKCEChallengeCode()
override 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" +
"&redirect_uri=${REDIRECT_URI}" + "&redirect_uri=$REDIRECT_URI" +
"&code_challenge=${getPKCEChallengeCode()}" + "&code_challenge=$codeVerifier" +
"&code_challenge_method=plain" "&code_challenge_method=plain"
override val isAuthorized: Boolean override val isAuthorized: Boolean
get() = storage.accessToken != null get() = storage.accessToken != null
override val cachedUser: ScrobblerUser? override val cachedUser: ScrobblerUser?
get() = TODO("Not yet implemented") get() {
return storage.user
}
override 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")
body.add("code", code)
body.add("code_verifier", getPKCEChallengeCode())
body.add("grant_type", "authorization_code") body.add("grant_type", "authorization_code")
body.add("code", code)
body.add("redirect_uri", REDIRECT_URI)
body.add("code_verifier", codeVerifier)
} }
val request = Request.Builder() val request = Request.Builder()
.post(body.build()) .post(body.build())
@ -64,7 +72,7 @@ class MALRepository(
override suspend fun loadUser(): ScrobblerUser { 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/@me")
val response = okHttp.newCall(request.build()).await().parseJson() val response = okHttp.newCall(request.build()).await().parseJson()
return MALUser(response).also { storage.user = it } return MALUser(response).also { storage.user = it }
} }
@ -74,23 +82,74 @@ class MALRepository(
} }
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> { override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
TODO("Not yet implemented") val pageOffset = offset % MANGA_PAGE_SIZE
val url = BASE_API_URL.toHttpUrl().newBuilder()
.addPathSegment("manga")
.addQueryParameter("offset", (pageOffset + 1).toString())
.addQueryParameter("nsfw", "true")
.addEncodedQueryParameter("q", query.take(64)) // WARNING! MAL API throws a 400 when the query is over 64 characters
.build()
val request = Request.Builder().url(url).get().build()
val response = okHttp.newCall(request).await().parseJson()
val data = response.getJSONArray("data")
val mangas = data.mapJSON { jsonToManga(it) }
return if (pageOffset != 0) mangas.drop(pageOffset) else mangas
} }
override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo { override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo {
TODO("Not yet implemented") val request = Request.Builder()
.get()
.url("${BASE_API_URL}/manga/$id")
val response = okHttp.newCall(request.build()).await().parseJson()
return ScrobblerMangaInfo(response)
} }
override suspend fun createRate(mangaId: Long, scrobblerMangaId: Long) { override suspend fun createRate(mangaId: Long, scrobblerMangaId: Long) {
TODO("Not yet implemented") val body = FormBody.Builder()
.add("status", "reading")
.add("score", "0")
val url = BASE_API_URL.toHttpUrl().newBuilder()
.addPathSegment("manga")
.addPathSegment(scrobblerMangaId.toString())
.addPathSegment("my_list_status")
.build()
val request = Request.Builder()
.url(url)
.put(body.build())
.build()
val response = okHttp.newCall(request).await().parseJson()
} }
override suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter) { override suspend fun updateRate(rateId: Int, mangaId: Long, chapter: MangaChapter) {
TODO("Not yet implemented") val body = FormBody.Builder()
.add("status", "reading")
.add("score", "0")
val url = BASE_API_URL.toHttpUrl().newBuilder()
.addPathSegment("manga")
.addPathSegment(mangaId.toString())
.addPathSegment("my_list_status")
.build()
val request = Request.Builder()
.url(url)
.put(body.build())
.build()
val response = okHttp.newCall(request).await().parseJson()
} }
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?) {
TODO("Not yet implemented") val body = FormBody.Builder()
.add("status", status!!)
.add("score", rating.toString())
val url = BASE_API_URL.toHttpUrl().newBuilder()
.addPathSegment("manga")
.addPathSegment(mangaId.toString())
.addPathSegment("my_list_status")
.build()
val request = Request.Builder()
.url(url)
.put(body.build())
.build()
val response = okHttp.newCall(request).await().parseJson()
} }
override fun logout() { override fun logout() {
@ -102,11 +161,39 @@ class MALRepository(
return codeVerifier return codeVerifier
} }
private fun jsonToManga(json: JSONObject): ScrobblerManga {
for (i in 0 until json.length()) {
val node = json.getJSONObject("node")
return ScrobblerManga(
id = node.getLong("id"),
name = node.getString("title"),
altName = null,
cover = node.getJSONObject("main_picture").getString("large"),
url = ""
)
}
return ScrobblerManga(
id = 1,
name = "",
altName = null,
cover = "",
url = ""
)
}
private fun ScrobblerMangaInfo(json: JSONObject) = ScrobblerMangaInfo(
id = json.getLong("id"),
name = json.getString("title"),
cover = json.getJSONObject("main_picture").getString("large"),
url = "",
descriptionHtml = json.getString("synopsis"),
)
private fun MALUser(json: JSONObject) = ScrobblerUser( private fun MALUser(json: JSONObject) = ScrobblerUser(
id = json.getLong("id"), id = json.getLong("id"),
nickname = json.getString("nickname"), nickname = json.getString("name"),
avatar = json.getString("avatar"), avatar = json.getString("picture") ?: AVATAR_STUB,
service = ScrobblerService.SHIKIMORI, service = ScrobblerService.MAL,
) )
} }

@ -5,17 +5,25 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.preference.Preference import androidx.preference.Preference
import coil.ImageLoader
import coil.request.ImageRequest
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.domain.model.ScrobblerUser import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerUser
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.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MALSettingsFragment : BasePreferenceFragment(R.string.mal) { class MALSettingsFragment : BasePreferenceFragment(R.string.mal) {
@Inject
lateinit var coil: ImageLoader
@Inject @Inject
lateinit var viewModelFactory: MALSettingsViewModel.Factory lateinit var viewModelFactory: MALSettingsViewModel.Factory
@ -47,6 +55,11 @@ class MALSettingsFragment : BasePreferenceFragment(R.string.mal) {
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)
ImageRequest.Builder(requireContext())
.data(user?.avatar)
.transformations(CircleCropTransformation())
.target(PreferenceIconTarget(pref))
.enqueueWith(coil)
findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null findPreference<Preference>(KEY_LOGOUT)?.isVisible = user != null
} }

@ -25,6 +25,7 @@ import org.koitharu.kotatsu.databinding.ActivitySettingsBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.scrobbling.anilist.ui.AniListSettingsFragment import org.koitharu.kotatsu.scrobbling.anilist.ui.AniListSettingsFragment
import org.koitharu.kotatsu.scrobbling.mal.ui.MALSettingsFragment
import org.koitharu.kotatsu.scrobbling.shikimori.ui.ShikimoriSettingsFragment import org.koitharu.kotatsu.scrobbling.shikimori.ui.ShikimoriSettingsFragment
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
@ -151,6 +152,9 @@ class SettingsActivity :
HOST_ANILIST_AUTH -> HOST_ANILIST_AUTH ->
return AniListSettingsFragment.newInstance(authCode = uri.getQueryParameter("code")) return AniListSettingsFragment.newInstance(authCode = uri.getQueryParameter("code"))
HOST_MAL_AUTH ->
return MALSettingsFragment.newInstance(authCode = uri.getQueryParameter("code"))
} }
finishAfterTransition() finishAfterTransition()
return null return null
@ -169,6 +173,7 @@ class SettingsActivity :
private const val HOST_SHIKIMORI_AUTH = "shikimori-auth" private const val HOST_SHIKIMORI_AUTH = "shikimori-auth"
private const val HOST_ANILIST_AUTH = "anilist-auth" private const val HOST_ANILIST_AUTH = "anilist-auth"
private const val HOST_MAL_AUTH = "mal-auth"
fun newIntent(context: Context) = Intent(context, SettingsActivity::class.java) fun newIntent(context: Context) = Intent(context, SettingsActivity::class.java)

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen <PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="true" android:defaultValue="true"

Loading…
Cancel
Save