forked from recloudstream/cloudstream
added Kitsu posters
This commit is contained in:
parent
81938a565f
commit
8fc790ab9b
10 changed files with 494 additions and 217 deletions
|
@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
|
||||||
import com.lagradost.cloudstream3.animeproviders.*
|
import com.lagradost.cloudstream3.animeproviders.*
|
||||||
import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider
|
import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider
|
||||||
import com.lagradost.cloudstream3.movieproviders.*
|
import com.lagradost.cloudstream3.movieproviders.*
|
||||||
|
@ -898,6 +899,14 @@ interface LoadResponse {
|
||||||
this.actors = actors?.map { actor -> ActorData(actor) }
|
this.actors = actors?.map { actor -> ActorData(actor) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun LoadResponse.getMalId() : String? {
|
||||||
|
return this.syncData[malIdPrefix]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LoadResponse.getAniListId() : String? {
|
||||||
|
return this.syncData[aniListIdPrefix]
|
||||||
|
}
|
||||||
|
|
||||||
fun LoadResponse.addMalId(id: Int?) {
|
fun LoadResponse.addMalId(id: Int?) {
|
||||||
this.syncData[malIdPrefix] = (id ?: return).toString()
|
this.syncData[malIdPrefix] = (id ?: return).toString()
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ class NineAnimeProvider : MainAPI() {
|
||||||
Pair("$mainUrl/ajax/home/widget?name=updated_sub&page=1", "Recently Updated (SUB)"),
|
Pair("$mainUrl/ajax/home/widget?name=updated_sub&page=1", "Recently Updated (SUB)"),
|
||||||
Pair(
|
Pair(
|
||||||
"$mainUrl/ajax/home/widget?name=updated_dub&page=1",
|
"$mainUrl/ajax/home/widget?name=updated_dub&page=1",
|
||||||
"Recently Updated (DUB)(DUB)"
|
"Recently Updated (DUB)"
|
||||||
),
|
),
|
||||||
Pair(
|
Pair(
|
||||||
"$mainUrl/ajax/home/widget?name=updated_chinese&page=1",
|
"$mainUrl/ajax/home/widget?name=updated_chinese&page=1",
|
||||||
|
@ -64,7 +64,8 @@ class NineAnimeProvider : MainAPI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Credits to https://github.com/jmir1
|
//Credits to https://github.com/jmir1
|
||||||
private val key = "c/aUAorINHBLxWTy3uRiPt8J+vjsOheFG1E0q2X9CYwDZlnmd4Kb5M6gSVzfk7pQ" //key credits to @Modder4869
|
private val key =
|
||||||
|
"c/aUAorINHBLxWTy3uRiPt8J+vjsOheFG1E0q2X9CYwDZlnmd4Kb5M6gSVzfk7pQ" //key credits to @Modder4869
|
||||||
|
|
||||||
private fun getVrf(id: String): String? {
|
private fun getVrf(id: String): String? {
|
||||||
val reversed = ue(encode(id) + "0000000").slice(0..5).reversed()
|
val reversed = ue(encode(id) + "0000000").slice(0..5).reversed()
|
||||||
|
@ -175,7 +176,10 @@ class NineAnimeProvider : MainAPI() {
|
||||||
return app.get(url).document.select("ul.anime-list li").mapNotNull {
|
return app.get(url).document.select("ul.anime-list li").mapNotNull {
|
||||||
val title = it.selectFirst("a.name")!!.text()
|
val title = it.selectFirst("a.name")!!.text()
|
||||||
val href =
|
val href =
|
||||||
fixUrlNull(it.selectFirst("a")!!.attr("href"))?.replace(Regex("(\\?ep=(\\d+)\$)"), "")
|
fixUrlNull(it.selectFirst("a")!!.attr("href"))?.replace(
|
||||||
|
Regex("(\\?ep=(\\d+)\$)"),
|
||||||
|
""
|
||||||
|
)
|
||||||
?: return@mapNotNull null
|
?: return@mapNotNull null
|
||||||
val image = it.selectFirst("a.poster img")!!.attr("src")
|
val image = it.selectFirst("a.poster img")!!.attr("src")
|
||||||
AnimeSearchResponse(
|
AnimeSearchResponse(
|
||||||
|
@ -199,7 +203,8 @@ class NineAnimeProvider : MainAPI() {
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
val validUrl = url.replace("https://9anime.to", mainUrl)
|
val validUrl = url.replace("https://9anime.to", mainUrl)
|
||||||
val doc = app.get(validUrl).document
|
val doc = app.get(validUrl).document
|
||||||
val animeid = doc.selectFirst("div.player-wrapper.watchpage")!!.attr("data-id") ?: return null
|
val animeid =
|
||||||
|
doc.selectFirst("div.player-wrapper.watchpage")!!.attr("data-id") ?: return null
|
||||||
val animeidencoded = encode(getVrf(animeid) ?: return null)
|
val animeidencoded = encode(getVrf(animeid) ?: return null)
|
||||||
val poster = doc.selectFirst("aside.main div.thumb div img")!!.attr("src")
|
val poster = doc.selectFirst("aside.main div.thumb div img")!!.attr("src")
|
||||||
val title = doc.selectFirst(".info .title")!!.text()
|
val title = doc.selectFirst(".info .title")!!.text()
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
package com.lagradost.cloudstream3.syncproviders.providers
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
|
||||||
|
// modified code from from https://github.com/saikou-app/saikou/blob/main/app/src/main/java/ani/saikou/others/Kitsu.kt
|
||||||
|
// GNU General Public License v3.0 https://github.com/saikou-app/saikou/blob/main/LICENSE.md
|
||||||
|
object Kitsu {
|
||||||
|
private suspend fun getKitsuData(query: String): KitsuResponse {
|
||||||
|
val headers = mapOf(
|
||||||
|
"Content-Type" to "application/json",
|
||||||
|
"Accept" to "application/json",
|
||||||
|
"Connection" to "keep-alive",
|
||||||
|
"DNT" to "1",
|
||||||
|
"Origin" to "https://kitsu.io"
|
||||||
|
)
|
||||||
|
|
||||||
|
return app.post(
|
||||||
|
"https://kitsu.io/api/graphql",
|
||||||
|
headers = headers,
|
||||||
|
data = mapOf("query" to query)
|
||||||
|
).parsed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val cache: MutableMap<Pair<String, String>, Map<Int, KitsuResponse.Node>> =
|
||||||
|
mutableMapOf()
|
||||||
|
|
||||||
|
suspend fun getEpisodesDetails(
|
||||||
|
malId: String?,
|
||||||
|
anilistId: String?
|
||||||
|
): Map<Int, KitsuResponse.Node>? {
|
||||||
|
if (anilistId != null) {
|
||||||
|
try {
|
||||||
|
val map = getKitsuEpisodesDetails(anilistId, "ANILIST_ANIME")
|
||||||
|
if (!map.isNullOrEmpty()) return map
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (malId != null) {
|
||||||
|
try {
|
||||||
|
val map = getKitsuEpisodesDetails(malId, "MYANIMELIST_ANIME")
|
||||||
|
if (!map.isNullOrEmpty()) return map
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws
|
||||||
|
suspend fun getKitsuEpisodesDetails(id: String, site: String): Map<Int, KitsuResponse.Node>? {
|
||||||
|
require(id.isNotBlank()) {
|
||||||
|
"Black id"
|
||||||
|
}
|
||||||
|
|
||||||
|
require(site.isNotBlank()) {
|
||||||
|
"invalid site"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache.containsKey(id to site)) {
|
||||||
|
return cache[id to site]
|
||||||
|
}
|
||||||
|
|
||||||
|
val query =
|
||||||
|
"""
|
||||||
|
query {
|
||||||
|
lookupMapping(externalId: $id, externalSite: $site) {
|
||||||
|
__typename
|
||||||
|
... on Anime {
|
||||||
|
id
|
||||||
|
episodes(first: 2000) {
|
||||||
|
nodes {
|
||||||
|
number
|
||||||
|
titles {
|
||||||
|
canonical
|
||||||
|
}
|
||||||
|
description
|
||||||
|
thumbnail {
|
||||||
|
original {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
val result = getKitsuData(query)
|
||||||
|
println("got getKitsuEpisodesDetails result $result")
|
||||||
|
|
||||||
|
val map = (result.data?.lookupMapping?.episodes?.nodes ?: return null).mapNotNull { ep ->
|
||||||
|
val num = ep?.num ?: return@mapNotNull null
|
||||||
|
num to ep
|
||||||
|
}.toMap()
|
||||||
|
println("got getKitsuEpisodesDetails map $map")
|
||||||
|
|
||||||
|
if (map.isNotEmpty()) {
|
||||||
|
cache[id to site] = map
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
data class KitsuResponse(
|
||||||
|
val data: Data? = null
|
||||||
|
) {
|
||||||
|
data class Data(
|
||||||
|
val lookupMapping: LookupMapping? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LookupMapping(
|
||||||
|
val id: String? = null,
|
||||||
|
val episodes: Episodes? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Episodes(
|
||||||
|
val nodes: List<Node?>? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Node(
|
||||||
|
@JsonProperty("number")
|
||||||
|
val num: Int? = null,
|
||||||
|
val titles: Titles? = null,
|
||||||
|
val description: Description? = null,
|
||||||
|
val thumbnail: Thumbnail? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Description(
|
||||||
|
val en: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Thumbnail(
|
||||||
|
val original: Original? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Original(
|
||||||
|
val url: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Titles(
|
||||||
|
val canonical: String? = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,6 +33,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
override val redirectUrl = "mallogin"
|
override val redirectUrl = "mallogin"
|
||||||
override val idPrefix = "mal"
|
override val idPrefix = "mal"
|
||||||
override var mainUrl = "https://myanimelist.net"
|
override var mainUrl = "https://myanimelist.net"
|
||||||
|
val apiUrl = "https://api.myanimelist.net"
|
||||||
override val icon = R.drawable.mal_logo
|
override val icon = R.drawable.mal_logo
|
||||||
override val requiresLogin = true
|
override val requiresLogin = true
|
||||||
|
|
||||||
|
@ -62,7 +63,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult> {
|
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult> {
|
||||||
val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
|
val url = "$apiUrl/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
|
||||||
val auth = getAuth() ?: return emptyList()
|
val auth = getAuth() ?: return emptyList()
|
||||||
val res = app.get(
|
val res = app.get(
|
||||||
url, headers = mapOf(
|
url, headers = mapOf(
|
||||||
|
@ -179,7 +180,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
name = node?.title ?: return null,
|
name = node?.title ?: return null,
|
||||||
apiName = this.name,
|
apiName = this.name,
|
||||||
syncId = node.id.toString(),
|
syncId = node.id.toString(),
|
||||||
url = "https://myanimelist.net/anime/${node.id}",
|
url = "$mainUrl/anime/${node.id}",
|
||||||
posterUrl = node.main_picture?.large
|
posterUrl = node.main_picture?.large
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -187,7 +188,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
override suspend fun getResult(id: String): SyncAPI.SyncResult? {
|
override suspend fun getResult(id: String): SyncAPI.SyncResult? {
|
||||||
val internalId = id.toIntOrNull() ?: return null
|
val internalId = id.toIntOrNull() ?: return null
|
||||||
val url =
|
val url =
|
||||||
"https://api.myanimelist.net/v2/anime/$internalId?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,created_at,updated_at,media_type,status,genres,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,related_manga,recommendations,studios,statistics"
|
"$apiUrl/v2/anime/$internalId?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,created_at,updated_at,media_type,status,genres,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,related_manga,recommendations,studios,statistics"
|
||||||
val res = app.get(
|
val res = app.get(
|
||||||
url, headers = mapOf(
|
url, headers = mapOf(
|
||||||
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||||
|
@ -203,7 +204,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
synopsis = malAnime.synopsis,
|
synopsis = malAnime.synopsis,
|
||||||
airStatus = when (malAnime.status) {
|
airStatus = when (malAnime.status) {
|
||||||
"finished_airing" -> ShowStatus.Completed
|
"finished_airing" -> ShowStatus.Completed
|
||||||
"airing" -> ShowStatus.Ongoing
|
"currently_airing" -> ShowStatus.Ongoing
|
||||||
|
//"not_yet_aired"
|
||||||
else -> null
|
else -> null
|
||||||
},
|
},
|
||||||
nextAiring = null,
|
nextAiring = null,
|
||||||
|
@ -260,7 +262,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val currentCode = sanitizer["code"]!!
|
val currentCode = sanitizer["code"]!!
|
||||||
|
|
||||||
val res = app.post(
|
val res = app.post(
|
||||||
"https://myanimelist.net/v1/oauth2/token",
|
"$mainUrl/v1/oauth2/token",
|
||||||
data = mapOf(
|
data = mapOf(
|
||||||
"client_id" to key,
|
"client_id" to key,
|
||||||
"code" to currentCode,
|
"code" to currentCode,
|
||||||
|
@ -292,7 +294,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
.replace("/", "_").replace("\n", "")
|
.replace("/", "_").replace("\n", "")
|
||||||
val codeChallenge = codeVerifier
|
val codeChallenge = codeVerifier
|
||||||
val request =
|
val request =
|
||||||
"https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId"
|
"$mainUrl/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId"
|
||||||
openBrowser(request)
|
openBrowser(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,7 +320,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
private suspend fun refreshToken() {
|
private suspend fun refreshToken() {
|
||||||
try {
|
try {
|
||||||
val res = app.post(
|
val res = app.post(
|
||||||
"https://myanimelist.net/v1/oauth2/token",
|
"$mainUrl/v1/oauth2/token",
|
||||||
data = mapOf(
|
data = mapOf(
|
||||||
"client_id" to key,
|
"client_id" to key,
|
||||||
"grant_type" to "refresh_token",
|
"grant_type" to "refresh_token",
|
||||||
|
@ -451,7 +453,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
// Very lackluster docs
|
// Very lackluster docs
|
||||||
// https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get
|
// https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get
|
||||||
val url =
|
val url =
|
||||||
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset"
|
"$apiUrl/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset"
|
||||||
val res = app.get(
|
val res = app.get(
|
||||||
url, headers = mapOf(
|
url, headers = mapOf(
|
||||||
"Authorization" to "Bearer $auth",
|
"Authorization" to "Bearer $auth",
|
||||||
|
@ -463,7 +465,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
private suspend fun getDataAboutMalId(id: Int): SmallMalAnime? {
|
private suspend fun getDataAboutMalId(id: Int): SmallMalAnime? {
|
||||||
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
|
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
|
||||||
val url =
|
val url =
|
||||||
"https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status"
|
"$apiUrl/v2/anime/$id?fields=id,title,num_episodes,my_list_status"
|
||||||
val res = app.get(
|
val res = app.get(
|
||||||
url, headers = mapOf(
|
url, headers = mapOf(
|
||||||
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||||
|
@ -481,7 +483,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
checkMalToken()
|
checkMalToken()
|
||||||
while (!isDone) {
|
while (!isDone) {
|
||||||
val res = app.get(
|
val res = app.get(
|
||||||
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}",
|
"$apiUrl/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}",
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
"Authorization" to "Bearer " + (getAuth() ?: return)
|
"Authorization" to "Bearer " + (getAuth() ?: return)
|
||||||
), cacheTime = 0
|
), cacheTime = 0
|
||||||
|
@ -532,10 +534,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkMalToken() {
|
private suspend fun checkMalToken() {
|
||||||
if (unixTime > getKey(
|
if (unixTime > (getKey(
|
||||||
accountId,
|
accountId,
|
||||||
MAL_UNIXTIME_KEY
|
MAL_UNIXTIME_KEY
|
||||||
) ?: 0L
|
) ?: 0L)
|
||||||
) {
|
) {
|
||||||
refreshToken()
|
refreshToken()
|
||||||
}
|
}
|
||||||
|
@ -544,7 +546,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
private suspend fun getMalUser(setSettings: Boolean = true): MalUser? {
|
private suspend fun getMalUser(setSettings: Boolean = true): MalUser? {
|
||||||
checkMalToken()
|
checkMalToken()
|
||||||
val res = app.get(
|
val res = app.get(
|
||||||
"https://api.myanimelist.net/v2/users/@me",
|
"$apiUrl/v2/users/@me",
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||||
), cacheTime = 0
|
), cacheTime = 0
|
||||||
|
@ -620,7 +622,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
).filter { it.value != null } as Map<String, String>
|
).filter { it.value != null } as Map<String, String>
|
||||||
|
|
||||||
return app.put(
|
return app.put(
|
||||||
"https://api.myanimelist.net/v2/anime/$id/my_list_status",
|
"$apiUrl/v2/anime/$id/my_list_status",
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||||
),
|
),
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.LayoutRes
|
import androidx.annotation.LayoutRes
|
||||||
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.ContentLoadingProgressBar
|
import androidx.core.widget.ContentLoadingProgressBar
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -21,7 +22,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
import kotlinx.android.synthetic.main.result_episode.view.*
|
import kotlinx.android.synthetic.main.result_episode.view.*
|
||||||
import kotlinx.android.synthetic.main.result_episode.view.episode_holder
|
|
||||||
import kotlinx.android.synthetic.main.result_episode.view.episode_text
|
import kotlinx.android.synthetic.main.result_episode.view.episode_text
|
||||||
import kotlinx.android.synthetic.main.result_episode_large.view.*
|
import kotlinx.android.synthetic.main.result_episode_large.view.*
|
||||||
import kotlinx.android.synthetic.main.result_episode_large.view.episode_filler
|
import kotlinx.android.synthetic.main.result_episode_large.view.episode_filler
|
||||||
|
@ -47,6 +47,7 @@ const val ACTION_SHOW_OPTIONS = 10
|
||||||
|
|
||||||
const val ACTION_CLICK_DEFAULT = 11
|
const val ACTION_CLICK_DEFAULT = 11
|
||||||
const val ACTION_SHOW_TOAST = 12
|
const val ACTION_SHOW_TOAST = 12
|
||||||
|
const val ACTION_SHOW_DESCRIPTION = 15
|
||||||
|
|
||||||
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE = 13
|
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE = 13
|
||||||
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14
|
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14
|
||||||
|
@ -93,10 +94,10 @@ class EpisodeAdapter(
|
||||||
@LayoutRes
|
@LayoutRes
|
||||||
private var layout: Int = 0
|
private var layout: Int = 0
|
||||||
fun updateLayout() {
|
fun updateLayout() {
|
||||||
layout =
|
// layout =
|
||||||
if (cardList.filter { it.poster != null }.size >= cardList.size / 2f) // If over half has posters then use the large layout
|
// if (cardList.filter { it.poster != null }.size >= cardList.size / 2f) // If over half has posters then use the large layout
|
||||||
R.layout.result_episode_large
|
// R.layout.result_episode_large
|
||||||
else R.layout.result_episode
|
// else R.layout.result_episode
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
@ -105,7 +106,7 @@ class EpisodeAdapter(
|
||||||
else R.layout.result_episode*/
|
else R.layout.result_episode*/
|
||||||
|
|
||||||
return EpisodeCardViewHolder(
|
return EpisodeCardViewHolder(
|
||||||
LayoutInflater.from(parent.context).inflate(layout, parent, false),
|
LayoutInflater.from(parent.context).inflate(R.layout.result_episode_both, parent, false),
|
||||||
hasDownloadSupport,
|
hasDownloadSupport,
|
||||||
clickCallback,
|
clickCallback,
|
||||||
downloadClickCallback
|
downloadClickCallback
|
||||||
|
@ -134,27 +135,39 @@ class EpisodeAdapter(
|
||||||
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder {
|
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder {
|
||||||
override var downloadButton = EasyDownloadButton()
|
override var downloadButton = EasyDownloadButton()
|
||||||
|
|
||||||
private val episodeText: TextView = itemView.episode_text
|
|
||||||
private val episodeFiller: MaterialButton? = itemView.episode_filler
|
|
||||||
private val episodeRating: TextView? = itemView.episode_rating
|
|
||||||
private val episodeDescript: TextView? = itemView.episode_descript
|
|
||||||
private val episodeProgress: ContentLoadingProgressBar? = itemView.episode_progress
|
|
||||||
private val episodePoster: ImageView? = itemView.episode_poster
|
|
||||||
|
|
||||||
private val episodeDownloadBar: ContentLoadingProgressBar = itemView.result_episode_progress_downloaded
|
|
||||||
private val episodeDownloadImage: ImageView = itemView.result_episode_download
|
|
||||||
|
|
||||||
private val episodeHolder = itemView.episode_holder
|
|
||||||
|
|
||||||
|
var episodeDownloadBar: ContentLoadingProgressBar? = null
|
||||||
|
var episodeDownloadImage: ImageView? = null
|
||||||
var localCard: ResultEpisode? = null
|
var localCard: ResultEpisode? = null
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
fun bind(card: ResultEpisode) {
|
fun bind(card: ResultEpisode) {
|
||||||
localCard = card
|
localCard = card
|
||||||
|
|
||||||
val name = if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}"
|
val (parentView,otherView) = if(card.poster == null) {
|
||||||
|
itemView.episode_holder to itemView.episode_holder_large
|
||||||
|
} else {
|
||||||
|
itemView.episode_holder_large to itemView.episode_holder
|
||||||
|
}
|
||||||
|
parentView.isVisible = true
|
||||||
|
otherView.isVisible = false
|
||||||
|
|
||||||
|
val episodeText: TextView = parentView.episode_text
|
||||||
|
val episodeFiller: MaterialButton? = parentView.episode_filler
|
||||||
|
val episodeRating: TextView? = parentView.episode_rating
|
||||||
|
val episodeDescript: TextView? = parentView.episode_descript
|
||||||
|
val episodeProgress: ContentLoadingProgressBar? = parentView.episode_progress
|
||||||
|
val episodePoster: ImageView? = parentView.episode_poster
|
||||||
|
|
||||||
|
episodeDownloadBar =
|
||||||
|
parentView.result_episode_progress_downloaded
|
||||||
|
episodeDownloadImage = parentView.result_episode_download
|
||||||
|
|
||||||
|
val name =
|
||||||
|
if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}"
|
||||||
episodeFiller?.isVisible = card.isFiller == true
|
episodeFiller?.isVisible = card.isFiller == true
|
||||||
episodeText.text = name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name
|
episodeText.text =
|
||||||
|
name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name
|
||||||
episodeText.isSelected = true // is needed for text repeating
|
episodeText.isSelected = true // is needed for text repeating
|
||||||
|
|
||||||
val displayPos = card.getDisplayPosition()
|
val displayPos = card.getDisplayPosition()
|
||||||
|
@ -171,16 +184,20 @@ class EpisodeAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (card.rating != null) {
|
if (card.rating != null) {
|
||||||
episodeRating?.text = episodeRating?.context?.getString(R.string.rated_format)?.format(card.rating.toFloat() / 10f)
|
episodeRating?.text = episodeRating?.context?.getString(R.string.rated_format)
|
||||||
|
?.format(card.rating.toFloat() / 10f)
|
||||||
} else {
|
} else {
|
||||||
episodeRating?.text = ""
|
episodeRating?.text = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if (card.description != null) {
|
episodeRating?.isGone = episodeRating?.text.isNullOrBlank()
|
||||||
episodeDescript?.visibility = View.VISIBLE
|
|
||||||
episodeDescript?.text = card.description
|
episodeDescript?.apply {
|
||||||
} else {
|
text = card.description ?: ""
|
||||||
episodeDescript?.visibility = View.GONE
|
isGone = text.isNullOrBlank()
|
||||||
|
setOnClickListener {
|
||||||
|
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
episodePoster?.setOnClickListener {
|
episodePoster?.setOnClickListener {
|
||||||
|
@ -192,34 +209,42 @@ class EpisodeAdapter(
|
||||||
return@setOnLongClickListener true
|
return@setOnLongClickListener true
|
||||||
}
|
}
|
||||||
|
|
||||||
episodeHolder.setOnClickListener {
|
parentView.setOnClickListener {
|
||||||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (episodeHolder.context.isTrueTvSettings()) {
|
if (parentView.context.isTrueTvSettings()) {
|
||||||
episodeHolder.isFocusable = true
|
parentView.isFocusable = true
|
||||||
episodeHolder.isFocusableInTouchMode = true
|
parentView.isFocusableInTouchMode = true
|
||||||
episodeHolder.touchscreenBlocksFocus = false
|
parentView.touchscreenBlocksFocus = false
|
||||||
}
|
}
|
||||||
|
|
||||||
episodeHolder.setOnLongClickListener {
|
parentView.setOnLongClickListener {
|
||||||
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
|
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
|
||||||
|
|
||||||
return@setOnLongClickListener true
|
return@setOnLongClickListener true
|
||||||
}
|
}
|
||||||
|
|
||||||
episodeDownloadImage.isVisible = hasDownloadSupport
|
episodeDownloadImage?.isVisible = hasDownloadSupport
|
||||||
episodeDownloadBar.isVisible = hasDownloadSupport
|
episodeDownloadBar?.isVisible = hasDownloadSupport
|
||||||
|
reattachDownloadButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reattachDownloadButton() {
|
override fun reattachDownloadButton() {
|
||||||
downloadButton.dispose()
|
downloadButton.dispose()
|
||||||
val card = localCard
|
val card = localCard
|
||||||
if (hasDownloadSupport && card != null) {
|
if (hasDownloadSupport && card != null) {
|
||||||
val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(itemView.context, card.id)
|
val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
|
||||||
|
itemView.context,
|
||||||
|
card.id
|
||||||
|
)
|
||||||
|
|
||||||
downloadButton.setUpButton(
|
downloadButton.setUpButton(
|
||||||
downloadInfo?.fileLength, downloadInfo?.totalBytes, episodeDownloadBar, episodeDownloadImage, null,
|
downloadInfo?.fileLength,
|
||||||
|
downloadInfo?.totalBytes,
|
||||||
|
episodeDownloadBar ?: return,
|
||||||
|
episodeDownloadImage ?: return,
|
||||||
|
null,
|
||||||
VideoDownloadHelper.DownloadEpisodeCached(
|
VideoDownloadHelper.DownloadEpisodeCached(
|
||||||
card.name,
|
card.name,
|
||||||
card.poster,
|
card.poster,
|
||||||
|
|
|
@ -987,6 +987,7 @@ class ResultFragment : ResultTrailerPlayer() {
|
||||||
}
|
}
|
||||||
ACTION_CHROME_CAST_EPISODE -> requireLinks(true)
|
ACTION_CHROME_CAST_EPISODE -> requireLinks(true)
|
||||||
ACTION_CHROME_CAST_MIRROR -> requireLinks(true)
|
ACTION_CHROME_CAST_MIRROR -> requireLinks(true)
|
||||||
|
ACTION_SHOW_DESCRIPTION -> true
|
||||||
else -> requireLinks(false)
|
else -> requireLinks(false)
|
||||||
}
|
}
|
||||||
if (!isLoaded) return@main // CANT LOAD
|
if (!isLoaded) return@main // CANT LOAD
|
||||||
|
@ -996,6 +997,14 @@ class ResultFragment : ResultTrailerPlayer() {
|
||||||
showToast(activity, R.string.play_episode_toast, Toast.LENGTH_SHORT)
|
showToast(activity, R.string.play_episode_toast, Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ACTION_SHOW_DESCRIPTION -> {
|
||||||
|
val builder: AlertDialog.Builder =
|
||||||
|
AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
|
||||||
|
builder.setMessage(episodeClick.data.description ?: return@main)
|
||||||
|
.setTitle(R.string.torrent_plot)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
ACTION_CLICK_DEFAULT -> {
|
ACTION_CLICK_DEFAULT -> {
|
||||||
context?.let { ctx ->
|
context?.let { ctx ->
|
||||||
if (ctx.isConnectedToChromecast()) {
|
if (ctx.isConnectedToChromecast()) {
|
||||||
|
@ -1419,6 +1428,7 @@ class ResultFragment : ResultTrailerPlayer() {
|
||||||
|
|
||||||
observe(syncModel.syncIds) {
|
observe(syncModel.syncIds) {
|
||||||
syncdata = it
|
syncdata = it
|
||||||
|
println("syncdata: $syncdata")
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentSyncProgress = 0
|
var currentSyncProgress = 0
|
||||||
|
@ -1443,7 +1453,7 @@ class ResultFragment : ResultTrailerPlayer() {
|
||||||
val d = meta.value
|
val d = meta.value
|
||||||
result_sync_episodes?.progress = currentSyncProgress * 1000
|
result_sync_episodes?.progress = currentSyncProgress * 1000
|
||||||
setSyncMaxEpisodes(d.totalEpisodes)
|
setSyncMaxEpisodes(d.totalEpisodes)
|
||||||
viewModel.setMeta(d)
|
viewModel.setMeta(d, syncdata)
|
||||||
}
|
}
|
||||||
is Resource.Loading -> {
|
is Resource.Loading -> {
|
||||||
result_sync_max_episodes?.text =
|
result_sync_max_episodes?.text =
|
||||||
|
|
|
@ -13,12 +13,16 @@ import com.lagradost.cloudstream3.APIHolder.getId
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
|
||||||
import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider
|
import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider
|
||||||
import com.lagradost.cloudstream3.animeproviders.NineAnimeProvider
|
import com.lagradost.cloudstream3.animeproviders.NineAnimeProvider
|
||||||
import com.lagradost.cloudstream3.metaproviders.SyncRedirector
|
import com.lagradost.cloudstream3.metaproviders.SyncRedirector
|
||||||
import com.lagradost.cloudstream3.mvvm.Resource
|
import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu.getEpisodesDetails
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
import com.lagradost.cloudstream3.ui.WatchType
|
import com.lagradost.cloudstream3.ui.WatchType
|
||||||
import com.lagradost.cloudstream3.ui.player.IGenerator
|
import com.lagradost.cloudstream3.ui.player.IGenerator
|
||||||
|
@ -119,19 +123,29 @@ class ResultViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastMeta: SyncAPI.SyncResult? = null
|
var lastMeta: SyncAPI.SyncResult? = null
|
||||||
private suspend fun applyMeta(resp: LoadResponse, meta: SyncAPI.SyncResult?): LoadResponse {
|
var lastSync: Map<String, String>? = null
|
||||||
if (meta == null) return resp
|
|
||||||
return resp.apply {
|
private suspend fun applyMeta(
|
||||||
|
resp: LoadResponse,
|
||||||
|
meta: SyncAPI.SyncResult?,
|
||||||
|
syncs: Map<String, String>? = null
|
||||||
|
): Pair<LoadResponse, Boolean> {
|
||||||
|
if (meta == null) return resp to false
|
||||||
|
var updateEpisodes = false
|
||||||
|
val out = resp.apply {
|
||||||
Log.i(TAG, "applyMeta")
|
Log.i(TAG, "applyMeta")
|
||||||
|
|
||||||
duration = duration ?: meta.duration
|
duration = duration ?: meta.duration
|
||||||
rating = rating ?: meta.publicScore
|
rating = rating ?: meta.publicScore
|
||||||
tags = tags ?: meta.genres
|
tags = tags ?: meta.genres
|
||||||
plot = if (plot.isNullOrBlank()) meta.synopsis else plot
|
plot = if (plot.isNullOrBlank()) meta.synopsis else plot
|
||||||
addTrailer(meta.trailers)
|
|
||||||
posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl
|
posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl
|
||||||
actors = actors ?: meta.actors
|
actors = actors ?: meta.actors
|
||||||
|
|
||||||
|
for ((k, v) in syncs ?: emptyMap()) {
|
||||||
|
syncData[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
val realRecommendations = ArrayList<SearchResponse>()
|
val realRecommendations = ArrayList<SearchResponse>()
|
||||||
val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
|
val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
|
||||||
meta.recommendations?.forEach { rec ->
|
meta.recommendations?.forEach { rec ->
|
||||||
|
@ -143,21 +157,54 @@ class ResultViewModel : ViewModel() {
|
||||||
recommendations = recommendations?.union(realRecommendations)?.toList()
|
recommendations = recommendations?.union(realRecommendations)?.toList()
|
||||||
?: realRecommendations
|
?: realRecommendations
|
||||||
|
|
||||||
println("THIS:$this")
|
argamap({
|
||||||
|
addTrailer(meta.trailers)
|
||||||
|
}, {
|
||||||
|
if (this !is AnimeLoadResponse) return@argamap
|
||||||
|
val map = getEpisodesDetails(getMalId(), getAniListId())
|
||||||
|
if (map.isNullOrEmpty()) return@argamap
|
||||||
|
updateEpisodes = DubStatus.values().map { dubStatus ->
|
||||||
|
val current =
|
||||||
|
this.episodes[dubStatus]?.sortedBy { it.episode ?: 0 }?.toMutableList()
|
||||||
|
if (current.isNullOrEmpty()) return@map false
|
||||||
|
val episodes = current.mapIndexed { index, ep -> ep.episode ?: (index + 1) }
|
||||||
|
var updateCount = 0
|
||||||
|
map.forEach { (episode, node) ->
|
||||||
|
episodes.binarySearch(episode).let { index ->
|
||||||
|
current.getOrNull(index)?.let { currentEp ->
|
||||||
|
current[index] = currentEp.apply {
|
||||||
|
updateCount++
|
||||||
|
this.description = this.description ?: node.description?.en
|
||||||
|
this.name = this.name ?: node.titles?.canonical
|
||||||
|
this.episode = this.episode ?: node.num ?: episodes[index]
|
||||||
|
this.posterUrl = this.posterUrl ?: node.thumbnail?.original?.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.episodes[dubStatus] = current
|
||||||
|
|
||||||
|
updateCount > 0
|
||||||
|
}.any { it }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
return out to updateEpisodes
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setMeta(meta: SyncAPI.SyncResult) = viewModelScope.launch {
|
fun setMeta(meta: SyncAPI.SyncResult, syncs: Map<String, String>?) =
|
||||||
Log.i(TAG, "setMeta")
|
viewModelScope.launch {
|
||||||
lastMeta = meta
|
Log.i(TAG, "setMeta")
|
||||||
ioWork {
|
lastMeta = meta
|
||||||
(result.value as? Resource.Success<LoadResponse>?)?.value?.let { resp ->
|
lastSync = syncs
|
||||||
val value = Resource.Success(applyMeta(resp, meta))
|
val (value, updateEpisodes) = ioWork {
|
||||||
println("POSTED: $value")
|
(result.value as? Resource.Success<LoadResponse>?)?.value?.let { resp ->
|
||||||
_resultResponse.postValue(value)
|
return@ioWork applyMeta(resp, meta, syncs)
|
||||||
|
}
|
||||||
|
return@ioWork null to null
|
||||||
}
|
}
|
||||||
|
_resultResponse.postValue(Resource.Success(value ?: return@launch))
|
||||||
|
if (updateEpisodes ?: return@launch) updateEpisodes(value, lastShowFillers)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadWatchStatus(localId: Int? = null) {
|
private fun loadWatchStatus(localId: Int? = null) {
|
||||||
val currentId = localId ?: id.value ?: return
|
val currentId = localId ?: id.value ?: return
|
||||||
|
@ -317,6 +364,159 @@ class ResultViewModel : ViewModel() {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lastShowFillers = false
|
||||||
|
private suspend fun updateEpisodes(loadResponse: LoadResponse, showFillers: Boolean) {
|
||||||
|
Log.i(TAG, "updateEpisodes")
|
||||||
|
try {
|
||||||
|
lastShowFillers = showFillers
|
||||||
|
val mainId = loadResponse.getId()
|
||||||
|
|
||||||
|
when (loadResponse) {
|
||||||
|
is AnimeLoadResponse -> {
|
||||||
|
if (loadResponse.episodes.isEmpty()) {
|
||||||
|
_dubSubEpisodes.postValue(emptyMap())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// val status = getDub(mainId)
|
||||||
|
val statuses = loadResponse.episodes.map { it.key }
|
||||||
|
|
||||||
|
// Extremely bruh to have to take in context here, but I'm not sure how to do this in a better way :(
|
||||||
|
val preferDub = context?.getApiDubstatusSettings()
|
||||||
|
?.contains(DubStatus.Dubbed) == true
|
||||||
|
|
||||||
|
// 3 statements because there can be only dub even if you do not prefer it.
|
||||||
|
val dubStatus =
|
||||||
|
if (preferDub && statuses.contains(DubStatus.Dubbed)) DubStatus.Dubbed
|
||||||
|
else if (!preferDub && statuses.contains(DubStatus.Subbed)) DubStatus.Subbed
|
||||||
|
else statuses.first()
|
||||||
|
|
||||||
|
val fillerEpisodes =
|
||||||
|
if (showFillers) safeApiCall { getFillerEpisodes(loadResponse.name) } else null
|
||||||
|
|
||||||
|
val existingEpisodes = HashSet<Int>()
|
||||||
|
val res = loadResponse.episodes.map { ep ->
|
||||||
|
val episodes = ArrayList<ResultEpisode>()
|
||||||
|
val idIndex = ep.key.id
|
||||||
|
for ((index, i) in ep.value.withIndex()) {
|
||||||
|
val episode = i.episode ?: (index + 1)
|
||||||
|
val id = mainId + episode + idIndex * 1000000
|
||||||
|
if (!existingEpisodes.contains(episode)) {
|
||||||
|
existingEpisodes.add(id)
|
||||||
|
episodes.add(buildResultEpisode(
|
||||||
|
loadResponse.name,
|
||||||
|
filterName(i.name),
|
||||||
|
i.posterUrl,
|
||||||
|
episode,
|
||||||
|
i.season,
|
||||||
|
i.data,
|
||||||
|
loadResponse.apiName,
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
i.rating,
|
||||||
|
i.description,
|
||||||
|
if (fillerEpisodes is Resource.Success) fillerEpisodes.value?.let {
|
||||||
|
it.contains(episode) && it[episode] == true
|
||||||
|
} ?: false else false,
|
||||||
|
loadResponse.type,
|
||||||
|
mainId
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Pair(ep.key, episodes)
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
// These posts needs to be in this order as to make the preferDub in ResultFragment work
|
||||||
|
_dubSubEpisodes.postValue(res)
|
||||||
|
res[dubStatus]?.let { episodes ->
|
||||||
|
updateEpisodes(mainId, episodes, -1)
|
||||||
|
}
|
||||||
|
_dubStatus.postValue(dubStatus)
|
||||||
|
_dubSubSelections.postValue(loadResponse.episodes.keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
is TvSeriesLoadResponse -> {
|
||||||
|
val episodes = ArrayList<ResultEpisode>()
|
||||||
|
val existingEpisodes = HashSet<Int>()
|
||||||
|
for ((index, episode) in loadResponse.episodes.sortedBy {
|
||||||
|
(it.season?.times(10000) ?: 0) + (it.episode ?: 0)
|
||||||
|
}.withIndex()) {
|
||||||
|
val episodeIndex = episode.episode ?: (index + 1)
|
||||||
|
val id =
|
||||||
|
mainId + (episode.season?.times(100000) ?: 0) + episodeIndex + 1
|
||||||
|
if (!existingEpisodes.contains(id)) {
|
||||||
|
existingEpisodes.add(id)
|
||||||
|
episodes.add(
|
||||||
|
buildResultEpisode(
|
||||||
|
loadResponse.name,
|
||||||
|
filterName(episode.name),
|
||||||
|
episode.posterUrl,
|
||||||
|
episodeIndex,
|
||||||
|
episode.season,
|
||||||
|
episode.data,
|
||||||
|
loadResponse.apiName,
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
episode.rating,
|
||||||
|
episode.description,
|
||||||
|
null,
|
||||||
|
loadResponse.type,
|
||||||
|
mainId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateEpisodes(mainId, episodes, -1)
|
||||||
|
}
|
||||||
|
is MovieLoadResponse -> {
|
||||||
|
buildResultEpisode(
|
||||||
|
loadResponse.name,
|
||||||
|
loadResponse.name,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
loadResponse.dataUrl,
|
||||||
|
loadResponse.apiName,
|
||||||
|
(mainId), // HAS SAME ID
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
loadResponse.type,
|
||||||
|
mainId
|
||||||
|
).let {
|
||||||
|
updateEpisodes(mainId, listOf(it), -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TorrentLoadResponse -> {
|
||||||
|
updateEpisodes(
|
||||||
|
mainId, listOf(
|
||||||
|
buildResultEpisode(
|
||||||
|
loadResponse.name,
|
||||||
|
loadResponse.name,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
loadResponse.torrent ?: loadResponse.magnet ?: "",
|
||||||
|
loadResponse.apiName,
|
||||||
|
(mainId), // HAS SAME ID
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
loadResponse.type,
|
||||||
|
mainId
|
||||||
|
)
|
||||||
|
), -1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
|
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
|
||||||
_publicEpisodes.postValue(Resource.Loading())
|
_publicEpisodes.postValue(Resource.Loading())
|
||||||
_resultResponse.postValue(Resource.Loading(url))
|
_resultResponse.postValue(Resource.Loading(url))
|
||||||
|
@ -363,8 +563,8 @@ class ResultViewModel : ViewModel() {
|
||||||
|
|
||||||
when (data) {
|
when (data) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
val loadResponse = if (lastMeta != null) ioWork {
|
val loadResponse = if (lastMeta != null || lastSync != null) ioWork {
|
||||||
applyMeta(data.value, lastMeta)
|
applyMeta(data.value, lastMeta, lastSync).first
|
||||||
} else data.value
|
} else data.value
|
||||||
_resultResponse.postValue(Resource.Success(loadResponse))
|
_resultResponse.postValue(Resource.Success(loadResponse))
|
||||||
val mainId = loadResponse.getId()
|
val mainId = loadResponse.getId()
|
||||||
|
@ -384,148 +584,7 @@ class ResultViewModel : ViewModel() {
|
||||||
System.currentTimeMillis(),
|
System.currentTimeMillis(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
updateEpisodes(loadResponse, showFillers)
|
||||||
when (loadResponse) {
|
|
||||||
is AnimeLoadResponse -> {
|
|
||||||
if (loadResponse.episodes.isEmpty()) {
|
|
||||||
_dubSubEpisodes.postValue(emptyMap())
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
// val status = getDub(mainId)
|
|
||||||
val statuses = loadResponse.episodes.map { it.key }
|
|
||||||
|
|
||||||
// Extremely bruh to have to take in context here, but I'm not sure how to do this in a better way :(
|
|
||||||
val preferDub = context?.getApiDubstatusSettings()
|
|
||||||
?.contains(DubStatus.Dubbed) == true
|
|
||||||
|
|
||||||
// 3 statements because there can be only dub even if you do not prefer it.
|
|
||||||
val dubStatus =
|
|
||||||
if (preferDub && statuses.contains(DubStatus.Dubbed)) DubStatus.Dubbed
|
|
||||||
else if (!preferDub && statuses.contains(DubStatus.Subbed)) DubStatus.Subbed
|
|
||||||
else statuses.first()
|
|
||||||
|
|
||||||
val fillerEpisodes =
|
|
||||||
if (showFillers) safeApiCall { getFillerEpisodes(loadResponse.name) } else null
|
|
||||||
|
|
||||||
val existingEpisodes = HashSet<Int>()
|
|
||||||
val res = loadResponse.episodes.map { ep ->
|
|
||||||
val episodes = ArrayList<ResultEpisode>()
|
|
||||||
val idIndex = ep.key.id
|
|
||||||
for ((index, i) in ep.value.withIndex()) {
|
|
||||||
val episode = i.episode ?: (index + 1)
|
|
||||||
val id = mainId + episode + idIndex * 1000000
|
|
||||||
if (!existingEpisodes.contains(episode)) {
|
|
||||||
existingEpisodes.add(id)
|
|
||||||
episodes.add(buildResultEpisode(
|
|
||||||
loadResponse.name,
|
|
||||||
filterName(i.name),
|
|
||||||
i.posterUrl,
|
|
||||||
episode,
|
|
||||||
i.season,
|
|
||||||
i.data,
|
|
||||||
apiName,
|
|
||||||
id,
|
|
||||||
index,
|
|
||||||
i.rating,
|
|
||||||
i.description,
|
|
||||||
if (fillerEpisodes is Resource.Success) fillerEpisodes.value?.let {
|
|
||||||
it.contains(episode) && it[episode] == true
|
|
||||||
} ?: false else false,
|
|
||||||
loadResponse.type,
|
|
||||||
mainId
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Pair(ep.key, episodes)
|
|
||||||
}.toMap()
|
|
||||||
|
|
||||||
// These posts needs to be in this order as to make the preferDub in ResultFragment work
|
|
||||||
_dubSubEpisodes.postValue(res)
|
|
||||||
res[dubStatus]?.let { episodes ->
|
|
||||||
updateEpisodes(mainId, episodes, -1)
|
|
||||||
}
|
|
||||||
_dubStatus.postValue(dubStatus)
|
|
||||||
_dubSubSelections.postValue(loadResponse.episodes.keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
is TvSeriesLoadResponse -> {
|
|
||||||
val episodes = ArrayList<ResultEpisode>()
|
|
||||||
val existingEpisodes = HashSet<Int>()
|
|
||||||
for ((index, episode) in loadResponse.episodes.sortedBy {
|
|
||||||
(it.season?.times(10000) ?: 0) + (it.episode ?: 0)
|
|
||||||
}.withIndex()) {
|
|
||||||
val episodeIndex = episode.episode ?: (index + 1)
|
|
||||||
val id =
|
|
||||||
mainId + (episode.season?.times(100000) ?: 0) + episodeIndex + 1
|
|
||||||
if (!existingEpisodes.contains(id)) {
|
|
||||||
existingEpisodes.add(id)
|
|
||||||
episodes.add(
|
|
||||||
buildResultEpisode(
|
|
||||||
loadResponse.name,
|
|
||||||
filterName(episode.name),
|
|
||||||
episode.posterUrl,
|
|
||||||
episodeIndex,
|
|
||||||
episode.season,
|
|
||||||
episode.data,
|
|
||||||
apiName,
|
|
||||||
id,
|
|
||||||
index,
|
|
||||||
episode.rating,
|
|
||||||
episode.description,
|
|
||||||
null,
|
|
||||||
loadResponse.type,
|
|
||||||
mainId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateEpisodes(mainId, episodes, -1)
|
|
||||||
}
|
|
||||||
is MovieLoadResponse -> {
|
|
||||||
buildResultEpisode(
|
|
||||||
loadResponse.name,
|
|
||||||
loadResponse.name,
|
|
||||||
null,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
loadResponse.dataUrl,
|
|
||||||
loadResponse.apiName,
|
|
||||||
(mainId), // HAS SAME ID
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
loadResponse.type,
|
|
||||||
mainId
|
|
||||||
).let {
|
|
||||||
updateEpisodes(mainId, listOf(it), -1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is TorrentLoadResponse -> {
|
|
||||||
updateEpisodes(
|
|
||||||
mainId, listOf(
|
|
||||||
buildResultEpisode(
|
|
||||||
loadResponse.name,
|
|
||||||
loadResponse.name,
|
|
||||||
null,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
loadResponse.torrent ?: loadResponse.magnet ?: "",
|
|
||||||
loadResponse.apiName,
|
|
||||||
(mainId), // HAS SAME ID
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
loadResponse.type,
|
|
||||||
mainId
|
|
||||||
)
|
|
||||||
), -1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,11 @@ object FillerEpisodeCheck {
|
||||||
private const val MAIN_URL = "https://www.animefillerlist.com"
|
private const val MAIN_URL = "https://www.animefillerlist.com"
|
||||||
|
|
||||||
var list: HashMap<String, String>? = null
|
var list: HashMap<String, String>? = null
|
||||||
|
var cache: HashMap<String, HashMap<Int, Boolean>> = hashMapOf()
|
||||||
|
|
||||||
private fun fixName(name: String): String {
|
private fun fixName(name: String): String {
|
||||||
return name.lowercase(Locale.ROOT)/*.replace(" ", "")*/.replace("-", " ").replace("[^a-zA-Z0-9 ]".toRegex(), "")
|
return name.lowercase(Locale.ROOT)/*.replace(" ", "")*/.replace("-", " ")
|
||||||
|
.replace("[^a-zA-Z0-9 ]".toRegex(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getFillerList(): Boolean {
|
private suspend fun getFillerList(): Boolean {
|
||||||
|
@ -61,6 +63,9 @@ object FillerEpisodeCheck {
|
||||||
|
|
||||||
suspend fun getFillerEpisodes(query: String): HashMap<Int, Boolean>? {
|
suspend fun getFillerEpisodes(query: String): HashMap<Int, Boolean>? {
|
||||||
try {
|
try {
|
||||||
|
cache[query]?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
if (!getFillerList()) return null
|
if (!getFillerList()) return null
|
||||||
val localList = list ?: return null
|
val localList = list ?: return null
|
||||||
|
|
||||||
|
@ -75,9 +80,15 @@ object FillerEpisodeCheck {
|
||||||
"(\\d+)" // year
|
"(\\d+)" // year
|
||||||
)
|
)
|
||||||
val blackListRegex =
|
val blackListRegex =
|
||||||
Regex(""" (${blackList.joinToString(separator = "|").replace("(", "\\(").replace(")", "\\)")})""")
|
Regex(
|
||||||
|
""" (${
|
||||||
|
blackList.joinToString(separator = "|").replace("(", "\\(")
|
||||||
|
.replace(")", "\\)")
|
||||||
|
})"""
|
||||||
|
)
|
||||||
|
|
||||||
val realQuery = fixName(query.replace(blackListRegex, "")).replace("shippuuden", "shippuden")
|
val realQuery =
|
||||||
|
fixName(query.replace(blackListRegex, "")).replace("shippuuden", "shippuden")
|
||||||
if (!localList.containsKey(realQuery)) return null
|
if (!localList.containsKey(realQuery)) return null
|
||||||
val href = localList[realQuery]?.replace(MAIN_URL, "") ?: return null // JUST IN CASE
|
val href = localList[realQuery]?.replace(MAIN_URL, "") ?: return null // JUST IN CASE
|
||||||
val result = app.get("$MAIN_URL$href").text
|
val result = app.get("$MAIN_URL$href").text
|
||||||
|
@ -90,6 +101,7 @@ object FillerEpisodeCheck {
|
||||||
hashMap[episodeNumber] = type
|
hashMap[episodeNumber] = type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cache[query] = hashMap
|
||||||
return hashMap
|
return hashMap
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
8
app/src/main/res/layout/result_episode_both.xml
Normal file
8
app/src/main/res/layout/result_episode_both.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<include android:visibility="gone" layout="@layout/result_episode" />
|
||||||
|
<include android:visibility="gone" layout="@layout/result_episode_large" />
|
||||||
|
</FrameLayout>
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
android:nextFocusLeft="@id/episode_poster"
|
android:nextFocusLeft="@id/episode_poster"
|
||||||
android:nextFocusRight="@id/result_episode_download"
|
android:nextFocusRight="@id/result_episode_download"
|
||||||
android:id="@+id/episode_holder"
|
android:id="@+id/episode_holder_large"
|
||||||
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -142,11 +142,13 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:maxLines="4"
|
||||||
|
android:ellipsize="end"
|
||||||
android:paddingTop="10dp"
|
android:paddingTop="10dp"
|
||||||
android:paddingBottom="10dp"
|
android:paddingBottom="10dp"
|
||||||
android:id="@+id/episode_descript"
|
android:id="@+id/episode_descript"
|
||||||
android:textColor="?attr/grayTextColor"
|
android:textColor="?attr/grayTextColor"
|
||||||
tools:text="Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart. "
|
tools:text="Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart. Jon and Daenerys arrive in Winterfell and are met with skepticism. Sam learns about the fate of his family. Cersei gives Euron the reward he aims for. Theon follows his heart."
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
Loading…
Reference in a new issue