Merge remote-tracking branch 'origin/master' into dialog2

This commit is contained in:
IndusAryan 2024-05-14 19:40:38 +05:30
commit f4fbde2147
54 changed files with 1500 additions and 166 deletions

7
.idea/gradle.xml generated
View file

@ -4,17 +4,16 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="delegatedBuild" value="true" />
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" /> <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/library" />
</set> </set>
</option> </option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

View file

@ -1,5 +1,6 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import org.jetbrains.dokka.gradle.DokkaTask import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.net.URL import java.net.URL
@ -13,6 +14,7 @@ plugins {
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/" val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first() val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
var isLibraryDebug = false
fun String.execute() = ByteArrayOutputStream().use { baot -> fun String.execute() = ByteArrayOutputStream().use { baot ->
if (project.exec { if (project.exec {
@ -103,6 +105,7 @@ android {
) )
} }
debug { debug {
isLibraryDebug = true
isDebuggable = true isDebuggable = true
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
proguardFiles( proguardFiles(
@ -199,7 +202,7 @@ dependencies {
// PlayBack // PlayBack
implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker
implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs
implementation("com.github.teamnewpipe:NewPipeExtractor:6dc25f7") /* For Trailers implementation("com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:6dc25f7b97") /* For Trailers
^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */
implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding
@ -232,18 +235,37 @@ dependencies {
implementation("androidx.work:work-runtime:2.9.0") implementation("androidx.work:work-runtime:2.9.0")
implementation("androidx.work:work-runtime-ktx:2.9.0") implementation("androidx.work:work-runtime-ktx:2.9.0")
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
implementation(project(":library") {
this.extra.set("isDebug", isLibraryDebug)
})
} }
tasks.register("androidSourcesJar", Jar::class) { tasks.register<Jar>("androidSourcesJar") {
archiveClassifier.set("sources") archiveClassifier.set("sources")
from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources
} }
// For GradLew Plugin tasks.register<Copy>("copyJar") {
tasks.register("makeJar", Copy::class) { from(
from("build/intermediates/compile_app_classes_jar/prereleaseDebug") "build/intermediates/compile_app_classes_jar/prereleaseDebug",
into("build") "../library/build/libs"
include("classes.jar") )
into("build/app-classes")
include("classes.jar", "library-jvm*.jar")
// Remove the version
rename("library-jvm.*.jar", "library-jvm.jar")
}
// Merge the app classes and the library classes into classes.jar
tasks.register<Jar>("makeJar") {
dependsOn(tasks.getByName("copyJar"))
from(
zipTree("build/app-classes/classes.jar"),
zipTree("build/app-classes/library-jvm.jar")
)
destinationDirectory.set(layout.buildDirectory)
archivesName = "classes"
} }
tasks.withType<KotlinCompile> { tasks.withType<KotlinCompile> {

View file

@ -743,8 +743,6 @@ fun base64Encode(array: ByteArray): String {
} }
} }
class ErrorLoadingException(message: String? = null) : Exception(message)
fun MainAPI.fixUrlNull(url: String?): String? { fun MainAPI.fixUrlNull(url: String?): String? {
if (url.isNullOrEmpty()) { if (url.isNullOrEmpty()) {
return null return null
@ -1450,11 +1448,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,7 @@ 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.Requests
import com.lagradost.nicehttp.ResponseParser import com.lagradost.nicehttp.ResponseParser
import com.lagradost.safefile.SafeFile import com.lagradost.safefile.SafeFile
@ -1755,6 +1756,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
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
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
@ -83,23 +90,18 @@ open class Chillx : ExtractorApi() {
headers = headers headers = headers
).forEach(callback) ).forEach(callback)
} }
private fun decodeUnicodeEscape(input: String): String { private fun decodeUnicodeEscape(input: String): String {
val regex = Regex("u([0-9a-fA-F]{4})") val regex = Regex("u([0-9a-fA-F]{4})")
return regex.replace(input) { return regex.replace(input) {
it.groupValues[1].toInt(16).toChar().toString() it.groupValues[1].toInt(16).toChar().toString()
} }
} }
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(
@JsonProperty("file") val file: String? = null, data class Keys(
@JsonProperty("label") val label: String? = null, @JsonProperty("chillx") val key: List<String>
@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
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
} }

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

@ -25,9 +25,13 @@ open class Vidmoly : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit, subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit callback: (ExtractorLink) -> Unit
) { ) {
val headers = mapOf(
"User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36",
"Sec-Fetch-Dest" to "iframe"
)
val script = app.get( val script = app.get(
url, url,
headers = headers,
referer = referer, referer = referer,
).document.select("script") ).document.select("script")
.find { it.data().contains("sources:") }?.data() .find { it.data().contains("sources:") }?.data()
@ -66,4 +70,4 @@ open class Vidmoly : ExtractorApi() {
@JsonProperty("kind") val kind: String? = null, @JsonProperty("kind") val kind: String? = null,
) )
} }

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,19 +1,46 @@
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() {
override val name = "Voe" override val name = "Voe"
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,
@ -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, if (!link.isNullOrBlank()) {
"$mainUrl/", videoLinks.add(
headers = mapOf("Origin" to "$mainUrl/") when {
).forEach(callback) 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,
)
}

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

@ -0,0 +1,440 @@
package com.lagradost.cloudstream3.metaproviders
import android.net.Uri
import com.lagradost.cloudstream3.*
import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import java.util.Locale
import java.text.SimpleDateFormat
import kotlin.math.roundToInt
open class TraktProvider : MainAPI() {
override var name = "Trakt"
override val hasMainPage = true
override val providerType = ProviderType.MetaProvider
override val supportedTypes = setOf(
TvType.Movie,
TvType.TvSeries,
TvType.Anime,
)
private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==")
private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2")
override val mainPage = mainPageOf(
"$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now
"$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time
"$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now
"$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time
)
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page")
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
element.toSearchResponse()
}
return newHomePageResponse(request.name, results)
}
private fun MediaDetails.toSearchResponse(): SearchResponse {
val media = this.media ?: this
val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries
val poster = media.images?.poster?.firstOrNull()
if (mediaType == TvType.Movie) {
return newMovieSearchResponse(
name = media.title!!,
url = Data(
type = mediaType,
mediaDetails = media,
).toJson(),
type = TvType.Movie,
) {
posterUrl = fixPath(poster)
}
} else {
return newTvSeriesSearchResponse(
name = media.title!!,
url = Data(
type = mediaType,
mediaDetails = media,
).toJson(),
type = TvType.TvSeries,
) {
this.posterUrl = fixPath(poster)
}
}
}
override suspend fun search(query: String): List<SearchResponse>? {
val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query")
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
element.toSearchResponse()
}
return results
}
override suspend fun load(url: String): LoadResponse {
val data = parseJson<Data>(url)
val mediaDetails = data.mediaDetails
val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows"
val posterUrl = mediaDetails?.images?.poster?.firstOrNull()
val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull()
val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full")
val actors = parseJson<People>(resActor).cast?.map {
ActorData(
Actor(
name = it.person?.name!!,
image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500")
),
roleString = it.character
)
}
val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20")
val relatedMedia = parseJson<List<MediaDetails>>(resRelated).map { it.toSearchResponse() }
val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true
val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja")
val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko")
val isBollywood = mediaDetails?.country == "in"
if (data.type == TvType.Movie) {
val linkData = LinkData(
id = mediaDetails?.ids?.tmdb,
traktId = mediaDetails?.ids?.trakt,
traktSlug = mediaDetails?.ids?.slug,
tmdbId = mediaDetails?.ids?.tmdb,
imdbId = mediaDetails?.ids?.imdb.toString(),
tvdbId = mediaDetails?.ids?.tvdb,
tvrageId = mediaDetails?.ids?.tvrage,
type = data.type.toString(),
title = mediaDetails?.title,
year = mediaDetails?.year,
orgTitle = mediaDetails?.title,
isAnime = isAnime,
//jpTitle = later if needed as it requires another network request,
airedDate = mediaDetails?.released
?: mediaDetails?.firstAired,
isAsian = isAsian,
isBollywood = isBollywood,
).toJson()
return newMovieLoadResponse(
name = mediaDetails?.title!!,
url = data.toJson(),
dataUrl = linkData.toJson(),
type = if (isAnime) TvType.AnimeMovie else TvType.Movie,
) {
this.name = mediaDetails.title
this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
this.year = mediaDetails.year
this.plot = mediaDetails.overview
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
this.tags = mediaDetails.genres
this.duration = mediaDetails.runtime
this.recommendations = relatedMedia
this.actors = actors
this.comingSoon = isUpcoming(mediaDetails.released)
//posterHeaders
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
this.contentRating = mediaDetails.certification
addTrailer(mediaDetails.trailer)
addImdbId(mediaDetails.ids?.imdb)
addTMDbId(mediaDetails.ids?.tmdb.toString())
}
} else {
val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes")
val episodes = mutableListOf<Episode>()
val seasons = parseJson<List<Seasons>>(resSeasons)
val seasonsNames = mutableListOf<SeasonData>()
seasons.forEach { season ->
seasonsNames.add(
SeasonData(
season.number!!,
season.title
)
)
season.episodes?.map { episode ->
val linkData = LinkData(
id = mediaDetails?.ids?.tmdb,
traktId = mediaDetails?.ids?.trakt,
traktSlug = mediaDetails?.ids?.slug,
tmdbId = mediaDetails?.ids?.tmdb,
imdbId = mediaDetails?.ids?.imdb.toString(),
tvdbId = mediaDetails?.ids?.tvdb,
tvrageId = mediaDetails?.ids?.tvrage,
type = data.type.toString(),
season = episode.season,
episode = episode.number,
title = mediaDetails?.title,
year = mediaDetails?.year,
orgTitle = mediaDetails?.title,
isAnime = isAnime,
airedYear = mediaDetails?.year,
lastSeason = seasons.size,
epsTitle = episode.title,
//jpTitle = later if needed as it requires another network request,
date = episode.firstAired,
airedDate = episode.firstAired,
isAsian = isAsian,
isBollywood = isBollywood,
isCartoon = isCartoon
).toJson()
episodes.add(
Episode(
data = linkData.toJson(),
name = episode.title,
season = episode.season,
episode = episode.number,
posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()),
rating = episode.rating?.times(10)?.roundToInt(),
description = episode.overview,
).apply {
this.addDate(episode.firstAired)
}
)
}
}
return newTvSeriesLoadResponse(
name = mediaDetails?.title!!,
url = data.toJson(),
type = if (isAnime) TvType.Anime else TvType.TvSeries,
episodes = episodes
) {
this.name = mediaDetails.title
this.type = if (isAnime) TvType.Anime else TvType.TvSeries
this.episodes = episodes
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
this.year = mediaDetails.year
this.plot = mediaDetails.overview
this.showStatus = getStatus(mediaDetails.status)
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
this.tags = mediaDetails.genres
this.duration = mediaDetails.runtime
this.recommendations = relatedMedia
this.actors = actors
this.comingSoon = isUpcoming(mediaDetails.released)
//posterHeaders
this.seasonNames = seasonsNames
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
this.contentRating = mediaDetails.certification
addTrailer(mediaDetails.trailer)
addImdbId(mediaDetails.ids?.imdb)
addTMDbId(mediaDetails.ids?.tmdb.toString())
}
}
}
private suspend fun getApi(url: String) : String {
return app.get(
url = url,
headers = mapOf(
"Content-Type" to "application/json",
"trakt-api-version" to "2",
"trakt-api-key" to traktClientId,
)
).toString()
}
private fun isUpcoming(dateString: String?): Boolean {
return try {
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
APIHolder.unixTimeMS < dateTime
} catch (t: Throwable) {
logError(t)
false
}
}
private fun getStatus(t: String?): ShowStatus {
return when (t) {
"returning series" -> ShowStatus.Ongoing
"continuing" -> ShowStatus.Ongoing
else -> ShowStatus.Completed
}
}
private fun fixPath(url: String?): String? {
url ?: return null
return "https://$url"
}
private fun getWidthImageUrl(path: String?, width: String) : String? {
if (path == null) return null
if (!path.contains("image.tmdb.org")) return fixPath(path)
val fileName = Uri.parse(path).lastPathSegment ?: return null
return "https://image.tmdb.org/t/p/${width}/${fileName}"
}
private fun getOriginalWidthImageUrl(path: String?) : String? {
if (path == null) return null
if (!path.contains("image.tmdb.org")) return fixPath(path)
return getWidthImageUrl(path, "original")
}
data class Data(
val type: TvType? = null,
val mediaDetails: MediaDetails? = null,
)
data class MediaDetails(
@JsonProperty("title") val title: String? = null,
@JsonProperty("year") val year: Int? = null,
@JsonProperty("ids") val ids: Ids? = null,
@JsonProperty("tagline") val tagline: String? = null,
@JsonProperty("overview") val overview: String? = null,
@JsonProperty("released") val released: String? = null,
@JsonProperty("runtime") val runtime: Int? = null,
@JsonProperty("country") val country: String? = null,
@JsonProperty("updatedAt") val updatedAt: String? = null,
@JsonProperty("trailer") val trailer: String? = null,
@JsonProperty("homepage") val homepage: String? = null,
@JsonProperty("status") val status: String? = null,
@JsonProperty("rating") val rating: Double? = null,
@JsonProperty("votes") val votes: Long? = null,
@JsonProperty("comment_count") val commentCount: Long? = null,
@JsonProperty("language") val language: String? = null,
@JsonProperty("languages") val languages: List<String>? = null,
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
@JsonProperty("genres") val genres: List<String>? = null,
@JsonProperty("certification") val certification: String? = null,
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
@JsonProperty("first_aired") val firstAired: String? = null,
@JsonProperty("airs") val airs: Airs? = null,
@JsonProperty("network") val network: String? = null,
@JsonProperty("images") val images: Images? = null,
@JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null
)
data class Airs(
@JsonProperty("day") val day: String? = null,
@JsonProperty("time") val time: String? = null,
@JsonProperty("timezone") val timezone: String? = null,
)
data class Ids(
@JsonProperty("trakt") val trakt: Int? = null,
@JsonProperty("slug") val slug: String? = null,
@JsonProperty("tvdb") val tvdb: Int? = null,
@JsonProperty("imdb") val imdb: String? = null,
@JsonProperty("tmdb") val tmdb: Int? = null,
@JsonProperty("tvrage") val tvrage: String? = null,
)
data class Images(
@JsonProperty("fanart") val fanart: List<String>? = null,
@JsonProperty("poster") val poster: List<String>? = null,
@JsonProperty("logo") val logo: List<String>? = null,
@JsonProperty("clearart") val clearart: List<String>? = null,
@JsonProperty("banner") val banner: List<String>? = null,
@JsonProperty("thumb") val thumb: List<String>? = null,
@JsonProperty("screenshot") val screenshot: List<String>? = null,
@JsonProperty("headshot") val headshot: List<String>? = null,
)
data class People(
@JsonProperty("cast") val cast: List<Cast>? = null,
)
data class Cast(
@JsonProperty("character") val character: String? = null,
@JsonProperty("characters") val characters: List<String>? = null,
@JsonProperty("episode_count") val episodeCount: Long? = null,
@JsonProperty("person") val person: Person? = null,
@JsonProperty("images") val images: Images? = null,
)
data class Person(
@JsonProperty("name") val name: String? = null,
@JsonProperty("ids") val ids: Ids? = null,
@JsonProperty("images") val images: Images? = null,
)
data class Seasons(
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
@JsonProperty("episode_count") val episodeCount: Int? = null,
@JsonProperty("episodes") val episodes: List<TraktEpisode>? = null,
@JsonProperty("first_aired") val firstAired: String? = null,
@JsonProperty("ids") val ids: Ids? = null,
@JsonProperty("images") val images: Images? = null,
@JsonProperty("network") val network: String? = null,
@JsonProperty("number") val number: Int? = null,
@JsonProperty("overview") val overview: String? = null,
@JsonProperty("rating") val rating: Double? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("updated_at") val updatedAt: String? = null,
@JsonProperty("votes") val votes: Int? = null,
)
data class TraktEpisode(
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
@JsonProperty("comment_count") val commentCount: Int? = null,
@JsonProperty("episode_type") val episodeType: String? = null,
@JsonProperty("first_aired") val firstAired: String? = null,
@JsonProperty("ids") val ids: Ids? = null,
@JsonProperty("images") val images: Images? = null,
@JsonProperty("number") val number: Int? = null,
@JsonProperty("number_abs") val numberAbs: Int? = null,
@JsonProperty("overview") val overview: String? = null,
@JsonProperty("rating") val rating: Double? = null,
@JsonProperty("runtime") val runtime: Int? = null,
@JsonProperty("season") val season: Int? = null,
@JsonProperty("title") val title: String? = null,
@JsonProperty("updated_at") val updatedAt: String? = null,
@JsonProperty("votes") val votes: Int? = null,
)
data class LinkData(
val id: Int? = null,
val traktId: Int? = null,
val traktSlug: String? = null,
val tmdbId: Int? = null,
val imdbId: String? = null,
val tvdbId: Int? = null,
val tvrageId: String? = null,
val type: String? = null,
val season: Int? = null,
val episode: Int? = null,
val aniId: String? = null,
val animeId: String? = null,
val title: String? = null,
val year: Int? = null,
val orgTitle: String? = null,
val isAnime: Boolean = false,
val airedYear: Int? = null,
val lastSeason: Int? = null,
val epsTitle: String? = null,
val jpTitle: String? = null,
val date: String? = null,
val airedDate: String? = null,
val isAsian: Boolean = false,
val isBollywood: Boolean = false,
val isCartoon: Boolean = false,
)
}

View file

@ -0,0 +1,16 @@
package com.lagradost.cloudstream3.mvvm
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
/** NOTE: Only one observer at a time per value */
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.removeObservers(this)
liveData.observe(this) { it?.let { t -> action(t) } }
}
/** NOTE: Only one observer at a time per value */
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.removeObservers(this)
liveData.observe(this) { action(it) }
}

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

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

@ -1,7 +1,10 @@
package com.lagradost.cloudstream3.ui.player package com.lagradost.cloudstream3.ui.player
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.* import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.AnimatedVectorDrawable
import android.media.metrics.PlaybackErrorEvent import android.media.metrics.PlaybackErrorEvent
@ -24,11 +27,7 @@ import androidx.fragment.app.Fragment
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.*
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView
import androidx.media3.ui.SubtitleView
import androidx.media3.ui.TimeBar
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.github.rubensousa.previewseekbar.PreviewBar import com.github.rubensousa.previewseekbar.PreviewBar
@ -442,6 +441,9 @@ abstract class AbstractPlayerFragment(
is VideoEndedEvent -> { is VideoEndedEvent -> {
context?.let { ctx -> context?.let { ctx ->
// Resets subtitle delay on ended video
player.setSubtitleOffset(0)
// Only play next episode if autoplay is on (default) // Only play next episode if autoplay is on (default)
if (PreferenceManager.getDefaultSharedPreferences(ctx) if (PreferenceManager.getDefaultSharedPreferences(ctx)
?.getBoolean( ?.getBoolean(

View file

@ -1118,6 +1118,9 @@ class CS3IPlayer : IPlayer {
} }
Player.STATE_ENDED -> { Player.STATE_ENDED -> {
// Resets subtitle delay on ended video
setSubtitleOffset(0)
// Only play next episode if autoplay is on (default) // Only play next episode if autoplay is on (default)
if (PreferenceManager.getDefaultSharedPreferences(context) if (PreferenceManager.getDefaultSharedPreferences(context)
?.getBoolean( ?.getBoolean(

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,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)) {

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

@ -783,7 +783,10 @@ class ResultFragmentTv : Fragment() {
// resultEpisodeLoading.isVisible = episodes is Resource.Loading // resultEpisodeLoading.isVisible = episodes is Resource.Loading
if (episodes is Resource.Success) { if (episodes is Resource.Success) {
val lastWatchedIndex = episodes.value.indexOfLast { ep -> ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f } val lastWatchedIndex = episodes.value.indexOfLast { ep ->
ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f || ep.videoWatchState == VideoWatchState.Watched
}
val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() } val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() }
if (firstUnwatched != null) { if (firstUnwatched != null) {

View file

@ -83,6 +83,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
@ -197,7 +201,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)
}
} }
} }
} }
@ -1099,13 +1107,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() }
@ -1514,6 +1523,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) {
@ -1689,6 +1705,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,
@ -2277,7 +2326,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
@ -2326,7 +2376,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

@ -1,13 +1,13 @@
package com.lagradost.cloudstream3.ui.settings package com.lagradost.cloudstream3.ui.settings
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.Preference import androidx.preference.Preference
@ -18,12 +18,15 @@ import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.databinding.MainSettingsBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
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.UIHelper import com.lagradost.cloudstream3.utils.UIHelper
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
@ -82,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)
@ -96,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)
@ -133,7 +140,6 @@ class SettingsFragment : Fragment() {
val localBinding = MainSettingsBinding.inflate(inflater, container, false) val localBinding = MainSettingsBinding.inflate(inflater, container, false)
binding = localBinding binding = localBinding
return localBinding.root return localBinding.root
//return inflater.inflate(R.layout.main_settings, container, false)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -141,21 +147,44 @@ class SettingsFragment : Fragment() {
activity?.navigate(id, Bundle()) activity?.navigate(id, Bundle())
} }
// used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}") /** used to debug leaks
showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} :
${VideoDownloadManager.downloadProgressEvent.size}") **/
for (syncApi in accountManagers) { fun hasProfilePictureFromAccountManagers(accountManagers: List<AccountManager>): Boolean {
val login = syncApi.loginInfo() for (syncApi in accountManagers) {
val pic = login?.profilePicture ?: continue val login = syncApi.loginInfo()
if (binding?.settingsProfilePic?.setImage( val pic = login?.profilePicture ?: continue
pic,
errorImageDrawable = HomeFragment.errorProfilePic if (binding?.settingsProfilePic?.setImage(
) == true pic,
) { errorImageDrawable = HomeFragment.errorProfilePic
binding?.settingsProfileText?.text = login.name ) == true
binding?.settingsProfile?.isVisible = true ) {
break binding?.settingsProfileText?.text = login.name
return true // sync profile exists
}
} }
return false // not syncing
} }
// display local account information if not syncing
if (!hasProfilePictureFromAccountManagers(accountManagers)) {
val activity = activity ?: return
val currentAccount = try {
DataStoreHelper.accounts.firstOrNull {
it.keyIndex == DataStoreHelper.selectedKeyIndex
} ?: activity.let { DataStoreHelper.getDefaultAccount(activity) }
} catch (t: IllegalStateException) {
Log.e("AccountManager", "Activity not found", t)
null
}
binding?.settingsProfilePic?.setImage(currentAccount?.image)
binding?.settingsProfileText?.text = currentAccount?.name
}
binding?.apply { binding?.apply {
listOf( listOf(
settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general, settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general,

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

@ -50,7 +50,7 @@ class JsUnpacker(packedJS: String?) {
throw Exception("Unknown p.a.c.k.e.r. encoding") throw Exception("Unknown p.a.c.k.e.r. encoding")
} }
val unbase = Unbase(radix) val unbase = Unbase(radix)
p = Pattern.compile("\\b\\w+\\b") p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""")
m = p.matcher(payload) m = p.matcher(payload)
val decoded = StringBuilder(payload) val decoded = StringBuilder(payload)
var replaceOffset = 0 var replaceOffset = 0

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

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@android:color/white">
<item>
<shape android:shape="oval">
<stroke
android:width="2dp"
android:color="?attr/white" />
<corners android:radius="10dp" />
</shape>
</item>
</ripple>

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

View file

@ -24,7 +24,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="20dp" android:padding="20dp"
android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
@ -36,7 +35,11 @@
android:id="@+id/settings_profile_pic" android:id="@+id/settings_profile_pic"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:ignore="ContentDescription" /> android:scaleType="centerCrop"
android:foreground="@drawable/rounded_outline"
tools:src="@drawable/profile_bg_orange"
android:contentDescription="@string/account"/>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<TextView <TextView
@ -50,7 +53,7 @@
android:textColor="?attr/textColor" android:textColor="?attr/textColor"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="normal" android:textStyle="normal"
tools:text="Hello world" /> tools:text="Quick Brown Fox" />
</LinearLayout> </LinearLayout>
<TextView <TextView

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>
@ -358,6 +360,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>
@ -635,7 +638,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>

View file

@ -8,6 +8,8 @@ buildscript {
classpath("com.android.tools.build:gradle:8.2.2") classpath("com.android.tools.build:gradle:8.2.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")
classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10")
// Universal build config
classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:0.15.1")
} }
} }
@ -22,6 +24,6 @@ plugins {
id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false
} }
tasks.register<Delete>("clean") { //tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory) // delete(rootProject.layout.buildDirectory)
} //}

68
library/build.gradle.kts Normal file
View file

@ -0,0 +1,68 @@
import com.codingfeline.buildkonfig.compiler.FieldSpec
plugins {
kotlin("multiplatform")
id("maven-publish")
id("com.android.library")
id("com.codingfeline.buildkonfig")
}
kotlin {
version = "1.0.0"
androidTarget()
jvm()
sourceSets {
commonMain.dependencies {
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser
^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API
Level 25 or Less. */
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
}
}
}
repositories {
mavenLocal()
maven("https://jitpack.io")
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildkonfig {
packageName = "com.lagradost.api"
exposeObjectWithName = "BuildConfig"
defaultConfigs {
val isDebug = kotlin.runCatching { extra.get("isDebug") }.getOrNull() == true
buildConfigField(FieldSpec.Type.BOOLEAN, "DEBUG", isDebug.toString())
}
}
android {
compileSdk = 34
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = 21
targetSdk = 33
}
// If this is the same com.lagradost.cloudstream3.R stops working
namespace = "com.lagradost.api"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
publishing {
publications {
withType<MavenPublication> {
groupId = "com.lagradost.api"
}
}
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View file

@ -0,0 +1,21 @@
package com.lagradost.api
import android.util.Log
actual object Log {
actual fun d(tag: String, message: String) {
Log.d(tag, message)
}
actual fun i(tag: String, message: String) {
Log.i(tag, message)
}
actual fun w(tag: String, message: String) {
Log.w(tag, message)
}
actual fun e(tag: String, message: String) {
Log.e(tag, message)
}
}

View file

@ -0,0 +1,8 @@
package com.lagradost.api
expect object Log {
fun d(tag: String, message: String)
fun i(tag: String, message: String)
fun w(tag: String, message: String)
fun e(tag: String, message: String)
}

View file

@ -0,0 +1,3 @@
package com.lagradost.cloudstream3
class ErrorLoadingException(message: String? = null) : Exception(message)

View file

@ -1,10 +1,7 @@
package com.lagradost.cloudstream3.mvvm package com.lagradost.cloudstream3.mvvm
import android.util.Log import com.lagradost.api.BuildConfig
import androidx.lifecycle.LifecycleOwner import com.lagradost.api.Log
import androidx.lifecycle.LiveData
import com.bumptech.glide.load.HttpException
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.ErrorLoadingException
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.InterruptedIOException import java.io.InterruptedIOException
@ -49,18 +46,6 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) {
} }
} }
/** NOTE: Only one observer at a time per value */
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.removeObservers(this)
liveData.observe(this) { it?.let { t -> action(t) } }
}
/** NOTE: Only one observer at a time per value */
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.removeObservers(this)
liveData.observe(this) { action(it) }
}
sealed class Resource<out T> { sealed class Resource<out T> {
data class Success<out T>(val value: T) : Resource<T>() data class Success<out T>(val value: T) : Resource<T>()
data class Failure( data class Failure(
@ -158,14 +143,14 @@ fun<T> throwAbleToResource(
"Connection Timeout\nPlease try again later." "Connection Timeout\nPlease try again later."
) )
} }
is HttpException -> { // is HttpException -> {
Resource.Failure( // Resource.Failure(
false, // false,
throwable.statusCode, // throwable.statusCode,
null, // null,
throwable.message ?: "HttpException" // throwable.message ?: "HttpException"
) // )
} // }
is UnknownHostException -> { is UnknownHostException -> {
Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}") Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}")
} }

View file

@ -0,0 +1,19 @@
package com.lagradost.api
actual object Log {
actual fun d(tag: String, message: String) {
println("DEBUG $tag: $message")
}
actual fun i(tag: String, message: String) {
println("INFO $tag: $message")
}
actual fun w(tag: String, message: String) {
println("WARNING $tag: $message")
}
actual fun e(tag: String, message: String) {
println("ERROR $tag: $message")
}
}

View file

@ -1,3 +1,4 @@
rootProject.name = "CloudStream" rootProject.name = "CloudStream"
include(":app") include(":app")
include(":library")