compiling, not done 3

This commit is contained in:
LagradOst 2022-08-02 02:43:42 +02:00
parent f57b12d89c
commit 64ea5e2f4b
11 changed files with 782 additions and 1372 deletions

View file

@ -970,6 +970,10 @@ interface LoadResponse {
private val aniListIdPrefix = aniListApi.idPrefix private val aniListIdPrefix = aniListApi.idPrefix
var isTrailersEnabled = true var isTrailersEnabled = true
fun LoadResponse.isMovie() : Boolean {
return this.type.isMovieType()
}
@JvmName("addActorNames") @JvmName("addActorNames")
fun LoadResponse.addActors(actors: List<String>?) { fun LoadResponse.addActors(actors: List<String>?) {
this.actors = actors?.map { ActorData(Actor(it)) } this.actors = actors?.map { ActorData(Actor(it)) }

View file

@ -18,7 +18,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
object DownloadButtonSetup { object DownloadButtonSetup {
fun handleDownloadClick(activity: Activity?, headerName: String?, click: DownloadClickEvent) { fun handleDownloadClick(activity: Activity?, click: DownloadClickEvent) {
val id = click.data.id val id = click.data.id
if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return
when (click.action) { when (click.action) {

View file

@ -84,7 +84,7 @@ class DownloadChildFragment : Fragment() {
DownloadChildAdapter( DownloadChildAdapter(
ArrayList(), ArrayList(),
) { click -> ) { click ->
handleDownloadClick(activity, name, click) handleDownloadClick(activity, click)
} }
downloadDeleteEventListener = { id: Int -> downloadDeleteEventListener = { id: Int ->

View file

@ -153,7 +153,7 @@ class DownloadFragment : Fragment() {
}, },
{ downloadClickEvent -> { downloadClickEvent ->
if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter
handleDownloadClick(activity, downloadClickEvent.data.name, downloadClickEvent) handleDownloadClick(activity, downloadClickEvent)
if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
context?.let { ctx -> context?.let { ctx ->
downloadsViewModel.updateList(ctx) downloadsViewModel.updateList(ctx)

View file

@ -10,6 +10,7 @@ import androidx.annotation.LayoutRes
import androidx.core.view.isGone 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.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
@ -56,7 +57,7 @@ const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
class EpisodeAdapter( class EpisodeAdapter(
var cardList: List<ResultEpisode>, private var cardList: MutableList<ResultEpisode>,
private val hasDownloadSupport: Boolean, private val hasDownloadSupport: Boolean,
private val clickCallback: (EpisodeClickEvent) -> Unit, private val clickCallback: (EpisodeClickEvent) -> Unit,
private val downloadClickCallback: (DownloadClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit,
@ -92,6 +93,17 @@ class EpisodeAdapter(
} }
} }
fun updateList(newList: List<ResultEpisode>) {
val diffResult = DiffUtil.calculateDiff(
ResultDiffCallback(this.cardList, newList)
)
cardList.clear()
cardList.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
@LayoutRes @LayoutRes
private var layout: Int = 0 private var layout: Int = 0
fun updateLayout() { fun updateLayout() {
@ -263,3 +275,19 @@ class EpisodeAdapter(
} }
} }
} }
class ResultDiffCallback(
private val oldList: List<ResultEpisode>,
private val newList: List<ResultEpisode>
) :
DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition].id == newList[newItemPosition].id
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition] == newList[newItemPosition]
}

View file

@ -1,11 +1,6 @@
package com.lagradost.cloudstream3.ui.result package com.lagradost.cloudstream3.ui.result
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Context.CLIPBOARD_SERVICE
import android.content.Intent import android.content.Intent
import android.content.Intent.* import android.content.Intent.*
import android.content.res.ColorStateList import android.content.res.ColorStateList
@ -17,12 +12,11 @@ import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.AbsListView
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.FileProvider
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
@ -37,63 +31,43 @@ import com.google.android.gms.cast.framework.CastState
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiFromName import com.lagradost.cloudstream3.APIHolder.getApiFromName
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.getCastSession
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.*
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast
import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getFolderName
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFileName
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import kotlinx.android.synthetic.main.fragment_result.* import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.fragment_trailer.* import kotlinx.android.synthetic.main.fragment_trailer.*
import kotlinx.android.synthetic.main.result_recommendations.* import kotlinx.android.synthetic.main.result_recommendations.*
import kotlinx.android.synthetic.main.result_sync.* import kotlinx.android.synthetic.main.result_sync.*
import kotlinx.android.synthetic.main.trailer_custom_layout.* import kotlinx.android.synthetic.main.trailer_custom_layout.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import java.io.File
const val START_ACTION_NORMAL = 0 const val START_ACTION_NORMAL = 0
const val START_ACTION_RESUME_LATEST = 1 const val START_ACTION_RESUME_LATEST = 1
@ -230,223 +204,6 @@ class ResultFragment : ResultTrailerPlayer() {
} }
private var updateUIListener: (() -> Unit)? = null private var updateUIListener: (() -> Unit)? = null
private fun downloadSubtitle(
context: Context?,
link: SubtitleData,
meta: VideoDownloadManager.DownloadEpisodeMetadata,
) {
context?.let { ctx ->
val fileName = getFileName(ctx, meta)
val folder = getFolder(meta.type ?: return, meta.mainName)
downloadSubtitle(
ctx,
ExtractorSubtitleLink(link.name, link.url, ""),
fileName,
folder
)
}
}
private fun downloadSubtitle(
context: Context?,
link: ExtractorSubtitleLink,
fileName: String,
folder: String
) {
ioSafe {
VideoDownloadManager.downloadThing(
context ?: return@ioSafe,
link,
"$fileName ${link.name}",
folder,
if (link.url.contains(".srt")) ".srt" else "vtt",
false,
null
) {
// no notification
}
}
}
private fun getMeta(
episode: ResultEpisode,
titleName: String,
apiName: String,
currentPoster: String,
currentIsMovie: Boolean,
tvType: TvType,
): VideoDownloadManager.DownloadEpisodeMetadata {
return VideoDownloadManager.DownloadEpisodeMetadata(
episode.id,
sanitizeFilename(titleName),
apiName,
episode.poster ?: currentPoster,
episode.name,
if (currentIsMovie) null else episode.season,
if (currentIsMovie) null else episode.episode,
tvType,
)
}
private fun getFolder(currentType: TvType, titleName: String): String {
val sanitizedFileName = sanitizeFilename(titleName)
return when (currentType) {
TvType.Anime -> "Anime/$sanitizedFileName"
TvType.Movie -> "Movies"
TvType.AnimeMovie -> "Movies"
TvType.TvSeries -> "TVSeries/$sanitizedFileName"
TvType.OVA -> "OVA"
TvType.Cartoon -> "Cartoons/$sanitizedFileName"
TvType.Torrent -> "Torrent"
TvType.Documentary -> "Documentaries"
TvType.AsianDrama -> "AsianDrama"
TvType.Live -> "LiveStreams"
}
}
fun startDownload(
context: Context?,
episode: ResultEpisode,
currentIsMovie: Boolean,
currentHeaderName: String,
currentType: TvType,
currentPoster: String,
apiName: String,
parentId: Int,
url: String,
links: List<ExtractorLink>,
subs: List<SubtitleData>?
) {
try {
if (context == null) return
val meta =
getMeta(
episode,
currentHeaderName,
apiName,
currentPoster,
currentIsMovie,
currentType
)
val folder = getFolder(currentType, currentHeaderName)
val src = "$DOWNLOAD_NAVIGATE_TO/$parentId" // url ?: return@let
// SET VISUAL KEYS
setKey(
DOWNLOAD_HEADER_CACHE,
parentId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
apiName,
url,
currentType,
currentHeaderName,
currentPoster,
parentId,
System.currentTimeMillis(),
)
)
setKey(
getFolderName(
DOWNLOAD_EPISODE_CACHE,
parentId.toString()
), // 3 deep folder for faster acess
episode.id.toString(),
VideoDownloadHelper.DownloadEpisodeCached(
episode.name,
episode.poster,
episode.episode,
episode.season,
episode.id,
parentId,
episode.rating,
episode.description,
System.currentTimeMillis(),
)
)
// DOWNLOAD VIDEO
VideoDownloadManager.downloadEpisodeUsingWorker(
context,
src,//url ?: return,
folder,
meta,
links
)
// 1. Checks if the lang should be downloaded
// 2. Makes it into the download format
// 3. Downloads it as a .vtt file
val downloadList = getDownloadSubsLanguageISO639_1()
subs?.let { subsList ->
subsList.filter {
downloadList.contains(
SubtitleHelper.fromLanguageToTwoLetters(
it.name,
true
)
)
}
.map { ExtractorSubtitleLink(it.name, it.url, "") }
.forEach { link ->
val fileName = getFileName(context, meta)
downloadSubtitle(context, link, fileName, folder)
}
}
} catch (e: Exception) {
logError(e)
}
}
suspend fun downloadEpisode(
activity: Activity?,
episode: ResultEpisode,
currentIsMovie: Boolean,
currentHeaderName: String,
currentType: TvType,
currentPoster: String,
apiName: String,
parentId: Int,
url: String,
) {
safeApiCall {
val generator = RepoLinkGenerator(listOf(episode))
val currentLinks = mutableSetOf<ExtractorLink>()
val currentSubs = mutableSetOf<SubtitleData>()
generator.generateLinks(clearCache = false, isCasting = false, callback = {
it.first?.let { link ->
currentLinks.add(link)
}
}, subtitleCallback = { sub ->
currentSubs.add(sub)
})
if (currentLinks.isEmpty()) {
main {
showToast(activity, R.string.no_links_found_toast, Toast.LENGTH_SHORT)
}
return@safeApiCall
}
startDownload(
activity,
episode,
currentIsMovie,
currentHeaderName,
currentType,
currentPoster,
apiName,
parentId,
url,
sortUrls(currentLinks),
sortSubs(currentSubs),
)
}
}
} }
private var currentLoadingCount = private var currentLoadingCount =
@ -470,7 +227,7 @@ class ResultFragment : ResultTrailerPlayer() {
override fun onDestroyView() { override fun onDestroyView() {
updateUIListener = null updateUIListener = null
(result_episodes?.adapter as EpisodeAdapter?)?.killAdapter() (result_episodes?.adapter as EpisodeAdapter?)?.killAdapter()
downloadButton?.dispose() //downloadButton?.dispose() //TODO READD
//somehow this still leaks and I dont know why???? //somehow this still leaks and I dont know why????
// todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt
PanelsChildGestureRegionObserver.Provider.get().removeGestureRegionsUpdateListener(this) PanelsChildGestureRegionObserver.Provider.get().removeGestureRegionsUpdateListener(this)
@ -502,7 +259,7 @@ class ResultFragment : ResultTrailerPlayer() {
result_loading?.isVisible = false result_loading?.isVisible = false
result_finish_loading?.isVisible = false result_finish_loading?.isVisible = false
result_loading_error?.isVisible = true result_loading_error?.isVisible = true
result_reload_connection_open_in_browser?.isVisible = url != null result_reload_connection_open_in_browser?.isVisible = true
} }
2 -> { 2 -> {
result_bookmark_fab?.isGone = result_bookmark_fab?.context?.isTvSettings() == true result_bookmark_fab?.isGone = result_bookmark_fab?.context?.isTvSettings() == true
@ -528,13 +285,6 @@ class ResultFragment : ResultTrailerPlayer() {
} }
} }
private var currentPoster: String? = null
private var currentId: Int? = null
private var currentIsMovie: Boolean? = null
private var episodeRanges: List<String>? = null
private var dubRange: Set<DubStatus>? = null
var url: String? = null
private fun fromIndexToSeasonText(selection: Int?): String { private fun fromIndexToSeasonText(selection: Int?): String {
return when (selection) { return when (selection) {
null -> getString(R.string.no_season) null -> getString(R.string.no_season)
@ -566,42 +316,6 @@ class ResultFragment : ResultTrailerPlayer() {
} }
} }
private fun handleDownloadButton(downloadClickEvent: DownloadClickEvent) {
if (downloadClickEvent.action == DOWNLOAD_ACTION_DOWNLOAD) {
currentEpisodes?.firstOrNull()?.let { episode ->
handleAction(
EpisodeClickEvent(
ACTION_DOWNLOAD_EPISODE,
ResultEpisode(
currentHeaderName ?: return@let,
currentHeaderName,
null,
0,
null,
episode.data,
apiName,
currentId ?: return@let,
0,
0L,
0L,
null,
null,
null,
currentType ?: return@let,
currentId ?: return@let,
)
)
)
}
} else {
DownloadButtonSetup.handleDownloadClick(
activity,
currentHeaderName,
downloadClickEvent
)
}
}
private fun loadTrailer(index: Int? = null) { private fun loadTrailer(index: Int? = null) {
val isSuccess = val isSuccess =
currentTrailers.getOrNull(index ?: currentTrailerIndex)?.let { trailer -> currentTrailers.getOrNull(index ?: currentTrailerIndex)?.let { trailer ->
@ -699,420 +413,12 @@ class ResultFragment : ResultTrailerPlayer() {
fixGrid() fixGrid()
} }
private fun lateFixDownloadButton(show: Boolean) {
if (!show || currentType?.isMovieType() == false) {
result_movie_parent.visibility = GONE
result_episodes_text.visibility = VISIBLE
result_episodes.visibility = VISIBLE
} else {
result_movie_parent.visibility = VISIBLE
result_episodes_text.visibility = GONE
result_episodes.visibility = GONE
}
}
private fun updateUI() { private fun updateUI() {
syncModel.updateUserData() syncModel.updateUserData()
viewModel.reloadEpisodes() viewModel.reloadEpisodes()
} }
var apiName: String = "" var apiName: String = ""
private fun handleAction(episodeClick: EpisodeClickEvent): Job = main {
if (episodeClick.action == ACTION_DOWNLOAD_EPISODE) {
val isMovie = currentIsMovie ?: return@main
val headerName = currentHeaderName ?: return@main
val tvType = currentType ?: return@main
val poster = currentPoster ?: return@main
val id = currentId ?: return@main
val curl = url ?: return@main
showToast(activity, R.string.download_started, Toast.LENGTH_SHORT)
downloadEpisode(
activity,
episodeClick.data,
isMovie,
headerName,
tvType,
poster,
apiName,
id,
curl,
)
return@main
}
var currentLinks: Set<ExtractorLink>? = null
var currentSubs: Set<SubtitleData>? = null
//val id = episodeClick.data.id
currentLoadingCount++
val showTitle =
episodeClick.data.name ?: context?.getString(R.string.episode_name_format)
?.format(
getString(R.string.episode),
episodeClick.data.episode
)
fun acquireSingleExtractorLink(
links: List<ExtractorLink>,
title: String,
callback: (ExtractorLink) -> Unit
) {
val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
builder.setTitle(title)
builder.setItems(links.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" }
.toTypedArray()) { dia, which ->
callback.invoke(links[which])
dia?.dismiss()
}
builder.create().show()
}
fun acquireSingleSubtitleLink(
links: List<SubtitleData>,
title: String,
callback: (SubtitleData) -> Unit
) {
val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom)
builder.setTitle(title)
builder.setItems(links.map { it.name }.toTypedArray()) { dia, which ->
callback.invoke(links[which])
dia?.dismiss()
}
builder.create().show()
}
fun acquireSingeExtractorLink(title: String, callback: (ExtractorLink) -> Unit) {
acquireSingleExtractorLink(sortUrls(currentLinks ?: return), title, callback)
}
fun startChromecast(startIndex: Int) {
val eps = currentEpisodes ?: return
activity?.getCastSession()?.startCast(
apiName,
currentIsMovie ?: return,
currentHeaderName,
currentPoster,
episodeClick.data.index,
eps,
sortUrls(currentLinks ?: return),
sortSubs(currentSubs ?: return),
startTime = episodeClick.data.getRealPosition(),
startIndex = startIndex
)
}
suspend fun requireLinks(isCasting: Boolean, displayLoading: Boolean = true): Boolean {
val skipLoading = getApiFromName(apiName).instantLinkLoading
var loadingDialog: AlertDialog? = null
val currentLoad = currentLoadingCount
if (!skipLoading && displayLoading) {
val builder =
AlertDialog.Builder(requireContext(), R.style.AlertDialogCustomTransparent)
val customLayout = layoutInflater.inflate(R.layout.dialog_loading, null)
builder.setView(customLayout)
loadingDialog = builder.create()
loadingDialog.show()
loadingDialog.setOnDismissListener {
currentLoadingCount++
}
}
val data = viewModel.loadEpisode(episodeClick.data, isCasting)
if (currentLoadingCount != currentLoad) return false
loadingDialog?.dismissSafe(activity)
when (data) {
is Resource.Success -> {
currentLinks = data.value.first
currentSubs = data.value.second
return true
}
is Resource.Failure -> {
showToast(
activity,
R.string.error_loading_links_toast,
Toast.LENGTH_SHORT
)
}
else -> Unit
}
return false
}
val isLoaded = when (episodeClick.action) {
ACTION_PLAY_EPISODE_IN_PLAYER -> true
ACTION_CLICK_DEFAULT -> true
ACTION_SHOW_TOAST -> true
ACTION_DOWNLOAD_EPISODE -> {
showToast(activity, R.string.download_started, Toast.LENGTH_SHORT)
requireLinks(false, false)
}
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
when (episodeClick.action) {
ACTION_SHOW_TOAST -> {
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()) {
handleAction(
EpisodeClickEvent(
ACTION_CHROME_CAST_EPISODE,
episodeClick.data
)
)
} else {
handleAction(
EpisodeClickEvent(
ACTION_PLAY_EPISODE_IN_PLAYER,
episodeClick.data
)
)
}
}
}
ACTION_DOWNLOAD_EPISODE_SUBTITLE -> {
acquireSingleSubtitleLink(
sortSubs(
currentSubs ?: return@main
),//(currentLinks ?: return@main).filter { !it.isM3u8 },
getString(R.string.episode_action_download_subtitle)
) { link ->
downloadSubtitle(
context,
link,
getMeta(
episodeClick.data,
currentHeaderName ?: return@acquireSingleSubtitleLink,
apiName,
currentPoster ?: return@acquireSingleSubtitleLink,
currentIsMovie ?: return@acquireSingleSubtitleLink,
currentType ?: return@acquireSingleSubtitleLink
)
)
showToast(activity, R.string.download_started, Toast.LENGTH_SHORT)
}
}
ACTION_SHOW_OPTIONS -> {
context?.let { ctx ->
val builder = AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
var dialog: AlertDialog? = null
builder.setTitle(showTitle)
val options =
requireContext().resources.getStringArray(R.array.episode_long_click_options)
val optionsValues =
requireContext().resources.getIntArray(R.array.episode_long_click_options_values)
val verifiedOptions = ArrayList<String>()
val verifiedOptionsValues = ArrayList<Int>()
val hasDownloadSupport = getApiFromName(apiName).hasDownloadSupport
for (i in options.indices) {
val opv = optionsValues[i]
val op = options[i]
val isConnected = ctx.isConnectedToChromecast()
val add = when (opv) {
ACTION_CHROME_CAST_EPISODE -> isConnected
ACTION_CHROME_CAST_MIRROR -> isConnected
ACTION_DOWNLOAD_EPISODE_SUBTITLE -> !currentSubs.isNullOrEmpty()
ACTION_DOWNLOAD_EPISODE -> hasDownloadSupport
ACTION_DOWNLOAD_MIRROR -> hasDownloadSupport
ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> context?.isAppInstalled(
VLC_PACKAGE
) ?: false
else -> true
}
if (add) {
verifiedOptions.add(op)
verifiedOptionsValues.add(opv)
}
}
builder.setItems(
verifiedOptions.toTypedArray()
) { _, which ->
handleAction(
EpisodeClickEvent(
verifiedOptionsValues[which],
episodeClick.data
)
)
dialog?.dismissSafe(activity)
}
dialog = builder.create()
dialog.show()
}
}
ACTION_COPY_LINK -> {
activity?.let { act ->
try {
acquireSingeExtractorLink(act.getString(R.string.episode_action_copy_link)) { link ->
val serviceClipboard =
(act.getSystemService(CLIPBOARD_SERVICE) as? ClipboardManager?)
?: return@acquireSingeExtractorLink
val clip = ClipData.newPlainText(link.name, link.url)
serviceClipboard.setPrimaryClip(clip)
showToast(act, R.string.copy_link_toast, Toast.LENGTH_SHORT)
}
} catch (e: Exception) {
showToast(act, e.toString(), Toast.LENGTH_LONG)
logError(e)
}
}
}
ACTION_PLAY_EPISODE_IN_BROWSER -> {
acquireSingeExtractorLink(getString(R.string.episode_action_play_in_browser)) { link ->
try {
val i = Intent(ACTION_VIEW)
i.data = Uri.parse(link.url)
startActivity(i)
} catch (e: Exception) {
logError(e)
}
}
}
ACTION_CHROME_CAST_MIRROR -> {
acquireSingeExtractorLink(getString(R.string.episode_action_chromecast_mirror)) { link ->
val mirrorIndex = currentLinks?.indexOf(link) ?: -1
startChromecast(if (mirrorIndex == -1) 0 else mirrorIndex)
}
}
ACTION_CHROME_CAST_EPISODE -> {
startChromecast(0)
}
ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> {
activity?.let { act ->
try {
if (!act.checkWrite()) {
act.requestRW()
if (act.checkWrite()) return@main
}
val data = currentLinks ?: return@main
val subs = currentSubs ?: return@main
val outputDir = act.cacheDir
val outputFile = withContext(Dispatchers.IO) {
File.createTempFile("mirrorlist", ".m3u8", outputDir)
}
var text = "#EXTM3U"
for (sub in sortSubs(subs)) {
text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${sub.name}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.name}\",URI=\"${sub.url}\""
}
for (link in data.sortedBy { -it.quality }) {
text += "\n#EXTINF:, ${link.name}\n${link.url}"
}
outputFile.writeText(text)
val vlcIntent = Intent(VLC_INTENT_ACTION_RESULT)
vlcIntent.setPackage(VLC_PACKAGE)
vlcIntent.addFlags(FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
vlcIntent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION)
vlcIntent.addFlags(FLAG_GRANT_READ_URI_PERMISSION)
vlcIntent.addFlags(FLAG_GRANT_WRITE_URI_PERMISSION)
vlcIntent.setDataAndType(
FileProvider.getUriForFile(
act,
act.applicationContext.packageName + ".provider",
outputFile
), "video/*"
)
val startId = VLC_FROM_PROGRESS
var position = startId
if (startId == VLC_FROM_START) {
position = 1
} else if (startId == VLC_FROM_PROGRESS) {
position = 0
}
vlcIntent.putExtra("position", position)
vlcIntent.component = VLC_COMPONENT
act.setKey(VLC_LAST_ID_KEY, episodeClick.data.id)
act.startActivityForResult(vlcIntent, VLC_REQUEST_CODE)
} catch (e: Exception) {
logError(e)
showToast(act, e.toString(), Toast.LENGTH_LONG)
}
}
}
ACTION_PLAY_EPISODE_IN_PLAYER -> {
viewModel.getGenerator(episodeClick.data)
?.let { generator ->
activity?.navigate(
R.id.global_to_navigation_player,
GeneratorPlayer.newInstance(
generator, syncdata?.let { HashMap(it) }
)
)
}
}
ACTION_RELOAD_EPISODE -> {
viewModel.loadEpisode(episodeClick.data, false, clearCache = true)
}
ACTION_DOWNLOAD_MIRROR -> {
acquireSingleExtractorLink(
sortUrls(
currentLinks ?: return@main
),//(currentLinks ?: return@main).filter { !it.isM3u8 },
context?.getString(R.string.episode_action_download_mirror) ?: ""
) { link ->
startDownload(
context,
episodeClick.data,
currentIsMovie ?: return@acquireSingleExtractorLink,
currentHeaderName ?: return@acquireSingleExtractorLink,
currentType ?: return@acquireSingleExtractorLink,
currentPoster ?: return@acquireSingleExtractorLink,
apiName,
currentId ?: return@acquireSingleExtractorLink,
url ?: return@acquireSingleExtractorLink,
listOf(link),
sortSubs(currentSubs ?: return@acquireSingleExtractorLink),
)
showToast(activity, R.string.download_started, Toast.LENGTH_SHORT)
}
}
}
}
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -1159,7 +465,7 @@ class ResultFragment : ResultTrailerPlayer() {
// activity?.fixPaddingStatusbar(result_toolbar) // activity?.fixPaddingStatusbar(result_toolbar)
url = arguments?.getString(URL_BUNDLE) val url = arguments?.getString(URL_BUNDLE)
apiName = arguments?.getString(API_NAME_BUNDLE) ?: return apiName = arguments?.getString(API_NAME_BUNDLE) ?: return
startAction = arguments?.getInt(START_ACTION_BUNDLE) ?: START_ACTION_NORMAL startAction = arguments?.getInt(START_ACTION_BUNDLE) ?: START_ACTION_NORMAL
startValue = arguments?.getInt(START_VALUE_BUNDLE) startValue = arguments?.getInt(START_VALUE_BUNDLE)
@ -1218,10 +524,10 @@ class ResultFragment : ResultTrailerPlayer() {
ArrayList(), ArrayList(),
api.hasDownloadSupport, api.hasDownloadSupport,
{ episodeClick -> { episodeClick ->
handleAction(episodeClick) viewModel.handleAction(activity, episodeClick)
}, },
{ downloadClickEvent -> { downloadClickEvent ->
handleDownloadClick(activity, currentHeaderName, downloadClickEvent) handleDownloadClick(activity, downloadClickEvent)
} }
) )
@ -1350,10 +656,6 @@ class ResultFragment : ResultTrailerPlayer() {
(result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.mapNotNull { it.icon }) (result_mini_sync?.adapter as? ImageAdapter?)?.updateList(newList.mapNotNull { it.icon })
} }
observe(syncModel.syncIds) {
syncdata = it
}
var currentSyncProgress = 0 var currentSyncProgress = 0
fun setSyncMaxEpisodes(totalEpisodes: Int?) { fun setSyncMaxEpisodes(totalEpisodes: Int?) {
@ -1376,7 +678,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, syncdata) viewModel.setMeta(d, syncModel.getSyncs())
} }
is Resource.Loading -> { is Resource.Loading -> {
result_sync_max_episodes?.text = result_sync_max_episodes?.text =
@ -1571,11 +873,7 @@ class ResultFragment : ResultTrailerPlayer() {
is Resource.Success -> { is Resource.Success -> {
//result_episodes?.isVisible = true //result_episodes?.isVisible = true
result_episode_loading?.isVisible = false result_episode_loading?.isVisible = false
if (result_episodes == null || result_episodes.adapter == null) return@observe (result_episodes?.adapter as? EpisodeAdapter?)?.updateList(episodes.value)
currentEpisodes = episodes.value
(result_episodes?.adapter as? EpisodeAdapter?)?.cardList = episodes.value
(result_episodes?.adapter as? EpisodeAdapter?)?.updateLayout()
(result_episodes?.adapter as? EpisodeAdapter?)?.notifyDataSetChanged()
} }
} }
} }
@ -1705,12 +1003,12 @@ class ResultFragment : ResultTrailerPlayer() {
result_meta_year.setText(d.yearText) result_meta_year.setText(d.yearText)
result_meta_duration.setText(d.durationText) result_meta_duration.setText(d.durationText)
result_meta_rating.setText(d.ratingText) result_meta_rating.setText(d.ratingText)
result_description.setTextHtml(d.plotText)
result_cast_text.setText(d.actorsText) result_cast_text.setText(d.actorsText)
result_next_airing.setText(d.nextAiringEpisode) result_next_airing.setText(d.nextAiringEpisode)
result_next_airing_time.setText(d.nextAiringDate) result_next_airing_time.setText(d.nextAiringDate)
result_poster.setImage(d.posterImage) result_poster.setImage(d.posterImage)
result_play_movie.setText(d.playMovieText)
result_cast_items?.isVisible = d.actors != null result_cast_items?.isVisible = d.actors != null
@ -1751,8 +1049,7 @@ class ResultFragment : ResultTrailerPlayer() {
syncModel.addFromUrl(d.url) syncModel.addFromUrl(d.url)
} }
result_play_movie.setText(d.playMovieText) result_description.setTextHtml(d.plotText)
result_description?.setOnClickListener { view -> result_description?.setOnClickListener { view ->
view.context?.let { ctx -> view.context?.let { ctx ->
val builder: AlertDialog.Builder = val builder: AlertDialog.Builder =

View file

@ -1,627 +0,0 @@
package com.lagradost.cloudstream3.ui.result
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.getApiFromUrlNull
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
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.FillerEpisodeCheck.getFillerEpisodes
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.collections.set
const val EPISODE_RANGE_SIZE = 50
const val EPISODE_RANGE_OVERLOAD = 60
class ResultViewModel : ViewModel() {
private var repo: APIRepository? = null
private var generator: IGenerator? = null
private val _resultResponse: MutableLiveData<Resource<LoadResponse>> = MutableLiveData()
private val _episodes: MutableLiveData<List<ResultEpisode>> = MutableLiveData()
private val episodeById: MutableLiveData<HashMap<Int, Int>> =
MutableLiveData() // lookup by ID to get Index
private val _publicEpisodes: MutableLiveData<Resource<List<ResultEpisode>>> = MutableLiveData()
private val _publicEpisodesCount: MutableLiveData<Int> = MutableLiveData() // before the sorting
private val _rangeOptions: MutableLiveData<List<String>> = MutableLiveData()
val selectedRange: MutableLiveData<String> = MutableLiveData()
private val selectedRangeInt: MutableLiveData<Int> = MutableLiveData()
val rangeOptions: LiveData<List<String>> = _rangeOptions
val result: LiveData<Resource<LoadResponse>> get() = _resultResponse
val episodes: LiveData<List<ResultEpisode>> get() = _episodes
val publicEpisodes: LiveData<Resource<List<ResultEpisode>>> get() = _publicEpisodes
val publicEpisodesCount: LiveData<Int> get() = _publicEpisodesCount
val dubStatus: LiveData<DubStatus> get() = _dubStatus
private val _dubStatus: MutableLiveData<DubStatus> = MutableLiveData()
val id: MutableLiveData<Int> = MutableLiveData()
val selectedSeason: MutableLiveData<Int> = MutableLiveData(-2)
val seasonSelections: MutableLiveData<List<Pair<String?, Int?>>> = MutableLiveData()
val dubSubSelections: LiveData<Set<DubStatus>> get() = _dubSubSelections
private val _dubSubSelections: MutableLiveData<Set<DubStatus>> = MutableLiveData()
val dubSubEpisodes: LiveData<Map<DubStatus, List<ResultEpisode>>?> get() = _dubSubEpisodes
private val _dubSubEpisodes: MutableLiveData<Map<DubStatus, List<ResultEpisode>>?> =
MutableLiveData()
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData()
val watchStatus: LiveData<WatchType> get() = _watchStatus
fun updateWatchStatus(status: WatchType) = viewModelScope.launch {
val currentId = id.value ?: return@launch
_watchStatus.postValue(status)
val resultPage = _resultResponse.value
withContext(Dispatchers.IO) {
setResultWatchState(currentId, status.internalId)
if (resultPage != null && resultPage is Resource.Success) {
val resultPageData = resultPage.value
val current = getBookmarkedData(currentId)
val currentTime = System.currentTimeMillis()
setBookmarkedData(
currentId,
DataStoreHelper.BookmarkedData(
currentId,
current?.bookmarkedTime ?: currentTime,
currentTime,
resultPageData.name,
resultPageData.url,
resultPageData.apiName,
resultPageData.type,
resultPageData.posterUrl,
resultPageData.year
)
)
}
}
}
companion object {
const val TAG = "RVM"
}
var lastMeta: SyncAPI.SyncResult? = null
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
posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl
actors = actors ?: meta.actors
if (this is EpisodeResponse) {
nextAiring = nextAiring ?: meta.nextAiring
}
for ((k, v) in syncs ?: emptyMap()) {
syncData[k] = v
}
val realRecommendations = ArrayList<SearchResponse>()
val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
meta.recommendations?.forEach { rec ->
apiNames.forEach { name ->
realRecommendations.add(rec.copy(apiName = name))
}
}
recommendations = recommendations?.union(realRecommendations)?.toList()
?: realRecommendations
argamap({
addTrailer(meta.trailers)
}, {
if (this !is AnimeLoadResponse) return@argamap
val map = getEpisodesDetails(getMalId(), getAniListId(), isResponseRequired = false)
if (map.isNullOrEmpty()) return@argamap
updateEpisodes = DubStatus.values().map { dubStatus ->
val current =
this.episodes[dubStatus]?.mapIndexed { index, episode ->
episode.apply {
this.episode = this.episode ?: (index + 1)
}
}?.sortedBy { it.episode ?: 0 }?.toMutableList()
if (current.isNullOrEmpty()) return@map false
val episodeNumbers = current.map { ep -> ep.episode!! }
var updateCount = 0
map.forEach { (episode, node) ->
episodeNumbers.binarySearch(episode).let { index ->
current.getOrNull(index)?.let { currentEp ->
current[index] = currentEp.apply {
updateCount++
val currentBack = this
this.description = this.description ?: node.description?.en
this.name = this.name ?: node.titles?.canonical
this.episode = this.episode ?: node.num ?: episodeNumbers[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, 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
val currentWatch = getResultWatchState(currentId)
_watchStatus.postValue(currentWatch)
}
private fun filterEpisodes(list: List<ResultEpisode>?, selection: Int?, range: Int?) {
if (list == null) return
val seasonTypes = HashMap<Int?, Boolean>()
for (i in list) {
if (!seasonTypes.containsKey(i.season)) {
seasonTypes[i.season] = true
}
}
val seasons = seasonTypes.toList().map { null to it.first }.sortedBy { it.second }
seasonSelections.postValue(seasons)
if (seasons.isEmpty()) { // WHAT THE FUCK DID YOU DO????? HOW DID YOU DO THIS
_publicEpisodes.postValue(Resource.Success(emptyList()))
return
}
val realSelection =
if (!seasonTypes.containsKey(selection)) seasons.first().second else selection
val internalId = id.value
if (internalId != null) setResultSeason(internalId, realSelection)
selectedSeason.postValue(realSelection ?: -2)
var currentList = list.filter { it.season == realSelection }
_publicEpisodesCount.postValue(currentList.size)
val rangeList = ArrayList<String>()
for (i in currentList.indices step EPISODE_RANGE_SIZE) {
if (i + EPISODE_RANGE_SIZE < currentList.size) {
rangeList.add("${i + 1}-${i + EPISODE_RANGE_SIZE}")
} else {
rangeList.add("${i + 1}-${currentList.size}")
}
}
val cRange = range ?: if (selection != null) {
0
} else {
selectedRangeInt.value ?: 0
}
val realRange = if (cRange * EPISODE_RANGE_SIZE > currentList.size) {
currentList.size / EPISODE_RANGE_SIZE
} else {
cRange
}
if (currentList.size > EPISODE_RANGE_OVERLOAD) {
currentList = currentList.subList(
realRange * EPISODE_RANGE_SIZE,
minOf(currentList.size, (realRange + 1) * EPISODE_RANGE_SIZE)
)
_rangeOptions.postValue(rangeList)
selectedRangeInt.postValue(realRange)
selectedRange.postValue(rangeList[realRange])
} else {
val allRange = "1-${currentList.size}"
_rangeOptions.postValue(listOf(allRange))
selectedRangeInt.postValue(0)
selectedRange.postValue(allRange)
}
_publicEpisodes.postValue(Resource.Success(currentList))
}
fun changeSeason(selection: Int?) {
filterEpisodes(_episodes.value, selection, null)
}
fun changeRange(range: Int?) {
filterEpisodes(_episodes.value, null, range)
}
fun changeDubStatus(status: DubStatus?) {
if (status == null) return
dubSubEpisodes.value?.get(status)?.let { episodes ->
id.value?.let {
setDub(it, status)
}
_dubStatus.postValue(status!!)
updateEpisodes(null, episodes, null)
}
}
suspend fun loadEpisode(
episode: ResultEpisode,
isCasting: Boolean,
clearCache: Boolean = false
): Resource<Pair<Set<ExtractorLink>, Set<SubtitleData>>> {
return safeApiCall {
val index = _episodes.value?.indexOf(episode) ?: episode.index
val currentLinks = mutableSetOf<ExtractorLink>()
val currentSubs = mutableSetOf<SubtitleData>()
generator?.goto(index)
generator?.generateLinks(clearCache, isCasting, {
it.first?.let { link ->
currentLinks.add(link)
}
}, { sub ->
currentSubs.add(sub)
})
return@safeApiCall Pair(
currentLinks.toSet(),
currentSubs.toSet()
)
}
}
fun getGenerator(episode: ResultEpisode): IGenerator? {
val index = _episodes.value?.indexOf(episode) ?: episode.index
generator?.goto(index)
return generator
}
private fun updateEpisodes(localId: Int?, list: List<ResultEpisode>, selection: Int?) {
_episodes.postValue(list)
generator = RepoLinkGenerator(list)
val set = HashMap<Int, Int>()
val range = selectedRangeInt.value
list.withIndex().forEach { set[it.value.id] = it.index }
episodeById.postValue(set)
filterEpisodes(
list,
if (selection == -1) getResultSeason(localId ?: id.value ?: return) else selection,
range
)
}
fun reloadEpisodes() {
val current = _episodes.value ?: return
val copy = current.map {
val posDur = getViewPos(it.id)
it.copy(position = posDur?.position ?: 0, duration = posDur?.duration ?: 0)
}
updateEpisodes(null, copy, selectedSeason.value)
}
private fun filterName(name: String?): String? {
if (name == null) return null
Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let {
if (it.isEmpty())
return null
}
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 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 LiveStreamLoadResponse -> {
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))
val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url)
if (api == null) {
_resultResponse.postValue(
Resource.Failure(
false,
null,
null,
"This provider does not exist"
)
)
return@launch
}
val validUrlResource = safeApiCall {
SyncRedirector.redirect(
url,
api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime")
.replace(GogoanimeProvider().mainUrl, "gogoanime")
)
}
if (validUrlResource !is Resource.Success) {
if (validUrlResource is Resource.Failure) {
_resultResponse.postValue(validUrlResource)
}
return@launch
}
val validUrl = validUrlResource.value
_resultResponse.postValue(Resource.Loading(validUrl))
_apiName.postValue(apiName)
repo = APIRepository(api)
val data = repo?.load(validUrl) ?: return@launch
_resultResponse.postValue(data)
when (data) {
is Resource.Success -> {
val loadResponse = if (lastMeta != null || lastSync != null) ioWork {
applyMeta(data.value, lastMeta, lastSync).first
} else data.value
_resultResponse.postValue(Resource.Success(loadResponse))
val mainId = loadResponse.getId()
id.postValue(mainId)
loadWatchStatus(mainId)
setKey(
DOWNLOAD_HEADER_CACHE,
mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
apiName,
validUrl,
loadResponse.type,
loadResponse.name,
loadResponse.posterUrl,
mainId,
System.currentTimeMillis(),
)
)
updateEpisodes(loadResponse, showFillers)
}
else -> Unit
}
}
private var _apiName: MutableLiveData<String> = MutableLiveData()
val apiName: LiveData<String> get() = _apiName
}

View file

@ -1,6 +1,14 @@
package com.lagradost.cloudstream3.ui.result package com.lagradost.cloudstream3.ui.result
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -8,9 +16,11 @@ import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.CommonActivity.getCastSession
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.getAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie
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
@ -18,9 +28,26 @@ import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.player.IGenerator import com.lagradost.cloudstream3.ui.player.IGenerator
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast
import com.lagradost.cloudstream3.utils.CastHelper.startCast
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -43,6 +70,7 @@ data class ResultData(
val comingSoon: Boolean, val comingSoon: Boolean,
val backgroundPosterUrl: String?, val backgroundPosterUrl: String?,
val title: String, val title: String,
var syncData: Map<String, String>,
val posterImage: UiImage?, val posterImage: UiImage?,
val plotText: UiText, val plotText: UiText,
@ -73,7 +101,7 @@ fun txt(status: DubStatus?): UiText? {
} }
fun LoadResponse.toResultData(repo: APIRepository): ResultData { fun LoadResponse.toResultData(repo: APIRepository): ResultData {
debugAssert({ repo.name == apiName }) { debugAssert({ repo.name != apiName }) {
"Api returned wrong apiName" "Api returned wrong apiName"
} }
@ -116,6 +144,7 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
} }
return ResultData( return ResultData(
syncData = syncData,
plotHeaderText = txt( plotHeaderText = txt(
when (this.type) { when (this.type) {
TvType.Torrent -> R.string.torrent_plot TvType.Torrent -> R.string.torrent_plot
@ -165,7 +194,7 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
), ),
yearText = txt(year), yearText = txt(year),
apiName = txt(apiName), apiName = txt(apiName),
ratingText = rating?.div(1000f)?.let { UiText.StringResource(R.string.rating_format, it) }, ratingText = rating?.div(1000f)?.let { txt(R.string.rating_format, it) },
vpnText = txt( vpnText = txt(
when (repo.vpnStatus) { when (repo.vpnStatus) {
VPNStatus.None -> null VPNStatus.None -> null
@ -192,6 +221,48 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
) )
} }
data class LinkProgress(
val linksLoaded: Int,
val subsLoaded: Int,
)
data class LinkLoadingResult(
val links: List<ExtractorLink>,
val subs: List<SubtitleData>,
)
sealed class SelectPopup {
data class SelectText(
val text: UiText,
val options: List<UiText>,
val callback: (Int?) -> Unit
) : SelectPopup()
data class SelectArray(
val text: UiText,
val options: Int,
val map: Int?,
val callback: (Int?) -> Unit
) : SelectPopup()
fun SelectPopup.transformResult(context: Context, input: Int?): Int? {
if (input == null) return null
return when (this) {
is SelectArray -> context.resources.getIntArray(map ?: return input).getOrNull(input)
?: input
is SelectText -> input
}
}
fun SelectPopup.getOptions(context: Context): List<String> {
return when (this) {
is SelectArray -> context.resources.getStringArray(options).toList()
is SelectText -> options.map { it.asString(context) }
}
}
}
class ResultViewModel2 : ViewModel() { class ResultViewModel2 : ViewModel() {
private var currentResponse: LoadResponse? = null private var currentResponse: LoadResponse? = null
@ -215,6 +286,9 @@ class ResultViewModel2 : ViewModel() {
private var preferDubStatus: DubStatus? = null private var preferDubStatus: DubStatus? = null
private var preferStartEpisode: Int? = null private var preferStartEpisode: Int? = null
private var preferStartSeason: Int? = null private var preferStartSeason: Int? = null
//private val currentIsMovie get() = currentResponse?.isEpisodeBased() == false
//private val currentHeaderName get() = currentResponse?.name
private val _page: MutableLiveData<Resource<ResultData>> = private val _page: MutableLiveData<Resource<ResultData>> =
MutableLiveData(Resource.Loading()) MutableLiveData(Resource.Loading())
@ -236,10 +310,12 @@ class ResultViewModel2 : ViewModel() {
MutableLiveData(emptyList()) MutableLiveData(emptyList())
val dubSubSelections: LiveData<List<Pair<UiText?, DubStatus>>> = _dubSubSelections val dubSubSelections: LiveData<List<Pair<UiText?, DubStatus>>> = _dubSubSelections
private val _rangeSelections: MutableLiveData<List<Pair<UiText?, EpisodeRange>>> = MutableLiveData(emptyList()) private val _rangeSelections: MutableLiveData<List<Pair<UiText?, EpisodeRange>>> =
MutableLiveData(emptyList())
val rangeSelections: LiveData<List<Pair<UiText?, EpisodeRange>>> = _rangeSelections val rangeSelections: LiveData<List<Pair<UiText?, EpisodeRange>>> = _rangeSelections
private val _seasonSelections: MutableLiveData<List<Pair<UiText?, Int>>> = MutableLiveData(emptyList()) private val _seasonSelections: MutableLiveData<List<Pair<UiText?, Int>>> =
MutableLiveData(emptyList())
val seasonSelections: LiveData<List<Pair<UiText?, Int>>> = _seasonSelections val seasonSelections: LiveData<List<Pair<UiText?, Int>>> = _seasonSelections
@ -258,6 +334,9 @@ class ResultViewModel2 : ViewModel() {
private val _selectedDubStatus: MutableLiveData<UiText?> = MutableLiveData(null) private val _selectedDubStatus: MutableLiveData<UiText?> = MutableLiveData(null)
val selectedDubStatus: LiveData<UiText?> = _selectedDubStatus val selectedDubStatus: LiveData<UiText?> = _selectedDubStatus
private val _loadedLinks: MutableLiveData<LinkProgress?> = MutableLiveData(null)
val loadedLinks: LiveData<LinkProgress?> = _loadedLinks
companion object { companion object {
const val TAG = "RVM2" const val TAG = "RVM2"
private const val EPISODE_RANGE_SIZE = 50 private const val EPISODE_RANGE_SIZE = 50
@ -375,6 +454,607 @@ class ResultViewModel2 : ViewModel() {
index to list index to list
}.toMap() }.toMap()
} }
private fun downloadSubtitle(
context: Context?,
link: ExtractorSubtitleLink,
fileName: String,
folder: String
) {
ioSafe {
VideoDownloadManager.downloadThing(
context ?: return@ioSafe,
link,
"$fileName ${link.name}",
folder,
if (link.url.contains(".srt")) ".srt" else "vtt",
false,
null
) {
// no notification
}
}
}
private fun getFolder(currentType: TvType, titleName: String): String {
val sanitizedFileName = VideoDownloadManager.sanitizeFilename(titleName)
return when (currentType) {
TvType.Anime -> "Anime/$sanitizedFileName"
TvType.Movie -> "Movies"
TvType.AnimeMovie -> "Movies"
TvType.TvSeries -> "TVSeries/$sanitizedFileName"
TvType.OVA -> "OVA"
TvType.Cartoon -> "Cartoons/$sanitizedFileName"
TvType.Torrent -> "Torrent"
TvType.Documentary -> "Documentaries"
TvType.AsianDrama -> "AsianDrama"
TvType.Live -> "LiveStreams"
}
}
private fun downloadSubtitle(
context: Context?,
link: SubtitleData,
meta: VideoDownloadManager.DownloadEpisodeMetadata,
) {
context?.let { ctx ->
val fileName = VideoDownloadManager.getFileName(ctx, meta)
val folder = getFolder(meta.type ?: return, meta.mainName)
downloadSubtitle(
ctx,
ExtractorSubtitleLink(link.name, link.url, ""),
fileName,
folder
)
}
}
fun startDownload(
context: Context?,
episode: ResultEpisode,
currentIsMovie: Boolean,
currentHeaderName: String,
currentType: TvType,
currentPoster: String?,
apiName: String,
parentId: Int,
url: String,
links: List<ExtractorLink>,
subs: List<SubtitleData>?
) {
try {
if (context == null) return
val meta =
getMeta(
episode,
currentHeaderName,
apiName,
currentPoster,
currentIsMovie,
currentType
)
val folder = getFolder(currentType, currentHeaderName)
val src = "$DOWNLOAD_NAVIGATE_TO/$parentId" // url ?: return@let
// SET VISUAL KEYS
AcraApplication.setKey(
DOWNLOAD_HEADER_CACHE,
parentId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
apiName,
url,
currentType,
currentHeaderName,
currentPoster,
parentId,
System.currentTimeMillis(),
)
)
AcraApplication.setKey(
DataStore.getFolderName(
DOWNLOAD_EPISODE_CACHE,
parentId.toString()
), // 3 deep folder for faster acess
episode.id.toString(),
VideoDownloadHelper.DownloadEpisodeCached(
episode.name,
episode.poster,
episode.episode,
episode.season,
episode.id,
parentId,
episode.rating,
episode.description,
System.currentTimeMillis(),
)
)
// DOWNLOAD VIDEO
VideoDownloadManager.downloadEpisodeUsingWorker(
context,
src,//url ?: return,
folder,
meta,
links
)
// 1. Checks if the lang should be downloaded
// 2. Makes it into the download format
// 3. Downloads it as a .vtt file
val downloadList = SubtitlesFragment.getDownloadSubsLanguageISO639_1()
subs?.let { subsList ->
subsList.filter {
downloadList.contains(
SubtitleHelper.fromLanguageToTwoLetters(
it.name,
true
)
)
}
.map { ExtractorSubtitleLink(it.name, it.url, "") }
.forEach { link ->
val fileName = VideoDownloadManager.getFileName(context, meta)
downloadSubtitle(context, link, fileName, folder)
}
}
} catch (e: Exception) {
logError(e)
}
}
suspend fun downloadEpisode(
activity: Activity?,
episode: ResultEpisode,
currentIsMovie: Boolean,
currentHeaderName: String,
currentType: TvType,
currentPoster: String?,
apiName: String,
parentId: Int,
url: String,
) {
safeApiCall {
val generator = RepoLinkGenerator(listOf(episode))
val currentLinks = mutableSetOf<ExtractorLink>()
val currentSubs = mutableSetOf<SubtitleData>()
generator.generateLinks(clearCache = false, isCasting = false, callback = {
it.first?.let { link ->
currentLinks.add(link)
}
}, subtitleCallback = { sub ->
currentSubs.add(sub)
})
if (currentLinks.isEmpty()) {
Coroutines.main {
CommonActivity.showToast(
activity,
R.string.no_links_found_toast,
Toast.LENGTH_SHORT
)
}
return@safeApiCall
}
startDownload(
activity,
episode,
currentIsMovie,
currentHeaderName,
currentType,
currentPoster,
apiName,
parentId,
url,
sortUrls(currentLinks),
sortSubs(currentSubs),
)
}
}
private fun getMeta(
episode: ResultEpisode,
titleName: String,
apiName: String,
currentPoster: String?,
currentIsMovie: Boolean,
tvType: TvType,
): VideoDownloadManager.DownloadEpisodeMetadata {
return VideoDownloadManager.DownloadEpisodeMetadata(
episode.id,
VideoDownloadManager.sanitizeFilename(titleName),
apiName,
episode.poster ?: currentPoster,
episode.name,
if (currentIsMovie) null else episode.season,
if (currentIsMovie) null else episode.episode,
tvType,
)
}
}
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData(WatchType.NONE)
val watchStatus: LiveData<WatchType> get() = _watchStatus
private val _selectPopup: MutableLiveData<SelectPopup?> = MutableLiveData(null)
val selectPopup: LiveData<SelectPopup?> get() = _selectPopup
fun updateWatchStatus(status: WatchType) {
val currentId = currentId ?: return
val resultPage = currentResponse ?: return
_watchStatus.postValue(status)
DataStoreHelper.setResultWatchState(currentId, status.internalId)
val current = DataStoreHelper.getBookmarkedData(currentId)
val currentTime = System.currentTimeMillis()
DataStoreHelper.setBookmarkedData(
currentId,
DataStoreHelper.BookmarkedData(
currentId,
current?.bookmarkedTime ?: currentTime,
currentTime,
resultPage.name,
resultPage.url,
resultPage.apiName,
resultPage.type,
resultPage.posterUrl,
resultPage.year
)
)
}
private suspend fun startChromecast(
activity: Activity?,
result: ResultEpisode,
isVisible: Boolean = true
) {
if (activity == null) return
val data = loadLinks(result, isVisible = isVisible, isCasting = true)
startChromecast(activity, result, data.links, data.subs, 0)
}
private fun startChromecast(
activity: Activity?,
result: ResultEpisode,
links: List<ExtractorLink>,
subs: List<SubtitleData>,
startIndex: Int,
) {
if (activity == null) return
val response = currentResponse ?: return
val eps = currentEpisodes[currentIndex ?: return] ?: return
activity.getCastSession()?.startCast(
response.apiName,
response.isMovie(),
response.name,
response.posterUrl,
result.index,
eps,
links,
subs,
startTime = result.getRealPosition(),
startIndex = startIndex
)
}
private val popupCallback: ((Int) -> Unit)? = null
fun cancelLinks() {
currentLoadLinkJob?.cancel()
_loadedLinks.postValue(null)
}
private var currentLoadLinkJob: Job? = null
private suspend fun acquireSingleLink(
result: ResultEpisode,
isCasting: Boolean,
text: UiText,
callback: (Pair<LinkLoadingResult, Int>) -> Unit,
) {
currentLoadLinkJob = viewModelScope.launch {
val links = loadLinks(result, isVisible = true, isCasting = isCasting)
_selectPopup.postValue(
SelectPopup.SelectText(
text,
links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) {
callback.invoke(links to (it ?: return@SelectText))
})
}
}
private suspend fun acquireSingleSubtitle(
result: ResultEpisode,
isCasting: Boolean,
text: UiText,
callback: (Pair<LinkLoadingResult, Int>) -> Unit,
) {
currentLoadLinkJob = viewModelScope.launch {
val links = loadLinks(result, isVisible = true, isCasting = isCasting)
_selectPopup.postValue(
SelectPopup.SelectText(
text,
links.subs.map { txt(it.name) }) {
callback.invoke(links to (it ?: return@SelectText))
})
}
}
suspend fun loadLinks(
result: ResultEpisode,
isVisible: Boolean,
isCasting: Boolean,
clearCache: Boolean = false,
): LinkLoadingResult {
val tempGenerator = RepoLinkGenerator(listOf(result))
val links: MutableSet<ExtractorLink> = mutableSetOf()
val subs: MutableSet<SubtitleData> = mutableSetOf()
fun updatePage() {
if (isVisible) {
_loadedLinks.postValue(LinkProgress(links.size, subs.size))
}
}
try {
tempGenerator.generateLinks(clearCache, isCasting, { (link, _) ->
if (link != null) {
links += link
updatePage()
}
}, { sub ->
subs += sub
updatePage()
})
} catch (e: Exception) {
logError(e)
} finally {
_loadedLinks.postValue(null)
}
return LinkLoadingResult(sortUrls(links), sortSubs(subs))
}
private fun playWithVlc(act: Activity?, data: LinkLoadingResult, id: Int) = ioSafe {
if (act == null) return@ioSafe
try {
if (!act.checkWrite()) {
act.requestRW()
if (act.checkWrite()) return@ioSafe
}
val outputDir = act.cacheDir
val outputFile = withContext(Dispatchers.IO) {
File.createTempFile("mirrorlist", ".m3u8", outputDir)
}
var text = "#EXTM3U"
for (sub in data.subs) {
text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${sub.name}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.name}\",URI=\"${sub.url}\""
}
for (link in data.links) {
text += "\n#EXTINF:, ${link.name}\n${link.url}"
}
outputFile.writeText(text)
val vlcIntent = Intent(VLC_INTENT_ACTION_RESULT)
vlcIntent.setPackage(VLC_PACKAGE)
vlcIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
vlcIntent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
vlcIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
vlcIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
vlcIntent.setDataAndType(
FileProvider.getUriForFile(
act,
act.applicationContext.packageName + ".provider",
outputFile
), "video/*"
)
val startId = VLC_FROM_PROGRESS
var position = startId
if (startId == VLC_FROM_START) {
position = 1
} else if (startId == VLC_FROM_PROGRESS) {
position = 0
}
vlcIntent.putExtra("position", position)
vlcIntent.component = VLC_COMPONENT
act.setKey(VLC_LAST_ID_KEY, id)
act.startActivityForResult(vlcIntent, VLC_REQUEST_CODE)
} catch (e: Exception) {
logError(e)
CommonActivity.showToast(act, e.toString(), Toast.LENGTH_LONG)
}
}
fun handleAction(activity: Activity?, click: EpisodeClickEvent) = viewModelScope.launch {
handleEpisodeClickEvent(activity, click)
}
private suspend fun handleEpisodeClickEvent(activity: Activity?, click: EpisodeClickEvent) {
when (click.action) {
ACTION_SHOW_OPTIONS -> {
_selectPopup.postValue(
SelectPopup.SelectArray(
txt(""), // TODO FIX
R.array.episode_long_click_options,
R.array.episode_long_click_options_values
) { result ->
if (result == null) return@SelectArray
viewModelScope.launch {
handleEpisodeClickEvent(
activity,
click.copy(action = result)
)
}
})
}
ACTION_CLICK_DEFAULT -> {
activity?.let { ctx ->
if (ctx.isConnectedToChromecast()) {
handleEpisodeClickEvent(
activity,
click.copy(action = ACTION_CHROME_CAST_EPISODE)
)
} else {
handleEpisodeClickEvent(
activity,
click.copy(action = ACTION_PLAY_EPISODE_IN_PLAYER)
)
}
}
}
ACTION_DOWNLOAD_EPISODE_SUBTITLE -> {
val response = currentResponse ?: return
acquireSingleSubtitle(
click.data,
false,
txt(R.string.episode_action_download_subtitle)
) { (links, index) ->
downloadSubtitle(
activity,
links.subs[index],
getMeta(
click.data,
response.name,
response.apiName,
response.posterUrl,
response.isMovie(),
response.type
)
)
CommonActivity.showToast(
activity,
R.string.download_started,
Toast.LENGTH_SHORT
)
}
}
ACTION_SHOW_TOAST -> {
CommonActivity.showToast(activity, R.string.play_episode_toast, Toast.LENGTH_SHORT)
}
ACTION_DOWNLOAD_EPISODE -> {
val response = currentResponse ?: return
downloadEpisode(
activity,
click.data,
response.isMovie(),
response.name,
response.type,
response.posterUrl,
response.apiName,
response.getId(),
response.url
)
}
ACTION_DOWNLOAD_MIRROR -> {
val response = currentResponse ?: return
acquireSingleLink(
click.data,
false,
txt(R.string.episode_action_download_mirror)
) { (result, index) ->
startDownload(
activity,
click.data,
response.isMovie(),
response.name,
response.type,
response.posterUrl,
response.apiName,
response.getId(),
response.url,
listOf(result.links[index]),
result.subs,
)
CommonActivity.showToast(
activity,
R.string.download_started,
Toast.LENGTH_SHORT
)
}
}
ACTION_RELOAD_EPISODE -> {
loadLinks(click.data, isVisible = false, isCasting = false, clearCache = true)
}
ACTION_CHROME_CAST_MIRROR -> {
acquireSingleLink(
click.data,
false,
txt(R.string.episode_action_chromecast_mirror)
) { (result, index) ->
startChromecast(activity, click.data, result.links, result.subs, index)
}
}
ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink(
click.data,
false,
txt(R.string.episode_action_play_in_browser)
) { (result, index) ->
try {
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse(result.links[index].url)
activity?.startActivity(i)
} catch (e: Exception) {
logError(e)
}
}
ACTION_COPY_LINK -> {
acquireSingleLink(
click.data,
false,
txt(R.string.episode_action_copy_link)
) { (result, index) ->
val act = activity ?: return@acquireSingleLink
val serviceClipboard =
(act.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)
?: return@acquireSingleLink
val link = result.links[index]
val clip = ClipData.newPlainText(link.name, link.url)
serviceClipboard.setPrimaryClip(clip)
CommonActivity.showToast(act, R.string.copy_link_toast, Toast.LENGTH_SHORT)
}
}
ACTION_CHROME_CAST_EPISODE -> {
startChromecast(activity, click.data)
}
ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> {
currentLoadLinkJob = viewModelScope.launch {
playWithVlc(activity, loadLinks(click.data, true, true), click.data.id)
}
}
ACTION_PLAY_EPISODE_IN_PLAYER -> {
val data = currentResponse?.syncData?.toList() ?: emptyList()
val list =
HashMap<String, String>().apply { putAll(data) }
activity?.navigate(
R.id.global_to_navigation_player,
GeneratorPlayer.newInstance(
generator?.also {
it.getAll() // I know kinda shit to itterate all, but it is 100% sure to work
?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id }
?.let { index ->
if (index > 0)
it.goto(index)
}
} ?: return, list
)
)
}
}
} }
private suspend fun applyMeta( private suspend fun applyMeta(
@ -385,7 +1065,7 @@ class ResultViewModel2 : ViewModel() {
if (meta == null) return resp to false if (meta == null) return resp to false
var updateEpisodes = false var updateEpisodes = false
val out = resp.apply { val out = resp.apply {
Log.i(ResultViewModel.TAG, "applyMeta") Log.i(TAG, "applyMeta")
duration = duration ?: meta.duration duration = duration ?: meta.duration
rating = rating ?: meta.publicScore rating = rating ?: meta.publicScore
@ -497,8 +1177,6 @@ class ResultViewModel2 : ViewModel() {
} }
private fun getEpisodes(indexer: EpisodeIndexer, range: EpisodeRange): List<ResultEpisode> { private fun getEpisodes(indexer: EpisodeIndexer, range: EpisodeRange): List<ResultEpisode> {
//TODO ADD GENERATOR
val startIndex = range.startIndex val startIndex = range.startIndex
val length = range.length val length = range.length
@ -535,7 +1213,7 @@ class ResultViewModel2 : ViewModel() {
_episodesCountText.postValue( _episodesCountText.postValue(
txt( txt(
R.string.episode_format, R.string.episode_format,
if (size == 1) R.string.episode else R.string.episodes, txt(if (size == 1) R.string.episode else R.string.episodes),
size size
) )
) )
@ -563,6 +1241,10 @@ class ResultViewModel2 : ViewModel() {
preferStartSeason = indexer.season preferStartSeason = indexer.season
preferDubStatus = indexer.dubStatus preferDubStatus = indexer.dubStatus
generator = currentEpisodes[indexer]?.let { list ->
RepoLinkGenerator(list)
}
val ret = getEpisodes(indexer, range) val ret = getEpisodes(indexer, range)
_episodes.postValue(Resource.Success(ret)) _episodes.postValue(Resource.Success(ret))
} }

View file

@ -44,9 +44,13 @@ class SyncViewModel : ViewModel() {
// prefix, id // prefix, id
private var syncs = mutableMapOf<String, String>() private var syncs = mutableMapOf<String, String>()
private val _syncIds: MutableLiveData<MutableMap<String, String>> = //private val _syncIds: MutableLiveData<MutableMap<String, String>> =
MutableLiveData(mutableMapOf()) // MutableLiveData(mutableMapOf())
val syncIds: LiveData<MutableMap<String, String>> get() = _syncIds //val syncIds: LiveData<MutableMap<String, String>> get() = _syncIds
fun getSyncs() : Map<String,String> {
return syncs
}
private val _currentSynced: MutableLiveData<List<CurrentSynced>> = private val _currentSynced: MutableLiveData<List<CurrentSynced>> =
MutableLiveData(getMissing()) MutableLiveData(getMissing())
@ -76,7 +80,7 @@ class SyncViewModel : ViewModel() {
Log.i(TAG, "addSync $idPrefix = $id") Log.i(TAG, "addSync $idPrefix = $id")
syncs[idPrefix] = id syncs[idPrefix] = id
_syncIds.postValue(syncs) //_syncIds.postValue(syncs)
return true return true
} }

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.ui.result package com.lagradost.cloudstream3.ui.result
import android.content.Context import android.content.Context
import android.util.Log
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
@ -12,16 +13,25 @@ import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
sealed class UiText { sealed class UiText {
data class DynamicString(val value: String) : UiText() companion object {
const val TAG = "UiText"
}
data class DynamicString(val value: String) : UiText() {
override fun toString(): String = value
}
class StringResource( class StringResource(
@StringRes val resId: Int, @StringRes val resId: Int,
vararg val args: Any val args: List<Any>
) : UiText() ) : UiText() {
override fun toString(): String = "resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}"
}
fun asStringNull(context: Context?): String? { fun asStringNull(context: Context?): String? {
try { try {
return asString(context ?: return null) return asString(context ?: return null)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Got invalid data from $this")
logError(e) logError(e)
return null return null
} }
@ -30,7 +40,19 @@ sealed class UiText {
fun asString(context: Context): String { fun asString(context: Context): String {
return when (this) { return when (this) {
is DynamicString -> value is DynamicString -> value
is StringResource -> context.getString(resId, *args) is StringResource -> {
val str = context.getString(resId)
if (args.isEmpty()) {
str
} else {
str.format(*args.map {
when (it) {
is UiText -> it.asString(context)
else -> it
}
}.toTypedArray())
}
}
} }
} }
} }
@ -98,7 +120,7 @@ fun txt(value: String?): UiText? {
} }
fun txt(@StringRes resId: Int, vararg args: Any): UiText { fun txt(@StringRes resId: Int, vararg args: Any): UiText {
return UiText.StringResource(resId, args) return UiText.StringResource(resId, args.toList())
} }
@JvmName("txtNull") @JvmName("txtNull")
@ -106,7 +128,7 @@ fun txt(@StringRes resId: Int?, vararg args: Any?): UiText? {
if (resId == null || args.any { it == null }) { if (resId == null || args.any { it == null }) {
return null return null
} }
return UiText.StringResource(resId, args) return UiText.StringResource(resId, args.filterNotNull().toList())
} }
fun TextView?.setText(text: UiText?) { fun TextView?.setText(text: UiText?) {

View file

@ -27,7 +27,7 @@ object SearchHelper {
} else { } else {
if (card.isFromDownload) { if (card.isFromDownload) {
handleDownloadClick( handleDownloadClick(
activity, card.name, DownloadClickEvent( activity, DownloadClickEvent(
DOWNLOAD_ACTION_PLAY_FILE, DOWNLOAD_ACTION_PLAY_FILE,
VideoDownloadHelper.DownloadEpisodeCached( VideoDownloadHelper.DownloadEpisodeCached(
card.name, card.name,