mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
b5323df2b8
36 changed files with 813 additions and 102 deletions
|
@ -1454,11 +1454,24 @@ fun TvType?.isEpisodeBased(): Boolean {
|
||||||
return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama)
|
return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
data class NextAiring(
|
data class NextAiring(
|
||||||
val episode: Int,
|
val episode: Int,
|
||||||
val unixTime: Long,
|
val unixTime: Long,
|
||||||
)
|
val season: Int? = null,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Secondary constructor for backwards compatibility without season.
|
||||||
|
* TODO Remove this constructor after there is a new stable release and extensions are updated to support season.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
episode: Int,
|
||||||
|
unixTime: Long,
|
||||||
|
) : this (
|
||||||
|
episode,
|
||||||
|
unixTime,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param season To be mapped with episode season, not shown in UI if displaySeason is defined
|
* @param season To be mapped with episode season, not shown in UI if displaySeason is defined
|
||||||
|
|
|
@ -161,6 +161,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||||
|
import com.lagradost.cloudstream3.utils.fcast.FcastManager
|
||||||
|
import com.lagradost.nicehttp.Requests
|
||||||
|
import com.lagradost.nicehttp.ResponseParser
|
||||||
import com.lagradost.safefile.SafeFile
|
import com.lagradost.safefile.SafeFile
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
@ -1729,6 +1732,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener,
|
||||||
runAutoUpdate()
|
runAutoUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FcastManager().init(this, false)
|
||||||
|
|
||||||
APIRepository.dubStatusActive = getApiDubstatusSettings()
|
APIRepository.dubStatusActive = getApiDubstatusSettings()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -2,9 +2,7 @@ package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.extractors.helper.*
|
|
||||||
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
|
||||||
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.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
@ -28,30 +26,39 @@ open class Chillx : ExtractorApi() {
|
||||||
override val name = "Chillx"
|
override val name = "Chillx"
|
||||||
override val mainUrl = "https://chillx.top"
|
override val mainUrl = "https://chillx.top"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
private var key: String? = null
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private var key: String? = null
|
||||||
|
|
||||||
|
suspend fun fetchKey(): String {
|
||||||
|
return if (key != null) {
|
||||||
|
key!!
|
||||||
|
} else {
|
||||||
|
val fetch = app.get("https://raw.githubusercontent.com/rushi-chavan/multi-keys/keys/keys.json").parsedSafe<Keys>()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key")
|
||||||
|
key = fetch
|
||||||
|
key!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("NAME_SHADOWING")
|
||||||
override suspend fun getUrl(
|
override suspend fun getUrl(
|
||||||
url: String,
|
url: String,
|
||||||
referer: String?,
|
referer: String?,
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val master = Regex("\\s*=\\s*'([^']+)").find(
|
val master = Regex("""JScript[\w+]?\s*=\s*'([^']+)""").find(
|
||||||
app.get(
|
app.get(
|
||||||
url,
|
url,
|
||||||
referer = referer ?: "",
|
referer = url,
|
||||||
headers = mapOf(
|
|
||||||
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
||||||
"Accept-Language" to "en-US,en;q=0.5",
|
|
||||||
)
|
|
||||||
).text
|
).text
|
||||||
)?.groupValues?.get(1)
|
)?.groupValues?.get(1)
|
||||||
val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
val key = fetchKey()
|
||||||
|
val decrypt = cryptoAESHandler(master ?: "", key.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||||
val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
||||||
|
|
||||||
val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
||||||
val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex()
|
val subtitlePattern = """\[(.*?)](https?://[^\s,]+)""".toRegex()
|
||||||
val matches = subtitlePattern.findAll(subtitles ?: "")
|
val matches = subtitlePattern.findAll(subtitles ?: "")
|
||||||
val languageUrlPairs = matches.map { matchResult ->
|
val languageUrlPairs = matches.map { matchResult ->
|
||||||
val (language, url) = matchResult.destructured
|
val (language, url) = matchResult.destructured
|
||||||
|
@ -91,15 +98,10 @@ open class Chillx : ExtractorApi() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getKey() = key ?: fetchKey().also { key = it }
|
|
||||||
|
|
||||||
private suspend fun fetchKey(): String {
|
|
||||||
return app.get("https://raw.githubusercontent.com/Sofie99/Resources/main/chillix_key.json").parsed()
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Tracks(
|
data class Keys(
|
||||||
@JsonProperty("file") val file: String? = null,
|
@JsonProperty("chillx") val key: List<String>
|
||||||
@JsonProperty("label") val label: String? = null,
|
|
||||||
@JsonProperty("kind") val kind: String? = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,16 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
|
class Geodailymotion : Dailymotion() {
|
||||||
|
override val name = "GeoDailymotion"
|
||||||
|
override val mainUrl = "https://geo.dailymotion.com"
|
||||||
|
}
|
||||||
|
|
||||||
open class Dailymotion : ExtractorApi() {
|
open class Dailymotion : ExtractorApi() {
|
||||||
override val mainUrl = "https://www.dailymotion.com"
|
override val mainUrl = "https://www.dailymotion.com"
|
||||||
override val name = "Dailymotion"
|
override val name = "Dailymotion"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
private val baseUrl = "https://www.dailymotion.com"
|
||||||
|
|
||||||
@Suppress("RegExpSimplifiable")
|
@Suppress("RegExpSimplifiable")
|
||||||
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
||||||
|
@ -34,7 +40,7 @@ open class Dailymotion : ExtractorApi() {
|
||||||
val dmV1st = config.dmInternalData.v1st
|
val dmV1st = config.dmInternalData.v1st
|
||||||
val dmTs = config.dmInternalData.ts
|
val dmTs = config.dmInternalData.ts
|
||||||
val embedder = config.context.embedder
|
val embedder = config.context.embedder
|
||||||
val metaDataUrl = "$mainUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||||
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies)
|
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies)
|
||||||
.parsedSafe<MetaData>() ?: return
|
.parsedSafe<MetaData>() ?: return
|
||||||
metaData.qualities.forEach { (_, video) ->
|
metaData.qualities.forEach { (_, video) ->
|
||||||
|
@ -45,16 +51,19 @@ open class Dailymotion : ExtractorApi() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEmbedUrl(url: String): String? {
|
private fun getEmbedUrl(url: String): String? {
|
||||||
if (url.contains("/embed/")) {
|
if (url.contains("/embed/") || url.contains("/video/")) {
|
||||||
return url
|
return url
|
||||||
}
|
|
||||||
val vid = getVideoId(url) ?: return null
|
|
||||||
return "$mainUrl/embed/video/$vid"
|
|
||||||
}
|
}
|
||||||
|
if (url.contains("geo.dailymotion.com")) {
|
||||||
|
val videoId = url.substringAfter("video=")
|
||||||
|
return "$baseUrl/embed/video/$videoId"
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
private fun getVideoId(url: String): String? {
|
private fun getVideoId(url: String): String? {
|
||||||
val path = URL(url).path
|
val path = URL(url).path
|
||||||
val id = path.substringAfter("video/")
|
val id = path.substringAfter("/video/")
|
||||||
if (id.matches(videoIdRegex)) {
|
if (id.matches(videoIdRegex)) {
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.amap
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
class VidSrcTo : ExtractorApi() {
|
||||||
|
override val name = "VidSrcTo"
|
||||||
|
override val mainUrl = "https://vidsrc.to"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return
|
||||||
|
val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe<VidsrctoEpisodeSources>() ?: return
|
||||||
|
if (res.status != 200) return
|
||||||
|
res.result?.amap { source ->
|
||||||
|
val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe<VidsrctoEmbedSource>() ?: return@amap
|
||||||
|
val finalUrl = DecryptUrl(embedRes.result.encUrl)
|
||||||
|
if(finalUrl.equals(embedRes.result.encUrl)) return@amap
|
||||||
|
when (source.title) {
|
||||||
|
"Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback)
|
||||||
|
"Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DecryptUrl(encUrl: String): String {
|
||||||
|
var data = encUrl.toByteArray()
|
||||||
|
data = Base64.decode(data, Base64.URL_SAFE)
|
||||||
|
val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4")
|
||||||
|
val cipher = Cipher.getInstance("RC4")
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters)
|
||||||
|
data = cipher.doFinal(data)
|
||||||
|
return URLDecoder.decode(data.toString(Charsets.UTF_8), "utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class VidsrctoEpisodeSources(
|
||||||
|
@JsonProperty("status") val status: Int,
|
||||||
|
@JsonProperty("result") val result: List<VidsrctoResult>?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VidsrctoResult(
|
||||||
|
@JsonProperty("id") val id: String,
|
||||||
|
@JsonProperty("title") val title: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VidsrctoEmbedSource(
|
||||||
|
@JsonProperty("status") val status: Int,
|
||||||
|
@JsonProperty("result") val result: VidsrctoUrl
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VidsrctoUrl(@JsonProperty("url") val encUrl: String)
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import org.mozilla.javascript.Context
|
||||||
|
import org.mozilla.javascript.NativeJSON
|
||||||
|
import org.mozilla.javascript.NativeObject
|
||||||
|
import org.mozilla.javascript.Scriptable
|
||||||
|
import java.util.Base64
|
||||||
|
|
||||||
|
open class Vidguardto : ExtractorApi() {
|
||||||
|
override val name = "Vidguard"
|
||||||
|
override val mainUrl = "https://vidguard.to"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val res = app.get(url)
|
||||||
|
val resc = res.document.select("script:containsData(eval)").firstOrNull()?.data()
|
||||||
|
resc?.let {
|
||||||
|
val jsonStr2 = AppUtils.parseJson<SvgObject>(runJS2(it))
|
||||||
|
val watchlink = sigDecode(jsonStr2.stream)
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
name,
|
||||||
|
watchlink,
|
||||||
|
this.mainUrl,
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
INFER_TYPE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sigDecode(url: String): String {
|
||||||
|
val sig = url.split("sig=")[1].split("&")[0]
|
||||||
|
var t = ""
|
||||||
|
for (v in sig.chunked(2)) {
|
||||||
|
val byteValue = Integer.parseInt(v, 16) xor 2
|
||||||
|
t += byteValue.toChar()
|
||||||
|
}
|
||||||
|
val padding = when (t.length % 4) {
|
||||||
|
2 -> "=="
|
||||||
|
3 -> "="
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
val decoded = Base64.getDecoder().decode((t + padding).toByteArray(Charsets.UTF_8))
|
||||||
|
t = String(decoded).dropLast(5).reversed()
|
||||||
|
val charArray = t.toCharArray()
|
||||||
|
for (i in 0 until charArray.size - 1 step 2) {
|
||||||
|
val temp = charArray[i]
|
||||||
|
charArray[i] = charArray[i + 1]
|
||||||
|
charArray[i + 1] = temp
|
||||||
|
}
|
||||||
|
val modifiedSig = String(charArray).dropLast(5)
|
||||||
|
return url.replace(sig, modifiedSig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runJS2(hideMyHtmlContent: String): String {
|
||||||
|
Log.d("runJS", "start")
|
||||||
|
val rhino = Context.enter()
|
||||||
|
rhino.initSafeStandardObjects()
|
||||||
|
rhino.optimizationLevel = -1
|
||||||
|
val scope: Scriptable = rhino.initSafeStandardObjects()
|
||||||
|
scope.put("window", scope, scope)
|
||||||
|
var result = ""
|
||||||
|
try {
|
||||||
|
Log.d("runJS", "Executing JavaScript: $hideMyHtmlContent")
|
||||||
|
rhino.evaluateString(scope, hideMyHtmlContent, "JavaScript", 1, null)
|
||||||
|
val svgObject = scope.get("svg", scope)
|
||||||
|
result = if (svgObject is NativeObject) {
|
||||||
|
NativeJSON.stringify(Context.getCurrentContext(), scope, svgObject, null, null).toString()
|
||||||
|
} else {
|
||||||
|
Context.toString(svgObject)
|
||||||
|
}
|
||||||
|
Log.d("runJS", "Result: $result")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("runJS", "Error executing JavaScript", e)
|
||||||
|
} finally {
|
||||||
|
Context.exit()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SvgObject(
|
||||||
|
val stream: String,
|
||||||
|
val hash: String
|
||||||
|
)
|
||||||
|
}
|
|
@ -13,6 +13,10 @@ import javax.crypto.spec.SecretKeySpec
|
||||||
// Code found in https://github.com/KillerDogeEmpire/vidplay-keys
|
// Code found in https://github.com/KillerDogeEmpire/vidplay-keys
|
||||||
// special credits to @KillerDogeEmpire for providing key
|
// special credits to @KillerDogeEmpire for providing key
|
||||||
|
|
||||||
|
class AnyVidplay(hostUrl: String) : Vidplay() {
|
||||||
|
override val mainUrl = hostUrl
|
||||||
|
}
|
||||||
|
|
||||||
class MyCloud : Vidplay() {
|
class MyCloud : Vidplay() {
|
||||||
override val name = "MyCloud"
|
override val name = "MyCloud"
|
||||||
override val mainUrl = "https://mcloud.bz"
|
override val mainUrl = "https://mcloud.bz"
|
||||||
|
|
|
@ -1,13 +1,37 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
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.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
|
||||||
class Tubeless : Voe() {
|
class Tubeless : Voe() {
|
||||||
override var mainUrl = "https://tubelessceliolymph.com"
|
override val name = "Tubeless"
|
||||||
|
override val mainUrl = "https://tubelessceliolymph.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Simpulumlamerop : Voe() {
|
||||||
|
override val name = "Simplum"
|
||||||
|
override var mainUrl = "https://simpulumlamerop.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Urochsunloath : Voe() {
|
||||||
|
override val name = "Uroch"
|
||||||
|
override var mainUrl = "https://urochsunloath.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Yipsu : Voe() {
|
||||||
|
override val name = "Yipsu"
|
||||||
|
override var mainUrl = "https://yip.su"
|
||||||
|
}
|
||||||
|
|
||||||
|
class MetaGnathTuggers : Voe() {
|
||||||
|
override val name = "Metagnath"
|
||||||
|
override val mainUrl = "https://metagnathtuggers.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
open class Voe : ExtractorApi() {
|
open class Voe : ExtractorApi() {
|
||||||
|
@ -15,6 +39,9 @@ open class Voe : ExtractorApi() {
|
||||||
override val mainUrl = "https://voe.sx"
|
override val mainUrl = "https://voe.sx"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex()
|
||||||
|
private val base64Regex = Regex("'.*'")
|
||||||
|
|
||||||
override suspend fun getUrl(
|
override suspend fun getUrl(
|
||||||
url: String,
|
url: String,
|
||||||
referer: String?,
|
referer: String?,
|
||||||
|
@ -25,12 +52,33 @@ open class Voe : ExtractorApi() {
|
||||||
val script = res.select("script").find { it.data().contains("sources =") }?.data()
|
val script = res.select("script").find { it.data().contains("sources =") }?.data()
|
||||||
val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1)
|
val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1)
|
||||||
|
|
||||||
M3u8Helper.generateM3u8(
|
val videoLinks = mutableListOf<String>()
|
||||||
name,
|
|
||||||
link ?: return,
|
|
||||||
"$mainUrl/",
|
|
||||||
headers = mapOf("Origin" to "$mainUrl/")
|
|
||||||
).forEach(callback)
|
|
||||||
|
|
||||||
|
if (!link.isNullOrBlank()) {
|
||||||
|
videoLinks.add(
|
||||||
|
when {
|
||||||
|
linkRegex.matches(link) -> link
|
||||||
|
else -> String(Base64.decode(link, Base64.DEFAULT))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val link2 = base64Regex.find(script)?.value ?: return
|
||||||
|
val decoded = Base64.decode(link2, Base64.DEFAULT).toString()
|
||||||
|
val videoLinkDTO = AppUtils.parseJson<WcoSources>(decoded)
|
||||||
|
videoLinkDTO.let { videoLinks.add(it.toString()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
videoLinks.forEach { videoLink ->
|
||||||
|
M3u8Helper.generateM3u8(
|
||||||
|
name,
|
||||||
|
videoLink,
|
||||||
|
"$mainUrl/",
|
||||||
|
headers = mapOf("Origin" to "$mainUrl/")
|
||||||
|
).forEach(callback)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class WcoSources(
|
||||||
|
@JsonProperty("VideoLinkDTO") val VideoLinkDTO: String,
|
||||||
|
)
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
|
@ -105,6 +105,7 @@ open class TmdbProvider : MainAPI() {
|
||||||
this.id,
|
this.id,
|
||||||
episode.episode_number,
|
episode.episode_number,
|
||||||
episode.season_number,
|
episode.season_number,
|
||||||
|
this.name ?: this.original_name,
|
||||||
).toJson(),
|
).toJson(),
|
||||||
episode.name,
|
episode.name,
|
||||||
episode.season_number,
|
episode.season_number,
|
||||||
|
@ -122,6 +123,7 @@ open class TmdbProvider : MainAPI() {
|
||||||
this.id,
|
this.id,
|
||||||
episodeNum,
|
episodeNum,
|
||||||
season.season_number,
|
season.season_number,
|
||||||
|
this.name ?: this.original_name,
|
||||||
).toJson(),
|
).toJson(),
|
||||||
season = season.season_number
|
season = season.season_number
|
||||||
)
|
)
|
||||||
|
|
|
@ -118,8 +118,12 @@ open class TraktProvider : MainAPI() {
|
||||||
|
|
||||||
val linkData = LinkData(
|
val linkData = LinkData(
|
||||||
id = mediaDetails?.ids?.tmdb,
|
id = mediaDetails?.ids?.tmdb,
|
||||||
|
traktId = mediaDetails?.ids?.trakt,
|
||||||
|
traktSlug = mediaDetails?.ids?.slug,
|
||||||
|
tmdbId = mediaDetails?.ids?.tmdb,
|
||||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||||
tvdbId = mediaDetails?.ids?.tvdb,
|
tvdbId = mediaDetails?.ids?.tvdb,
|
||||||
|
tvrageId = mediaDetails?.ids?.tvrage,
|
||||||
type = data.type.toString(),
|
type = data.type.toString(),
|
||||||
title = mediaDetails?.title,
|
title = mediaDetails?.title,
|
||||||
year = mediaDetails?.year,
|
year = mediaDetails?.year,
|
||||||
|
@ -139,7 +143,6 @@ open class TraktProvider : MainAPI() {
|
||||||
type = if (isAnime) TvType.AnimeMovie else TvType.Movie,
|
type = if (isAnime) TvType.AnimeMovie else TvType.Movie,
|
||||||
) {
|
) {
|
||||||
this.name = mediaDetails.title
|
this.name = mediaDetails.title
|
||||||
this.apiName = "Trakt"
|
|
||||||
this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie
|
this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie
|
||||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||||
this.year = mediaDetails.year
|
this.year = mediaDetails.year
|
||||||
|
@ -177,8 +180,12 @@ open class TraktProvider : MainAPI() {
|
||||||
|
|
||||||
val linkData = LinkData(
|
val linkData = LinkData(
|
||||||
id = mediaDetails?.ids?.tmdb,
|
id = mediaDetails?.ids?.tmdb,
|
||||||
|
traktId = mediaDetails?.ids?.trakt,
|
||||||
|
traktSlug = mediaDetails?.ids?.slug,
|
||||||
|
tmdbId = mediaDetails?.ids?.tmdb,
|
||||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
imdbId = mediaDetails?.ids?.imdb.toString(),
|
||||||
tvdbId = mediaDetails?.ids?.tvdb,
|
tvdbId = mediaDetails?.ids?.tvdb,
|
||||||
|
tvrageId = mediaDetails?.ids?.tvrage,
|
||||||
type = data.type.toString(),
|
type = data.type.toString(),
|
||||||
season = episode.season,
|
season = episode.season,
|
||||||
episode = episode.number,
|
episode = episode.number,
|
||||||
|
@ -220,7 +227,6 @@ open class TraktProvider : MainAPI() {
|
||||||
episodes = episodes
|
episodes = episodes
|
||||||
) {
|
) {
|
||||||
this.name = mediaDetails.title
|
this.name = mediaDetails.title
|
||||||
this.apiName = "Trakt"
|
|
||||||
this.type = if (isAnime) TvType.Anime else TvType.TvSeries
|
this.type = if (isAnime) TvType.Anime else TvType.TvSeries
|
||||||
this.episodes = episodes
|
this.episodes = episodes
|
||||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
||||||
|
@ -406,8 +412,12 @@ open class TraktProvider : MainAPI() {
|
||||||
|
|
||||||
data class LinkData(
|
data class LinkData(
|
||||||
val id: Int? = null,
|
val id: Int? = null,
|
||||||
|
val traktId: Int? = null,
|
||||||
|
val traktSlug: String? = null,
|
||||||
|
val tmdbId: Int? = null,
|
||||||
val imdbId: String? = null,
|
val imdbId: String? = null,
|
||||||
val tvdbId: Int? = null,
|
val tvdbId: Int? = null,
|
||||||
|
val tvrageId: String? = null,
|
||||||
val type: String? = null,
|
val type: String? = null,
|
||||||
val season: Int? = null,
|
val season: Int? = null,
|
||||||
val episode: Int? = null,
|
val episode: Int? = null,
|
||||||
|
|
|
@ -11,10 +11,14 @@ import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding
|
||||||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
||||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -85,13 +89,15 @@ class DownloadChildFragment : Fragment() {
|
||||||
|
|
||||||
binding?.downloadChildToolbar?.apply {
|
binding?.downloadChildToolbar?.apply {
|
||||||
title = name
|
title = name
|
||||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
if (isLayout(PHONE or EMULATOR)) {
|
||||||
setNavigationOnClickListener {
|
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
setNavigationOnClickListener {
|
||||||
|
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
setAppBarNoScrollFlagsOnTV()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
||||||
DownloadChildAdapter(
|
DownloadChildAdapter(
|
||||||
ArrayList(),
|
ArrayList(),
|
||||||
|
|
|
@ -41,6 +41,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
@ -97,6 +98,8 @@ class DownloadFragment : Fragment() {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
|
|
||||||
|
binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV()
|
||||||
|
|
||||||
observe(downloadsViewModel.noDownloadsText) {
|
observe(downloadsViewModel.noDownloadsText) {
|
||||||
binding?.textNoDownloads?.text = it
|
binding?.textNoDownloads?.text = it
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@ import androidx.annotation.MainThread
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE
|
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE
|
||||||
|
@ -25,6 +27,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
|
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES
|
||||||
|
|
||||||
|
|
||||||
open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
|
@ -167,6 +170,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
|
||||||
this.setPersistentId(card.id)
|
this.setPersistentId(card.id)
|
||||||
view.setOnClickListener {
|
view.setOnClickListener {
|
||||||
if (isZeroBytes) {
|
if (isZeroBytes) {
|
||||||
|
removeKey(KEY_RESUME_PACKAGES, card.id.toString())
|
||||||
callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card))
|
callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card))
|
||||||
//callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data))
|
//callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -10,7 +10,8 @@ enum class LoadType {
|
||||||
InAppDownload,
|
InAppDownload,
|
||||||
ExternalApp,
|
ExternalApp,
|
||||||
Browser,
|
Browser,
|
||||||
Chromecast
|
Chromecast,
|
||||||
|
Fcast
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LoadType.toSet() : Set<ExtractorLinkType> {
|
fun LoadType.toSet() : Set<ExtractorLinkType> {
|
||||||
|
@ -29,12 +30,17 @@ fun LoadType.toSet() : Set<ExtractorLinkType> {
|
||||||
ExtractorLinkType.VIDEO,
|
ExtractorLinkType.VIDEO,
|
||||||
ExtractorLinkType.M3U8
|
ExtractorLinkType.M3U8
|
||||||
)
|
)
|
||||||
LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet()
|
LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.entries.toSet()
|
||||||
LoadType.Chromecast -> setOf(
|
LoadType.Chromecast -> setOf(
|
||||||
ExtractorLinkType.VIDEO,
|
ExtractorLinkType.VIDEO,
|
||||||
ExtractorLinkType.DASH,
|
ExtractorLinkType.DASH,
|
||||||
ExtractorLinkType.M3U8
|
ExtractorLinkType.M3U8
|
||||||
)
|
)
|
||||||
|
LoadType.Fcast -> setOf(
|
||||||
|
ExtractorLinkType.VIDEO,
|
||||||
|
ExtractorLinkType.DASH,
|
||||||
|
ExtractorLinkType.M3U8
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,9 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchViewModel
|
import com.lagradost.cloudstream3.ui.search.SearchViewModel
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
|
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
|
||||||
|
@ -274,8 +277,13 @@ class QuickSearchFragment : Fragment() {
|
||||||
// UIHelper.showInputMethod(view.findFocus())
|
// UIHelper.showInputMethod(view.findFocus())
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
binding?.quickSearchBack?.setOnClickListener {
|
if (isLayout(PHONE or EMULATOR)) {
|
||||||
activity?.popCurrentPage()
|
binding?.quickSearchBack?.apply {
|
||||||
|
isVisible = true
|
||||||
|
setOnClickListener {
|
||||||
|
activity?.popCurrentPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLayout(TV)) {
|
if (isLayout(TV)) {
|
||||||
|
|
|
@ -9,9 +9,11 @@ import androidx.core.view.isVisible
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding
|
import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding
|
||||||
import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding
|
import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable
|
||||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
|
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
|
||||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK
|
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK
|
||||||
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
|
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
|
||||||
|
@ -23,6 +25,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.html
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
const val ACTION_PLAY_EPISODE_IN_PLAYER = 1
|
const val ACTION_PLAY_EPISODE_IN_PLAYER = 1
|
||||||
|
@ -51,6 +55,8 @@ const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16
|
||||||
const val ACTION_PLAY_EPISODE_IN_MPV = 17
|
const val ACTION_PLAY_EPISODE_IN_MPV = 17
|
||||||
|
|
||||||
const val ACTION_MARK_AS_WATCHED = 18
|
const val ACTION_MARK_AS_WATCHED = 18
|
||||||
|
const val ACTION_FCAST = 19
|
||||||
|
|
||||||
const val TV_EP_SIZE_LARGE = 400
|
const val TV_EP_SIZE_LARGE = 400
|
||||||
const val TV_EP_SIZE_SMALL = 300
|
const val TV_EP_SIZE_SMALL = 300
|
||||||
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
|
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
|
||||||
|
@ -104,7 +110,7 @@ class EpisodeAdapter(
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
override fun getItemViewType(position: Int): Int {
|
||||||
val item = getItem(position)
|
val item = getItem(position)
|
||||||
return if (item.poster.isNullOrBlank()) 0 else 1
|
return if (item.poster.isNullOrBlank() && item.description.isNullOrBlank()) 0 else 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -260,6 +266,33 @@ class EpisodeAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (card.airDate != null) {
|
||||||
|
val isUpcoming = unixTimeMS < card.airDate
|
||||||
|
|
||||||
|
if (isUpcoming) {
|
||||||
|
episodePlayIcon.isVisible = false
|
||||||
|
episodeUpcomingIcon.isVisible = !episodePoster.isVisible
|
||||||
|
episodeDate.setText(
|
||||||
|
txt(
|
||||||
|
R.string.episode_upcoming_format,
|
||||||
|
secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
episodeUpcomingIcon.isVisible = false
|
||||||
|
|
||||||
|
val formattedAirDate = SimpleDateFormat.getDateInstance(
|
||||||
|
DateFormat.LONG,
|
||||||
|
Locale.getDefault()
|
||||||
|
).apply {
|
||||||
|
}.format(Date(card.airDate))
|
||||||
|
|
||||||
|
episodeDate.setText(txt(formattedAirDate))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
episodeDate.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
if (isLayout(EMULATOR or PHONE)) {
|
if (isLayout(EMULATOR or PHONE)) {
|
||||||
episodePoster.setOnClickListener {
|
episodePoster.setOnClickListener {
|
||||||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||||
|
@ -271,6 +304,7 @@ class EpisodeAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ data class ResultEpisode(
|
||||||
val videoWatchState: VideoWatchState,
|
val videoWatchState: VideoWatchState,
|
||||||
/** Sum of all previous season episode counts + episode */
|
/** Sum of all previous season episode counts + episode */
|
||||||
val totalEpisodeIndex: Int? = null,
|
val totalEpisodeIndex: Int? = null,
|
||||||
|
val airDate: Long? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun ResultEpisode.getRealPosition(): Long {
|
fun ResultEpisode.getRealPosition(): Long {
|
||||||
|
@ -85,6 +86,7 @@ fun buildResultEpisode(
|
||||||
tvType: TvType,
|
tvType: TvType,
|
||||||
parentId: Int,
|
parentId: Int,
|
||||||
totalEpisodeIndex: Int? = null,
|
totalEpisodeIndex: Int? = null,
|
||||||
|
airDate: Long? = null,
|
||||||
): ResultEpisode {
|
): ResultEpisode {
|
||||||
val posDur = getViewPos(id)
|
val posDur = getViewPos(id)
|
||||||
val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None
|
val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None
|
||||||
|
@ -107,7 +109,8 @@ fun buildResultEpisode(
|
||||||
tvType,
|
tvType,
|
||||||
parentId,
|
parentId,
|
||||||
videoWatchState,
|
videoWatchState,
|
||||||
totalEpisodeIndex
|
totalEpisodeIndex,
|
||||||
|
airDate,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,10 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
|
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
|
import com.lagradost.cloudstream3.utils.fcast.FcastManager
|
||||||
|
import com.lagradost.cloudstream3.utils.fcast.FcastSession
|
||||||
|
import com.lagradost.cloudstream3.utils.fcast.Opcode
|
||||||
|
import com.lagradost.cloudstream3.utils.fcast.PlayMessage
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -205,7 +209,11 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}?.also {
|
}?.also {
|
||||||
nextAiringEpisode = txt(R.string.next_episode_format, airing.episode)
|
nextAiringEpisode = when (airing.season) {
|
||||||
|
|
||||||
|
null -> txt(R.string.next_episode_format, airing.episode)
|
||||||
|
else -> txt(R.string.next_season_episode_format, airing.season, airing.episode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1107,13 +1115,14 @@ class ResultViewModel2 : ViewModel() {
|
||||||
|
|
||||||
val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse ->
|
val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse ->
|
||||||
val librarySyncData = it.syncData
|
val librarySyncData = it.syncData
|
||||||
|
val yearCheck = year == it.year || year == null || it.year == null
|
||||||
|
|
||||||
val checks = listOf(
|
val checks = listOf(
|
||||||
{ imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId },
|
{ imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId },
|
||||||
{ tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId },
|
{ tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId },
|
||||||
{ malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId },
|
{ malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId },
|
||||||
{ aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId },
|
{ aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId },
|
||||||
{ normalizedName == normalizeString(it.name) && year == it.year }
|
{ normalizedName == normalizeString(it.name) && yearCheck }
|
||||||
)
|
)
|
||||||
|
|
||||||
checks.any { it() }
|
checks.any { it() }
|
||||||
|
@ -1522,6 +1531,13 @@ class ResultViewModel2 : ViewModel() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (FcastManager.currentDevices.isNotEmpty()) {
|
||||||
|
options.add(
|
||||||
|
txt(R.string.player_settings_play_in_fcast) to ACTION_FCAST
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER)
|
options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER)
|
||||||
|
|
||||||
for (app in apps) {
|
for (app in apps) {
|
||||||
|
@ -1697,6 +1713,39 @@ class ResultViewModel2 : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ACTION_FCAST -> {
|
||||||
|
val devices = FcastManager.currentDevices.toList()
|
||||||
|
postPopup(
|
||||||
|
txt(R.string.player_settings_select_cast_device),
|
||||||
|
devices.map { txt(it.name) }) { index ->
|
||||||
|
if (index == null) return@postPopup
|
||||||
|
val device = devices.getOrNull(index)
|
||||||
|
|
||||||
|
acquireSingleLink(
|
||||||
|
click.data,
|
||||||
|
LoadType.Fcast,
|
||||||
|
txt(R.string.episode_action_cast_mirror)
|
||||||
|
) { (result, index) ->
|
||||||
|
val host = device?.host ?: return@acquireSingleLink
|
||||||
|
val link = result.links.firstOrNull() ?: return@acquireSingleLink
|
||||||
|
|
||||||
|
FcastSession(host).use { session ->
|
||||||
|
session.sendMessage(
|
||||||
|
Opcode.Play,
|
||||||
|
PlayMessage(
|
||||||
|
link.type.getMimeType(),
|
||||||
|
link.url,
|
||||||
|
headers = mapOf(
|
||||||
|
"referer" to link.referer,
|
||||||
|
"user-agent" to USER_AGENT
|
||||||
|
) + link.headers
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink(
|
ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink(
|
||||||
click.data,
|
click.data,
|
||||||
LoadType.Browser,
|
LoadType.Browser,
|
||||||
|
@ -2285,7 +2334,8 @@ class ResultViewModel2 : ViewModel() {
|
||||||
fillers.getOrDefault(episode, false),
|
fillers.getOrDefault(episode, false),
|
||||||
loadResponse.type,
|
loadResponse.type,
|
||||||
mainId,
|
mainId,
|
||||||
totalIndex
|
totalIndex,
|
||||||
|
airDate = i.date
|
||||||
)
|
)
|
||||||
|
|
||||||
val season = eps.seasonIndex ?: 0
|
val season = eps.seasonIndex ?: 0
|
||||||
|
@ -2334,7 +2384,8 @@ class ResultViewModel2 : ViewModel() {
|
||||||
null,
|
null,
|
||||||
loadResponse.type,
|
loadResponse.type,
|
||||||
mainId,
|
mainId,
|
||||||
totalIndex
|
totalIndex,
|
||||||
|
airDate = episode.date
|
||||||
)
|
)
|
||||||
|
|
||||||
val season = ep.seasonIndex ?: 0
|
val season = ep.seasonIndex ?: 0
|
||||||
|
|
|
@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.account
|
||||||
import com.lagradost.cloudstream3.ui.home.HomeFragment
|
import com.lagradost.cloudstream3.ui.home.HomeFragment
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
|
@ -84,9 +85,11 @@ class SettingsFragment : Fragment() {
|
||||||
|
|
||||||
settingsToolbar.apply {
|
settingsToolbar.apply {
|
||||||
setTitle(title)
|
setTitle(title)
|
||||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
if (isLayout(PHONE or EMULATOR)) {
|
||||||
setNavigationOnClickListener {
|
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
setNavigationOnClickListener {
|
||||||
|
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UIHelper.fixPaddingStatusbar(settingsToolbar)
|
UIHelper.fixPaddingStatusbar(settingsToolbar)
|
||||||
|
@ -98,10 +101,12 @@ class SettingsFragment : Fragment() {
|
||||||
|
|
||||||
settingsToolbar.apply {
|
settingsToolbar.apply {
|
||||||
setTitle(title)
|
setTitle(title)
|
||||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
if (isLayout(PHONE or EMULATOR)) {
|
||||||
children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
|
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||||
setNavigationOnClickListener {
|
children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
|
||||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
setNavigationOnClickListener {
|
||||||
|
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UIHelper.fixPaddingStatusbar(settingsToolbar)
|
UIHelper.fixPaddingStatusbar(settingsToolbar)
|
||||||
|
|
|
@ -35,6 +35,9 @@ import okhttp3.internal.closeQuietly
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.lang.System.currentTimeMillis
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class SettingsUpdates : PreferenceFragmentCompat() {
|
class SettingsUpdates : PreferenceFragmentCompat() {
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
@ -125,12 +128,12 @@ class SettingsUpdates : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.saveBtt.setOnClickListener {
|
binding.saveBtt.setOnClickListener {
|
||||||
|
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
|
||||||
var fileStream: OutputStream? = null
|
var fileStream: OutputStream? = null
|
||||||
try {
|
try {
|
||||||
fileStream =
|
fileStream = VideoDownloadManager.setupStream(
|
||||||
VideoDownloadManager.setupStream(
|
|
||||||
it.context,
|
it.context,
|
||||||
"logcat",
|
"logcat_${date}",
|
||||||
null,
|
null,
|
||||||
"txt",
|
"txt",
|
||||||
false
|
false
|
||||||
|
|
|
@ -53,6 +53,7 @@ import com.lagradost.cloudstream3.extractors.FileMoonIn
|
||||||
import com.lagradost.cloudstream3.extractors.FileMoonSx
|
import com.lagradost.cloudstream3.extractors.FileMoonSx
|
||||||
import com.lagradost.cloudstream3.extractors.Filesim
|
import com.lagradost.cloudstream3.extractors.Filesim
|
||||||
import com.lagradost.cloudstream3.extractors.Fplayer
|
import com.lagradost.cloudstream3.extractors.Fplayer
|
||||||
|
import com.lagradost.cloudstream3.extractors.Geodailymotion
|
||||||
import com.lagradost.cloudstream3.extractors.GMPlayer
|
import com.lagradost.cloudstream3.extractors.GMPlayer
|
||||||
import com.lagradost.cloudstream3.extractors.Gdriveplayer
|
import com.lagradost.cloudstream3.extractors.Gdriveplayer
|
||||||
import com.lagradost.cloudstream3.extractors.Gdriveplayerapi
|
import com.lagradost.cloudstream3.extractors.Gdriveplayerapi
|
||||||
|
@ -83,6 +84,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream
|
||||||
import com.lagradost.cloudstream3.extractors.Mcloud
|
import com.lagradost.cloudstream3.extractors.Mcloud
|
||||||
import com.lagradost.cloudstream3.extractors.Megacloud
|
import com.lagradost.cloudstream3.extractors.Megacloud
|
||||||
import com.lagradost.cloudstream3.extractors.Meownime
|
import com.lagradost.cloudstream3.extractors.Meownime
|
||||||
|
import com.lagradost.cloudstream3.extractors.MetaGnathTuggers
|
||||||
import com.lagradost.cloudstream3.extractors.Minoplres
|
import com.lagradost.cloudstream3.extractors.Minoplres
|
||||||
import com.lagradost.cloudstream3.extractors.MixDrop
|
import com.lagradost.cloudstream3.extractors.MixDrop
|
||||||
import com.lagradost.cloudstream3.extractors.MixDropBz
|
import com.lagradost.cloudstream3.extractors.MixDropBz
|
||||||
|
@ -139,6 +141,7 @@ import com.lagradost.cloudstream3.extractors.Sbspeed
|
||||||
import com.lagradost.cloudstream3.extractors.Sbthe
|
import com.lagradost.cloudstream3.extractors.Sbthe
|
||||||
import com.lagradost.cloudstream3.extractors.Sendvid
|
import com.lagradost.cloudstream3.extractors.Sendvid
|
||||||
import com.lagradost.cloudstream3.extractors.ShaveTape
|
import com.lagradost.cloudstream3.extractors.ShaveTape
|
||||||
|
import com.lagradost.cloudstream3.extractors.Simpulumlamerop
|
||||||
import com.lagradost.cloudstream3.extractors.Solidfiles
|
import com.lagradost.cloudstream3.extractors.Solidfiles
|
||||||
import com.lagradost.cloudstream3.extractors.Ssbstream
|
import com.lagradost.cloudstream3.extractors.Ssbstream
|
||||||
import com.lagradost.cloudstream3.extractors.StreamM4u
|
import com.lagradost.cloudstream3.extractors.StreamM4u
|
||||||
|
@ -175,6 +178,7 @@ import com.lagradost.cloudstream3.extractors.UpstreamExtractor
|
||||||
import com.lagradost.cloudstream3.extractors.Uqload
|
import com.lagradost.cloudstream3.extractors.Uqload
|
||||||
import com.lagradost.cloudstream3.extractors.Uqload1
|
import com.lagradost.cloudstream3.extractors.Uqload1
|
||||||
import com.lagradost.cloudstream3.extractors.Uqload2
|
import com.lagradost.cloudstream3.extractors.Uqload2
|
||||||
|
import com.lagradost.cloudstream3.extractors.Urochsunloath
|
||||||
import com.lagradost.cloudstream3.extractors.Userload
|
import com.lagradost.cloudstream3.extractors.Userload
|
||||||
import com.lagradost.cloudstream3.extractors.Userscloud
|
import com.lagradost.cloudstream3.extractors.Userscloud
|
||||||
import com.lagradost.cloudstream3.extractors.Uservideo
|
import com.lagradost.cloudstream3.extractors.Uservideo
|
||||||
|
@ -182,10 +186,12 @@ import com.lagradost.cloudstream3.extractors.Vanfem
|
||||||
import com.lagradost.cloudstream3.extractors.Vicloud
|
import com.lagradost.cloudstream3.extractors.Vicloud
|
||||||
import com.lagradost.cloudstream3.extractors.VidSrcExtractor
|
import com.lagradost.cloudstream3.extractors.VidSrcExtractor
|
||||||
import com.lagradost.cloudstream3.extractors.VidSrcExtractor2
|
import com.lagradost.cloudstream3.extractors.VidSrcExtractor2
|
||||||
|
import com.lagradost.cloudstream3.extractors.VidSrcTo
|
||||||
import com.lagradost.cloudstream3.extractors.VideoVard
|
import com.lagradost.cloudstream3.extractors.VideoVard
|
||||||
import com.lagradost.cloudstream3.extractors.VideovardSX
|
import com.lagradost.cloudstream3.extractors.VideovardSX
|
||||||
import com.lagradost.cloudstream3.extractors.Vidgomunime
|
import com.lagradost.cloudstream3.extractors.Vidgomunime
|
||||||
import com.lagradost.cloudstream3.extractors.Vidgomunimesb
|
import com.lagradost.cloudstream3.extractors.Vidgomunimesb
|
||||||
|
import com.lagradost.cloudstream3.extractors.Vidguardto
|
||||||
import com.lagradost.cloudstream3.extractors.VidhideExtractor
|
import com.lagradost.cloudstream3.extractors.VidhideExtractor
|
||||||
import com.lagradost.cloudstream3.extractors.Vidmoly
|
import com.lagradost.cloudstream3.extractors.Vidmoly
|
||||||
import com.lagradost.cloudstream3.extractors.Vidmolyme
|
import com.lagradost.cloudstream3.extractors.Vidmolyme
|
||||||
|
@ -207,6 +213,7 @@ import com.lagradost.cloudstream3.extractors.Watchx
|
||||||
import com.lagradost.cloudstream3.extractors.WcoStream
|
import com.lagradost.cloudstream3.extractors.WcoStream
|
||||||
import com.lagradost.cloudstream3.extractors.Wibufile
|
import com.lagradost.cloudstream3.extractors.Wibufile
|
||||||
import com.lagradost.cloudstream3.extractors.XStreamCdn
|
import com.lagradost.cloudstream3.extractors.XStreamCdn
|
||||||
|
import com.lagradost.cloudstream3.extractors.Yipsu
|
||||||
import com.lagradost.cloudstream3.extractors.YourUpload
|
import com.lagradost.cloudstream3.extractors.YourUpload
|
||||||
import com.lagradost.cloudstream3.extractors.YoutubeExtractor
|
import com.lagradost.cloudstream3.extractors.YoutubeExtractor
|
||||||
import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor
|
import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor
|
||||||
|
@ -301,7 +308,18 @@ enum class ExtractorLinkType {
|
||||||
/** No support at the moment */
|
/** No support at the moment */
|
||||||
TORRENT,
|
TORRENT,
|
||||||
/** No support at the moment */
|
/** No support at the moment */
|
||||||
MAGNET,
|
MAGNET;
|
||||||
|
|
||||||
|
// See https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||||
|
fun getMimeType(): String {
|
||||||
|
return when (this) {
|
||||||
|
VIDEO -> "video/mp4"
|
||||||
|
M3U8 -> "application/x-mpegURL"
|
||||||
|
DASH -> "application/dash+xml"
|
||||||
|
TORRENT -> "application/x-bittorrent"
|
||||||
|
MAGNET -> "application/x-bittorrent"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun inferTypeFromUrl(url: String): ExtractorLinkType {
|
private fun inferTypeFromUrl(url: String): ExtractorLinkType {
|
||||||
|
@ -871,6 +889,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
||||||
Streamlare(),
|
Streamlare(),
|
||||||
VidSrcExtractor(),
|
VidSrcExtractor(),
|
||||||
VidSrcExtractor2(),
|
VidSrcExtractor2(),
|
||||||
|
VidSrcTo(),
|
||||||
PlayLtXyz(),
|
PlayLtXyz(),
|
||||||
AStreamHub(),
|
AStreamHub(),
|
||||||
Vidplay(),
|
Vidplay(),
|
||||||
|
@ -888,7 +907,14 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
||||||
StreamWishExtractor(),
|
StreamWishExtractor(),
|
||||||
EmturbovidExtractor(),
|
EmturbovidExtractor(),
|
||||||
Vtbe(),
|
Vtbe(),
|
||||||
EPlayExtractor()
|
EPlayExtractor(),
|
||||||
|
Vidguardto(),
|
||||||
|
Simpulumlamerop(),
|
||||||
|
Urochsunloath(),
|
||||||
|
Yipsu(),
|
||||||
|
MetaGnathTuggers(),
|
||||||
|
Geodailymotion(),
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ import androidx.core.view.marginBottom
|
||||||
import androidx.core.view.marginLeft
|
import androidx.core.view.marginLeft
|
||||||
import androidx.core.view.marginRight
|
import androidx.core.view.marginRight
|
||||||
import androidx.core.view.marginTop
|
import androidx.core.view.marginTop
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
|
@ -58,6 +59,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||||
import com.bumptech.glide.request.RequestListener
|
import com.bumptech.glide.request.RequestListener
|
||||||
import com.bumptech.glide.request.RequestOptions.bitmapTransform
|
import com.bumptech.glide.request.RequestOptions.bitmapTransform
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.chip.ChipDrawable
|
import com.google.android.material.chip.ChipDrawable
|
||||||
import com.google.android.material.chip.ChipGroup
|
import com.google.android.material.chip.ChipGroup
|
||||||
|
@ -208,6 +210,14 @@ object UIHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun View?.setAppBarNoScrollFlagsOnTV() {
|
||||||
|
if (isLayout(Globals.TV or EMULATOR)) {
|
||||||
|
this?.updateLayoutParams<AppBarLayout.LayoutParams> {
|
||||||
|
scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun Activity.hideKeyboard() {
|
fun Activity.hideKeyboard() {
|
||||||
window?.decorView?.clearFocus()
|
window?.decorView?.clearFocus()
|
||||||
this.findViewById<View>(android.R.id.content)?.rootView?.let {
|
this.findViewById<View>(android.R.id.content)?.rootView?.let {
|
||||||
|
|
|
@ -187,7 +187,7 @@ object VideoDownloadManager {
|
||||||
private val DOWNLOAD_BAD_CONFIG =
|
private val DOWNLOAD_BAD_CONFIG =
|
||||||
DownloadStatus(retrySame = false, tryNext = false, success = false)
|
DownloadStatus(retrySame = false, tryNext = false, success = false)
|
||||||
|
|
||||||
private const val KEY_RESUME_PACKAGES = "download_resume"
|
const val KEY_RESUME_PACKAGES = "download_resume"
|
||||||
const val KEY_DOWNLOAD_INFO = "download_info"
|
const val KEY_DOWNLOAD_INFO = "download_info"
|
||||||
private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume"
|
private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
package com.lagradost.cloudstream3.utils.fcast
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.nsd.NsdManager
|
||||||
|
import android.net.nsd.NsdManager.ResolveListener
|
||||||
|
import android.net.nsd.NsdServiceInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
|
|
||||||
|
class FcastManager {
|
||||||
|
private var nsdManager: NsdManager? = null
|
||||||
|
|
||||||
|
// Used for receiver
|
||||||
|
private val registrationListenerTcp = DefaultRegistrationListener()
|
||||||
|
private fun getDeviceName(): String {
|
||||||
|
return "${Build.MANUFACTURER}-${Build.MODEL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the fcast service
|
||||||
|
* @param registerReceiver If true will register the app as a compatible fcast receiver for discovery in other app
|
||||||
|
*/
|
||||||
|
fun init(context: Context, registerReceiver: Boolean) = ioSafe {
|
||||||
|
nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||||||
|
val serviceType = "_fcast._tcp"
|
||||||
|
|
||||||
|
if (registerReceiver) {
|
||||||
|
val serviceName = "$APP_PREFIX-${getDeviceName()}"
|
||||||
|
|
||||||
|
val serviceInfo = NsdServiceInfo().apply {
|
||||||
|
this.serviceName = serviceName
|
||||||
|
this.serviceType = serviceType
|
||||||
|
this.port = TCP_PORT
|
||||||
|
}
|
||||||
|
|
||||||
|
nsdManager?.registerService(
|
||||||
|
serviceInfo,
|
||||||
|
NsdManager.PROTOCOL_DNS_SD,
|
||||||
|
registrationListenerTcp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
nsdManager?.discoverServices(
|
||||||
|
serviceType,
|
||||||
|
NsdManager.PROTOCOL_DNS_SD,
|
||||||
|
DefaultDiscoveryListener()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
nsdManager?.unregisterService(registrationListenerTcp)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class DefaultDiscoveryListener : NsdManager.DiscoveryListener {
|
||||||
|
val tag = "DiscoveryListener"
|
||||||
|
override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {
|
||||||
|
Log.d(tag, "Discovery failed: $serviceType, error code: $errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) {
|
||||||
|
Log.d(tag, "Stop discovery failed: $serviceType, error code: $errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryStarted(serviceType: String?) {
|
||||||
|
Log.d(tag, "Discovery started: $serviceType")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDiscoveryStopped(serviceType: String?) {
|
||||||
|
Log.d(tag, "Discovery stopped: $serviceType")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceFound(serviceInfo: NsdServiceInfo?) {
|
||||||
|
if (serviceInfo == null) return
|
||||||
|
nsdManager?.resolveService(serviceInfo, object : ResolveListener {
|
||||||
|
override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceResolved(serviceInfo: NsdServiceInfo?) {
|
||||||
|
if (serviceInfo == null) return
|
||||||
|
|
||||||
|
currentDevices.add(PublicDeviceInfo(serviceInfo))
|
||||||
|
|
||||||
|
Log.d(
|
||||||
|
tag,
|
||||||
|
"Service found: ${serviceInfo.serviceName}, Net: ${serviceInfo.host.hostAddress}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {
|
||||||
|
if (serviceInfo == null) return
|
||||||
|
|
||||||
|
// May remove duplicates, but net and port is null here, preventing device specific identification
|
||||||
|
currentDevices.removeAll {
|
||||||
|
it.rawName == serviceInfo.serviceName
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(tag, "Service lost: ${serviceInfo.serviceName}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val APP_PREFIX = "CloudStream"
|
||||||
|
val currentDevices: MutableList<PublicDeviceInfo> = mutableListOf()
|
||||||
|
|
||||||
|
class DefaultRegistrationListener : NsdManager.RegistrationListener {
|
||||||
|
val tag = "DiscoveryService"
|
||||||
|
override fun onServiceRegistered(serviceInfo: NsdServiceInfo) {
|
||||||
|
Log.d(tag, "Service registered: ${serviceInfo.serviceName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||||
|
Log.e(tag, "Service registration failed: errorCode=$errorCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) {
|
||||||
|
Log.d(tag, "Service unregistered: ${serviceInfo.serviceName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||||||
|
Log.e(tag, "Service unregistration failed: errorCode=$errorCode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const val TCP_PORT = 46899
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PublicDeviceInfo(serviceInfo: NsdServiceInfo) {
|
||||||
|
val rawName: String = serviceInfo.serviceName
|
||||||
|
val host: String? = serviceInfo.host.hostAddress
|
||||||
|
val name = rawName.replace("-", " ") + host?.let { " $it" }
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package com.lagradost.cloudstream3.utils.fcast
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
|
import com.lagradost.safefile.closeQuietly
|
||||||
|
import java.io.DataOutputStream
|
||||||
|
import java.net.Socket
|
||||||
|
import kotlin.jvm.Throws
|
||||||
|
|
||||||
|
class FcastSession(private val hostAddress: String): AutoCloseable {
|
||||||
|
val tag = "FcastSession"
|
||||||
|
|
||||||
|
private var socket: Socket? = null
|
||||||
|
@Throws
|
||||||
|
@WorkerThread
|
||||||
|
fun open(): Socket {
|
||||||
|
val socket = Socket(hostAddress, FcastManager.TCP_PORT)
|
||||||
|
this.socket = socket
|
||||||
|
return socket
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
socket?.closeQuietly()
|
||||||
|
socket = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws
|
||||||
|
private fun acquireSocket(): Socket {
|
||||||
|
return socket ?: open()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ping() {
|
||||||
|
sendMessage(Opcode.Ping, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> sendMessage(opcode: Opcode, message: T) {
|
||||||
|
ioSafe {
|
||||||
|
val socket = acquireSocket()
|
||||||
|
val outputStream = DataOutputStream(socket.getOutputStream())
|
||||||
|
|
||||||
|
val json = message?.toJson()
|
||||||
|
val content = json?.toByteArray() ?: ByteArray(0)
|
||||||
|
|
||||||
|
// Little endian starting from 1
|
||||||
|
// https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1
|
||||||
|
val size = content.size + 1
|
||||||
|
|
||||||
|
val sizeArray = ByteArray(4) { num ->
|
||||||
|
(size shr 8 * num and 0xff).toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(tag, "Sending message with size: $size, opcode: $opcode")
|
||||||
|
outputStream.write(sizeArray)
|
||||||
|
outputStream.write(ByteArray(1) { opcode.value })
|
||||||
|
outputStream.write(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package com.lagradost.cloudstream3.utils.fcast
|
||||||
|
|
||||||
|
// See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1
|
||||||
|
enum class Opcode(val value: Byte) {
|
||||||
|
None(0),
|
||||||
|
Play(1),
|
||||||
|
Pause(2),
|
||||||
|
Resume(3),
|
||||||
|
Stop(4),
|
||||||
|
Seek(5),
|
||||||
|
PlaybackUpdate(6),
|
||||||
|
VolumeUpdate(7),
|
||||||
|
SetVolume(8),
|
||||||
|
PlaybackError(9),
|
||||||
|
SetSpeed(10),
|
||||||
|
Version(11),
|
||||||
|
Ping(12),
|
||||||
|
Pong(13);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class PlayMessage(
|
||||||
|
val container: String,
|
||||||
|
val url: String? = null,
|
||||||
|
val content: String? = null,
|
||||||
|
val time: Double? = null,
|
||||||
|
val speed: Double? = null,
|
||||||
|
val headers: Map<String, String>? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SeekMessage(
|
||||||
|
val time: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PlaybackUpdateMessage(
|
||||||
|
val generationTime: Long,
|
||||||
|
val time: Double,
|
||||||
|
val duration: Double,
|
||||||
|
val state: Int,
|
||||||
|
val speed: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VolumeUpdateMessage(
|
||||||
|
val generationTime: Long,
|
||||||
|
val volume: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PlaybackErrorMessage(
|
||||||
|
val message: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SetSpeedMessage(
|
||||||
|
val speed: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SetVolumeMessage(
|
||||||
|
val volume: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VersionMessage(
|
||||||
|
val version: Long
|
||||||
|
)
|
9
app/src/main/res/drawable/hourglass_24.xml
Normal file
9
app/src/main/res/drawable/hourglass_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#9BA0A4"
|
||||||
|
android:pathData="M320,800h320v-120q0,-66 -47,-113t-113,-47q-66,0 -113,47t-47,113v120ZM480,440q66,0 113,-47t47,-113v-120L320,160v120q0,66 47,113t113,47ZM160,880v-80h80v-120q0,-61 28.5,-114.5T348,480q-51,-32 -79.5,-85.5T240,280v-120h-80v-80h640v80h-80v120q0,61 -28.5,114.5T612,480q51,32 79.5,85.5T720,680v120h80v80L160,880ZM480,800ZM480,160Z"/>
|
||||||
|
</vector>
|
|
@ -40,7 +40,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:layout_marginEnd="30dp">
|
android:layout_marginEnd="40dp">
|
||||||
|
|
||||||
<androidx.appcompat.widget.SearchView
|
<androidx.appcompat.widget.SearchView
|
||||||
android:id="@+id/subtitles_search"
|
android:id="@+id/subtitles_search"
|
||||||
|
@ -91,7 +91,8 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center_vertical|end"
|
android:layout_gravity="center_vertical|end"
|
||||||
android:layout_marginEnd="20dp"
|
android:nextFocusLeft="@id/subtitles_search"
|
||||||
|
android:nextFocusRight="@id/search_filter"
|
||||||
android:text="@string/none"
|
android:text="@string/none"
|
||||||
/>
|
/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -106,7 +107,7 @@
|
||||||
android:layout_margin="10dp"
|
android:layout_margin="10dp"
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
android:contentDescription="@string/change_providers_img_des"
|
android:contentDescription="@string/change_providers_img_des"
|
||||||
android:nextFocusLeft="@id/main_search"
|
android:nextFocusLeft="@id/year_btt"
|
||||||
android:nextFocusRight="@id/main_search"
|
android:nextFocusRight="@id/main_search"
|
||||||
android:nextFocusUp="@id/nav_rail_view"
|
android:nextFocusUp="@id/nav_rail_view"
|
||||||
android:nextFocusDown="@id/search_autofit_results"
|
android:nextFocusDown="@id/search_autofit_results"
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
android:layout_marginBottom="5dp"
|
android:layout_marginBottom="5dp"
|
||||||
android:foreground="@drawable/outline_drawable"
|
android:foreground="@drawable/outline_drawable"
|
||||||
|
android:focusable="true"
|
||||||
android:nextFocusLeft="@id/nav_rail_view"
|
android:nextFocusLeft="@id/nav_rail_view"
|
||||||
android:nextFocusRight="@id/download_button"
|
android:nextFocusRight="@id/download_button"
|
||||||
app:cardBackgroundColor="@color/transparent"
|
app:cardBackgroundColor="@color/transparent"
|
||||||
|
@ -84,7 +85,9 @@
|
||||||
android:layout_height="@dimen/download_size"
|
android:layout_height="@dimen/download_size"
|
||||||
android:layout_gravity="center_vertical|end"
|
android:layout_gravity="center_vertical|end"
|
||||||
android:layout_marginStart="-50dp"
|
android:layout_marginStart="-50dp"
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
android:foreground="@drawable/outline_drawable"
|
||||||
|
android:focusable="true"
|
||||||
|
android:nextFocusLeft="@id/download_child_episode_holder"
|
||||||
android:padding="10dp" />
|
android:padding="10dp" />
|
||||||
</GridLayout>
|
</GridLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
|
@ -9,6 +9,8 @@
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_marginEnd="10dp"
|
android:layout_marginEnd="10dp"
|
||||||
android:foreground="@drawable/outline_drawable"
|
android:foreground="@drawable/outline_drawable"
|
||||||
|
android:focusable="true"
|
||||||
|
android:nextFocusRight="@id/download_button"
|
||||||
app:cardBackgroundColor="?attr/boxItemBackground"
|
app:cardBackgroundColor="?attr/boxItemBackground"
|
||||||
app:cardCornerRadius="@dimen/rounded_image_radius">
|
app:cardCornerRadius="@dimen/rounded_image_radius">
|
||||||
|
|
||||||
|
@ -71,7 +73,9 @@
|
||||||
android:layout_height="@dimen/download_size"
|
android:layout_height="@dimen/download_size"
|
||||||
android:layout_gravity="center_vertical|end"
|
android:layout_gravity="center_vertical|end"
|
||||||
android:layout_marginStart="-50dp"
|
android:layout_marginStart="-50dp"
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
android:foreground="@drawable/outline_drawable"
|
||||||
|
android:focusable="true"
|
||||||
|
android:nextFocusLeft="@id/episode_holder"
|
||||||
android:padding="10dp" />
|
android:padding="10dp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
|
@ -178,42 +178,40 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
tools:text="The Perfect Run The Perfect Run" />
|
tools:text="The Perfect Run The Perfect Run" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/result_episodes_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:textColor="?attr/textColor"
|
||||||
|
android:textSize="17sp"
|
||||||
|
android:textStyle="normal"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"
|
||||||
|
tools:text="8 Episodes" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/result_next_airing_holder"
|
android:id="@+id/result_next_airing_holder"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="start"
|
android:layout_gravity="start"
|
||||||
android:orientation="vertical">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/result_episodes_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:layout_marginEnd="20dp"
|
|
||||||
android:textColor="?attr/textColor"
|
|
||||||
android:textSize="17sp"
|
|
||||||
android:textStyle="normal"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:text="8 Episodes" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/result_next_airing"
|
android:id="@+id/result_next_airing"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_vertical"
|
android:gravity="start"
|
||||||
android:gravity="center"
|
|
||||||
android:textColor="?attr/grayTextColor"
|
android:textColor="?attr/grayTextColor"
|
||||||
android:textSize="17sp"
|
android:textSize="17sp"
|
||||||
android:textStyle="normal"
|
android:textStyle="normal"
|
||||||
tools:text="Episode 1022 will be released in" />
|
android:layout_marginEnd="5dp"
|
||||||
|
tools:text="Season 2 Episode 1022 will be released in" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/result_next_airing_time"
|
android:id="@+id/result_next_airing_time"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:gravity="start"
|
android:gravity="start"
|
||||||
android:textColor="?attr/textColor"
|
android:textColor="?attr/textColor"
|
||||||
android:textSize="17sp"
|
android:textSize="17sp"
|
||||||
|
|
|
@ -23,11 +23,10 @@
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
android:src="@drawable/ic_baseline_arrow_back_24"
|
android:src="@drawable/ic_baseline_arrow_back_24"
|
||||||
app:tint="@android:color/white"
|
app:tint="@android:color/white"
|
||||||
android:focusable="true"
|
android:visibility="gone"
|
||||||
android:layout_width="25dp"
|
android:layout_width="25dp"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
tools:visibility="visible">
|
||||||
<requestFocus />
|
|
||||||
</ImageView>
|
</ImageView>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
|
|
|
@ -43,14 +43,26 @@
|
||||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||||
android:nextFocusRight="@id/download_button"
|
android:nextFocusRight="@id/download_button"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
tools:src="@drawable/example_poster" />
|
tools:src="@drawable/example_poster"
|
||||||
|
tools:visibility="invisible"/>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
android:id="@+id/episode_play_icon"
|
||||||
android:layout_width="36dp"
|
android:layout_width="36dp"
|
||||||
android:layout_height="36dp"
|
android:layout_height="36dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:contentDescription="@string/play_episode"
|
android:contentDescription="@string/play_episode"
|
||||||
android:src="@drawable/play_button" />
|
android:src="@drawable/play_button"
|
||||||
|
tools:visibility="invisible"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/episode_upcoming_icon"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:src="@drawable/hourglass_24"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<androidx.core.widget.ContentLoadingProgressBar
|
<androidx.core.widget.ContentLoadingProgressBar
|
||||||
android:id="@+id/episode_progress"
|
android:id="@+id/episode_progress"
|
||||||
|
@ -100,6 +112,13 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="?attr/grayTextColor"
|
android:textColor="?attr/grayTextColor"
|
||||||
tools:text="Rated: 8.8" />
|
tools:text="Rated: 8.8" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/episode_date"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="?attr/grayTextColor"
|
||||||
|
tools:text="15 Apr 2024" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<com.lagradost.cloudstream3.ui.download.button.PieFetchButton
|
<com.lagradost.cloudstream3.ui.download.button.PieFetchButton
|
||||||
|
|
|
@ -84,6 +84,7 @@
|
||||||
<string name="app_dub_sub_episode_text_format" formatted="true">%1$s Ep %2$d</string>
|
<string name="app_dub_sub_episode_text_format" formatted="true">%1$s Ep %2$d</string>
|
||||||
<string name="cast_format" formatted="true">Cast: %s</string>
|
<string name="cast_format" formatted="true">Cast: %s</string>
|
||||||
<string name="next_episode_format" formatted="true">Episode %d will be released in</string>
|
<string name="next_episode_format" formatted="true">Episode %d will be released in</string>
|
||||||
|
<string name="next_season_episode_format" formatted="true">Season %1$d Episode %2$d will be released in</string>
|
||||||
<string name="next_episode_time_day_format" formatted="true">%1$dd %2$dh %3$dm</string>
|
<string name="next_episode_time_day_format" formatted="true">%1$dd %2$dh %3$dm</string>
|
||||||
<string name="next_episode_time_hour_format" formatted="true">%1$dh %2$dm</string>
|
<string name="next_episode_time_hour_format" formatted="true">%1$dh %2$dm</string>
|
||||||
<string name="next_episode_time_min_format" formatted="true">%dm</string>
|
<string name="next_episode_time_min_format" formatted="true">%dm</string>
|
||||||
|
@ -292,6 +293,7 @@
|
||||||
<string name="episodes">Episodes</string>
|
<string name="episodes">Episodes</string>
|
||||||
<string name="episodes_range">%1$d-%2$d</string>
|
<string name="episodes_range">%1$d-%2$d</string>
|
||||||
<string name="episode_format" formatted="true">%1$d %2$s</string>
|
<string name="episode_format" formatted="true">%1$d %2$s</string>
|
||||||
|
<string name="episode_upcoming_format" formatted="true">Upcoming in %s</string>
|
||||||
<string name="season_short">S</string>
|
<string name="season_short">S</string>
|
||||||
<string name="episode_short">E</string>
|
<string name="episode_short">E</string>
|
||||||
<string name="no_episodes_found">No Episodes found</string>
|
<string name="no_episodes_found">No Episodes found</string>
|
||||||
|
@ -355,6 +357,7 @@
|
||||||
<string name="storage_error">Download error, check storage permissions</string>
|
<string name="storage_error">Download error, check storage permissions</string>
|
||||||
<string name="episode_action_chromecast_episode">Chromecast episode</string>
|
<string name="episode_action_chromecast_episode">Chromecast episode</string>
|
||||||
<string name="episode_action_chromecast_mirror">Chromecast mirror</string>
|
<string name="episode_action_chromecast_mirror">Chromecast mirror</string>
|
||||||
|
<string name="episode_action_cast_mirror">Cast mirror</string>
|
||||||
<string name="episode_action_play_in_app">Play in app</string>
|
<string name="episode_action_play_in_app">Play in app</string>
|
||||||
<string name="episode_action_play_in_format">Play in %s</string>
|
<string name="episode_action_play_in_format">Play in %s</string>
|
||||||
<string name="episode_action_play_in_browser">Play in browser</string>
|
<string name="episode_action_play_in_browser">Play in browser</string>
|
||||||
|
@ -632,7 +635,9 @@
|
||||||
<string name="player_settings_play_in_vlc">VLC</string>
|
<string name="player_settings_play_in_vlc">VLC</string>
|
||||||
<string name="player_settings_play_in_mpv">MPV</string>
|
<string name="player_settings_play_in_mpv">MPV</string>
|
||||||
<string name="player_settings_play_in_web">Web Video Cast</string>
|
<string name="player_settings_play_in_web">Web Video Cast</string>
|
||||||
|
<string name="player_settings_play_in_fcast">Fcast</string>
|
||||||
<string name="player_settings_play_in_browser">Web browser</string>
|
<string name="player_settings_play_in_browser">Web browser</string>
|
||||||
|
<string name="player_settings_select_cast_device">Select cast device</string>
|
||||||
<string name="app_not_found_error">App not found</string>
|
<string name="app_not_found_error">App not found</string>
|
||||||
<string name="all_languages_preference">All Languages</string>
|
<string name="all_languages_preference">All Languages</string>
|
||||||
<string name="skip_type_format" formatted="true">Skip %s</string>
|
<string name="skip_type_format" formatted="true">Skip %s</string>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue