Rework code to enable UI rendered error messages (#172)

This commit is contained in:
she11sh0cked 2020-11-29 22:24:25 +01:00 committed by GitHub
parent 9d16b0efd2
commit 2cefc93797
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -4,15 +4,18 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SMangaImpl import eu.kanade.tachiyomi.source.model.SMangaImpl
import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
import eu.kanade.tachiyomi.util.lang.asObservable
import exh.log.maybeInjectEHLogger import exh.log.maybeInjectEHLogger
import exh.util.MangaType import exh.util.MangaType
import exh.util.mangaType import exh.util.mangaType
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.async
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.supervisorScope
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
@ -28,6 +31,7 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import timber.log.Timber import timber.log.Timber
abstract class API(_endpoint: String) { abstract class API(_endpoint: String) {
@ -37,21 +41,14 @@ abstract class API(_endpoint: String) {
.build() .build()
val scope = CoroutineScope(Job() + Dispatchers.Default) val scope = CoroutineScope(Job() + Dispatchers.Default)
abstract fun getRecsBySearch( abstract suspend fun getRecsBySearch(search: String): List<SMangaImpl>
search: String,
callback: (onResolve: List<SMangaImpl>?, onReject: Throwable?) -> Unit
)
} }
class MyAnimeList : API("https://api.jikan.moe/v3/") { class MyAnimeList : API("https://api.jikan.moe/v3/") {
private fun getRecsById( private suspend fun getRecsById(id: String): List<SMangaImpl> {
id: String,
callback: (resolve: List<SMangaImpl>?, reject: Throwable?) -> Unit
) {
val httpUrl = endpoint.toHttpUrlOrNull() val httpUrl = endpoint.toHttpUrlOrNull()
if (httpUrl == null) { if (httpUrl == null) {
callback.invoke(null, Exception("Could not convert endpoint url")) throw Exception("Could not convert endpoint url")
return
} }
val urlBuilder = httpUrl.newBuilder() val urlBuilder = httpUrl.newBuilder()
urlBuilder.addPathSegment("manga") urlBuilder.addPathSegment("manga")
@ -64,43 +61,32 @@ class MyAnimeList : API("https://api.jikan.moe/v3/") {
.get() .get()
.build() .build()
val handler = CoroutineExceptionHandler { _, exception -> val response = client.newCall(request).await()
callback.invoke(null, exception) val body = response.body?.string().orEmpty()
if (body.isEmpty()) {
throw Exception("Null Response")
} }
val data = Json.decodeFromString<JsonObject>(body)
scope.launch(handler) { val recommendations = data["recommendations"] as? JsonArray
val response = client.newCall(request).await() ?: throw Exception("Unexpected response")
val body = response.body?.string().orEmpty() val recs = recommendations.map { rec ->
if (body.isEmpty()) { rec as? JsonObject ?: throw Exception("Invalid json")
throw Exception("Null Response") Timber.tag("RECOMMENDATIONS").d("MYANIMELIST > RECOMMENDATION: %s", rec["title"]!!.jsonPrimitive.content)
SMangaImpl().apply {
this.title = rec["title"]!!.jsonPrimitive.content
this.thumbnail_url = rec["image_url"]!!.jsonPrimitive.content
this.initialized = true
this.url = rec["url"]!!.jsonPrimitive.content
} }
val data = Json.decodeFromString<JsonObject>(body)
val recommendations = data["recommendations"] as? JsonArray
?: throw Exception("Unexpected response")
val recs = recommendations.map { rec ->
rec as? JsonObject ?: throw Exception("Invalid json")
Timber.tag("RECOMMENDATIONS")
.d("MYANIMELIST > FOUND RECOMMENDATION > %s", rec["title"]!!.jsonPrimitive.content)
SMangaImpl().apply {
this.title = rec["title"]!!.jsonPrimitive.content
this.thumbnail_url = rec["image_url"]!!.jsonPrimitive.content
this.initialized = true
this.url = rec["url"]!!.jsonPrimitive.content
}
}
callback.invoke(recs, null)
} }
return recs
} }
override fun getRecsBySearch( override suspend fun getRecsBySearch(search: String): List<SMangaImpl> {
search: String,
callback: (recs: List<SMangaImpl>?, error: Throwable?) -> Unit
) {
val httpUrl = val httpUrl =
endpoint.toHttpUrlOrNull() endpoint.toHttpUrlOrNull()
if (httpUrl == null) { if (httpUrl == null) {
callback.invoke(null, Exception("Could not convert endpoint url")) throw Exception("Could not convert endpoint url")
return
} }
val urlBuilder = httpUrl.newBuilder() val urlBuilder = httpUrl.newBuilder()
urlBuilder.addPathSegment("search") urlBuilder.addPathSegment("search")
@ -113,27 +99,19 @@ class MyAnimeList : API("https://api.jikan.moe/v3/") {
.get() .get()
.build() .build()
val handler = CoroutineExceptionHandler { _, exception -> val response = client.newCall(request).await()
callback.invoke(null, exception) val body = response.body?.string().orEmpty()
if (body.isEmpty()) {
throw Exception("Null Response")
} }
val data = Json.decodeFromString<JsonObject>(body)
scope.launch(handler) { val results = data["results"] as? JsonArray ?: throw Exception("Unexpected response")
val response = client.newCall(request).await() if (results.size <= 0) {
val body = response.body?.string().orEmpty() throw Exception("'$search' not found")
if (body.isEmpty()) {
throw Exception("Null Response")
}
val data = Json.decodeFromString<JsonObject>(body)
val results = data["results"] as? JsonArray ?: throw Exception("Unexpected response")
if (results.size <= 0) {
throw Exception("'$search' not found")
}
val result = results.first().jsonObject
Timber.tag("RECOMMENDATIONS")
.d("MYANIMELIST > FOUND TITLE > %s", result["title"]!!.jsonPrimitive.content)
val id = result["mal_id"]!!.jsonPrimitive.content
getRecsById(id, callback)
} }
val result = results.first().jsonObject
val id = result["mal_id"]!!.jsonPrimitive.content
return getRecsById(id)
} }
} }
@ -157,10 +135,7 @@ class Anilist : API("https://graphql.anilist.co/") {
} }
} }
override fun getRecsBySearch( override suspend fun getRecsBySearch(search: String): List<SMangaImpl> {
search: String,
callback: (onResolve: List<SMangaImpl>?, onReject: Throwable?) -> Unit
) {
val query = val query =
""" """
|query Recommendations(${'$'}search: String!) { |query Recommendations(${'$'}search: String!) {
@ -207,47 +182,38 @@ class Anilist : API("https://graphql.anilist.co/") {
.post(payloadBody) .post(payloadBody)
.build() .build()
val handler = CoroutineExceptionHandler { _, exception -> val response = client.newCall(request).await()
callback.invoke(null, exception) val body = response.body?.string().orEmpty()
if (body.isEmpty()) {
throw Exception("Null Response")
} }
val data = Json.decodeFromString<JsonObject>(body)["data"] as? JsonObject
scope.launch(handler) { ?: throw Exception("Unexpected response")
val response = client.newCall(request).await() val page = data["Page"]!!.jsonObject
val body = response.body?.string().orEmpty() val media = page["media"]!!.jsonArray
if (body.isEmpty()) { if (media.size <= 0) {
throw Exception("Null Response") throw Exception("'$search' not found")
}
val data = Json.decodeFromString<JsonObject>(body)["data"] as? JsonObject
?: throw Exception("Unexpected response")
val page = data["Page"]!!.jsonObject
val media = page["media"]!!.jsonArray
if (media.size <= 0) {
throw Exception("'$search' not found")
}
val result = media.sortedWith(
compareBy(
{ languageContains(it.jsonObject, "romaji", search) },
{ languageContains(it.jsonObject, "english", search) },
{ languageContains(it.jsonObject, "native", search) },
{ countOccurrence(it.jsonObject["synonyms"]!!.jsonArray, search) > 0 }
)
).last().jsonObject
Timber.tag("RECOMMENDATIONS")
.d("ANILIST > FOUND TITLE > %s", getTitle(result))
val recommendations = result["recommendations"]!!.jsonObject["edges"]!!.jsonArray
val recs = recommendations.map {
val rec = it.jsonObject["node"]!!.jsonObject["mediaRecommendation"]!!.jsonObject
Timber.tag("RECOMMENDATIONS")
.d("ANILIST: FOUND RECOMMENDATION: %s", getTitle(rec))
SMangaImpl().apply {
this.title = getTitle(rec)
this.thumbnail_url = rec["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content
this.initialized = true
this.url = rec["siteUrl"]!!.jsonPrimitive.content
}
}
callback.invoke(recs, null)
} }
val result = media.sortedWith(
compareBy(
{ languageContains(it.jsonObject, "romaji", search) },
{ languageContains(it.jsonObject, "english", search) },
{ languageContains(it.jsonObject, "native", search) },
{ countOccurrence(it.jsonObject["synonyms"]!!.jsonArray, search) > 0 }
)
).last().jsonObject
val recommendations = result["recommendations"]!!.jsonObject["edges"]!!.jsonArray
val recs = recommendations.map {
val rec = it.jsonObject["node"]!!.jsonObject["mediaRecommendation"]!!.jsonObject
Timber.tag("RECOMMENDATIONS").d("ANILIST > RECOMMENDATION: %s", getTitle(rec))
SMangaImpl().apply {
this.title = getTitle(rec)
this.thumbnail_url = rec["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content
this.initialized = true
this.url = rec["siteUrl"]!!.jsonPrimitive.content
}
}
return recs
} }
} }
@ -256,59 +222,43 @@ open class RecommendsPager(
private val smart: Boolean = true, private val smart: Boolean = true,
private var preferredApi: API = API.MYANIMELIST private var preferredApi: API = API.MYANIMELIST
) : Pager() { ) : Pager() {
private val apiList = API_MAP.toMutableMap()
private var currentApi: API? = null
private fun handleSuccess(recs: List<SMangaImpl>) {
if (recs.isEmpty()) {
Timber.tag("RECOMMENDATIONS").e("%s > Couldn't find any", currentApi.toString())
apiList.remove(currentApi)
val list = apiList.toList()
currentApi = if (list.isEmpty()) {
null
} else {
apiList.toList().first().first
}
if (currentApi != null) {
getRecs(currentApi!!)
} else {
Timber.tag("RECOMMENDATIONS").e("Couldn't find any")
onPageReceived(MangasPage(recs, false))
}
} else {
onPageReceived(MangasPage(recs, false))
}
}
private fun handleError(error: Throwable) {
Timber.tag("RECOMMENDATIONS").e(error)
handleSuccess(listOf()) // tmp workaround until errors can be displayed in app
}
private fun getRecs(api: API) {
Timber.tag("RECOMMENDATIONS").d("USING > %s", api.toString())
apiList[api]?.getRecsBySearch(manga.originalTitle) { recs, error ->
if (error != null) {
handleError(error)
}
if (recs != null) {
handleSuccess(recs)
}
}
}
override fun requestNext(): Observable<MangasPage> { override fun requestNext(): Observable<MangasPage> {
if (smart) { return flow {
preferredApi = if (smart) preferredApi = if (manga.mangaType() != MangaType.TYPE_MANGA) API.ANILIST else preferredApi
if (manga.mangaType() != MangaType.TYPE_MANGA) API.ANILIST else preferredApi
Timber.tag("RECOMMENDATIONS").d("SMART > %s", preferredApi.toString()) val apiList = mapOf(preferredApi to API_MAP[preferredApi]!!) + API_MAP.filter { it.key != preferredApi }.toList()
val recs = supervisorScope {
apiList
.asSequence()
.map { (key, api) ->
async(Dispatchers.Default) {
try {
val recs = api.getRecsBySearch(manga.originalTitle).orEmpty()
Timber.tag("RECOMMENDATIONS").d("%s > Results: %s", key, recs.count())
recs
} catch (e: Exception) {
Timber.tag("RECOMMENDATIONS").e("%s > Error: %s", key, e.message)
listOf()
}
}
}
.firstOrNull { it.await().isNotEmpty() }
?.await().orEmpty()
}
val page = MangasPage(recs, false)
emit(page)
} }
currentApi = preferredApi .asObservable()
.observeOn(AndroidSchedulers.mainThread())
getRecs(currentApi!!) .doOnNext {
if (it.mangas.isNotEmpty()) {
return Observable.just(MangasPage(listOf(), false)) onPageReceived(it)
} else {
throw NoResultsException()
}
}
} }
companion object { companion object {