switched from isM3u8 to ExtractorLinkType

This commit is contained in:
LagradOst 2023-09-03 23:32:43 +02:00
parent 9c991f2abd
commit 6211b02e85
17 changed files with 269 additions and 109 deletions

View file

@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.INFER_TYPE
import com.lagradost.cloudstream3.utils.extractorApis import com.lagradost.cloudstream3.utils.extractorApis
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor
@ -66,7 +67,7 @@ open class Pelisplus(val mainUrl: String) {
href, href,
page.url, page.url,
getQualityFromName(qual), getQualityFromName(qual),
element.attr("href").contains(".m3u8") type = INFER_TYPE
) )
) )
} }

View file

@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.argamap import com.lagradost.cloudstream3.argamap
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.INFER_TYPE
import com.lagradost.cloudstream3.utils.extractorApis import com.lagradost.cloudstream3.utils.extractorApis
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor
@ -70,7 +71,7 @@ class Vidstream(val mainUrl: String) {
href, href,
page.url, page.url,
getQualityFromName(qual), getQualityFromName(qual),
element.attr("href").contains(".m3u8") type = INFER_TYPE
) )
) )
} }

View file

@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.extractors.helper.NineAnimeHelper.encrypt
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.INFER_TYPE
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
class Vidstreamz : WcoStream() { class Vidstreamz : WcoStream() {
@ -126,8 +127,7 @@ open class WcoStream : ExtractorApi() {
if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server") if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server")
return response.parsed<Response>().data.media.sources.map { return response.parsed<Response>().data.media.sources.map {
ExtractorLink(name, it.file,it.file,host,Qualities.Unknown.value,it.file.contains(".m3u8")) ExtractorLink(name, it.file, it.file, host, Qualities.Unknown.value, type = INFER_TYPE)
} }
} }
} }

View file

@ -4,8 +4,8 @@ import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.INFER_TYPE
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import java.net.URI
open class Wibufile : ExtractorApi() { open class Wibufile : ExtractorApi() {
override val name: String = "Wibufile" override val name: String = "Wibufile"
@ -28,10 +28,8 @@ open class Wibufile : ExtractorApi() {
video ?: return, video ?: return,
"$mainUrl/", "$mainUrl/",
Qualities.Unknown.value, Qualities.Unknown.value,
URI(url).path.endsWith(".m3u8") type = INFER_TYPE
) )
) )
} }
} }

View file

@ -1,16 +1,24 @@
package com.lagradost.cloudstream3.ui package com.lagradost.cloudstream3.ui
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.HomePageResponse
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.MainPageRequest
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.fixUrl
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope.coroutineContext import kotlinx.coroutines.GlobalScope.coroutineContext
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -174,7 +182,7 @@ class APIRepository(val api: MainAPI) {
data: String, data: String,
isCasting: Boolean, isCasting: Boolean,
subtitleCallback: (SubtitleFile) -> Unit, subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit callback: (ExtractorLink) -> Unit,
): Boolean { ): Boolean {
if (isInvalidData(data)) return false // this makes providers cleaner if (isInvalidData(data)) return false // this makes providers cleaner
return try { return try {

View file

@ -25,6 +25,7 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.sortSubs import com.lagradost.cloudstream3.sortSubs
import com.lagradost.cloudstream3.sortUrls import com.lagradost.cloudstream3.sortUrls
import com.lagradost.cloudstream3.ui.player.LoadType
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.result.ResultEpisode
@ -294,7 +295,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
val generator = RepoLinkGenerator(listOf(epData)) val generator = RepoLinkGenerator(listOf(epData))
val isSuccessful = safeApiCall { val isSuccessful = safeApiCall {
generator.generateLinks(clearCache = false, isCasting = true, generator.generateLinks(
clearCache = false, type = LoadType.Chromecast,
callback = { callback = {
it.first?.let { link -> it.first?.let { link ->
currentLinks.add(link) currentLinks.add(link)

View file

@ -53,9 +53,11 @@ import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.ExtractorUri
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import java.io.File import java.io.File
import java.lang.IllegalArgumentException
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSession import javax.net.ssl.SSLSession
@ -1257,10 +1259,12 @@ class CS3IPlayer : IPlayer {
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
} }
val mime = when { val mime = when(link.type) {
link.isM3u8 -> MimeTypes.APPLICATION_M3U8 ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
link.isDash -> MimeTypes.APPLICATION_MPD ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
else -> MimeTypes.VIDEO_MP4 ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4
ExtractorLinkType.TORRENT -> throw IllegalArgumentException("No torrent support")
ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support")
} }
val mediaItems = if (link is ExtractorLinkPlayList) { val mediaItems = if (link is ExtractorLinkPlayList) {

View file

@ -56,10 +56,10 @@ class DownloadFileGenerator(
override suspend fun generateLinks( override suspend fun generateLinks(
clearCache: Boolean, clearCache: Boolean,
isCasting: Boolean, type: LoadType,
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit, callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
subtitleCallback: (SubtitleData) -> Unit, subtitleCallback: (SubtitleData) -> Unit,
offset: Int, offset: Int
): Boolean { ): Boolean {
val meta = episodes[currentIndex + offset] val meta = episodes[currentIndex + offset]
callback(null to meta) callback(null to meta)

View file

@ -37,14 +37,17 @@ class ExtractorLinkGenerator(
override suspend fun generateLinks( override suspend fun generateLinks(
clearCache: Boolean, clearCache: Boolean,
isCasting: Boolean, type: LoadType,
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit, callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
subtitleCallback: (SubtitleData) -> Unit, subtitleCallback: (SubtitleData) -> Unit,
offset: Int offset: Int
): Boolean { ): Boolean {
subtitles.forEach(subtitleCallback) subtitles.forEach(subtitleCallback)
val allowedTypes = type.toSet()
links.forEach { links.forEach {
callback.invoke(it to null) if(allowedTypes.contains(it.type)) {
callback.invoke(it to null)
}
} }
return true return true

View file

@ -1,8 +1,43 @@
package com.lagradost.cloudstream3.ui.player package com.lagradost.cloudstream3.ui.player
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.ExtractorUri
enum class LoadType {
Unknown,
InApp,
InAppDownload,
ExternalApp,
Browser,
Chromecast
}
fun LoadType.toSet() : Set<ExtractorLinkType> {
return when(this) {
LoadType.InApp -> setOf(
ExtractorLinkType.VIDEO,
ExtractorLinkType.DASH,
ExtractorLinkType.M3U8
)
LoadType.Browser -> setOf(
ExtractorLinkType.VIDEO,
ExtractorLinkType.DASH,
ExtractorLinkType.M3U8
)
LoadType.InAppDownload -> setOf(
ExtractorLinkType.VIDEO,
ExtractorLinkType.M3U8
)
LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet()
LoadType.Chromecast -> setOf(
ExtractorLinkType.VIDEO,
ExtractorLinkType.DASH,
ExtractorLinkType.M3U8
)
}
}
interface IGenerator { interface IGenerator {
val hasCache: Boolean val hasCache: Boolean
@ -13,15 +48,15 @@ interface IGenerator {
fun goto(index: Int) fun goto(index: Int)
fun getCurrentId(): Int? // this is used to save data or read data about this id fun getCurrentId(): Int? // this is used to save data or read data about this id
fun getCurrent(offset : Int = 0): Any? // this is used to get metadata about the current playing, can return null fun getCurrent(offset: Int = 0): Any? // this is used to get metadata about the current playing, can return null
fun getAll() : List<Any>? // this us used to get the metadata about all entries, not needed fun getAll(): List<Any>? // this us used to get the metadata about all entries, not needed
/* not safe, must use try catch */ /* not safe, must use try catch */
suspend fun generateLinks( suspend fun generateLinks(
clearCache: Boolean, clearCache: Boolean,
isCasting: Boolean, type: LoadType,
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit, callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
subtitleCallback: (SubtitleData) -> Unit, subtitleCallback: (SubtitleData) -> Unit,
offset : Int = 0, offset: Int = 0,
): Boolean ): Boolean
} }

View file

@ -48,7 +48,7 @@ class LinkGenerator(
override suspend fun generateLinks( override suspend fun generateLinks(
clearCache: Boolean, clearCache: Boolean,
isCasting: Boolean, type: LoadType,
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit, callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
subtitleCallback: (SubtitleData) -> Unit, subtitleCallback: (SubtitleData) -> Unit,
offset: Int offset: Int

View file

@ -78,10 +78,10 @@ class PlayerGeneratorViewModel : ViewModel() {
if (generator?.hasCache == true && generator?.hasNext() == true) { if (generator?.hasCache == true && generator?.hasNext() == true) {
safeApiCall { safeApiCall {
generator?.generateLinks( generator?.generateLinks(
type = LoadType.InApp,
clearCache = false, clearCache = false,
isCasting = false, callback = {},
{}, subtitleCallback = {},
{},
offset = 1 offset = 1
) )
} }
@ -147,7 +147,7 @@ class PlayerGeneratorViewModel : ViewModel() {
} }
} }
fun loadLinks(clearCache: Boolean = false, isCasting: Boolean = false) { fun loadLinks(clearCache: Boolean = false, type: LoadType = LoadType.InApp) {
Log.i(TAG, "loadLinks") Log.i(TAG, "loadLinks")
currentJob?.cancel() currentJob?.cancel()
@ -162,14 +162,14 @@ class PlayerGeneratorViewModel : ViewModel() {
// load more data // load more data
_loadingLinks.postValue(Resource.Loading()) _loadingLinks.postValue(Resource.Loading())
val loadingState = safeApiCall { val loadingState = safeApiCall {
generator?.generateLinks(clearCache = clearCache, isCasting = isCasting, { generator?.generateLinks(type = type,clearCache = clearCache, callback = {
currentLinks.add(it) currentLinks.add(it)
// Clone to prevent ConcurrentModificationException // Clone to prevent ConcurrentModificationException
normalSafeApiCall { normalSafeApiCall {
// Extra normalSafeApiCall since .toSet() iterates. // Extra normalSafeApiCall since .toSet() iterates.
_currentLinks.postValue(currentLinks.toSet()) _currentLinks.postValue(currentLinks.toSet())
} }
}, { }, subtitleCallback = {
currentSubs.add(it) currentSubs.add(it)
normalSafeApiCall { normalSafeApiCall {
_currentSubs.postValue(currentSubs.toSet()) _currentSubs.postValue(currentSubs.toSet())

View file

@ -67,18 +67,19 @@ class RepoLinkGenerator(
override suspend fun generateLinks( override suspend fun generateLinks(
clearCache: Boolean, clearCache: Boolean,
isCasting: Boolean, type: LoadType,
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit, callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
subtitleCallback: (SubtitleData) -> Unit, subtitleCallback: (SubtitleData) -> Unit,
offset: Int, offset: Int
): Boolean { ): Boolean {
val allowedTypes = type.toSet()
val index = currentIndex val index = currentIndex
val current = episodes.getOrNull(index + offset) ?: return false val current = episodes.getOrNull(index + offset) ?: return false
val (currentLinkCache, currentSubsCache) = if (clearCache) { val (currentLinkCache, currentSubsCache) = if (clearCache) {
Pair(mutableSetOf(), mutableSetOf()) Pair(mutableSetOf(), mutableSetOf())
} else { } else {
cache[Pair(current.apiName, current.id)] ?: Pair(mutableSetOf(), mutableSetOf()) cache[current.apiName to current.id] ?: Pair(mutableSetOf(), mutableSetOf())
} }
//val currentLinkCache = if (clearCache) mutableSetOf() else linkCache[index].toMutableSet() //val currentLinkCache = if (clearCache) mutableSetOf() else linkCache[index].toMutableSet()
@ -88,9 +89,9 @@ class RepoLinkGenerator(
val currentSubsUrls = mutableSetOf<String>() // makes all subs urls unique val currentSubsUrls = mutableSetOf<String>() // makes all subs urls unique
val currentSubsNames = mutableSetOf<String>() // makes all subs names unique val currentSubsNames = mutableSetOf<String>() // makes all subs names unique
currentLinkCache.forEach { link -> currentLinkCache.filter { allowedTypes.contains(it.type) }.forEach { link ->
currentLinks.add(link.url) currentLinks.add(link.url)
callback(Pair(link, null)) callback(link to null)
} }
currentSubsCache.forEach { sub -> currentSubsCache.forEach { sub ->
@ -108,8 +109,8 @@ class RepoLinkGenerator(
val result = APIRepository( val result = APIRepository(
getApiFromNameNull(current.apiName) ?: throw Exception("This provider does not exist") getApiFromNameNull(current.apiName) ?: throw Exception("This provider does not exist")
).loadLinks(current.data, ).loadLinks(current.data,
isCasting, isCasting = LoadType.Chromecast == type,
{ file -> subtitleCallback = { file ->
val correctFile = PlayerSubtitleHelper.getSubtitleData(file) val correctFile = PlayerSubtitleHelper.getSubtitleData(file)
if (!currentSubsUrls.contains(correctFile.url)) { if (!currentSubsUrls.contains(correctFile.url)) {
currentSubsUrls.add(correctFile.url) currentSubsUrls.add(correctFile.url)
@ -132,12 +133,14 @@ class RepoLinkGenerator(
} }
} }
}, },
{ link -> callback = { link ->
Log.d(TAG, "Loaded ExtractorLink: $link") Log.d(TAG, "Loaded ExtractorLink: $link")
if (!currentLinks.contains(link.url)) { if (!currentLinks.contains(link.url)) {
if (!currentLinkCache.contains(link)) { if (!currentLinkCache.contains(link)) {
currentLinks.add(link.url) currentLinks.add(link.url)
callback(Pair(link, null)) if (allowedTypes.contains(link.type)) {
callback(Pair(link, null))
}
currentLinkCache.add(link) currentLinkCache.add(link)
//linkCache[index] = currentLinkCache //linkCache[index] = currentLinkCache
} }

View file

@ -36,6 +36,7 @@ import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer 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.LoadType
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction
@ -745,7 +746,7 @@ class ResultViewModel2 : ViewModel() {
val generator = RepoLinkGenerator(listOf(episode)) val generator = RepoLinkGenerator(listOf(episode))
val currentLinks = mutableSetOf<ExtractorLink>() val currentLinks = mutableSetOf<ExtractorLink>()
val currentSubs = mutableSetOf<SubtitleData>() val currentSubs = mutableSetOf<SubtitleData>()
generator.generateLinks(clearCache = false, isCasting = false, callback = { generator.generateLinks(clearCache = false, LoadType.Chromecast, callback = {
it.first?.let { link -> it.first?.let { link ->
currentLinks.add(link) currentLinks.add(link)
} }
@ -825,7 +826,7 @@ class ResultViewModel2 : ViewModel() {
isVisible: Boolean = true isVisible: Boolean = true
) { ) {
if (activity == null) return if (activity == null) return
loadLinks(result, isVisible = isVisible, isCasting = true) { data -> loadLinks(result, isVisible = isVisible, LoadType.Chromecast) { data ->
startChromecast(activity, result, data.links, data.subs, 0) startChromecast(activity, result, data.links, data.subs, 0)
} }
} }
@ -936,7 +937,7 @@ class ResultViewModel2 : ViewModel() {
private fun loadLinks( private fun loadLinks(
result: ResultEpisode, result: ResultEpisode,
isVisible: Boolean, isVisible: Boolean,
isCasting: Boolean, type: LoadType,
clearCache: Boolean = false, clearCache: Boolean = false,
work: suspend (CoroutineScope.(LinkLoadingResult) -> Unit) work: suspend (CoroutineScope.(LinkLoadingResult) -> Unit)
) { ) {
@ -945,7 +946,7 @@ class ResultViewModel2 : ViewModel() {
val links = loadLinks( val links = loadLinks(
result, result,
isVisible = isVisible, isVisible = isVisible,
isCasting = isCasting, type = type,
clearCache = clearCache clearCache = clearCache
) )
if (!this.isActive) return@ioSafe if (!this.isActive) return@ioSafe
@ -956,11 +957,11 @@ class ResultViewModel2 : ViewModel() {
private var currentLoadLinkJob: Job? = null private var currentLoadLinkJob: Job? = null
private fun acquireSingleLink( private fun acquireSingleLink(
result: ResultEpisode, result: ResultEpisode,
isCasting: Boolean, type: LoadType,
text: UiText, text: UiText,
callback: (Pair<LinkLoadingResult, Int>) -> Unit, callback: (Pair<LinkLoadingResult, Int>) -> Unit,
) { ) {
loadLinks(result, isVisible = true, isCasting = isCasting) { links -> loadLinks(result, isVisible = true, type) { links ->
postPopup( postPopup(
text, text,
links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) {
@ -971,11 +972,10 @@ class ResultViewModel2 : ViewModel() {
private fun acquireSingleSubtitle( private fun acquireSingleSubtitle(
result: ResultEpisode, result: ResultEpisode,
isCasting: Boolean,
text: UiText, text: UiText,
callback: (Pair<LinkLoadingResult, Int>) -> Unit, callback: (Pair<LinkLoadingResult, Int>) -> Unit,
) { ) {
loadLinks(result, isVisible = true, isCasting = isCasting) { links -> loadLinks(result, isVisible = true, type = LoadType.Unknown) { links ->
postPopup( postPopup(
text, text,
links.subs.map { txt(it.name) }) links.subs.map { txt(it.name) })
@ -988,7 +988,7 @@ class ResultViewModel2 : ViewModel() {
private suspend fun CoroutineScope.loadLinks( private suspend fun CoroutineScope.loadLinks(
result: ResultEpisode, result: ResultEpisode,
isVisible: Boolean, isVisible: Boolean,
isCasting: Boolean, type: LoadType,
clearCache: Boolean = false, clearCache: Boolean = false,
): LinkLoadingResult { ): LinkLoadingResult {
val tempGenerator = RepoLinkGenerator(listOf(result)) val tempGenerator = RepoLinkGenerator(listOf(result))
@ -1002,7 +1002,7 @@ class ResultViewModel2 : ViewModel() {
} }
try { try {
updatePage() updatePage()
tempGenerator.generateLinks(clearCache, isCasting, { (link, _) -> tempGenerator.generateLinks(clearCache, type, { (link, _) ->
if (link != null) { if (link != null) {
links += link links += link
updatePage() updatePage()
@ -1272,7 +1272,6 @@ class ResultViewModel2 : ViewModel() {
acquireSingleSubtitle( acquireSingleSubtitle(
click.data, click.data,
false,
txt(R.string.episode_action_download_subtitle) txt(R.string.episode_action_download_subtitle)
) { (links, index) -> ) { (links, index) ->
downloadSubtitle( downloadSubtitle(
@ -1317,7 +1316,7 @@ class ResultViewModel2 : ViewModel() {
val response = currentResponse ?: return val response = currentResponse ?: return
acquireSingleLink( acquireSingleLink(
click.data, click.data,
false, LoadType.InAppDownload,
txt(R.string.episode_action_download_mirror) txt(R.string.episode_action_download_mirror)
) { (result, index) -> ) { (result, index) ->
ioSafe { ioSafe {
@ -1347,7 +1346,7 @@ class ResultViewModel2 : ViewModel() {
loadLinks( loadLinks(
click.data, click.data,
isVisible = false, isVisible = false,
isCasting = false, type = LoadType.InApp,
clearCache = true clearCache = true
) )
} }
@ -1356,7 +1355,7 @@ class ResultViewModel2 : ViewModel() {
ACTION_CHROME_CAST_MIRROR -> { ACTION_CHROME_CAST_MIRROR -> {
acquireSingleLink( acquireSingleLink(
click.data, click.data,
isCasting = true, LoadType.Chromecast,
txt(R.string.episode_action_chromecast_mirror) txt(R.string.episode_action_chromecast_mirror)
) { (result, index) -> ) { (result, index) ->
startChromecast(activity, click.data, result.links, result.subs, index) startChromecast(activity, click.data, result.links, result.subs, index)
@ -1365,7 +1364,7 @@ class ResultViewModel2 : ViewModel() {
ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink(
click.data, click.data,
isCasting = true, LoadType.Browser,
txt(R.string.episode_action_play_in_browser) txt(R.string.episode_action_play_in_browser)
) { (result, index) -> ) { (result, index) ->
try { try {
@ -1380,7 +1379,7 @@ class ResultViewModel2 : ViewModel() {
ACTION_COPY_LINK -> { ACTION_COPY_LINK -> {
acquireSingleLink( acquireSingleLink(
click.data, click.data,
isCasting = true, LoadType.ExternalApp,
txt(R.string.episode_action_copy_link) txt(R.string.episode_action_copy_link)
) { (result, index) -> ) { (result, index) ->
val act = activity ?: return@acquireSingleLink val act = activity ?: return@acquireSingleLink
@ -1399,7 +1398,7 @@ class ResultViewModel2 : ViewModel() {
} }
ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> { ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> {
loadLinks(click.data, isVisible = true, isCasting = true) { links -> loadLinks(click.data, isVisible = true, LoadType.ExternalApp) { links ->
if (links.links.isEmpty()) { if (links.links.isEmpty()) {
showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
return@loadLinks return@loadLinks
@ -1415,7 +1414,7 @@ class ResultViewModel2 : ViewModel() {
ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink( ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink(
click.data, click.data,
isCasting = true, LoadType.Chromecast,
txt( txt(
R.string.episode_action_play_in_format, R.string.episode_action_play_in_format,
txt(R.string.player_settings_play_in_web) txt(R.string.player_settings_play_in_web)
@ -1432,7 +1431,7 @@ class ResultViewModel2 : ViewModel() {
ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink( ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink(
click.data, click.data,
isCasting = true, LoadType.Chromecast,
txt( txt(
R.string.episode_action_play_in_format, R.string.episode_action_play_in_format,
txt(R.string.player_settings_play_in_mpv) txt(R.string.player_settings_play_in_mpv)
@ -1461,7 +1460,6 @@ class ResultViewModel2 : ViewModel() {
if (index >= 0) if (index >= 0)
it.goto(index) it.goto(index)
} }
} ?: return, list } ?: return, list
) )
) )
@ -2173,7 +2171,7 @@ class ResultViewModel2 : ViewModel() {
trailerData.extractorUrl, trailerData.extractorUrl,
trailerData.referer ?: "", trailerData.referer ?: "",
Qualities.Unknown.value, Qualities.Unknown.value,
trailerData.extractorUrl.contains(".m3u8") type = INFER_TYPE
) )
) to arrayListOf() ) to arrayListOf()
} else { } else {

View file

@ -55,7 +55,11 @@ object CastHelper {
val builder = MediaInfo.Builder(link.url) val builder = MediaInfo.Builder(link.url)
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
.setContentType(if (link.isM3u8) MimeTypes.APPLICATION_M3U8 else MimeTypes.VIDEO_MP4) .setContentType(when(link.type) {
ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
else -> MimeTypes.VIDEO_MP4
})
.setMetadata(movieMetadata) .setMetadata(movieMetadata)
.setMediaTracks(tracks) .setMediaTracks(tracks)
data?.let { data?.let {

View file

@ -4,8 +4,10 @@ import android.net.Uri
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.extractors.* import com.lagradost.cloudstream3.extractors.*
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.jsoup.Jsoup import org.jsoup.Jsoup
import java.net.URL
import kotlin.collections.MutableList import kotlin.collections.MutableList
/** /**
@ -35,35 +37,101 @@ data class ExtractorLinkPlayList(
val playlist: List<PlayListItem>, val playlist: List<PlayListItem>,
override val referer: String, override val referer: String,
override val quality: Int, override val quality: Int,
override val isM3u8: Boolean = false, val isM3u8: Boolean = false,
override val headers: Map<String, String> = mapOf(), override val headers: Map<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */ /** Used for getExtractorVerifierJob() */
override val extractorData: String? = null, override val extractorData: String? = null,
override val type: ExtractorLinkType,
) : ExtractorLink( ) : ExtractorLink(
source, source = source,
name, name = name,
// Blank as un-used url = "",
"", referer = referer,
referer, quality = quality,
quality, headers = headers,
isM3u8, extractorData = extractorData,
headers, type = type
extractorData ) {
) constructor(
source: String,
name: String,
playlist: List<PlayListItem>,
referer: String,
quality: Int,
isM3u8: Boolean = false,
headers: Map<String, String> = mapOf(),
extractorData: String? = null,
) : this(
source = source,
name = name,
playlist = playlist,
referer = referer,
quality = quality,
type = if (isM3u8) ExtractorLinkType.M3U8 else ExtractorLinkType.VIDEO,
headers = headers,
extractorData = extractorData,
)
}
/** Metadata about the file type used for downloads and exoplayer hint,
* if you respond with the wrong one the file will fail to download or be played */
enum class ExtractorLinkType {
/** Single stream of bytes no matter the actual file type */
VIDEO,
/** Split into several .ts files, has support for encrypted m3u8s */
M3U8,
/** Like m3u8 but uses xml, currently no download support */
DASH,
/** No support at the moment */
TORRENT,
/** No support at the moment */
MAGNET,
}
private fun inferTypeFromUrl(url: String): ExtractorLinkType {
val path = normalSafeApiCall { URL(url).path }
return when {
path?.endsWith(".m3u8") == true -> ExtractorLinkType.M3U8
path?.endsWith(".mpd") == true -> ExtractorLinkType.DASH
path?.endsWith(".torrent") == true -> ExtractorLinkType.TORRENT
url.startsWith("magnet:") -> ExtractorLinkType.MAGNET
else -> ExtractorLinkType.VIDEO
}
}
val INFER_TYPE : ExtractorLinkType? = null
open class ExtractorLink constructor( open class ExtractorLink constructor(
open val source: String, open val source: String,
open val name: String, open val name: String,
override val url: String, override val url: String,
override val referer: String, override val referer: String,
open val quality: Int, open val quality: Int,
open val isM3u8: Boolean = false,
override val headers: Map<String, String> = mapOf(), override val headers: Map<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */ /** Used for getExtractorVerifierJob() */
open val extractorData: String? = null, open val extractorData: String? = null,
open val isDash: Boolean = false, open val type: ExtractorLinkType,
) : VideoDownloadManager.IDownloadableMinimum { ) : VideoDownloadManager.IDownloadableMinimum {
constructor(
source: String,
name: String,
url: String,
referer: String,
quality: Int,
/** the type of the media, use INFER_TYPE if you want to auto infer the type from the url */
type: ExtractorLinkType?,
headers: Map<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */
extractorData: String? = null,
) : this(
source = source,
name = name,
url = url,
referer = referer,
quality = quality,
headers = headers,
extractorData = extractorData,
type = type ?: inferTypeFromUrl(url)
)
/** /**
* Old constructor without isDash, allows for backwards compatibility with extensions. * Old constructor without isDash, allows for backwards compatibility with extensions.
* Should be removed after all extensions have updated their cloudstream.jar * Should be removed after all extensions have updated their cloudstream.jar
@ -80,8 +148,30 @@ open class ExtractorLink constructor(
extractorData: String? = null extractorData: String? = null
) : this(source, name, url, referer, quality, isM3u8, headers, extractorData, false) ) : this(source, name, url, referer, quality, isM3u8, headers, extractorData, false)
constructor(
source: String,
name: String,
url: String,
referer: String,
quality: Int,
isM3u8: Boolean = false,
headers: Map<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */
extractorData: String? = null,
isDash: Boolean,
) : this(
source = source,
name = name,
url = url,
referer = referer,
quality = quality,
headers = headers,
extractorData = extractorData,
type = if (isDash) ExtractorLinkType.DASH else if (isM3u8) ExtractorLinkType.M3U8 else ExtractorLinkType.VIDEO
)
override fun toString(): String { override fun toString(): String {
return "ExtractorLink(name=$name, url=$url, referer=$referer, isM3u8=$isM3u8)" return "ExtractorLink(name=$name, url=$url, referer=$referer, type=$type)"
} }
} }
@ -135,6 +225,7 @@ enum class Qualities(var value: Int, val defaultPriority: Int) {
else -> "${qual}p" else -> "${qual}p"
} }
} }
fun getStringByIntFull(quality: Int): String { fun getStringByIntFull(quality: Int): String {
return when (quality) { return when (quality) {
0 -> "Auto" 0 -> "Auto"

View file

@ -53,7 +53,7 @@ import java.io.Closeable
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import java.net.URL import java.lang.IllegalArgumentException
import java.util.* import java.util.*
const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general"
@ -951,7 +951,10 @@ object VideoDownloadManager {
/** how many bytes every connection should be, by default it is 10 MiB */ /** how many bytes every connection should be, by default it is 10 MiB */
chuckSize: Long = (1 shl 20) * 10, chuckSize: Long = (1 shl 20) * 10,
/** maximum bytes in the buffer that responds */ /** maximum bytes in the buffer that responds */
bufferSize: Int = DEFAULT_BUFFER_SIZE bufferSize: Int = DEFAULT_BUFFER_SIZE,
/** how many bytes bytes it should require to use the parallel downloader instead,
* if we download a very small file we don't want it parallel */
maximumSmallSize : Long = chuckSize * 2
): LazyStreamDownloadData { ): LazyStreamDownloadData {
// we don't want to make a separate connection for every 1kb // we don't want to make a separate connection for every 1kb
require(chuckSize > 1000) require(chuckSize > 1000)
@ -963,7 +966,7 @@ object VideoDownloadManager {
var downloadLength: Long? = null var downloadLength: Long? = null
var totalLength: Long? = null var totalLength: Long? = null
val ranges = if (contentLength == null) { val ranges = if (contentLength == null || contentLength < maximumSmallSize) {
// is the equivalent of [startByte..EOF] as we don't know the size we can only do one // is the equivalent of [startByte..EOF] as we don't know the size we can only do one
// connection // connection
LongArray(1) { startByte } LongArray(1) { startByte }
@ -1024,6 +1027,7 @@ object VideoDownloadManager {
} }
} }
/** download a file that consist of a single stream of data*/
suspend fun downloadThing( suspend fun downloadThing(
context: Context, context: Context,
link: IDownloadableMinimum, link: IDownloadableMinimum,
@ -1035,8 +1039,7 @@ object VideoDownloadManager {
createNotificationCallback: (CreateNotificationMetadata) -> Unit, createNotificationCallback: (CreateNotificationMetadata) -> Unit,
parallelConnections: Int = 3 parallelConnections: Int = 3
): DownloadStatus = withContext(Dispatchers.IO) { ): DownloadStatus = withContext(Dispatchers.IO) {
// we cant download torrents with this implementation, aria2c might be used in the future if (parallelConnections < 1) {
if (link.url.startsWith("magnet") || link.url.endsWith(".torrent") || parallelConnections < 1) {
return@withContext DOWNLOAD_INVALID_INPUT return@withContext DOWNLOAD_INVALID_INPUT
} }
@ -1529,6 +1532,11 @@ object VideoDownloadManager {
notificationCallback: (Int, Notification) -> Unit, notificationCallback: (Int, Notification) -> Unit,
tryResume: Boolean = false, tryResume: Boolean = false,
): DownloadStatus { ): DownloadStatus {
// no support for these file formats
if(link.type == ExtractorLinkType.MAGNET || link.type == ExtractorLinkType.TORRENT || link.type == ExtractorLinkType.DASH) {
return DOWNLOAD_INVALID_INPUT
}
val name = getFileName(context, ep) val name = getFileName(context, ep)
// Make sure this is cancelled when download is done or cancelled. // Make sure this is cancelled when download is done or cancelled.
@ -1557,35 +1565,39 @@ object VideoDownloadManager {
} }
try { try {
if (link.isM3u8 || normalSafeApiCall { URL(link.url).path.endsWith(".m3u8") } == true) { when(link.type) {
val startIndex = if (tryResume) { ExtractorLinkType.M3U8 -> {
context.getKey<DownloadedFileInfo>( val startIndex = if (tryResume) {
KEY_DOWNLOAD_INFO, context.getKey<DownloadedFileInfo>(
ep.id.toString(), KEY_DOWNLOAD_INFO,
null ep.id.toString(),
)?.extraInfo?.toIntOrNull() null
} else null )?.extraInfo?.toIntOrNull()
} else null
return downloadHLS( return downloadHLS(
context, context,
link, link,
name, name,
folder ?: "", folder ?: "",
ep.id, ep.id,
startIndex, startIndex,
callback, parallelConnections = maxConcurrentConnections callback, parallelConnections = maxConcurrentConnections
) )
} else { }
return downloadThing( ExtractorLinkType.VIDEO -> {
context, return downloadThing(
link, context,
name, link,
folder ?: "", name,
"mp4", folder ?: "",
tryResume, "mp4",
ep.id, tryResume,
callback, parallelConnections = maxConcurrentConnections ep.id,
) callback, parallelConnections = maxConcurrentConnections
)
}
else -> throw IllegalArgumentException("unsuported download type")
} }
} catch (t: Throwable) { } catch (t: Throwable) {
return DOWNLOAD_FAILED return DOWNLOAD_FAILED