mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
compiling, not done 3
This commit is contained in:
parent
f57b12d89c
commit
64ea5e2f4b
11 changed files with 782 additions and 1372 deletions
|
@ -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)) }
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
|
}
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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?) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue