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

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

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

View file

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

View file

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

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.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,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<LoadResponse>?)?.value?.let { resp ->
val value = Resource.Success(applyMeta(resp, meta))
println("POSTED: $value")
_resultResponse.postValue(value)
fun setMeta(meta: SyncAPI.SyncResult, syncs: Map<String, String>?) =
viewModelScope.launch {
Log.i(TAG, "setMeta")
lastMeta = meta
lastSync = syncs
val (value, updateEpisodes) = ioWork {
(result.value as? Resource.Success<LoadResponse>?)?.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<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 {
_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<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
)
}
}
updateEpisodes(loadResponse, showFillers)
}
else -> Unit
}

View file

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

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