Auth, search

This commit is contained in:
Zakhar Timoshenko
2023-02-01 01:05:04 +03:00
parent 80be0e403d
commit 8514cc3da7
10 changed files with 124 additions and 28 deletions

View File

@@ -1,15 +1,18 @@
package org.koitharu.kotatsu.scrobbling.mal.data
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
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.json.mapJSON
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.data.ScrobblingEntity
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo
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 BASE_OAUTH_URL = "https://myanimelist.net"
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
@@ -29,28 +33,32 @@ class MALRepository(
private val db: MangaDatabase,
) : ScrobblerRepository {
private var codeVerifier: String = ""
private var codeVerifier: String = getPKCEChallengeCode()
override val oauthUrl: String
get() = "${BASE_OAUTH_URL}/v1/oauth2/authorize?" +
get() = "$BASE_OAUTH_URL/v1/oauth2/authorize?" +
"response_type=code" +
"&client_id=af16954886b040673378423f5d62cccd" +
"&redirect_uri=${REDIRECT_URI}" +
"&code_challenge=${getPKCEChallengeCode()}" +
"&redirect_uri=$REDIRECT_URI" +
"&code_challenge=$codeVerifier" +
"&code_challenge_method=plain"
override val isAuthorized: Boolean
get() = storage.accessToken != null
override val cachedUser: ScrobblerUser?
get() = TODO("Not yet implemented")
get() {
return storage.user
}
override suspend fun authorize(code: String?) {
val body = FormBody.Builder()
if (code != null) {
body.add("client_id", "af16954886b040673378423f5d62cccd")
body.add("code", code)
body.add("code_verifier", getPKCEChallengeCode())
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()
.post(body.build())
@@ -64,7 +72,7 @@ class MALRepository(
override suspend fun loadUser(): ScrobblerUser {
val request = Request.Builder()
.get()
.url("${BASE_API_URL}/users")
.url("${BASE_API_URL}/users/@me")
val response = okHttp.newCall(request.build()).await().parseJson()
return MALUser(response).also { storage.user = it }
}
@@ -74,23 +82,74 @@ class MALRepository(
}
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 {
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) {
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) {
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?) {
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() {
@@ -102,11 +161,39 @@ class MALRepository(
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(
id = json.getLong("id"),
nickname = json.getString("nickname"),
avatar = json.getString("avatar"),
service = ScrobblerService.SHIKIMORI,
nickname = json.getString("name"),
avatar = json.getString("picture") ?: AVATAR_STUB,
service = ScrobblerService.MAL,
)
}

View File

@@ -5,17 +5,25 @@ import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.preference.Preference
import coil.ImageLoader
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
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.enqueueWith
import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject
@AndroidEntryPoint
class MALSettingsFragment : BasePreferenceFragment(R.string.mal) {
@Inject
lateinit var coil: ImageLoader
@Inject
lateinit var viewModelFactory: MALSettingsViewModel.Factory
@@ -47,6 +55,11 @@ class MALSettingsFragment : BasePreferenceFragment(R.string.mal) {
val pref = findPreference<Preference>(KEY_USER) ?: return
pref.isSelectable = user == null
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
}

View File

@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.databinding.ActivitySettingsBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.MangaSource
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.settings.sources.SourcesSettingsFragment
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
@@ -151,6 +152,9 @@ class SettingsActivity :
HOST_ANILIST_AUTH ->
return AniListSettingsFragment.newInstance(authCode = uri.getQueryParameter("code"))
HOST_MAL_AUTH ->
return MALSettingsFragment.newInstance(authCode = uri.getQueryParameter("code"))
}
finishAfterTransition()
return null
@@ -169,6 +173,7 @@ class SettingsActivity :
private const val HOST_SHIKIMORI_AUTH = "shikimori-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)

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

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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
android:defaultValue="true"