mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
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.json.JsonMapper
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addMalId
|
||||
import com.lagradost.cloudstream3.animeproviders.*
|
||||
import com.lagradost.cloudstream3.metaproviders.CrossTmdbProvider
|
||||
import com.lagradost.cloudstream3.movieproviders.*
|
||||
|
@ -898,6 +899,14 @@ interface LoadResponse {
|
|||
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?) {
|
||||
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_dub&page=1",
|
||||
"Recently Updated (DUB)(DUB)"
|
||||
"Recently Updated (DUB)"
|
||||
),
|
||||
Pair(
|
||||
"$mainUrl/ajax/home/widget?name=updated_chinese&page=1",
|
||||
|
@ -64,7 +64,8 @@ class NineAnimeProvider : MainAPI() {
|
|||
}
|
||||
|
||||
//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? {
|
||||
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 {
|
||||
val title = it.selectFirst("a.name")!!.text()
|
||||
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
|
||||
val image = it.selectFirst("a.poster img")!!.attr("src")
|
||||
AnimeSearchResponse(
|
||||
|
@ -199,7 +203,8 @@ class NineAnimeProvider : MainAPI() {
|
|||
override suspend fun load(url: String): LoadResponse? {
|
||||
val validUrl = url.replace("https://9anime.to", mainUrl)
|
||||
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 poster = doc.selectFirst("aside.main div.thumb div img")!!.attr("src")
|
||||
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 idPrefix = "mal"
|
||||
override var mainUrl = "https://myanimelist.net"
|
||||
val apiUrl = "https://api.myanimelist.net"
|
||||
override val icon = R.drawable.mal_logo
|
||||
override val requiresLogin = true
|
||||
|
||||
|
@ -62,7 +63,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
}
|
||||
|
||||
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 res = app.get(
|
||||
url, headers = mapOf(
|
||||
|
@ -179,7 +180,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
name = node?.title ?: return null,
|
||||
apiName = this.name,
|
||||
syncId = node.id.toString(),
|
||||
url = "https://myanimelist.net/anime/${node.id}",
|
||||
url = "$mainUrl/anime/${node.id}",
|
||||
posterUrl = node.main_picture?.large
|
||||
)
|
||||
}
|
||||
|
@ -187,7 +188,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
override suspend fun getResult(id: String): SyncAPI.SyncResult? {
|
||||
val internalId = id.toIntOrNull() ?: return null
|
||||
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(
|
||||
url, headers = mapOf(
|
||||
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||
|
@ -203,7 +204,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
synopsis = malAnime.synopsis,
|
||||
airStatus = when (malAnime.status) {
|
||||
"finished_airing" -> ShowStatus.Completed
|
||||
"airing" -> ShowStatus.Ongoing
|
||||
"currently_airing" -> ShowStatus.Ongoing
|
||||
//"not_yet_aired"
|
||||
else -> null
|
||||
},
|
||||
nextAiring = null,
|
||||
|
@ -260,7 +262,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
val currentCode = sanitizer["code"]!!
|
||||
|
||||
val res = app.post(
|
||||
"https://myanimelist.net/v1/oauth2/token",
|
||||
"$mainUrl/v1/oauth2/token",
|
||||
data = mapOf(
|
||||
"client_id" to key,
|
||||
"code" to currentCode,
|
||||
|
@ -292,7 +294,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
.replace("/", "_").replace("\n", "")
|
||||
val codeChallenge = codeVerifier
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -318,7 +320,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
private suspend fun refreshToken() {
|
||||
try {
|
||||
val res = app.post(
|
||||
"https://myanimelist.net/v1/oauth2/token",
|
||||
"$mainUrl/v1/oauth2/token",
|
||||
data = mapOf(
|
||||
"client_id" to key,
|
||||
"grant_type" to "refresh_token",
|
||||
|
@ -451,7 +453,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
// Very lackluster docs
|
||||
// https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get
|
||||
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(
|
||||
url, headers = mapOf(
|
||||
"Authorization" to "Bearer $auth",
|
||||
|
@ -463,7 +465,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
private suspend fun getDataAboutMalId(id: Int): SmallMalAnime? {
|
||||
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
|
||||
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(
|
||||
url, headers = mapOf(
|
||||
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||
|
@ -481,7 +483,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
checkMalToken()
|
||||
while (!isDone) {
|
||||
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(
|
||||
"Authorization" to "Bearer " + (getAuth() ?: return)
|
||||
), cacheTime = 0
|
||||
|
@ -532,10 +534,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
}
|
||||
|
||||
private suspend fun checkMalToken() {
|
||||
if (unixTime > getKey(
|
||||
if (unixTime > (getKey(
|
||||
accountId,
|
||||
MAL_UNIXTIME_KEY
|
||||
) ?: 0L
|
||||
) ?: 0L)
|
||||
) {
|
||||
refreshToken()
|
||||
}
|
||||
|
@ -544,7 +546,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
private suspend fun getMalUser(setSettings: Boolean = true): MalUser? {
|
||||
checkMalToken()
|
||||
val res = app.get(
|
||||
"https://api.myanimelist.net/v2/users/@me",
|
||||
"$apiUrl/v2/users/@me",
|
||||
headers = mapOf(
|
||||
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||
), cacheTime = 0
|
||||
|
@ -620,7 +622,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
).filter { it.value != null } as Map<String, String>
|
||||
|
||||
return app.put(
|
||||
"https://api.myanimelist.net/v2/anime/$id/my_list_status",
|
||||
"$apiUrl/v2/anime/$id/my_list_status",
|
||||
headers = mapOf(
|
||||
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||
),
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.view.ViewGroup
|
|||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.ContentLoadingProgressBar
|
||||
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.VideoDownloadManager
|
||||
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_large.view.*
|
||||
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_SHOW_TOAST = 12
|
||||
const val ACTION_SHOW_DESCRIPTION = 15
|
||||
|
||||
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE = 13
|
||||
const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14
|
||||
|
@ -93,10 +94,10 @@ class EpisodeAdapter(
|
|||
@LayoutRes
|
||||
private var layout: Int = 0
|
||||
fun updateLayout() {
|
||||
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
|
||||
else R.layout.result_episode
|
||||
// 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
|
||||
// else R.layout.result_episode
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
@ -105,7 +106,7 @@ class EpisodeAdapter(
|
|||
else R.layout.result_episode*/
|
||||
|
||||
return EpisodeCardViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(layout, parent, false),
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.result_episode_both, parent, false),
|
||||
hasDownloadSupport,
|
||||
clickCallback,
|
||||
downloadClickCallback
|
||||
|
@ -134,27 +135,39 @@ class EpisodeAdapter(
|
|||
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder {
|
||||
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
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(card: ResultEpisode) {
|
||||
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
|
||||
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
|
||||
|
||||
val displayPos = card.getDisplayPosition()
|
||||
|
@ -171,16 +184,20 @@ class EpisodeAdapter(
|
|||
}
|
||||
|
||||
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 {
|
||||
episodeRating?.text = ""
|
||||
}
|
||||
|
||||
if (card.description != null) {
|
||||
episodeDescript?.visibility = View.VISIBLE
|
||||
episodeDescript?.text = card.description
|
||||
} else {
|
||||
episodeDescript?.visibility = View.GONE
|
||||
episodeRating?.isGone = episodeRating?.text.isNullOrBlank()
|
||||
|
||||
episodeDescript?.apply {
|
||||
text = card.description ?: ""
|
||||
isGone = text.isNullOrBlank()
|
||||
setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card))
|
||||
}
|
||||
}
|
||||
|
||||
episodePoster?.setOnClickListener {
|
||||
|
@ -192,34 +209,42 @@ class EpisodeAdapter(
|
|||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
episodeHolder.setOnClickListener {
|
||||
parentView.setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||
}
|
||||
|
||||
if (episodeHolder.context.isTrueTvSettings()) {
|
||||
episodeHolder.isFocusable = true
|
||||
episodeHolder.isFocusableInTouchMode = true
|
||||
episodeHolder.touchscreenBlocksFocus = false
|
||||
if (parentView.context.isTrueTvSettings()) {
|
||||
parentView.isFocusable = true
|
||||
parentView.isFocusableInTouchMode = true
|
||||
parentView.touchscreenBlocksFocus = false
|
||||
}
|
||||
|
||||
episodeHolder.setOnLongClickListener {
|
||||
parentView.setOnLongClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
|
||||
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
episodeDownloadImage.isVisible = hasDownloadSupport
|
||||
episodeDownloadBar.isVisible = hasDownloadSupport
|
||||
episodeDownloadImage?.isVisible = hasDownloadSupport
|
||||
episodeDownloadBar?.isVisible = hasDownloadSupport
|
||||
reattachDownloadButton()
|
||||
}
|
||||
|
||||
override fun reattachDownloadButton() {
|
||||
downloadButton.dispose()
|
||||
val card = localCard
|
||||
if (hasDownloadSupport && card != null) {
|
||||
val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(itemView.context, card.id)
|
||||
val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
|
||||
itemView.context,
|
||||
card.id
|
||||
)
|
||||
|
||||
downloadButton.setUpButton(
|
||||
downloadInfo?.fileLength, downloadInfo?.totalBytes, episodeDownloadBar, episodeDownloadImage, null,
|
||||
downloadInfo?.fileLength,
|
||||
downloadInfo?.totalBytes,
|
||||
episodeDownloadBar ?: return,
|
||||
episodeDownloadImage ?: return,
|
||||
null,
|
||||
VideoDownloadHelper.DownloadEpisodeCached(
|
||||
card.name,
|
||||
card.poster,
|
||||
|
|
|
@ -987,6 +987,7 @@ class ResultFragment : ResultTrailerPlayer() {
|
|||
}
|
||||
ACTION_CHROME_CAST_EPISODE -> requireLinks(true)
|
||||
ACTION_CHROME_CAST_MIRROR -> requireLinks(true)
|
||||
ACTION_SHOW_DESCRIPTION -> true
|
||||
else -> requireLinks(false)
|
||||
}
|
||||
if (!isLoaded) return@main // CANT LOAD
|
||||
|
@ -996,6 +997,14 @@ class ResultFragment : ResultTrailerPlayer() {
|
|||
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 -> {
|
||||
context?.let { ctx ->
|
||||
if (ctx.isConnectedToChromecast()) {
|
||||
|
@ -1419,6 +1428,7 @@ class ResultFragment : ResultTrailerPlayer() {
|
|||
|
||||
observe(syncModel.syncIds) {
|
||||
syncdata = it
|
||||
println("syncdata: $syncdata")
|
||||
}
|
||||
|
||||
var currentSyncProgress = 0
|
||||
|
@ -1443,7 +1453,7 @@ class ResultFragment : ResultTrailerPlayer() {
|
|||
val d = meta.value
|
||||
result_sync_episodes?.progress = currentSyncProgress * 1000
|
||||
setSyncMaxEpisodes(d.totalEpisodes)
|
||||
viewModel.setMeta(d)
|
||||
viewModel.setMeta(d, syncdata)
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
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.setKey
|
||||
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.NineAnimeProvider
|
||||
import com.lagradost.cloudstream3.metaproviders.SyncRedirector
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
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.WatchType
|
||||
import com.lagradost.cloudstream3.ui.player.IGenerator
|
||||
|
@ -119,19 +123,29 @@ class ResultViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
var lastMeta: SyncAPI.SyncResult? = null
|
||||
private suspend fun applyMeta(resp: LoadResponse, meta: SyncAPI.SyncResult?): LoadResponse {
|
||||
if (meta == null) return resp
|
||||
return resp.apply {
|
||||
var lastSync: Map<String, String>? = null
|
||||
|
||||
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")
|
||||
|
||||
duration = duration ?: meta.duration
|
||||
rating = rating ?: meta.publicScore
|
||||
tags = tags ?: meta.genres
|
||||
plot = if (plot.isNullOrBlank()) meta.synopsis else plot
|
||||
addTrailer(meta.trailers)
|
||||
posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl
|
||||
actors = actors ?: meta.actors
|
||||
|
||||
for ((k, v) in syncs ?: emptyMap()) {
|
||||
syncData[k] = v
|
||||
}
|
||||
|
||||
val realRecommendations = ArrayList<SearchResponse>()
|
||||
val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
|
||||
meta.recommendations?.forEach { rec ->
|
||||
|
@ -143,20 +157,53 @@ class ResultViewModel : ViewModel() {
|
|||
recommendations = recommendations?.union(realRecommendations)?.toList()
|
||||
?: 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>?) =
|
||||
viewModelScope.launch {
|
||||
Log.i(TAG, "setMeta")
|
||||
lastMeta = meta
|
||||
ioWork {
|
||||
lastSync = syncs
|
||||
val (value, updateEpisodes) = ioWork {
|
||||
(result.value as? Resource.Success<LoadResponse>?)?.value?.let { resp ->
|
||||
val value = Resource.Success(applyMeta(resp, meta))
|
||||
println("POSTED: $value")
|
||||
_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) {
|
||||
|
@ -317,79 +364,18 @@ class ResultViewModel : ViewModel() {
|
|||
return name
|
||||
}
|
||||
|
||||
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
|
||||
_publicEpisodes.postValue(Resource.Loading())
|
||||
_resultResponse.postValue(Resource.Loading(url))
|
||||
|
||||
val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url)
|
||||
if (api == null) {
|
||||
_resultResponse.postValue(
|
||||
Resource.Failure(
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
"This provider does not exist"
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val validUrlResource = safeApiCall {
|
||||
SyncRedirector.redirect(
|
||||
url,
|
||||
api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime")
|
||||
.replace(GogoanimeProvider().mainUrl, "gogoanime")
|
||||
)
|
||||
}
|
||||
|
||||
if (validUrlResource !is Resource.Success) {
|
||||
if (validUrlResource is Resource.Failure) {
|
||||
_resultResponse.postValue(validUrlResource)
|
||||
}
|
||||
|
||||
return@launch
|
||||
}
|
||||
val validUrl = validUrlResource.value
|
||||
|
||||
_resultResponse.postValue(Resource.Loading(validUrl))
|
||||
|
||||
_apiName.postValue(apiName)
|
||||
|
||||
repo = APIRepository(api)
|
||||
|
||||
val data = repo?.load(validUrl) ?: return@launch
|
||||
|
||||
_resultResponse.postValue(data)
|
||||
|
||||
when (data) {
|
||||
is Resource.Success -> {
|
||||
val loadResponse = if (lastMeta != null) ioWork {
|
||||
applyMeta(data.value, lastMeta)
|
||||
} else data.value
|
||||
_resultResponse.postValue(Resource.Success(loadResponse))
|
||||
var lastShowFillers = false
|
||||
private suspend fun updateEpisodes(loadResponse: LoadResponse, showFillers: Boolean) {
|
||||
Log.i(TAG, "updateEpisodes")
|
||||
try {
|
||||
lastShowFillers = showFillers
|
||||
val mainId = loadResponse.getId()
|
||||
id.postValue(mainId)
|
||||
loadWatchStatus(mainId)
|
||||
|
||||
setKey(
|
||||
DOWNLOAD_HEADER_CACHE,
|
||||
mainId.toString(),
|
||||
VideoDownloadHelper.DownloadHeaderCached(
|
||||
apiName,
|
||||
validUrl,
|
||||
loadResponse.type,
|
||||
loadResponse.name,
|
||||
loadResponse.posterUrl,
|
||||
mainId,
|
||||
System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
|
||||
when (loadResponse) {
|
||||
is AnimeLoadResponse -> {
|
||||
if (loadResponse.episodes.isEmpty()) {
|
||||
_dubSubEpisodes.postValue(emptyMap())
|
||||
return@launch
|
||||
return
|
||||
}
|
||||
|
||||
// val status = getDub(mainId)
|
||||
|
@ -424,7 +410,7 @@ class ResultViewModel : ViewModel() {
|
|||
episode,
|
||||
i.season,
|
||||
i.data,
|
||||
apiName,
|
||||
loadResponse.apiName,
|
||||
id,
|
||||
index,
|
||||
i.rating,
|
||||
|
@ -469,7 +455,7 @@ class ResultViewModel : ViewModel() {
|
|||
episodeIndex,
|
||||
episode.season,
|
||||
episode.data,
|
||||
apiName,
|
||||
loadResponse.apiName,
|
||||
id,
|
||||
index,
|
||||
episode.rating,
|
||||
|
@ -526,6 +512,79 @@ class ResultViewModel : ViewModel() {
|
|||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
|
||||
_publicEpisodes.postValue(Resource.Loading())
|
||||
_resultResponse.postValue(Resource.Loading(url))
|
||||
|
||||
val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url)
|
||||
if (api == null) {
|
||||
_resultResponse.postValue(
|
||||
Resource.Failure(
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
"This provider does not exist"
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val validUrlResource = safeApiCall {
|
||||
SyncRedirector.redirect(
|
||||
url,
|
||||
api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime")
|
||||
.replace(GogoanimeProvider().mainUrl, "gogoanime")
|
||||
)
|
||||
}
|
||||
|
||||
if (validUrlResource !is Resource.Success) {
|
||||
if (validUrlResource is Resource.Failure) {
|
||||
_resultResponse.postValue(validUrlResource)
|
||||
}
|
||||
|
||||
return@launch
|
||||
}
|
||||
val validUrl = validUrlResource.value
|
||||
|
||||
_resultResponse.postValue(Resource.Loading(validUrl))
|
||||
|
||||
_apiName.postValue(apiName)
|
||||
|
||||
repo = APIRepository(api)
|
||||
|
||||
val data = repo?.load(validUrl) ?: return@launch
|
||||
|
||||
_resultResponse.postValue(data)
|
||||
|
||||
when (data) {
|
||||
is Resource.Success -> {
|
||||
val loadResponse = if (lastMeta != null || lastSync != null) ioWork {
|
||||
applyMeta(data.value, lastMeta, lastSync).first
|
||||
} else data.value
|
||||
_resultResponse.postValue(Resource.Success(loadResponse))
|
||||
val mainId = loadResponse.getId()
|
||||
id.postValue(mainId)
|
||||
loadWatchStatus(mainId)
|
||||
|
||||
setKey(
|
||||
DOWNLOAD_HEADER_CACHE,
|
||||
mainId.toString(),
|
||||
VideoDownloadHelper.DownloadHeaderCached(
|
||||
apiName,
|
||||
validUrl,
|
||||
loadResponse.type,
|
||||
loadResponse.name,
|
||||
loadResponse.posterUrl,
|
||||
mainId,
|
||||
System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
updateEpisodes(loadResponse, showFillers)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
|
|
@ -11,9 +11,11 @@ object FillerEpisodeCheck {
|
|||
private const val MAIN_URL = "https://www.animefillerlist.com"
|
||||
|
||||
var list: HashMap<String, String>? = null
|
||||
var cache: HashMap<String, HashMap<Int, Boolean>> = hashMapOf()
|
||||
|
||||
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 {
|
||||
|
@ -61,6 +63,9 @@ object FillerEpisodeCheck {
|
|||
|
||||
suspend fun getFillerEpisodes(query: String): HashMap<Int, Boolean>? {
|
||||
try {
|
||||
cache[query]?.let {
|
||||
return it
|
||||
}
|
||||
if (!getFillerList()) return null
|
||||
val localList = list ?: return null
|
||||
|
||||
|
@ -75,9 +80,15 @@ object FillerEpisodeCheck {
|
|||
"(\\d+)" // year
|
||||
)
|
||||
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
|
||||
val href = localList[realQuery]?.replace(MAIN_URL, "") ?: return null // JUST IN CASE
|
||||
val result = app.get("$MAIN_URL$href").text
|
||||
|
@ -90,6 +101,7 @@ object FillerEpisodeCheck {
|
|||
hashMap[episodeNumber] = type
|
||||
}
|
||||
}
|
||||
cache[query] = hashMap
|
||||
return hashMap
|
||||
} catch (e: Exception) {
|
||||
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:nextFocusRight="@id/result_episode_download"
|
||||
android:id="@+id/episode_holder"
|
||||
android:id="@+id/episode_holder_large"
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -142,11 +142,13 @@
|
|||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:maxLines="4"
|
||||
android:ellipsize="end"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:id="@+id/episode_descript"
|
||||
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_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
|
Loading…
Reference in a new issue