Rework code to enable UI rendered error messages (#172)
This commit is contained in:
parent
9d16b0efd2
commit
2cefc93797
@ -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,11 +61,6 @@ class MyAnimeList : API("https://api.jikan.moe/v3/") {
|
|||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
|
||||||
callback.invoke(null, exception)
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.launch(handler) {
|
|
||||||
val response = client.newCall(request).await()
|
val response = client.newCall(request).await()
|
||||||
val body = response.body?.string().orEmpty()
|
val body = response.body?.string().orEmpty()
|
||||||
if (body.isEmpty()) {
|
if (body.isEmpty()) {
|
||||||
@ -79,8 +71,7 @@ class MyAnimeList : API("https://api.jikan.moe/v3/") {
|
|||||||
?: throw Exception("Unexpected response")
|
?: throw Exception("Unexpected response")
|
||||||
val recs = recommendations.map { rec ->
|
val recs = recommendations.map { rec ->
|
||||||
rec as? JsonObject ?: throw Exception("Invalid json")
|
rec as? JsonObject ?: throw Exception("Invalid json")
|
||||||
Timber.tag("RECOMMENDATIONS")
|
Timber.tag("RECOMMENDATIONS").d("MYANIMELIST > RECOMMENDATION: %s", rec["title"]!!.jsonPrimitive.content)
|
||||||
.d("MYANIMELIST > FOUND RECOMMENDATION > %s", rec["title"]!!.jsonPrimitive.content)
|
|
||||||
SMangaImpl().apply {
|
SMangaImpl().apply {
|
||||||
this.title = rec["title"]!!.jsonPrimitive.content
|
this.title = rec["title"]!!.jsonPrimitive.content
|
||||||
this.thumbnail_url = rec["image_url"]!!.jsonPrimitive.content
|
this.thumbnail_url = rec["image_url"]!!.jsonPrimitive.content
|
||||||
@ -88,19 +79,14 @@ class MyAnimeList : API("https://api.jikan.moe/v3/") {
|
|||||||
this.url = rec["url"]!!.jsonPrimitive.content
|
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,11 +99,6 @@ class MyAnimeList : API("https://api.jikan.moe/v3/") {
|
|||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
|
||||||
callback.invoke(null, exception)
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.launch(handler) {
|
|
||||||
val response = client.newCall(request).await()
|
val response = client.newCall(request).await()
|
||||||
val body = response.body?.string().orEmpty()
|
val body = response.body?.string().orEmpty()
|
||||||
if (body.isEmpty()) {
|
if (body.isEmpty()) {
|
||||||
@ -129,11 +110,8 @@ class MyAnimeList : API("https://api.jikan.moe/v3/") {
|
|||||||
throw Exception("'$search' not found")
|
throw Exception("'$search' not found")
|
||||||
}
|
}
|
||||||
val result = results.first().jsonObject
|
val result = results.first().jsonObject
|
||||||
Timber.tag("RECOMMENDATIONS")
|
|
||||||
.d("MYANIMELIST > FOUND TITLE > %s", result["title"]!!.jsonPrimitive.content)
|
|
||||||
val id = result["mal_id"]!!.jsonPrimitive.content
|
val id = result["mal_id"]!!.jsonPrimitive.content
|
||||||
getRecsById(id, callback)
|
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,11 +182,6 @@ class Anilist : API("https://graphql.anilist.co/") {
|
|||||||
.post(payloadBody)
|
.post(payloadBody)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
|
||||||
callback.invoke(null, exception)
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.launch(handler) {
|
|
||||||
val response = client.newCall(request).await()
|
val response = client.newCall(request).await()
|
||||||
val body = response.body?.string().orEmpty()
|
val body = response.body?.string().orEmpty()
|
||||||
if (body.isEmpty()) {
|
if (body.isEmpty()) {
|
||||||
@ -232,13 +202,10 @@ class Anilist : API("https://graphql.anilist.co/") {
|
|||||||
{ countOccurrence(it.jsonObject["synonyms"]!!.jsonArray, search) > 0 }
|
{ countOccurrence(it.jsonObject["synonyms"]!!.jsonArray, search) > 0 }
|
||||||
)
|
)
|
||||||
).last().jsonObject
|
).last().jsonObject
|
||||||
Timber.tag("RECOMMENDATIONS")
|
|
||||||
.d("ANILIST > FOUND TITLE > %s", getTitle(result))
|
|
||||||
val recommendations = result["recommendations"]!!.jsonObject["edges"]!!.jsonArray
|
val recommendations = result["recommendations"]!!.jsonObject["edges"]!!.jsonArray
|
||||||
val recs = recommendations.map {
|
val recs = recommendations.map {
|
||||||
val rec = it.jsonObject["node"]!!.jsonObject["mediaRecommendation"]!!.jsonObject
|
val rec = it.jsonObject["node"]!!.jsonObject["mediaRecommendation"]!!.jsonObject
|
||||||
Timber.tag("RECOMMENDATIONS")
|
Timber.tag("RECOMMENDATIONS").d("ANILIST > RECOMMENDATION: %s", getTitle(rec))
|
||||||
.d("ANILIST: FOUND RECOMMENDATION: %s", getTitle(rec))
|
|
||||||
SMangaImpl().apply {
|
SMangaImpl().apply {
|
||||||
this.title = getTitle(rec)
|
this.title = getTitle(rec)
|
||||||
this.thumbnail_url = rec["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content
|
this.thumbnail_url = rec["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content
|
||||||
@ -246,8 +213,7 @@ class Anilist : API("https://graphql.anilist.co/") {
|
|||||||
this.url = rec["siteUrl"]!!.jsonPrimitive.content
|
this.url = rec["siteUrl"]!!.jsonPrimitive.content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
callback.invoke(recs, null)
|
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()
|
||||||
}
|
}
|
||||||
currentApi = preferredApi
|
|
||||||
|
|
||||||
getRecs(currentApi!!)
|
val page = MangasPage(recs, false)
|
||||||
|
emit(page)
|
||||||
return Observable.just(MangasPage(listOf(), false))
|
}
|
||||||
|
.asObservable()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnNext {
|
||||||
|
if (it.mangas.isNotEmpty()) {
|
||||||
|
onPageReceived(it)
|
||||||
|
} else {
|
||||||
|
throw NoResultsException()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user