added Kitsu posters

This commit is contained in:
LagradOst 2022-06-18 02:30:39 +02:00
parent 81938a565f
commit 8fc790ab9b
10 changed files with 494 additions and 217 deletions

View file

@ -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()
} }

View file

@ -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()

View file

@ -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
)
}
}

View file

@ -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)
), ),

View file

@ -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,

View file

@ -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 =

View file

@ -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
} }

View file

@ -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()

View 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>

View file

@ -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>