Merge branch 'master' into master

This commit is contained in:
firelight 2024-05-18 13:45:43 +02:00 committed by GitHub
commit b5323df2b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 813 additions and 102 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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
companion object {
private var key: String? = null 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,
) )
} }

View file

@ -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 if (url.contains("geo.dailymotion.com")) {
return "$mainUrl/embed/video/$vid" 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
} }

View file

@ -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.*

View file

@ -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)
}

View file

@ -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
)
}

View file

@ -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"

View file

@ -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)
val videoLinks = mutableListOf<String>()
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( M3u8Helper.generateM3u8(
name, name,
link ?: return, videoLink,
"$mainUrl/", "$mainUrl/",
headers = mapOf("Origin" to "$mainUrl/") headers = mapOf("Origin" to "$mainUrl/")
).forEach(callback) ).forEach(callback)
} }
}
data class WcoSources(
@JsonProperty("VideoLinkDTO") val VideoLinkDTO: String,
)
} }

View file

@ -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.*

View file

@ -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
) )

View file

@ -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,

View file

@ -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,12 +89,14 @@ class DownloadChildFragment : Fragment() {
binding?.downloadChildToolbar?.apply { binding?.downloadChildToolbar?.apply {
title = name title = name
if (isLayout(PHONE or EMULATOR)) {
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
setNavigationOnClickListener { setNavigationOnClickListener {
activity?.onBackPressedDispatcher?.onBackPressed() activity?.onBackPressedDispatcher?.onBackPressed()
} }
} }
setAppBarNoScrollFlagsOnTV()
}
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
DownloadChildAdapter( DownloadChildAdapter(

View file

@ -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
} }

View file

@ -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 {

View file

@ -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
)
} }
} }

View file

@ -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,9 +277,14 @@ class QuickSearchFragment : Fragment() {
// UIHelper.showInputMethod(view.findFocus()) // UIHelper.showInputMethod(view.findFocus())
// } // }
//} //}
binding?.quickSearchBack?.setOnClickListener { if (isLayout(PHONE or EMULATOR)) {
binding?.quickSearchBack?.apply {
isVisible = true
setOnClickListener {
activity?.popCurrentPage() activity?.popCurrentPage()
} }
}
}
if (isLayout(TV)) { if (isLayout(TV)) {
binding?.quickSearch?.requestFocus() binding?.quickSearch?.requestFocus()

View file

@ -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))
} }

View file

@ -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,
) )
} }

View file

@ -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

View file

@ -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,11 +85,13 @@ class SettingsFragment : Fragment() {
settingsToolbar.apply { settingsToolbar.apply {
setTitle(title) setTitle(title)
if (isLayout(PHONE or EMULATOR)) {
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
setNavigationOnClickListener { setNavigationOnClickListener {
activity?.onBackPressedDispatcher?.onBackPressed() activity?.onBackPressedDispatcher?.onBackPressed()
} }
} }
}
UIHelper.fixPaddingStatusbar(settingsToolbar) UIHelper.fixPaddingStatusbar(settingsToolbar)
} }
@ -98,12 +101,14 @@ class SettingsFragment : Fragment() {
settingsToolbar.apply { settingsToolbar.apply {
setTitle(title) setTitle(title)
if (isLayout(PHONE or EMULATOR)) {
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
setNavigationOnClickListener { setNavigationOnClickListener {
activity?.onBackPressedDispatcher?.onBackPressed() activity?.onBackPressedDispatcher?.onBackPressed()
} }
} }
}
UIHelper.fixPaddingStatusbar(settingsToolbar) UIHelper.fixPaddingStatusbar(settingsToolbar)
} }

View file

@ -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

View file

@ -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(),
) )

View file

@ -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 {

View file

@ -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"

View file

@ -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" }
}

View file

@ -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)
}
}
}

View file

@ -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
)

View 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>

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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" />
<LinearLayout
android:id="@+id/result_next_airing_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:orientation="vertical">
<TextView <TextView
android:id="@+id/result_episodes_text" android:id="@+id/result_episodes_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginEnd="20dp"
android:textColor="?attr/textColor" android:textColor="?attr/textColor"
android:textSize="17sp" android:textSize="17sp"
android:textStyle="normal" android:textStyle="normal"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"
tools:text="8 Episodes" /> tools:text="8 Episodes" />
<LinearLayout
android:id="@+id/result_next_airing_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:orientation="horizontal">
<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"

View file

@ -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

View file

@ -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

View file

@ -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>