From 8fc790ab9b7f9e1d362f5e75303f4a6ef08b87e4 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 18 Jun 2022 02:30:39 +0200 Subject: [PATCH] added Kitsu posters --- .../com/lagradost/cloudstream3/MainAPI.kt | 9 + .../animeproviders/NineAnimeProvider.kt | 13 +- .../syncproviders/providers/KitsuApi.kt | 145 +++++++ .../syncproviders/providers/MALApi.kt | 30 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 95 +++-- .../cloudstream3/ui/result/ResultFragment.kt | 12 +- .../cloudstream3/ui/result/ResultViewModel.kt | 375 ++++++++++-------- .../cloudstream3/utils/FillerEpisodeCheck.kt | 18 +- .../main/res/layout/result_episode_both.xml | 8 + .../main/res/layout/result_episode_large.xml | 6 +- 10 files changed, 494 insertions(+), 217 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt create mode 100644 app/src/main/res/layout/result_episode_both.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 8c8b3a8f..106ddea0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -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() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/NineAnimeProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/NineAnimeProvider.kt index f2851d0f..52d9cfd4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/NineAnimeProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/NineAnimeProvider.kt @@ -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() diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt new file mode 100644 index 00000000..d646b272 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt @@ -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, Map> = + mutableMapOf() + + suspend fun getEpisodesDetails( + malId: String?, + anilistId: String? + ): Map? { + 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? { + 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? = 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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index 70aa8030..28a23731 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -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 { - 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 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) ), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index b7b1c57a..99924f22 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -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, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index f331dc0c..63221641 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -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 = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt index 31aa1301..cdeda5b2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt @@ -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? = null + + private suspend fun applyMeta( + resp: LoadResponse, + meta: SyncAPI.SyncResult?, + syncs: Map? = null + ): Pair { + 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() val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name) meta.recommendations?.forEach { rec -> @@ -143,21 +157,54 @@ 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 { - Log.i(TAG, "setMeta") - lastMeta = meta - ioWork { - (result.value as? Resource.Success?)?.value?.let { resp -> - val value = Resource.Success(applyMeta(resp, meta)) - println("POSTED: $value") - _resultResponse.postValue(value) + fun setMeta(meta: SyncAPI.SyncResult, syncs: Map?) = + viewModelScope.launch { + Log.i(TAG, "setMeta") + lastMeta = meta + lastSync = syncs + val (value, updateEpisodes) = ioWork { + (result.value as? Resource.Success?)?.value?.let { resp -> + 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) { val currentId = localId ?: id.value ?: return @@ -317,6 +364,159 @@ class ResultViewModel : ViewModel() { 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() + val res = loadResponse.episodes.map { ep -> + val episodes = ArrayList() + 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() + val existingEpisodes = HashSet() + 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 { _publicEpisodes.postValue(Resource.Loading()) _resultResponse.postValue(Resource.Loading(url)) @@ -363,8 +563,8 @@ class ResultViewModel : ViewModel() { when (data) { is Resource.Success -> { - val loadResponse = if (lastMeta != null) ioWork { - applyMeta(data.value, lastMeta) + 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() @@ -384,148 +584,7 @@ class ResultViewModel : ViewModel() { System.currentTimeMillis(), ) ) - - 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() - val res = loadResponse.episodes.map { ep -> - val episodes = ArrayList() - 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() - val existingEpisodes = HashSet() - 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 - ) - } - } + updateEpisodes(loadResponse, showFillers) } else -> Unit } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt index 911d58e7..14d1b055 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt @@ -11,9 +11,11 @@ object FillerEpisodeCheck { private const val MAIN_URL = "https://www.animefillerlist.com" var list: HashMap? = null + var cache: HashMap> = 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? { 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() diff --git a/app/src/main/res/layout/result_episode_both.xml b/app/src/main/res/layout/result_episode_both.xml new file mode 100644 index 00000000..8841726f --- /dev/null +++ b/app/src/main/res/layout/result_episode_both.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index 5842d42d..8fc917ec 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -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 @@