cloudstream/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt

2749 lines
101 KiB
Kotlin
Raw Normal View History

2022-08-01 01:00:48 +00:00
package com.lagradost.cloudstream3.ui.result
2022-08-02 00:43:42 +00:00
import android.app.Activity
import android.content.*
2022-08-02 00:43:42 +00:00
import android.net.Uri
2023-03-10 20:33:13 +00:00
import android.os.Build
2022-10-08 15:48:46 +00:00
import android.os.Bundle
2024-04-13 22:45:58 +00:00
import android.text.format.Formatter.formatFileSize
2022-08-01 02:46:43 +00:00
import android.util.Log
2022-08-02 00:43:42 +00:00
import android.widget.Toast
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
2022-08-02 00:43:42 +00:00
import androidx.core.content.FileProvider
2022-10-08 14:56:05 +00:00
import androidx.core.net.toUri
2022-08-01 01:00:48 +00:00
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.*
2023-01-28 22:38:02 +00:00
import com.lagradost.cloudstream3.APIHolder.apis
2022-08-01 01:00:48 +00:00
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
2022-10-08 20:29:17 +00:00
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.activity
2022-08-02 00:43:42 +00:00
import com.lagradost.cloudstream3.CommonActivity.getCastSession
2022-08-03 01:02:08 +00:00
import com.lagradost.cloudstream3.CommonActivity.showToast
2022-08-01 02:46:43 +00:00
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId
2022-08-02 00:43:42 +00:00
import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie
2022-08-01 01:00:48 +00:00
import com.lagradost.cloudstream3.metaproviders.SyncRedirector
import com.lagradost.cloudstream3.mvvm.*
2023-02-09 00:32:48 +00:00
import com.lagradost.cloudstream3.syncproviders.AccountManager
2024-03-09 14:24:38 +00:00
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable
2022-08-01 02:46:43 +00:00
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
2022-08-01 01:00:48 +00:00
import com.lagradost.cloudstream3.ui.APIRepository
2022-08-02 00:43:42 +00:00
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
2022-08-01 01:00:48 +00:00
import com.lagradost.cloudstream3.ui.player.IGenerator
import com.lagradost.cloudstream3.ui.player.LoadType
2022-08-02 00:43:42 +00:00
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction
2022-08-02 00:43:42 +00:00
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
2022-08-01 02:46:43 +00:00
import com.lagradost.cloudstream3.utils.*
2022-08-03 00:04:03 +00:00
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
2022-08-03 01:02:08 +00:00
import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled
2022-08-02 00:43:42 +00:00
import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
2022-08-02 00:43:42 +00:00
import com.lagradost.cloudstream3.utils.CastHelper.startCast
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
2022-08-04 01:19:59 +00:00
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
2022-08-18 00:54:05 +00:00
import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe
2022-08-07 23:03:54 +00:00
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
2022-08-04 01:19:59 +00:00
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
2023-10-13 23:02:12 +00:00
import com.lagradost.cloudstream3.utils.DataStoreHelper.getFavoritesData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched
2022-08-04 01:19:59 +00:00
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason
2022-08-03 00:04:03 +00:00
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState
2022-08-03 17:27:49 +00:00
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
2023-10-13 23:02:12 +00:00
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeFavoritesData
import com.lagradost.cloudstream3.utils.DataStoreHelper.removeSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setBookmarkedData
2022-08-04 01:19:59 +00:00
import com.lagradost.cloudstream3.utils.DataStoreHelper.setDub
2023-10-13 23:02:12 +00:00
import com.lagradost.cloudstream3.utils.DataStoreHelper.setFavoritesData
2022-08-04 01:19:59 +00:00
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultSeason
import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.setSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
2022-08-02 00:43:42 +00:00
import com.lagradost.cloudstream3.utils.UIHelper.navigate
2022-08-03 00:04:03 +00:00
import kotlinx.coroutines.*
2022-08-02 00:43:42 +00:00
import java.io.File
2022-08-01 01:00:48 +00:00
import java.util.concurrent.TimeUnit
/** This starts at 1 */
data class EpisodeRange(
// used to index data
val startIndex: Int,
val length: Int,
// used to display data
val startEpisode: Int,
val endEpisode: Int,
)
2022-08-04 01:19:59 +00:00
data class AutoResume(
val season: Int?,
val episode: Int?,
val id: Int?,
val startAction: Int,
)
2022-08-01 01:00:48 +00:00
data class ResultData(
val url: String,
val tags: List<String>,
val actors: List<ActorData>?,
val actorsText: UiText?,
val comingSoon: Boolean,
val backgroundPosterUrl: String?,
val title: String,
2022-08-02 00:43:42 +00:00
var syncData: Map<String, String>,
2022-08-01 01:00:48 +00:00
val posterImage: UiImage?,
val posterBackgroundImage: UiImage?,
2022-08-01 01:00:48 +00:00
val plotText: UiText,
val apiName: UiText,
val ratingText: UiText?,
val contentRatingText: UiText?,
2022-08-01 01:00:48 +00:00
val vpnText: UiText?,
val metaText: UiText?,
val durationText: UiText?,
val onGoingText: UiText?,
val noEpisodesFoundText: UiText?,
val titleText: UiText,
val typeText: UiText,
val yearText: UiText?,
val nextAiringDate: UiText?,
val nextAiringEpisode: UiText?,
2022-08-01 02:46:43 +00:00
val plotHeaderText: UiText,
2022-08-01 01:00:48 +00:00
)
data class CheckDuplicateData(
val name: String,
val year: Int?,
val syncData: Map<String, String>?
)
enum class LibraryListType {
BOOKMARKS,
FAVORITES,
SUBSCRIPTIONS
}
2022-08-01 02:46:43 +00:00
fun txt(status: DubStatus?): UiText? {
return txt(
when (status) {
DubStatus.Dubbed -> R.string.app_dubbed_text
DubStatus.Subbed -> R.string.app_subbed_text
else -> null
}
)
}
2022-08-01 01:00:48 +00:00
fun LoadResponse.toResultData(repo: APIRepository): ResultData {
2022-08-02 00:43:42 +00:00
debugAssert({ repo.name != apiName }) {
2022-08-01 01:00:48 +00:00
"Api returned wrong apiName"
}
val hasActorImages = actors?.firstOrNull()?.actor?.image?.isNotBlank() == true
var nextAiringEpisode: UiText? = null
var nextAiringDate: UiText? = null
if (this is EpisodeResponse) {
val airing = this.nextAiring
if (airing != null && airing.unixTime > unixTime) {
val seconds = airing.unixTime - unixTime
val days = TimeUnit.SECONDS.toDays(seconds)
val hours: Long = TimeUnit.SECONDS.toHours(seconds) - days * 24
val minute =
TimeUnit.SECONDS.toMinutes(seconds) - TimeUnit.SECONDS.toHours(seconds) * 60
2022-08-03 00:04:03 +00:00
nextAiringDate = when {
2022-08-01 01:00:48 +00:00
days > 0 -> {
txt(
R.string.next_episode_time_day_format,
days,
hours,
minute
)
}
2022-08-01 01:00:48 +00:00
hours > 0 -> txt(
R.string.next_episode_time_hour_format,
hours,
minute
)
2022-08-01 01:00:48 +00:00
minute > 0 -> txt(
R.string.next_episode_time_min_format,
minute
)
2022-08-01 01:00:48 +00:00
else -> null
}?.also {
2022-08-03 00:04:03 +00:00
nextAiringEpisode = txt(R.string.next_episode_format, airing.episode)
2022-08-01 01:00:48 +00:00
}
}
}
2022-08-03 00:04:03 +00:00
val dur = duration
2022-08-01 01:00:48 +00:00
return ResultData(
2022-08-02 00:43:42 +00:00
syncData = syncData,
2022-08-01 02:46:43 +00:00
plotHeaderText = txt(
when (this.type) {
TvType.Torrent -> R.string.torrent_plot
else -> R.string.result_plot
}
),
2022-08-01 01:00:48 +00:00
nextAiringDate = nextAiringDate,
nextAiringEpisode = nextAiringEpisode,
posterImage = img(
posterUrl, posterHeaders
) ?: img(R.drawable.default_cover),
posterBackgroundImage = img(
backgroundPosterUrl ?: posterUrl, posterHeaders
2022-08-01 01:00:48 +00:00
) ?: img(R.drawable.default_cover),
titleText = txt(name),
url = url,
tags = tags ?: emptyList(),
comingSoon = comingSoon,
actors = if (hasActorImages) actors else null,
2022-08-04 14:03:13 +00:00
actorsText = if (hasActorImages || actors.isNullOrEmpty()) null else txt(
2022-08-01 01:00:48 +00:00
R.string.cast_format,
actors?.joinToString { it.actor.name }),
plotText =
if (plot.isNullOrBlank()) txt(if (this is TorrentLoadResponse) R.string.torrent_no_plot else R.string.normal_no_plot) else txt(
plot!!
),
backgroundPosterUrl = backgroundPosterUrl,
title = name,
typeText = txt(
when (type) {
TvType.TvSeries -> R.string.tv_series_singular
TvType.Anime -> R.string.anime_singular
TvType.OVA -> R.string.ova_singular
TvType.AnimeMovie -> R.string.movies_singular
TvType.Cartoon -> R.string.cartoons_singular
TvType.Documentary -> R.string.documentaries_singular
TvType.Movie -> R.string.movies_singular
TvType.Torrent -> R.string.torrent_singular
TvType.AsianDrama -> R.string.asian_drama_singular
TvType.Live -> R.string.live_singular
2022-08-14 11:49:14 +00:00
TvType.Others -> R.string.other_singular
TvType.NSFW -> R.string.nsfw_singular
2024-03-25 00:38:39 +00:00
TvType.Music -> R.string.music_singlar
TvType.AudioBook -> R.string.audio_book_singular
TvType.CustomMedia -> R.string.custom_media_singluar
2022-08-01 01:00:48 +00:00
}
),
2022-08-03 00:04:03 +00:00
yearText = txt(year?.toString()),
2022-08-01 01:00:48 +00:00
apiName = txt(apiName),
2022-08-06 18:51:32 +00:00
ratingText = rating?.div(1000f)
?.let { if (it <= 0.1f) null else txt(R.string.rating_format, it) },
contentRatingText = txt(contentRating),
2022-08-01 01:00:48 +00:00
vpnText = txt(
when (repo.vpnStatus) {
VPNStatus.None -> null
VPNStatus.Torrent -> R.string.vpn_torrent
VPNStatus.MightBeNeeded -> R.string.vpn_might_be_needed
}
),
metaText =
if (repo.providerType == ProviderType.MetaProvider) txt(R.string.provider_info_meta) else null,
2022-08-03 00:04:03 +00:00
durationText = if (dur == null || dur <= 0) null else txt(
2024-03-09 14:24:38 +00:00
secondsToReadable(dur * 60, "0 mins")
2022-08-03 00:04:03 +00:00
),
2022-08-01 01:00:48 +00:00
onGoingText = if (this is EpisodeResponse) {
txt(
when (showStatus) {
ShowStatus.Ongoing -> R.string.status_ongoing
ShowStatus.Completed -> R.string.status_completed
else -> null
}
)
} else null,
noEpisodesFoundText =
if ((this is TvSeriesLoadResponse && this.episodes.isEmpty()) || (this is AnimeLoadResponse && !this.episodes.any { it.value.isNotEmpty() })) txt(
R.string.no_episodes_found
) else null
)
}
2022-08-02 00:43:42 +00:00
data class LinkProgress(
val linksLoaded: Int,
val subsLoaded: Int,
)
2022-08-03 17:27:49 +00:00
data class ResumeProgress(
val progress: Int,
val maxProgress: Int,
val progressLeft: UiText,
)
data class ResumeWatchingStatus(
val progress: ResumeProgress?,
val isMovie: Boolean,
val result: ResultEpisode,
)
2022-08-02 00:43:42 +00:00
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,
2022-08-03 01:02:08 +00:00
val options: List<Pair<UiText, Int>>,
2022-08-02 00:43:42 +00:00
val callback: (Int?) -> Unit
) : SelectPopup()
2022-08-03 00:04:03 +00:00
}
2022-08-02 00:43:42 +00:00
2022-08-03 01:02:08 +00:00
fun SelectPopup.callback(index: Int?) {
val ret = transformResult(index)
2022-08-03 00:04:03 +00:00
return when (this) {
is SelectPopup.SelectArray -> callback(ret)
is SelectPopup.SelectText -> callback(ret)
2022-08-02 00:43:42 +00:00
}
2022-08-03 00:04:03 +00:00
}
2022-08-02 00:43:42 +00:00
2022-08-03 01:02:08 +00:00
fun SelectPopup.transformResult(input: Int?): Int? {
2022-08-03 00:04:03 +00:00
if (input == null) return null
return when (this) {
2022-08-03 01:02:08 +00:00
is SelectPopup.SelectArray -> options.getOrNull(input)?.second
2022-08-03 00:04:03 +00:00
is SelectPopup.SelectText -> input
}
}
fun SelectPopup.getTitle(context: Context): String {
return when (this) {
is SelectPopup.SelectArray -> text.asString(context)
is SelectPopup.SelectText -> text.asString(context)
}
}
fun SelectPopup.getOptions(context: Context): List<String> {
return when (this) {
is SelectPopup.SelectArray -> {
2022-08-03 01:02:08 +00:00
this.options.map { it.first.asString(context) }
2022-08-02 00:43:42 +00:00
}
2022-08-03 00:04:03 +00:00
is SelectPopup.SelectText -> options.map { it.asString(context) }
2022-08-02 00:43:42 +00:00
}
}
2022-08-25 01:59:20 +00:00
data class ExtractedTrailerData(
var mirros: List<ExtractorLink>,
var subtitles: List<SubtitleFile> = emptyList(),
)
2022-08-01 01:00:48 +00:00
class ResultViewModel2 : ViewModel() {
private var currentResponse: LoadResponse? = null
var EPISODE_RANGE_SIZE: Int = 20
2023-01-21 22:22:48 +00:00
fun clear() {
currentResponse = null
_page.postValue(null)
}
2022-08-01 01:00:48 +00:00
data class EpisodeIndexer(
val dubStatus: DubStatus,
val season: Int,
)
/** map<dub, map<season, List<episode>>> */
private var currentEpisodes: Map<EpisodeIndexer, List<ResultEpisode>> = mapOf()
private var currentRanges: Map<EpisodeIndexer, List<EpisodeRange>> = mapOf()
2022-08-05 23:41:35 +00:00
private var currentSeasons: List<Int> = listOf()
private var currentDubStatus: List<DubStatus> = listOf()
2022-08-01 02:46:43 +00:00
private var currentMeta: SyncAPI.SyncResult? = null
private var currentSync: Map<String, String>? = null
2022-08-01 01:00:48 +00:00
private var currentIndex: EpisodeIndexer? = null
private var currentRange: EpisodeRange? = null
private var currentShowFillers: Boolean = false
2023-07-15 21:43:09 +00:00
var currentRepo: APIRepository? = null
2022-08-01 01:00:48 +00:00
private var currentId: Int? = null
private var fillers: Map<Int, Boolean> = emptyMap()
private var generator: IGenerator? = null
private var preferDubStatus: DubStatus? = null
private var preferStartEpisode: Int? = null
private var preferStartSeason: Int? = null
2022-08-02 00:43:42 +00:00
//private val currentIsMovie get() = currentResponse?.isEpisodeBased() == false
//private val currentHeaderName get() = currentResponse?.name
2022-08-01 01:00:48 +00:00
2023-01-21 22:22:48 +00:00
private val _page: MutableLiveData<Resource<ResultData>?> =
MutableLiveData(null)
val page: LiveData<Resource<ResultData>?> = _page
2022-08-01 01:00:48 +00:00
private val _episodes: MutableLiveData<Resource<List<ResultEpisode>>?> =
MutableLiveData(Resource.Loading())
val episodes: LiveData<Resource<List<ResultEpisode>>?> = _episodes
2022-08-03 00:04:03 +00:00
private val _movie: MutableLiveData<Resource<Pair<UiText, ResultEpisode>>?> =
MutableLiveData(null)
val movie: LiveData<Resource<Pair<UiText, ResultEpisode>>?> = _movie
2022-08-01 01:00:48 +00:00
private val _episodesCountText: MutableLiveData<UiText?> =
MutableLiveData(null)
val episodesCountText: LiveData<UiText?> = _episodesCountText
2022-08-01 01:00:48 +00:00
2022-08-25 01:59:20 +00:00
private val _trailers: MutableLiveData<List<ExtractedTrailerData>> =
MutableLiveData(mutableListOf())
val trailers: LiveData<List<ExtractedTrailerData>> = _trailers
2022-08-01 01:00:48 +00:00
2022-08-01 02:46:43 +00:00
private val _dubSubSelections: MutableLiveData<List<Pair<UiText?, DubStatus>>> =
MutableLiveData(emptyList())
val dubSubSelections: LiveData<List<Pair<UiText?, DubStatus>>> = _dubSubSelections
2022-08-02 00:43:42 +00:00
private val _rangeSelections: MutableLiveData<List<Pair<UiText?, EpisodeRange>>> =
MutableLiveData(emptyList())
2022-08-01 02:46:43 +00:00
val rangeSelections: LiveData<List<Pair<UiText?, EpisodeRange>>> = _rangeSelections
2022-08-02 00:43:42 +00:00
private val _seasonSelections: MutableLiveData<List<Pair<UiText?, Int>>> =
MutableLiveData(emptyList())
2022-08-01 02:46:43 +00:00
val seasonSelections: LiveData<List<Pair<UiText?, Int>>> = _seasonSelections
private val _recommendations: MutableLiveData<List<SearchResponse>> =
MutableLiveData(emptyList())
val recommendations: LiveData<List<SearchResponse>> = _recommendations
private val _selectedRange: MutableLiveData<UiText?> =
MutableLiveData(null)
val selectedRange: LiveData<UiText?> = _selectedRange
2022-08-01 02:46:43 +00:00
private val _selectedSeason: MutableLiveData<UiText?> =
MutableLiveData(null)
val selectedSeason: LiveData<UiText?> = _selectedSeason
2022-08-01 02:46:43 +00:00
private val _selectedDubStatus: MutableLiveData<UiText?> = MutableLiveData(null)
val selectedDubStatus: LiveData<UiText?> = _selectedDubStatus
2022-08-01 01:00:48 +00:00
2022-08-05 23:41:35 +00:00
private val _selectedRangeIndex: MutableLiveData<Int> =
MutableLiveData(-1)
val selectedRangeIndex: LiveData<Int> = _selectedRangeIndex
private val _selectedSeasonIndex: MutableLiveData<Int> =
MutableLiveData(-1)
val selectedSeasonIndex: LiveData<Int> = _selectedSeasonIndex
private val _selectedDubStatusIndex: MutableLiveData<Int> = MutableLiveData(-1)
val selectedDubStatusIndex: LiveData<Int> = _selectedDubStatusIndex
private val _loadedLinks: MutableLiveData<LinkProgress?> = MutableLiveData(null)
val loadedLinks: LiveData<LinkProgress?> = _loadedLinks
2022-08-02 00:43:42 +00:00
private val _resumeWatching: MutableLiveData<ResumeWatchingStatus?> =
MutableLiveData(null)
val resumeWatching: LiveData<ResumeWatchingStatus?> = _resumeWatching
2022-08-03 17:27:49 +00:00
private val _episodeSynopsis: MutableLiveData<String?> = MutableLiveData(null)
val episodeSynopsis: LiveData<String?> = _episodeSynopsis
private val _subscribeStatus: MutableLiveData<Boolean?> = MutableLiveData(null)
val subscribeStatus: LiveData<Boolean?> = _subscribeStatus
2023-10-13 23:02:12 +00:00
private val _favoriteStatus: MutableLiveData<Boolean?> = MutableLiveData(null)
val favoriteStatus: LiveData<Boolean?> = _favoriteStatus
2022-08-01 01:00:48 +00:00
companion object {
2022-08-01 02:46:43 +00:00
const val TAG = "RVM2"
//private const val EPISODE_RANGE_SIZE = 20
//private const val EPISODE_RANGE_OVERLOAD = 30
2022-08-01 01:00:48 +00:00
private fun List<SeasonData>?.getSeason(season: Int?): SeasonData? {
if (season == null) return null
return this?.firstOrNull { it.season == season }
}
2022-08-01 01:00:48 +00:00
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
}
fun singleMap(ep: ResultEpisode): Map<EpisodeIndexer, List<ResultEpisode>> =
mapOf(
EpisodeIndexer(DubStatus.None, 0) to listOf(
ep
)
)
private fun getRanges(
allEpisodes: Map<EpisodeIndexer, List<ResultEpisode>>,
EPISODE_RANGE_SIZE: Int
): Map<EpisodeIndexer, List<EpisodeRange>> {
2022-08-01 01:00:48 +00:00
return allEpisodes.keys.mapNotNull { index ->
val episodes =
allEpisodes[index] ?: return@mapNotNull null // this should never happened
// fast case
val EPISODE_RANGE_OVERLOAD = EPISODE_RANGE_SIZE + 10
2022-08-01 01:00:48 +00:00
if (episodes.size <= EPISODE_RANGE_OVERLOAD) {
return@mapNotNull index to listOf(
EpisodeRange(
0,
episodes.size,
episodes.minOf { it.episode },
episodes.maxOf { it.episode })
)
}
if (episodes.isEmpty()) {
return@mapNotNull null
}
val list = mutableListOf<EpisodeRange>()
val currentEpisode = episodes.first()
var currentIndex = 0
val maxIndex = episodes.size
var targetEpisode = 0
var currentMin = currentEpisode.episode
var currentMax = currentEpisode.episode
while (currentIndex < maxIndex) {
val startIndex = currentIndex
targetEpisode += EPISODE_RANGE_SIZE
while (currentIndex < maxIndex && episodes[currentIndex].episode <= targetEpisode) {
val episodeNumber = episodes[currentIndex].episode
if (episodeNumber < currentMin) {
currentMin = episodeNumber
}
if (episodeNumber > currentMax) {
2022-08-01 01:00:48 +00:00
currentMax = episodeNumber
}
++currentIndex
}
val length = currentIndex - startIndex
if (length <= 0) continue
list.add(
EpisodeRange(
startIndex,
length,
currentMin,
currentMax
)
)
2022-08-04 01:19:59 +00:00
currentMin = Int.MAX_VALUE
currentMax = Int.MIN_VALUE
2022-08-01 01:00:48 +00:00
}
/*var currentMin = Int.MAX_VALUE
var currentMax = Int.MIN_VALUE
var currentStartIndex = 0
var currentLength = 0
for (ep in episodes) {
val episodeNumber = ep.episode
if (episodeNumber < currentMin) {
currentMin = episodeNumber
} else if (episodeNumber > currentMax) {
currentMax = episodeNumber
}
if (++currentLength >= EPISODE_RANGE_SIZE) {
list.add(
EpisodeRange(
currentStartIndex,
currentLength,
currentMin,
currentMax
)
)
currentMin = Int.MAX_VALUE
currentMax = Int.MIN_VALUE
currentStartIndex += currentLength
currentLength = 0
}
}
if (currentLength > 0) {
list.add(
EpisodeRange(
currentStartIndex,
currentLength,
currentMin,
currentMax
)
)
}*/
index to list
}.toMap()
}
2022-08-02 00:43:42 +00:00
private fun downloadSubtitle(
context: Context?,
link: ExtractorSubtitleLink,
fileName: String,
folder: String
) {
ioSafe {
VideoDownloadManager.downloadThing(
context ?: return@ioSafe,
link,
"$fileName ${link.name}",
folder,
2023-08-24 19:17:42 +00:00
if (link.url.contains(".srt")) "srt" else "vtt",
2022-08-02 00:43:42 +00:00
false,
null, createNotificationCallback = {}
)
2022-08-02 00:43:42 +00:00
}
}
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"
TvType.NSFW -> "NSFW"
2022-08-14 11:49:14 +00:00
TvType.Others -> "Others"
2024-03-25 00:48:23 +00:00
TvType.Music -> "Music"
TvType.AudioBook -> "AudioBooks"
TvType.CustomMedia -> "Media"
2022-08-02 00:43:42 +00:00
}
}
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
2022-10-08 20:29:17 +00:00
setKey(
2022-08-02 00:43:42 +00:00
DOWNLOAD_HEADER_CACHE,
parentId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
apiName,
url,
currentType,
currentHeaderName,
currentPoster,
parentId,
System.currentTimeMillis(),
)
)
2022-10-08 20:29:17 +00:00
setKey(
2022-08-02 00:43:42 +00:00
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
)
)
}
2023-08-24 19:17:42 +00:00
.map { ExtractorSubtitleLink(it.name, it.url, "") }.take(3)
2022-08-02 00:43:42 +00:00
.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,
) {
2022-08-03 01:02:08 +00:00
ioSafe {
2022-08-02 00:43:42 +00:00
val generator = RepoLinkGenerator(listOf(episode))
val currentLinks = mutableSetOf<ExtractorLink>()
val currentSubs = mutableSetOf<SubtitleData>()
generator.generateLinks(clearCache = false, LoadType.Chromecast, callback = {
2022-08-02 00:43:42 +00:00
it.first?.let { link ->
currentLinks.add(link)
}
}, subtitleCallback = { sub ->
currentSubs.add(sub)
})
if (currentLinks.isEmpty()) {
2022-08-07 23:03:54 +00:00
main {
2022-08-03 17:27:49 +00:00
showToast(
2022-08-02 00:43:42 +00:00
R.string.no_links_found_toast,
Toast.LENGTH_SHORT
)
}
2022-08-03 01:02:08 +00:00
return@ioSafe
} else {
2022-08-07 23:03:54 +00:00
main {
2022-08-03 17:27:49 +00:00
showToast(
2022-08-03 01:02:08 +00:00
R.string.download_started,
Toast.LENGTH_SHORT
)
}
2022-08-02 00:43:42 +00:00
}
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?> = _selectPopup
2022-08-02 00:43:42 +00:00
fun updateWatchStatus(
status: WatchType,
context: Context?,
loadResponse: LoadResponse? = null,
statusChangedCallback: ((statusChanged: Boolean) -> Unit)? = null
) {
2023-12-20 23:07:39 +00:00
val (response,currentId) = loadResponse?.let { load ->
(load to load.getId())
} ?: ((currentResponse ?: return) to (currentId ?: return))
val currentStatus = getResultWatchState(currentId)
// If the current status is "NONE" and the new status is not "NONE",
// fetch the bookmarked data to check for duplicates, otherwise set this
// to an empty list, so that we don't show the duplicate warning dialog,
// but we still want to update the current bookmark and refresh the data anyway.
val bookmarkedData = if (currentStatus == WatchType.NONE && status != WatchType.NONE) {
getAllBookmarkedData()
} else emptyList()
checkAndWarnDuplicates(
context,
LibraryListType.BOOKMARKS,
CheckDuplicateData(
name = response.name,
year = response.year,
syncData = response.syncData,
),
bookmarkedData
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
if (!shouldContinue) return@checkAndWarnDuplicates
if (duplicateIds.isNotEmpty()) {
duplicateIds.forEach { duplicateId ->
deleteBookmarkedData(duplicateId)
}
}
setResultWatchState(currentId, status.internalId)
// We don't need to store if WatchType.NONE.
// The key is removed in setResultWatchState, we don't want to
// re-add it again here if it was just removed.
if (status != WatchType.NONE) {
val current = getBookmarkedData(currentId)
setBookmarkedData(
currentId,
DataStoreHelper.BookmarkedData(
current?.bookmarkedTime ?: unixTimeMS,
currentId,
unixTimeMS,
response.name,
response.url,
response.apiName,
response.type,
response.posterUrl,
response.year,
2023-12-20 23:07:39 +00:00
response.syncData,
plot = response.plot,
tags = response.tags,
rating = response.rating
)
)
}
if (currentStatus != status) {
MainActivity.bookmarksUpdatedEvent(true)
2023-12-20 23:07:39 +00:00
MainActivity.reloadLibraryEvent(true)
}
_watchStatus.postValue(status)
statusChangedCallback?.invoke(true)
}
2022-08-02 00:43:42 +00:00
}
2022-08-03 00:04:03 +00:00
private fun startChromecast(
2022-08-02 00:43:42 +00:00
activity: Activity?,
result: ResultEpisode,
isVisible: Boolean = true
) {
if (activity == null) return
loadLinks(result, isVisible = isVisible, LoadType.Chromecast) { data ->
2022-08-03 00:04:03 +00:00
startChromecast(activity, result, data.links, data.subs, 0)
}
2022-08-02 00:43:42 +00:00
}
/**
* Toggles the subscription status of an item.
*
* @param context The context to use for operations.
* @param statusChangedCallback A callback that is invoked when the subscription status changes.
* It provides the new subscription status (true if subscribed, false if unsubscribed, null if action was canceled).
*/
fun toggleSubscriptionStatus(
context: Context?,
statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null
) {
val isSubscribed = _subscribeStatus.value ?: return
val response = currentResponse ?: return
2023-12-20 23:07:39 +00:00
val currentId = currentId ?: return
// This might be a bit confusing, but even if the loadresponse is not a EpisodeResponse
// _subscribeStatus might be true.
if (isSubscribed) {
removeSubscribedData(currentId)
statusChangedCallback?.invoke(false)
_subscribeStatus.postValue(if (response is EpisodeResponse) false else null)
MainActivity.reloadLibraryEvent(true)
} else {
if (response !is EpisodeResponse) {
return
}
checkAndWarnDuplicates(
context,
LibraryListType.SUBSCRIPTIONS,
CheckDuplicateData(
name = response.name,
year = response.year,
syncData = response.syncData,
),
getAllSubscriptions(),
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
if (!shouldContinue) {
statusChangedCallback?.invoke(null)
return@checkAndWarnDuplicates
}
if (duplicateIds.isNotEmpty()) {
duplicateIds.forEach { duplicateId ->
removeSubscribedData(duplicateId)
}
}
val current = getSubscribedData(currentId)
setSubscribedData(
currentId,
DataStoreHelper.SubscribedData(
current?.subscribedTime ?: unixTimeMS,
response.getLatestEpisodes(),
currentId,
unixTimeMS,
response.name,
response.url,
response.apiName,
response.type,
response.posterUrl,
response.year,
2023-12-20 23:07:39 +00:00
response.syncData,
plot = response.plot,
rating = response.rating,
tags = response.tags
)
)
_subscribeStatus.postValue(true)
statusChangedCallback?.invoke(true)
MainActivity.reloadLibraryEvent(true)
}
}
}
2023-10-13 23:02:12 +00:00
/**
* Toggles the favorite status of an item.
*
* @param context The context to use.
* @param statusChangedCallback A callback that is invoked when the favorite status changes.
* It provides the new favorite status (true if added to favorites, false if removed, null if action was canceled).
*/
fun toggleFavoriteStatus(
context: Context?,
statusChangedCallback: ((newStatus: Boolean?) -> Unit)? = null
) {
val isFavorite = _favoriteStatus.value ?: return
val response = currentResponse ?: return
2023-10-13 23:02:12 +00:00
2023-12-20 23:07:39 +00:00
val currentId = currentId ?: return
2023-10-13 23:02:12 +00:00
if (isFavorite) {
removeFavoritesData(currentId)
statusChangedCallback?.invoke(false)
_favoriteStatus.postValue(false)
2024-01-10 18:10:34 +00:00
MainActivity.reloadLibraryEvent(true)
2023-10-13 23:02:12 +00:00
} else {
checkAndWarnDuplicates(
context,
LibraryListType.FAVORITES,
CheckDuplicateData(
name = response.name,
year = response.year,
syncData = response.syncData,
),
getAllFavorites(),
) { shouldContinue: Boolean, duplicateIds: List<Int?> ->
if (!shouldContinue) {
statusChangedCallback?.invoke(null)
return@checkAndWarnDuplicates
}
if (duplicateIds.isNotEmpty()) {
duplicateIds.forEach { duplicateId ->
removeFavoritesData(duplicateId)
}
}
2023-10-13 23:02:12 +00:00
val current = getFavoritesData(currentId)
setFavoritesData(
2023-10-13 23:02:12 +00:00
currentId,
DataStoreHelper.FavoritesData(
current?.favoritesTime ?: unixTimeMS,
currentId,
unixTimeMS,
response.name,
response.url,
response.apiName,
response.type,
response.posterUrl,
response.year,
2023-12-20 23:07:39 +00:00
response.syncData,
plot = response.plot,
rating = response.rating,
tags = response.tags
)
2023-10-13 23:02:12 +00:00
)
_favoriteStatus.postValue(true)
statusChangedCallback?.invoke(true)
2024-01-10 18:10:34 +00:00
MainActivity.reloadLibraryEvent(true)
}
}
}
@MainThread
private fun checkAndWarnDuplicates(
context: Context?,
listType: LibraryListType,
checkDuplicateData: CheckDuplicateData,
data: List<DataStoreHelper.LibrarySearchResponse>,
checkDuplicatesCallback: (shouldContinue: Boolean, duplicateIds: List<Int?>) -> Unit
) {
val whitespaceRegex = "\\s+".toRegex()
fun normalizeString(input: String): String {
/**
* Trim the input string and replace consecutive spaces with a single space.
* This covers some edge-cases where the title does not match exactly across providers,
* and one provider has the title with an extra whitespace. This is minor enough that
* it should still match in this case.
*/
return input.trim().replace(whitespaceRegex, " ")
}
val syncData = checkDuplicateData.syncData
val imdbId = getImdbIdFromSyncData(syncData)
val tmdbId = getTMDbIdFromSyncData(syncData)
val malId = syncData?.get(AccountManager.malApi.idPrefix)
val aniListId = syncData?.get(AccountManager.aniListApi.idPrefix)
val normalizedName = normalizeString(checkDuplicateData.name)
val year = checkDuplicateData.year
val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse ->
val librarySyncData = it.syncData
val checks = listOf(
{ imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId },
{ tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId },
{ malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId },
{ aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId },
{ normalizedName == normalizeString(it.name) && year == it.year }
)
checks.any { it() }
}
if (duplicateEntries.isEmpty() || context == null) {
checkDuplicatesCallback.invoke(true, emptyList())
return
}
val replaceMessage = if (duplicateEntries.size > 1) {
R.string.duplicate_replace_all
} else R.string.duplicate_replace
val message = if (duplicateEntries.size == 1) {
val list = when (listType) {
LibraryListType.BOOKMARKS -> getResultWatchState(duplicateEntries[0].id ?: 0).stringRes
LibraryListType.FAVORITES -> R.string.favorites_list_name
LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name
}
context.getString(R.string.duplicate_message_single,
"${normalizeString(duplicateEntries[0].name)} (${context.getString(list)}) — ${duplicateEntries[0].apiName}"
2023-10-13 23:02:12 +00:00
)
} else {
val bulletPoints = duplicateEntries.joinToString("\n") {
val list = when (listType) {
LibraryListType.BOOKMARKS -> getResultWatchState(it.id ?: 0).stringRes
LibraryListType.FAVORITES -> R.string.favorites_list_name
LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name
}
"${it.apiName}: ${normalizeString(it.name)} (${context.getString(list)})"
}
context.getString(R.string.duplicate_message_multiple, bulletPoints)
2023-10-13 23:02:12 +00:00
}
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
val dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
checkDuplicatesCallback.invoke(true, emptyList())
}
DialogInterface.BUTTON_NEGATIVE -> {
checkDuplicatesCallback.invoke(false, emptyList())
}
DialogInterface.BUTTON_NEUTRAL -> {
checkDuplicatesCallback.invoke(true, duplicateEntries.map { it.id })
}
}
}
builder.setTitle(R.string.duplicate_title)
.setMessage(message)
.setPositiveButton(R.string.duplicate_add, dialogClickListener)
.setNegativeButton(R.string.duplicate_cancel, dialogClickListener)
.setNeutralButton(replaceMessage, dialogClickListener)
.show().setDefaultFocus()
}
private fun getImdbIdFromSyncData(syncData: Map<String, String>?): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(
syncData?.get(AccountManager.simklApi.idPrefix)
)[SimklApi.Companion.SyncServices.Imdb]
}
}
private fun getTMDbIdFromSyncData(syncData: Map<String, String>?): String? {
return normalSafeApiCall {
SimklApi.readIdFromString(
syncData?.get(AccountManager.simklApi.idPrefix)
)[SimklApi.Companion.SyncServices.Tmdb]
}
2023-10-13 23:02:12 +00:00
}
2022-08-02 00:43:42 +00:00
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
2022-08-08 12:37:46 +00:00
// Main needed because getCastSession needs to be on main thread
main {
activity.getCastSession()?.startCast(
response.apiName,
response.isMovie(),
response.name,
response.posterUrl,
result.index,
eps,
links,
subs,
startTime = result.getRealPosition(),
startIndex = startIndex
)
}
2022-08-02 00:43:42 +00:00
}
fun cancelLinks() {
2022-08-03 00:04:03 +00:00
currentLoadLinkJob?.cancel()
currentLoadLinkJob = null
_loadedLinks.postValue(null)
2022-08-03 00:04:03 +00:00
}
private fun postPopup(text: UiText, options: List<UiText>, callback: suspend (Int?) -> Unit) {
_selectPopup.postValue(
SelectPopup.SelectText(
2022-08-03 00:04:03 +00:00
text,
options
) { value ->
2022-08-18 00:54:05 +00:00
viewModelScope.launchSafe {
_selectPopup.postValue(null)
2022-08-03 00:04:03 +00:00
callback.invoke(value)
}
}
2022-08-03 00:04:03 +00:00
)
}
2022-08-03 01:02:08 +00:00
@JvmName("postPopupArray")
2022-08-03 00:04:03 +00:00
private fun postPopup(
text: UiText,
2022-08-03 01:02:08 +00:00
options: List<Pair<UiText, Int>>,
2022-08-03 00:04:03 +00:00
callback: suspend (Int?) -> Unit
) {
_selectPopup.postValue(
SelectPopup.SelectArray(
2022-08-03 00:04:03 +00:00
text,
options,
) { value ->
2022-08-18 00:54:05 +00:00
viewModelScope.launchSafe {
_selectPopup.postValue(null)
2022-08-03 00:04:03 +00:00
callback.invoke(value)
}
}
2022-08-03 00:04:03 +00:00
)
}
2022-08-18 00:54:05 +00:00
private fun loadLinks(
2022-08-03 00:04:03 +00:00
result: ResultEpisode,
isVisible: Boolean,
type: LoadType,
2022-08-03 00:04:03 +00:00
clearCache: Boolean = false,
work: suspend (CoroutineScope.(LinkLoadingResult) -> Unit)
) {
2022-08-02 00:43:42 +00:00
currentLoadLinkJob?.cancel()
2022-08-03 00:04:03 +00:00
currentLoadLinkJob = ioSafe {
val links = loadLinks(
result,
isVisible = isVisible,
type = type,
2022-08-03 00:04:03 +00:00
clearCache = clearCache
)
if (!this.isActive) return@ioSafe
work(links)
}
2022-08-02 00:43:42 +00:00
}
private var currentLoadLinkJob: Job? = null
2022-08-03 00:04:03 +00:00
private fun acquireSingleLink(
2022-08-02 00:43:42 +00:00
result: ResultEpisode,
type: LoadType,
2022-08-02 00:43:42 +00:00
text: UiText,
callback: (Pair<LinkLoadingResult, Int>) -> Unit,
) {
loadLinks(result, isVisible = true, type) { links ->
2024-04-13 22:45:58 +00:00
// Could not find a better way to do this
val context = AcraApplication.context
2022-08-03 00:04:03 +00:00
postPopup(
text,
2024-04-13 22:45:58 +00:00
links.links.apmap {
val size = it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: ""
txt("${it.name} ${Qualities.getStringByInt(it.quality)}$size")
}) {
2022-08-03 00:04:03 +00:00
callback.invoke(links to (it ?: return@postPopup))
}
2022-08-02 00:43:42 +00:00
}
}
2022-08-03 00:04:03 +00:00
private fun acquireSingleSubtitle(
2022-08-02 00:43:42 +00:00
result: ResultEpisode,
text: UiText,
callback: (Pair<LinkLoadingResult, Int>) -> Unit,
) {
loadLinks(result, isVisible = true, type = LoadType.Unknown) { links ->
2022-08-03 00:04:03 +00:00
postPopup(
text,
links.subs.map { txt(it.name) })
{
callback.invoke(links to (it ?: return@postPopup))
}
2022-08-02 00:43:42 +00:00
}
}
2022-08-18 00:54:05 +00:00
private suspend fun CoroutineScope.loadLinks(
2022-08-02 00:43:42 +00:00
result: ResultEpisode,
isVisible: Boolean,
type: LoadType,
2022-08-02 00:43:42 +00:00
clearCache: Boolean = false,
): LinkLoadingResult {
val tempGenerator = RepoLinkGenerator(listOf(result))
val links: MutableSet<ExtractorLink> = mutableSetOf()
val subs: MutableSet<SubtitleData> = mutableSetOf()
fun updatePage() {
2022-08-03 00:04:03 +00:00
if (isVisible && isActive) {
_loadedLinks.postValue(LinkProgress(links.size, subs.size))
2022-08-02 00:43:42 +00:00
}
}
try {
2022-08-03 00:04:03 +00:00
updatePage()
tempGenerator.generateLinks(clearCache, type, { (link, _) ->
2022-08-02 00:43:42 +00:00
if (link != null) {
links += link
updatePage()
}
}, { sub ->
subs += sub
updatePage()
})
} catch (e: Exception) {
logError(e)
} finally {
_loadedLinks.postValue(null)
2022-08-02 00:43:42 +00:00
}
return LinkLoadingResult(sortUrls(links), sortSubs(subs))
}
2022-10-08 15:48:46 +00:00
private fun launchActivity(
activity: Activity?,
2022-10-08 20:29:17 +00:00
resumeApp: ResultResume,
id: Int? = null,
work: suspend (Intent.(Activity) -> Unit)
2022-10-08 15:48:46 +00:00
): Job? {
val act = activity ?: return null
return CoroutineScope(Dispatchers.IO).launch {
try {
2022-10-08 20:29:17 +00:00
resumeApp.launch(id) {
work(act)
}
2022-10-08 15:48:46 +00:00
} catch (t: Throwable) {
logError(t)
main {
if (t is ActivityNotFoundException) {
showToast(txt(R.string.app_not_found_error), Toast.LENGTH_LONG)
2022-10-08 15:48:46 +00:00
} else {
showToast(t.toString(), Toast.LENGTH_LONG)
2022-10-08 15:48:46 +00:00
}
}
}
}
}
private fun playInWebVideo(
activity: Activity?,
link: ExtractorLink,
title: String?,
posterUrl: String?,
subtitles: List<SubtitleData>
2022-10-08 20:29:17 +00:00
) = launchActivity(activity, WEB_VIDEO) {
setDataAndType(Uri.parse(link.url), "video/*")
putExtra("subs", subtitles.map { it.url.toUri() }.toTypedArray())
title?.let { putExtra("title", title) }
posterUrl?.let { putExtra("poster", posterUrl) }
2022-10-08 15:48:46 +00:00
val headers = Bundle().apply {
if (link.referer.isNotBlank())
putString("Referer", link.referer)
putString("User-Agent", USER_AGENT)
for ((key, value) in link.headers) {
putString(key, value)
}
}
2022-10-08 20:29:17 +00:00
putExtra("android.media.intent.extra.HTTP_HEADERS", headers)
putExtra("secure_uri", true)
}
2022-10-08 15:48:46 +00:00
2022-10-08 20:29:17 +00:00
private fun playWithMpv(
activity: Activity?,
id: Int,
link: ExtractorLink,
subtitles: List<SubtitleData>,
resume: Boolean = true,
) = launchActivity(activity, MPV, id) {
putExtra("subs", subtitles.map { it.url.toUri() }.toTypedArray())
putExtra("subs.name", subtitles.map { it.name }.toTypedArray())
putExtra("subs.filename", subtitles.map { it.name }.toTypedArray())
setDataAndType(Uri.parse(link.url), "video/*")
component = MPV_COMPONENT
putExtra("secure_uri", true)
putExtra("return_result", true)
val position = getViewPos(id)?.position
if (resume && position != null)
putExtra("position", position.toInt())
2022-10-08 15:48:46 +00:00
}
2022-10-08 14:56:05 +00:00
// https://wiki.videolan.org/Android_Player_Intents/
private fun playWithVlc(
2022-10-08 15:48:46 +00:00
activity: Activity?,
2022-10-08 14:56:05 +00:00
data: LinkLoadingResult,
id: Int,
resume: Boolean = true,
// if it is only a single link then resume works correctly
2022-10-08 14:58:14 +00:00
singleFile: Boolean? = null
2022-10-08 20:29:17 +00:00
) = launchActivity(activity, VLC, id) { act ->
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
2022-10-08 15:48:46 +00:00
val outputDir = act.cacheDir
if (singleFile ?: (data.links.size == 1)) {
2022-10-08 20:29:17 +00:00
setDataAndType(data.links.first().url.toUri(), "video/*")
2022-10-08 15:48:46 +00:00
} else {
val outputFile = File.createTempFile("mirrorlist", ".m3u8", outputDir)
2022-08-02 00:43:42 +00:00
2022-10-08 15:48:46 +00:00
var text = "#EXTM3U"
2022-08-02 00:43:42 +00:00
2022-10-08 15:48:46 +00:00
// With subtitles it doesn't work for no reason :(
2022-10-08 14:56:05 +00:00
// 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}\""
// }
2022-10-08 15:48:46 +00:00
for (link in data.links) {
text += "\n#EXTINF:, ${link.name}\n${link.url}"
}
outputFile.writeText(text)
2022-10-08 20:29:17 +00:00
setDataAndType(
2022-10-08 15:48:46 +00:00
FileProvider.getUriForFile(
act,
act.applicationContext.packageName + ".provider",
outputFile
), "video/*"
)
}
2022-10-08 14:56:05 +00:00
2022-10-08 15:48:46 +00:00
val position = if (resume) {
getViewPos(id)?.position ?: 0L
} else {
1L
}
2022-08-02 00:43:42 +00:00
2023-03-10 20:33:13 +00:00
// Component no longer safe to use in A13 for VLC
// https://code.videolan.org/videolan/vlc-android/-/issues/2776
// This will likely need to be updated once VLC fixes their documentation.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
component = VLC_COMPONENT
}
2022-10-08 20:29:17 +00:00
putExtra("from_start", !resume)
putExtra("position", position)
2022-10-08 15:48:46 +00:00
}
2022-08-02 00:43:42 +00:00
2022-10-08 20:29:17 +00:00
fun handleAction(click: EpisodeClickEvent) =
2022-10-08 15:48:46 +00:00
viewModelScope.launchSafe {
handleEpisodeClickEvent(click)
2022-08-02 00:43:42 +00:00
}
2022-10-08 15:48:46 +00:00
data class ExternalApp(
val packageString: String,
val name: Int,
val action: Int,
)
private val apps = listOf(
ExternalApp(
VLC_PACKAGE,
R.string.player_settings_play_in_vlc,
ACTION_PLAY_EPISODE_IN_VLC_PLAYER
), ExternalApp(
WEB_VIDEO_CAST_PACKAGE,
R.string.player_settings_play_in_web,
ACTION_PLAY_EPISODE_IN_WEB_VIDEO
2022-10-08 20:29:17 +00:00
),
ExternalApp(
MPV_PACKAGE,
R.string.player_settings_play_in_mpv,
ACTION_PLAY_EPISODE_IN_MPV
2022-10-08 15:48:46 +00:00
)
)
2022-08-02 00:43:42 +00:00
fun releaseEpisodeSynopsis() {
_episodeSynopsis.postValue(null)
}
private suspend fun handleEpisodeClickEvent(click: EpisodeClickEvent) {
2022-08-02 00:43:42 +00:00
when (click.action) {
ACTION_SHOW_OPTIONS -> {
2022-08-03 01:02:08 +00:00
val options = mutableListOf<Pair<UiText, Int>>()
if (activity?.isConnectedToChromecast() == true) {
options.addAll(
listOf(
txt(R.string.episode_action_chromecast_episode) to ACTION_CHROME_CAST_EPISODE,
txt(R.string.episode_action_chromecast_mirror) to ACTION_CHROME_CAST_MIRROR,
)
)
}
options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER)
2022-10-08 15:48:46 +00:00
for (app in apps) {
if (activity?.isAppInstalled(app.packageString) == true) {
options.add(
txt(
R.string.episode_action_play_in_format,
txt(app.name)
) to app.action
)
}
2022-08-03 01:02:08 +00:00
}
2022-10-08 15:48:46 +00:00
2022-08-03 01:02:08 +00:00
options.addAll(
listOf(
txt(R.string.episode_action_play_in_browser) to ACTION_PLAY_EPISODE_IN_BROWSER,
txt(R.string.episode_action_copy_link) to ACTION_COPY_LINK,
txt(R.string.episode_action_auto_download) to ACTION_DOWNLOAD_EPISODE,
txt(R.string.episode_action_download_mirror) to ACTION_DOWNLOAD_MIRROR,
txt(R.string.episode_action_download_subtitle) to ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR,
txt(R.string.episode_action_reload_links) to ACTION_RELOAD_EPISODE,
)
)
// Do not add mark as watched on movies
if (!listOf(TvType.Movie, TvType.AnimeMovie).contains(click.data.tvType)) {
val isWatched =
getVideoWatchState(click.data.id) == VideoWatchState.Watched
val watchedText = if (isWatched) R.string.action_remove_from_watched
else R.string.action_mark_as_watched
options.add(txt(watchedText) to ACTION_MARK_AS_WATCHED)
}
2022-08-03 00:04:03 +00:00
postPopup(
txt(
activity?.getNameFull(
click.data.name,
click.data.episode,
click.data.season
) ?: ""
), // TODO FIX
2022-08-03 01:02:08 +00:00
options
2022-08-03 00:04:03 +00:00
) { result ->
handleEpisodeClickEvent(
click.copy(action = result ?: return@postPopup)
)
}
2022-08-02 00:43:42 +00:00
}
2022-08-02 00:43:42 +00:00
ACTION_CLICK_DEFAULT -> {
activity?.let { ctx ->
if (ctx.isConnectedToChromecast()) {
handleEpisodeClickEvent(
click.copy(action = ACTION_CHROME_CAST_EPISODE)
)
} else {
val action = getPlayerAction(ctx)
2022-08-02 00:43:42 +00:00
handleEpisodeClickEvent(
click.copy(action = action)
2022-08-02 00:43:42 +00:00
)
}
}
}
ACTION_SHOW_DESCRIPTION -> {
_episodeSynopsis.postValue(click.data.description)
}
2022-08-03 01:02:08 +00:00
/* not implemented, not used
2022-08-02 00:43:42 +00:00
ACTION_DOWNLOAD_EPISODE_SUBTITLE -> {
2022-08-03 01:02:08 +00:00
loadLinks(click.data, isVisible = false, isCasting = false) { links ->
downloadSubtitle(activity,links.subs,)
}
}*/
ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR -> {
2022-08-02 00:43:42 +00:00
val response = currentResponse ?: return
acquireSingleSubtitle(
click.data,
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
)
)
2022-08-03 17:27:49 +00:00
showToast(
2022-08-02 00:43:42 +00:00
R.string.download_started,
Toast.LENGTH_SHORT
)
}
}
2022-08-02 00:43:42 +00:00
ACTION_SHOW_TOAST -> {
showToast(R.string.play_episode_toast, Toast.LENGTH_SHORT)
2022-08-02 00:43:42 +00:00
}
2022-08-02 00:43:42 +00:00
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
)
}
2022-08-02 00:43:42 +00:00
ACTION_DOWNLOAD_MIRROR -> {
val response = currentResponse ?: return
acquireSingleLink(
click.data,
LoadType.InAppDownload,
2022-08-02 00:43:42 +00:00
txt(R.string.episode_action_download_mirror)
) { (result, index) ->
2022-08-04 01:19:59 +00:00
ioSafe {
startDownload(
activity,
click.data,
response.isMovie(),
response.name,
response.type,
response.posterUrl,
response.apiName,
response.getId(),
response.url,
listOf(result.links[index]),
result.subs,
)
}
2022-08-03 17:27:49 +00:00
showToast(
2022-08-02 00:43:42 +00:00
R.string.download_started,
Toast.LENGTH_SHORT
)
}
}
2022-08-02 00:43:42 +00:00
ACTION_RELOAD_EPISODE -> {
2022-08-03 00:04:03 +00:00
ioSafe {
loadLinks(
click.data,
isVisible = false,
type = LoadType.InApp,
2022-08-03 00:04:03 +00:00
clearCache = true
)
}
showToast(
R.string.links_reloaded_toast,
Toast.LENGTH_SHORT
)
2022-08-02 00:43:42 +00:00
}
2022-08-02 00:43:42 +00:00
ACTION_CHROME_CAST_MIRROR -> {
acquireSingleLink(
click.data,
LoadType.Chromecast,
2022-08-02 00:43:42 +00:00
txt(R.string.episode_action_chromecast_mirror)
) { (result, index) ->
startChromecast(activity, click.data, result.links, result.subs, index)
}
}
2022-08-02 00:43:42 +00:00
ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink(
click.data,
LoadType.Browser,
2022-08-02 00:43:42 +00:00
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)
}
}
2022-08-02 00:43:42 +00:00
ACTION_COPY_LINK -> {
acquireSingleLink(
click.data,
LoadType.ExternalApp,
2022-08-02 00:43:42 +00:00
txt(R.string.episode_action_copy_link)
) { (result, index) ->
val link = result.links[index]
clipboardHelper(txt(link.name), link.url)
2022-08-02 00:43:42 +00:00
}
}
2022-08-02 00:43:42 +00:00
ACTION_CHROME_CAST_EPISODE -> {
startChromecast(activity, click.data)
}
2022-08-02 00:43:42 +00:00
ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> {
loadLinks(click.data, isVisible = true, LoadType.ExternalApp) { links ->
2022-10-08 15:48:46 +00:00
if (links.links.isEmpty()) {
showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
2022-10-08 15:48:46 +00:00
return@loadLinks
}
2022-08-03 00:04:03 +00:00
playWithVlc(
activity,
links,
click.data.id
)
2022-08-02 00:43:42 +00:00
}
}
2022-10-08 15:48:46 +00:00
ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink(
click.data,
LoadType.Chromecast,
2022-10-08 15:48:46 +00:00
txt(
R.string.episode_action_play_in_format,
txt(R.string.player_settings_play_in_web)
)
) { (result, index) ->
playInWebVideo(
activity,
result.links[index],
click.data.name ?: click.data.headerName,
click.data.poster,
result.subs
)
}
2022-10-08 20:29:17 +00:00
ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink(
click.data,
LoadType.Chromecast,
2022-10-08 20:29:17 +00:00
txt(
R.string.episode_action_play_in_format,
txt(R.string.player_settings_play_in_mpv)
)
) { (result, index) ->
playWithMpv(
activity,
click.data.id,
result.links[index],
result.subs
)
}
2022-08-02 00:43:42 +00:00
ACTION_PLAY_EPISODE_IN_PLAYER -> {
val data = currentResponse?.syncData?.toList() ?: emptyList()
val list =
HashMap<String, String>().apply { putAll(data) }
2024-03-25 00:38:39 +00:00
generator?.also {
it.getAll() // I know kinda shit to iterate 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)
}
}
if (currentResponse?.type == TvType.CustomMedia) {
generator?.generateLinks(
clearCache = true,
LoadType.Unknown,
callback = {},
subtitleCallback = {})
} else {
activity?.navigate(
R.id.global_to_navigation_player,
GeneratorPlayer.newInstance(
generator ?: return, list
)
2022-08-02 00:43:42 +00:00
)
2024-03-25 00:38:39 +00:00
}
2022-08-02 00:43:42 +00:00
}
ACTION_MARK_AS_WATCHED -> {
val isWatched =
getVideoWatchState(click.data.id) == VideoWatchState.Watched
if (isWatched) {
setVideoWatchState(click.data.id, VideoWatchState.None)
} else {
setVideoWatchState(click.data.id, VideoWatchState.Watched)
}
// Kinda dirty to reload all episodes :(
reloadEpisodes()
}
2022-08-02 00:43:42 +00:00
}
2022-08-01 01:00:48 +00:00
}
2022-08-01 02:46:43 +00:00
private suspend fun applyMeta(
resp: LoadResponse,
meta: SyncAPI.SyncResult?,
syncs: Map<String, String>? = null
): Pair<LoadResponse, Boolean> {
2023-02-09 00:32:48 +00:00
//if (meta == null) return resp to false
2022-08-01 02:46:43 +00:00
var updateEpisodes = false
val out = resp.apply {
2022-08-02 00:43:42 +00:00
Log.i(TAG, "applyMeta")
2022-08-01 02:46:43 +00:00
2023-02-09 00:32:48 +00:00
if (meta != null) {
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
2022-08-01 02:46:43 +00:00
2023-02-09 00:32:48 +00:00
if (this is EpisodeResponse) {
nextAiring = nextAiring ?: meta.nextAiring
}
val realRecommendations = ArrayList<SearchResponse>()
val apiNames = synchronized(apis) {
apis.filter {
it.name.contains("gogoanime", true) ||
it.name.contains("9anime", true)
}.map {
it.name
}
2023-02-09 00:32:48 +00:00
}
meta.recommendations?.forEach { rec ->
apiNames.forEach { name ->
realRecommendations.add(rec.copy(apiName = name))
}
}
recommendations = recommendations?.union(realRecommendations)?.toList()
?: realRecommendations
2022-08-01 02:46:43 +00:00
}
for ((k, v) in syncs ?: emptyMap()) {
syncData[k] = v
}
2023-02-09 00:32:48 +00:00
argamap(
{
if (this !is AnimeLoadResponse) return@argamap
2023-02-09 00:46:07 +00:00
// already exist, no need to run getTracker
if (this.getAniListId() != null && this.getMalId() != null) return@argamap
2023-02-09 00:32:48 +00:00
val res = APIHolder.getTracker(
listOfNotNull(
this.engName,
this.name,
this.japName
).filter { it.length > 2 }
2023-09-11 12:29:30 +00:00
.distinct().map {
2023-12-20 23:07:39 +00:00
// this actually would be nice if we improved a bit as 3rd season == season 3 == III ect
2023-09-11 12:29:30 +00:00
// right now it just removes the dubbed status
it.lowercase().replace(Regex("""\(?[ds]ub(bed)?\)?(\s|$)""") , "").trim()
2023-12-20 23:07:39 +00:00
},
2023-02-09 00:46:07 +00:00
TrackerType.getTypes(this.type),
this.year
2023-02-09 00:32:48 +00:00
)
2022-08-01 02:46:43 +00:00
2023-02-09 00:32:48 +00:00
val ids = arrayOf(
AccountManager.malApi.idPrefix to res?.malId?.toString(),
AccountManager.aniListApi.idPrefix to res?.aniId
2022-10-08 15:48:46 +00:00
)
2023-02-09 00:32:48 +00:00
if (ids.any { (id, new) ->
val current = syncData[id]
new != null && current != null && current != new
}
) {
// getTracker fucked up as it conflicts with current implementation
return@argamap
}
// set all the new data, prioritise old correct data
ids.forEach { (id, new) ->
new?.let {
syncData[id] = syncData[id] ?: it
}
}
// set posters, might fuck up due to headers idk
posterUrl = posterUrl ?: res?.image
backgroundPosterUrl = backgroundPosterUrl ?: res?.cover
},
{
2023-02-09 00:46:07 +00:00
if (meta == null) return@argamap
2023-02-09 00:32:48 +00:00
addTrailer(meta.trailers)
}, {
if (this !is AnimeLoadResponse) return@argamap
val map =
Kitsu.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++
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
}
2022-08-01 02:46:43 +00:00
}
}
}
2023-02-09 00:32:48 +00:00
this.episodes[dubStatus] = current
updateCount > 0
}.any { it }
})
2022-08-01 02:46:43 +00:00
}
return out to updateEpisodes
}
2022-08-03 01:02:08 +00:00
fun setMeta(meta: SyncAPI.SyncResult, syncs: Map<String, String>?) {
// I dont want to update everything if the metadata is not relevant
if (currentMeta == meta && currentSync == syncs) {
Log.i(TAG, "setMeta same")
return
}
Log.i(TAG, "setMeta")
2022-08-18 00:54:05 +00:00
viewModelScope.launchSafe {
2022-08-01 02:46:43 +00:00
currentMeta = meta
currentSync = syncs
2022-08-04 01:19:59 +00:00
val (value, updateEpisodes) = ioWork {
2022-08-01 02:46:43 +00:00
currentResponse?.let { resp ->
return@ioWork applyMeta(resp, meta, syncs)
}
return@ioWork null to null
}
postSuccessful(
2022-08-18 00:54:05 +00:00
value ?: return@launchSafe,
2023-12-20 23:07:39 +00:00
currentId ?: return@launchSafe,
2022-08-18 00:54:05 +00:00
currentRepo ?: return@launchSafe,
updateEpisodes ?: return@launchSafe,
2022-08-01 02:46:43 +00:00
false
)
}
2022-08-03 01:02:08 +00:00
}
2022-08-01 02:46:43 +00:00
2022-08-01 01:00:48 +00:00
private suspend fun updateFillers(name: String) {
fillers =
2022-08-18 00:54:05 +00:00
ioWorkSafe {
FillerEpisodeCheck.getFillerEpisodes(name)
2022-08-01 01:00:48 +00:00
} ?: emptyMap()
}
fun changeDubStatus(status: DubStatus) {
postEpisodeRange(currentIndex?.copy(dubStatus = status), currentRange)
}
fun changeRange(range: EpisodeRange) {
postEpisodeRange(currentIndex, range)
}
2022-08-01 02:46:43 +00:00
fun changeSeason(season: Int) {
postEpisodeRange(currentIndex?.copy(season = season), currentRange)
}
2022-08-03 00:04:03 +00:00
private fun getMovie(): ResultEpisode? {
return currentEpisodes.entries.firstOrNull()?.value?.firstOrNull()?.let { ep ->
2022-08-03 17:27:49 +00:00
val posDur = getViewPos(ep.id)
2022-08-03 00:04:03 +00:00
ep.copy(position = posDur?.position ?: 0, duration = posDur?.duration ?: 0)
}
}
2022-08-01 01:00:48 +00:00
private fun getEpisodes(indexer: EpisodeIndexer, range: EpisodeRange): List<ResultEpisode> {
val startIndex = range.startIndex
val length = range.length
return currentEpisodes[indexer]
?.let { list ->
val start = minOf(list.size, startIndex)
val end = minOf(list.size, start + length)
list.subList(start, end).map {
2022-08-03 17:27:49 +00:00
val posDur = getViewPos(it.id)
val watchState =
getVideoWatchState(it.id) ?: VideoWatchState.None
it.copy(
position = posDur?.position ?: 0,
duration = posDur?.duration ?: 0,
videoWatchState = watchState
)
2022-08-01 01:00:48 +00:00
}
}
?: emptyList()
}
2022-08-03 00:04:03 +00:00
private fun postMovie() {
val response = currentResponse
_episodes.postValue(null)
2022-08-03 00:04:03 +00:00
if (response == null) {
_movie.postValue(null)
2022-08-03 00:04:03 +00:00
return
}
val text = txt(
when (response.type) {
TvType.Torrent -> R.string.play_torrent_button
else -> {
if (response.type.isLiveStream())
R.string.play_livestream_button
else if (response.isMovie()) // this wont break compatibility as you only need to override isMovieType
2022-08-03 00:04:03 +00:00
R.string.play_movie_button
else null
}
}
)
val data = getMovie()
_episodes.postValue(null)
2022-08-03 00:04:03 +00:00
if (text == null || data == null) {
_movie.postValue(null)
2022-08-03 00:04:03 +00:00
} else {
_movie.postValue(Resource.Success(text to data))
2022-08-03 00:04:03 +00:00
}
}
2022-08-01 01:00:48 +00:00
fun reloadEpisodes() {
2022-08-03 00:04:03 +00:00
if (currentResponse?.isMovie() == true) {
postMovie()
} else {
_episodes.postValue(
Resource.Success(
2022-08-03 00:04:03 +00:00
getEpisodes(
currentIndex ?: return,
currentRange ?: return
)
2022-08-01 01:00:48 +00:00
)
)
_movie.postValue(null)
2022-08-03 00:04:03 +00:00
}
2022-08-03 17:27:49 +00:00
postResume()
2022-08-01 01:00:48 +00:00
}
private fun postSubscription(loadResponse: LoadResponse) {
val id = loadResponse.getId()
val data = getSubscribedData(id)
if (loadResponse.isEpisodeBased()) {
updateSubscribedData(id, data, loadResponse as? EpisodeResponse)
_subscribeStatus.postValue(data != null)
}
// lets say that we have subscribed, then we must be able to unsubscribe no matter what
else if (data != null) {
_subscribeStatus.postValue(true)
}
}
2023-10-13 23:02:12 +00:00
private fun postFavorites(loadResponse: LoadResponse) {
2023-12-20 23:07:39 +00:00
val id = loadResponse.getId()
val isFavorite = getFavoritesData(id) != null
_favoriteStatus.postValue(isFavorite)
2023-10-13 23:02:12 +00:00
}
2022-08-01 01:00:48 +00:00
private fun postEpisodeRange(indexer: EpisodeIndexer?, range: EpisodeRange?) {
if (range == null || indexer == null) {
return
}
2022-08-03 00:04:03 +00:00
val ranges = currentRanges[indexer]
2022-08-05 23:41:35 +00:00
if (ranges?.contains(range) != true) {
// if the current ranges does not include the range then select the range with the closest matching start episode
// this usually happends when dub has less episodes then sub -> the range does not exist
2023-01-21 22:22:48 +00:00
ranges?.minByOrNull { kotlin.math.abs(it.startEpisode - range.startEpisode) }
?.let { r ->
postEpisodeRange(indexer, r)
return
}
2022-08-05 23:41:35 +00:00
}
2022-08-03 00:04:03 +00:00
val isMovie = currentResponse?.isMovie() == true
currentIndex = indexer
currentRange = range
_rangeSelections.postValue(ranges?.map { r ->
val text = txt(R.string.episodes_range, r.startEpisode, r.endEpisode)
text to r
} ?: emptyList())
2022-08-01 02:46:43 +00:00
val size = currentEpisodes[indexer]?.size
2022-08-01 02:46:43 +00:00
_episodesCountText.postValue(
if (isMovie) null else
txt(
R.string.episode_format,
size,
txt(if (size == 1) R.string.episode else R.string.episodes),
)
2022-08-01 02:46:43 +00:00
)
2022-08-05 23:41:35 +00:00
_selectedSeasonIndex.postValue(
currentSeasons.indexOf(indexer.season)
)
2022-08-01 02:46:43 +00:00
_selectedSeason.postValue(
if (isMovie || currentSeasons.size <= 1) null else
when (indexer.season) {
0 -> txt(R.string.no_season)
else -> {
val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames
val seasonData = seasonNames.getSeason(indexer.season)
// If displaySeason is null then only show the name!
if (seasonData?.name != null && seasonData.displaySeason == null) {
txt(seasonData.name)
} else {
val suffix = seasonData?.name?.let { " $it" } ?: ""
txt(
R.string.season_format,
txt(R.string.season),
seasonData?.displaySeason ?: indexer.season,
suffix
)
}
2022-08-03 00:04:03 +00:00
}
}
2022-08-01 02:46:43 +00:00
)
2022-08-03 00:04:03 +00:00
2022-08-05 23:41:35 +00:00
_selectedRangeIndex.postValue(
ranges?.indexOf(range) ?: -1
)
2022-08-01 02:46:43 +00:00
_selectedRange.postValue(
if (isMovie) null else if ((currentRanges[indexer]?.size ?: 0) > 1) {
txt(R.string.episodes_range, range.startEpisode, range.endEpisode)
} else {
null
}
2022-08-03 00:04:03 +00:00
)
2022-08-05 23:41:35 +00:00
_selectedDubStatusIndex.postValue(
currentDubStatus.indexOf(indexer.dubStatus)
)
2022-08-03 00:04:03 +00:00
_selectedDubStatus.postValue(
if (isMovie || currentDubStatus.size <= 1) null else
txt(indexer.dubStatus)
2022-08-01 02:46:43 +00:00
)
2022-08-04 01:19:59 +00:00
currentId?.let { id ->
setDub(id, indexer.dubStatus)
setResultSeason(id, indexer.season)
setResultEpisode(id, range.startEpisode)
}
2022-08-01 01:00:48 +00:00
preferStartEpisode = range.startEpisode
preferStartSeason = indexer.season
preferDubStatus = indexer.dubStatus
2022-08-03 00:04:03 +00:00
generator = if (isMovie) {
getMovie()?.let { RepoLinkGenerator(listOf(it), page = currentResponse) }
2022-08-03 00:04:03 +00:00
} else {
val episodes = currentEpisodes.filter { it.key.dubStatus == indexer.dubStatus }
.toList()
.sortedBy { it.first.season }
.flatMap { it.second }
RepoLinkGenerator(episodes, page = currentResponse)
2022-08-02 00:43:42 +00:00
}
2022-08-03 00:04:03 +00:00
if (isMovie) {
postMovie()
} else {
val ret = getEpisodes(indexer, range)
2022-08-05 23:41:35 +00:00
/*if (ret.isEmpty()) {
val index = ranges?.indexOf(range)
if(index != null && index > 0) {
}
}*/
_episodes.postValue(Resource.Success(ret))
2022-08-03 00:04:03 +00:00
}
2022-08-01 01:00:48 +00:00
}
private suspend fun postSuccessful(
loadResponse: LoadResponse,
2023-12-20 23:07:39 +00:00
mainId : Int,
2022-08-01 01:00:48 +00:00
apiRepository: APIRepository,
updateEpisodes: Boolean,
updateFillers: Boolean,
) {
2023-12-20 23:07:39 +00:00
currentId = mainId
2022-08-01 01:00:48 +00:00
currentResponse = loadResponse
postPage(loadResponse, apiRepository)
postSubscription(loadResponse)
2023-10-13 23:02:12 +00:00
postFavorites(loadResponse)
2023-12-20 23:07:39 +00:00
_watchStatus.postValue(getResultWatchState(mainId))
2022-08-01 01:00:48 +00:00
if (updateEpisodes)
2023-12-20 23:07:39 +00:00
postEpisodes(loadResponse, mainId, updateFillers)
2022-08-01 01:00:48 +00:00
}
2023-12-20 23:07:39 +00:00
private suspend fun postEpisodes(loadResponse: LoadResponse, mainId : Int, updateFillers: Boolean) {
_episodes.postValue(Resource.Loading())
2022-08-01 01:00:48 +00:00
if (updateFillers && loadResponse is AnimeLoadResponse) {
updateFillers(loadResponse.name)
}
val allEpisodes = when (loadResponse) {
is AnimeLoadResponse -> {
val existingEpisodes = HashSet<Int>()
val episodes: MutableMap<EpisodeIndexer, MutableList<ResultEpisode>> =
mutableMapOf()
loadResponse.episodes.map { ep ->
val idIndex = ep.key.id
for ((index, i) in ep.value.withIndex()) {
val episode = i.episode ?: (index + 1)
val id =
2022-10-08 15:48:46 +00:00
mainId + episode + idIndex * 1_000_000 + (i.season?.times(10_000)
?: 0)
val totalIndex =
i.season?.let { season -> loadResponse.getTotalEpisodeIndex(episode, season) }
2022-09-12 15:22:48 +00:00
if (!existingEpisodes.contains(id)) {
2022-08-01 01:00:48 +00:00
existingEpisodes.add(id)
val seasonData = loadResponse.seasonNames.getSeason(i.season)
2022-08-01 01:00:48 +00:00
val eps =
buildResultEpisode(
loadResponse.name,
filterName(i.name),
i.posterUrl,
episode,
2022-09-12 15:22:48 +00:00
seasonData?.season ?: i.season,
2022-09-12 14:00:27 +00:00
if (seasonData != null) seasonData.displaySeason else i.season,
2022-08-01 01:00:48 +00:00
i.data,
loadResponse.apiName,
id,
index,
i.rating,
i.description,
fillers.getOrDefault(episode, false),
loadResponse.type,
mainId,
totalIndex,
airDate = i.date
2022-08-01 01:00:48 +00:00
)
2022-09-12 15:22:48 +00:00
val season = eps.seasonIndex ?: 0
2022-08-01 01:00:48 +00:00
val indexer = EpisodeIndexer(ep.key, season)
episodes[indexer]?.add(eps) ?: run {
episodes[indexer] = mutableListOf(eps)
}
}
}
}
episodes
}
2022-08-01 01:00:48 +00:00
is TvSeriesLoadResponse -> {
val episodes: MutableMap<EpisodeIndexer, MutableList<ResultEpisode>> =
mutableMapOf()
val existingEpisodes = HashSet<Int>()
for ((index, episode) in loadResponse.episodes.sortedBy {
2022-09-12 15:22:48 +00:00
(it.season?.times(10_000) ?: 0) + (it.episode ?: 0)
2022-08-01 01:00:48 +00:00
}.withIndex()) {
val episodeIndex = episode.episode ?: (index + 1)
val id =
2022-09-12 15:22:48 +00:00
mainId + (episode.season?.times(100_000) ?: 0) + episodeIndex + 1
2022-08-01 01:00:48 +00:00
if (!existingEpisodes.contains(id)) {
existingEpisodes.add(id)
2022-09-12 14:00:27 +00:00
val seasonData =
loadResponse.seasonNames.getSeason(episode.season)
2022-08-01 01:00:48 +00:00
val totalIndex =
episode.season?.let { season -> loadResponse.getTotalEpisodeIndex(episodeIndex, season) }
2022-08-01 01:00:48 +00:00
val ep =
buildResultEpisode(
loadResponse.name,
filterName(episode.name),
episode.posterUrl,
episodeIndex,
2022-09-12 15:22:48 +00:00
seasonData?.season ?: episode.season,
2022-09-12 14:00:27 +00:00
if (seasonData != null) seasonData.displaySeason else episode.season,
2022-08-01 01:00:48 +00:00
episode.data,
loadResponse.apiName,
id,
index,
episode.rating,
episode.description,
null,
loadResponse.type,
mainId,
totalIndex,
airDate = episode.date
2022-08-01 01:00:48 +00:00
)
2022-09-12 15:22:48 +00:00
val season = ep.seasonIndex ?: 0
2022-08-01 01:00:48 +00:00
val indexer = EpisodeIndexer(DubStatus.None, season)
episodes[indexer]?.add(ep) ?: kotlin.run {
episodes[indexer] = mutableListOf(ep)
}
}
}
episodes
}
2022-08-01 01:00:48 +00:00
is MovieLoadResponse -> {
singleMap(
buildResultEpisode(
loadResponse.name,
loadResponse.name,
null,
0,
null,
null,
loadResponse.dataUrl,
loadResponse.apiName,
(mainId), // HAS SAME ID
0,
null,
null,
null,
loadResponse.type,
mainId,
null
2022-08-01 01:00:48 +00:00
)
)
}
2022-08-01 01:00:48 +00:00
is LiveStreamLoadResponse -> {
singleMap(
buildResultEpisode(
loadResponse.name,
loadResponse.name,
null,
0,
null,
null,
loadResponse.dataUrl,
loadResponse.apiName,
(mainId), // HAS SAME ID
0,
null,
null,
null,
loadResponse.type,
mainId,
null
2022-08-01 01:00:48 +00:00
)
)
}
2022-08-01 01:00:48 +00:00
is TorrentLoadResponse -> {
singleMap(
buildResultEpisode(
loadResponse.name,
loadResponse.name,
null,
0,
null,
null,
loadResponse.torrent ?: loadResponse.magnet ?: "",
loadResponse.apiName,
(mainId), // HAS SAME ID
0,
null,
null,
null,
loadResponse.type,
mainId,
null
2022-08-01 01:00:48 +00:00
)
)
}
2022-08-01 01:00:48 +00:00
else -> {
mapOf()
}
}
2022-08-03 00:04:03 +00:00
val seasonsSelection = mutableSetOf<Int>()
val dubSelection = mutableSetOf<DubStatus>()
allEpisodes.keys.forEach { key ->
seasonsSelection += key.season
dubSelection += key.dubStatus
}
2022-08-05 23:41:35 +00:00
currentDubStatus = dubSelection.toList()
currentSeasons = seasonsSelection.toList()
2022-08-03 00:04:03 +00:00
_dubSubSelections.postValue(dubSelection.map { txt(it) to it })
if (loadResponse is EpisodeResponse) {
_seasonSelections.postValue(seasonsSelection.map { seasonNumber ->
val seasonData = loadResponse.seasonNames.getSeason(seasonNumber)
val fixedSeasonNumber = seasonData?.displaySeason ?: seasonNumber
val suffix = seasonData?.name?.let { " $it" } ?: ""
2022-09-12 14:00:27 +00:00
// If displaySeason is null then only show the name!
val name = if (seasonData?.name != null && seasonData.displaySeason == null) {
txt(seasonData.name)
} else {
txt(
R.string.season_format,
txt(R.string.season),
fixedSeasonNumber,
suffix
)
}
2022-08-03 00:04:03 +00:00
name to seasonNumber
})
}
2022-08-01 01:00:48 +00:00
currentEpisodes = allEpisodes
val ranges = getRanges(allEpisodes, EPISODE_RANGE_SIZE)
2022-08-01 01:00:48 +00:00
currentRanges = ranges
2022-08-03 00:04:03 +00:00
2022-08-01 01:00:48 +00:00
// this takes the indexer most preferable by the user given the current sorting
val min = ranges.keys.minByOrNull { index ->
kotlin.math.abs(
2022-12-22 12:11:37 +00:00
index.season - (preferStartSeason ?: 1)
2022-08-01 01:00:48 +00:00
) + if (index.dubStatus == preferDubStatus) 0 else 100000
}
// this takes the range most preferable by the user given the current sorting
val ranger = ranges[min]
val range = ranger?.firstOrNull {
it.startEpisode >= (preferStartEpisode ?: 0)
} ?: ranger?.lastOrNull()
postEpisodeRange(min, range)
2022-08-03 17:27:49 +00:00
postResume()
}
private fun postResume() {
_resumeWatching.postValue(resume())
2022-08-01 01:00:48 +00:00
}
2022-08-03 17:27:49 +00:00
private fun resume(): ResumeWatchingStatus? {
val correctId = currentId ?: return null
val resume = getLastWatched(correctId)
2022-08-03 17:27:49 +00:00
val resumeParentId = resume?.parentId
if (resumeParentId != correctId) return null // is null or smth went wrong with getLastWatched
val resumeId = resume.episodeId ?: return null// invalid episode id
val response = currentResponse ?: return null
// kinda ugly ik
val episode =
currentEpisodes.values.flatten().firstOrNull { it.id == resumeId } ?: return null
val isMovie = response.isMovie()
val progress = getViewPos(resume.episodeId)?.let { viewPos ->
ResumeProgress(
progress = (viewPos.position / 1000).toInt(),
maxProgress = (viewPos.duration / 1000).toInt(),
2024-03-09 14:24:38 +00:00
txt(R.string.resume_remaining, secondsToReadable(((viewPos.duration - viewPos.position) / 1_000).toInt(), "0 mins"))
2022-08-03 17:27:49 +00:00
)
}
return ResumeWatchingStatus(progress = progress, isMovie = isMovie, result = episode)
}
2022-08-25 01:59:20 +00:00
private fun loadTrailers(loadResponse: LoadResponse) = ioSafe {
2022-09-12 14:00:27 +00:00
_trailers.postValue(
getTrailers(
loadResponse,
3
)
) // we dont want to fetch too many trailers
2022-08-25 01:59:20 +00:00
}
private suspend fun getTrailers(
loadResponse: LoadResponse,
limit: Int = 0
): List<ExtractedTrailerData> =
coroutineScope {
2022-12-09 19:06:23 +00:00
val returnlist = ArrayList<ExtractedTrailerData>()
loadResponse.trailers.windowed(limit, limit, true).takeWhile { list ->
list.amap { trailerData ->
try {
val links = arrayListOf<ExtractorLink>()
val subs = arrayListOf<SubtitleFile>()
if (!loadExtractor(
2022-08-25 01:59:20 +00:00
trailerData.extractorUrl,
2022-12-09 19:06:23 +00:00
trailerData.referer,
{ subs.add(it) },
{ links.add(it) }) && trailerData.raw
) {
arrayListOf(
ExtractorLink(
"",
"Trailer",
trailerData.extractorUrl,
trailerData.referer ?: "",
Qualities.Unknown.value,
type = INFER_TYPE
2022-12-09 19:06:23 +00:00
)
) to arrayListOf()
} else {
links to subs
2022-08-25 01:59:20 +00:00
}
2022-12-09 19:06:23 +00:00
} catch (e: Throwable) {
logError(e)
null
2022-08-25 01:59:20 +00:00
}
2022-12-09 19:06:23 +00:00
}.filterNotNull().map { (links, subs) -> ExtractedTrailerData(links, subs) }.let {
returnlist.addAll(it)
2022-08-25 01:59:20 +00:00
}
2022-12-09 19:06:23 +00:00
returnlist.size < limit
}
return@coroutineScope returnlist
2022-08-25 01:59:20 +00:00
}
2022-08-03 17:27:49 +00:00
2022-08-01 01:00:48 +00:00
// this instantly updates the metadata on the page
private fun postPage(loadResponse: LoadResponse, apiRepository: APIRepository) {
2022-08-01 02:46:43 +00:00
_recommendations.postValue(loadResponse.recommendations ?: emptyList())
2022-08-01 01:00:48 +00:00
_page.postValue(Resource.Success(loadResponse.toResultData(apiRepository)))
}
2022-08-01 02:46:43 +00:00
fun hasLoaded() = currentResponse != null
2022-08-04 01:19:59 +00:00
private fun handleAutoStart(activity: Activity?, autostart: AutoResume?) =
2022-08-18 00:54:05 +00:00
viewModelScope.launchSafe {
if (autostart == null || activity == null) return@launchSafe
2022-08-04 01:19:59 +00:00
when (autostart.startAction) {
START_ACTION_RESUME_LATEST -> {
currentEpisodes[currentIndex]?.let { currentRange ->
for (ep in currentRange) {
if (ep.getWatchProgress() > 0.9) continue
handleAction(
EpisodeClickEvent(
getPlayerAction(activity),
ep
)
2022-08-04 01:19:59 +00:00
)
break
}
}
}
2022-08-04 01:19:59 +00:00
START_ACTION_LOAD_EP -> {
val all = currentEpisodes.values.flatten()
val episode =
autostart.id?.let { id -> all.firstOrNull { it.id == id } }
?: autostart.episode?.let { ep ->
currentEpisodes[currentIndex]?.firstOrNull { it.episode == ep && it.season == autostart.episode }
?: all.firstOrNull { it.episode == ep && it.season == autostart.episode }
}
2022-08-18 00:54:05 +00:00
?: return@launchSafe
2022-08-04 01:19:59 +00:00
handleAction(
EpisodeClickEvent(
getPlayerAction(activity),
episode
)
2022-08-04 01:19:59 +00:00
)
}
}
}
2023-12-20 23:07:39 +00:00
data class LoadResponseFromSearch(
override var name: String,
override var url: String,
override var apiName: String,
override var type: TvType,
override var posterUrl: String?,
override var year: Int? = null,
override var plot: String? = null,
override var rating: Int? = null,
override var tags: List<String>? = null,
override var duration: Int? = null,
override var trailers: MutableList<TrailerData> = mutableListOf(),
override var recommendations: List<SearchResponse>? = null,
override var actors: List<ActorData>? = null,
override var comingSoon: Boolean = false,
override var syncData: MutableMap<String, String> = mutableMapOf(),
override var posterHeaders: Map<String, String>? = null,
override var backgroundPosterUrl: String? = null,
override var contentRating: String? = null,
val id : Int?,
2023-12-20 23:07:39 +00:00
) : LoadResponse
fun loadSmall(activity: Activity?, searchResponse : SearchResponse) = ioSafe {
val url = searchResponse.url
_page.postValue(Resource.Loading(url))
_episodes.postValue(Resource.Loading())
val api = APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull(searchResponse.url) ?: APIRepository.noneApi
val repo = APIRepository(api)
val response = LoadResponseFromSearch(name = searchResponse.name, url = searchResponse.url, apiName = api.name, type = searchResponse.type ?: TvType.Others,
posterUrl = searchResponse.posterUrl, id = searchResponse.id).apply {
2023-12-20 23:07:39 +00:00
if (searchResponse is SyncAPI.LibraryItem) {
this.plot = searchResponse.plot
this.rating = searchResponse.personalRating?.times(100) ?: searchResponse.rating
this.tags = searchResponse.tags
}
2024-01-10 18:10:34 +00:00
if (searchResponse is DataStoreHelper.BookmarkedData) {
this.plot = searchResponse.plot
this.rating = searchResponse.rating
this.tags = searchResponse.tags
}
2023-12-20 23:07:39 +00:00
}
val mainId = response.getId()
2023-12-20 23:07:39 +00:00
postSuccessful(
loadResponse = response,
mainId = mainId,
apiRepository = repo,
updateEpisodes = false,
updateFillers = false)
2023-12-20 23:07:39 +00:00
}
2022-08-01 01:00:48 +00:00
fun load(
2022-08-04 01:19:59 +00:00
activity: Activity?,
2022-08-01 01:00:48 +00:00
url: String,
apiName: String,
showFillers: Boolean,
dubStatus: DubStatus,
2022-08-04 01:19:59 +00:00
autostart: AutoResume?,
2023-01-21 22:22:48 +00:00
loadTrailers: Boolean = true,
2022-08-01 01:00:48 +00:00
) =
2023-02-09 00:46:07 +00:00
ioSafe {
2022-08-01 01:00:48 +00:00
_page.postValue(Resource.Loading(url))
_episodes.postValue(Resource.Loading())
2022-08-01 01:00:48 +00:00
preferDubStatus = dubStatus
currentShowFillers = showFillers
// set api
val api = APIHolder.getApiFromNameNull(apiName) ?: APIHolder.getApiFromUrlNull(url)
if (api == null) {
_page.postValue(
Resource.Failure(
false,
null,
null,
"This provider does not exist"
)
)
2023-02-09 00:46:07 +00:00
return@ioSafe
2022-08-01 01:00:48 +00:00
}
// validate url
2022-08-18 00:54:05 +00:00
val validUrlResource = safeApiCall {
2022-08-01 01:00:48 +00:00
SyncRedirector.redirect(
url,
2023-01-28 22:38:02 +00:00
api
2022-08-01 01:00:48 +00:00
)
}
2023-02-09 00:46:07 +00:00
2022-08-01 01:00:48 +00:00
if (validUrlResource !is Resource.Success) {
if (validUrlResource is Resource.Failure) {
_page.postValue(validUrlResource)
}
2023-02-09 00:46:07 +00:00
return@ioSafe
2022-08-01 01:00:48 +00:00
}
2023-02-09 00:46:07 +00:00
2022-08-01 01:00:48 +00:00
val validUrl = validUrlResource.value
val repo = APIRepository(api)
currentRepo = repo
when (val data = repo.load(validUrl)) {
is Resource.Failure -> {
_page.postValue(data)
}
2022-08-01 01:00:48 +00:00
is Resource.Success -> {
2023-02-09 00:46:07 +00:00
if (!isActive) return@ioSafe
2022-08-04 01:19:59 +00:00
val loadResponse = ioWork {
2022-08-01 02:46:43 +00:00
applyMeta(data.value, currentMeta, currentSync).first
}
2023-02-09 00:46:07 +00:00
if (!isActive) return@ioSafe
2022-08-01 01:00:48 +00:00
val mainId = loadResponse.getId()
2022-08-04 01:19:59 +00:00
preferDubStatus = getDub(mainId) ?: preferDubStatus
preferStartEpisode = getResultEpisode(mainId)
2022-12-22 12:11:37 +00:00
preferStartSeason = getResultSeason(mainId) ?: 1
2022-08-04 01:19:59 +00:00
2022-10-08 20:29:17 +00:00
setKey(
2022-08-01 01:00:48 +00:00
DOWNLOAD_HEADER_CACHE,
mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached(
apiName,
validUrl,
loadResponse.type,
loadResponse.name,
loadResponse.posterUrl,
mainId,
System.currentTimeMillis(),
)
)
2023-01-21 22:22:48 +00:00
if (loadTrailers)
loadTrailers(data.value)
2022-08-01 01:00:48 +00:00
postSuccessful(
data.value,
2023-12-20 23:07:39 +00:00
mainId,
2022-08-01 01:00:48 +00:00
updateEpisodes = true,
updateFillers = showFillers,
apiRepository = repo
)
2023-02-09 00:46:07 +00:00
if (!isActive) return@ioSafe
2022-08-04 01:19:59 +00:00
handleAutoStart(activity, autostart)
2022-08-01 01:00:48 +00:00
}
2022-08-01 01:00:48 +00:00
is Resource.Loading -> {
debugException { "Invalid load result" }
}
}
}
}