diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index c5c0ff3b..d7c08c9c 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,17 +4,16 @@
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 02946e85..f854865d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,5 +1,6 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import org.jetbrains.dokka.gradle.DokkaTask
+import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.ByteArrayOutputStream
import java.net.URL
@@ -13,6 +14,7 @@ plugins {
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
+var isLibraryDebug = false
fun String.execute() = ByteArrayOutputStream().use { baot ->
if (project.exec {
@@ -103,6 +105,7 @@ android {
)
}
debug {
+ isLibraryDebug = true
isDebuggable = true
applicationIdSuffix = ".debug"
proguardFiles(
@@ -199,7 +202,7 @@ dependencies {
// PlayBack
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.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 */
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-ktx:2.9.0")
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("androidSourcesJar") {
archiveClassifier.set("sources")
from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources
}
-// For GradLew Plugin
-tasks.register("makeJar", Copy::class) {
- from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
- into("build")
- include("classes.jar")
+tasks.register("copyJar") {
+ from(
+ "build/intermediates/compile_app_classes_jar/prereleaseDebug",
+ "../library/build/libs"
+ )
+ 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("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 {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
index ecbdcbbc..699159b5 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
@@ -743,8 +743,6 @@ fun base64Encode(array: ByteArray): String {
}
}
-class ErrorLoadingException(message: String? = null) : Exception(message)
-
fun MainAPI.fixUrlNull(url: String?): String? {
if (url.isNullOrEmpty()) {
return null
@@ -1450,11 +1448,24 @@ fun TvType?.isEpisodeBased(): Boolean {
return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama)
}
-
data class NextAiring(
val episode: Int,
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
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
index 1f3baa8a..dc9e3e34 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -161,6 +161,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
+import com.lagradost.cloudstream3.utils.fcast.FcastManager
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser
import com.lagradost.safefile.SafeFile
@@ -1755,6 +1756,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
runAutoUpdate()
}
+ FcastManager().init(this, false)
+
APIRepository.dubStatusActive = getApiDubstatusSettings()
try {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt
index f03a5525..26567c7a 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt
@@ -2,9 +2,7 @@ package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
-import com.lagradost.cloudstream3.extractors.helper.*
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.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
@@ -28,30 +26,39 @@ open class Chillx : ExtractorApi() {
override val name = "Chillx"
override val mainUrl = "https://chillx.top"
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()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key")
+ key = fetch
+ key!!
+ }
+ }
+ }
+
+ @Suppress("NAME_SHADOWING")
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
- val master = Regex("\\s*=\\s*'([^']+)").find(
+ val master = Regex("""JScript[\w+]?\s*=\s*'([^']+)""").find(
app.get(
url,
- referer = referer ?: "",
- 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",
- )
+ referer = url,
).text
)?.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 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 languageUrlPairs = matches.map { matchResult ->
val (language, url) = matchResult.destructured
@@ -83,23 +90,18 @@ open class Chillx : ExtractorApi() {
headers = headers
).forEach(callback)
}
-
+
private fun decodeUnicodeEscape(input: String): String {
val regex = Regex("u([0-9a-fA-F]{4})")
return regex.replace(input) {
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,
- @JsonProperty("label") val label: String? = null,
- @JsonProperty("kind") val kind: String? = null,
+
+ data class Keys(
+ @JsonProperty("chillx") val key: List
)
+
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt
index 0df93dc5..2343a92e 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt
@@ -9,10 +9,16 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import java.net.URL
+class Geodailymotion : Dailymotion() {
+ override val name = "GeoDailymotion"
+ override val mainUrl = "https://geo.dailymotion.com"
+}
+
open class Dailymotion : ExtractorApi() {
override val mainUrl = "https://www.dailymotion.com"
override val name = "Dailymotion"
override val requiresReferer = false
+ private val baseUrl = "https://www.dailymotion.com"
@Suppress("RegExpSimplifiable")
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
@@ -34,7 +40,7 @@ open class Dailymotion : ExtractorApi() {
val dmV1st = config.dmInternalData.v1st
val dmTs = config.dmInternalData.ts
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)
.parsedSafe() ?: return
metaData.qualities.forEach { (_, video) ->
@@ -45,16 +51,19 @@ open class Dailymotion : ExtractorApi() {
}
private fun getEmbedUrl(url: String): String? {
- if (url.contains("/embed/")) {
- return url
- }
- val vid = getVideoId(url) ?: return null
- return "$mainUrl/embed/video/$vid"
+ if (url.contains("/embed/") || url.contains("/video/")) {
+ return url
}
+ if (url.contains("geo.dailymotion.com")) {
+ val videoId = url.substringAfter("video=")
+ return "$baseUrl/embed/video/$videoId"
+ }
+ return null
+ }
private fun getVideoId(url: String): String? {
val path = URL(url).path
- val id = path.substringAfter("video/")
+ val id = path.substringAfter("/video/")
if (id.matches(videoIdRegex)) {
return id
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt
index 565a2680..2cb12e16 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt
@@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.extractors
-import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt
new file mode 100644
index 00000000..b9065688
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt
@@ -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() ?: return
+ if (res.status != 200) return
+ res.result?.amap { source ->
+ val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: 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?
+ )
+
+ 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)
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt
new file mode 100644
index 00000000..230a9e1a
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt
@@ -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(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
+ )
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt
index 615cfd74..979fd8c5 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt
@@ -25,9 +25,13 @@ open class Vidmoly : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> 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(
url,
+ headers = headers,
referer = referer,
).document.select("script")
.find { it.data().contains("sources:") }?.data()
@@ -66,4 +70,4 @@ open class Vidmoly : ExtractorApi() {
@JsonProperty("kind") val kind: String? = null,
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt
index d5d0fb32..c5e01552 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt
@@ -13,6 +13,10 @@ import javax.crypto.spec.SecretKeySpec
// Code found in https://github.com/KillerDogeEmpire/vidplay-keys
// special credits to @KillerDogeEmpire for providing key
+class AnyVidplay(hostUrl: String) : Vidplay() {
+ override val mainUrl = hostUrl
+}
+
class MyCloud : Vidplay() {
override val name = "MyCloud"
override val mainUrl = "https://mcloud.bz"
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt
index 2c6998de..67fd7eea 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt
@@ -1,19 +1,46 @@
package com.lagradost.cloudstream3.extractors
+import android.util.Base64
+import com.fasterxml.jackson.annotation.JsonProperty
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.M3u8Helper
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() {
override val name = "Voe"
override val mainUrl = "https://voe.sx"
override val requiresReferer = true
+
+ private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex()
+ private val base64Regex = Regex("'.*'")
override suspend fun getUrl(
url: String,
@@ -25,12 +52,33 @@ open class Voe : ExtractorApi() {
val script = res.select("script").find { it.data().contains("sources =") }?.data()
val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1)
- M3u8Helper.generateM3u8(
- name,
- link ?: return,
- "$mainUrl/",
- headers = mapOf("Origin" to "$mainUrl/")
- ).forEach(callback)
-
+ val videoLinks = mutableListOf()
+
+ if (!link.isNullOrBlank()) {
+ videoLinks.add(
+ when {
+ linkRegex.matches(link) -> link
+ else -> String(Base64.decode(link, Base64.DEFAULT))
+ }
+ )
+ } else {
+ val link2 = base64Regex.find(script)?.value ?: return
+ val decoded = Base64.decode(link2, Base64.DEFAULT).toString()
+ val videoLinkDTO = AppUtils.parseJson(decoded)
+ videoLinkDTO.let { videoLinks.add(it.toString()) }
+ }
+
+ videoLinks.forEach { videoLink ->
+ M3u8Helper.generateM3u8(
+ name,
+ videoLink,
+ "$mainUrl/",
+ headers = mapOf("Origin" to "$mainUrl/")
+ ).forEach(callback)
+ }
}
-}
\ No newline at end of file
+
+ data class WcoSources(
+ @JsonProperty("VideoLinkDTO") val VideoLinkDTO: String,
+ )
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt
index 65af01ec..919a9cbd 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt
@@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.extractors
-import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
index 50301e22..c5b4d453 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
@@ -105,6 +105,7 @@ open class TmdbProvider : MainAPI() {
this.id,
episode.episode_number,
episode.season_number,
+ this.name ?: this.original_name,
).toJson(),
episode.name,
episode.season_number,
@@ -122,6 +123,7 @@ open class TmdbProvider : MainAPI() {
this.id,
episodeNum,
season.season_number,
+ this.name ?: this.original_name,
).toJson(),
season = season.season_number
)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
new file mode 100644
index 00000000..37c6be1b
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
@@ -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>(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? {
+ val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query")
+
+ val results = parseJson>(apiResponse).map { element ->
+ element.toSearchResponse()
+ }
+
+ return results
+ }
+ override suspend fun load(url: String): LoadResponse {
+
+ val data = parseJson(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(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>(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()
+ val seasons = parseJson>(resSeasons)
+ val seasonsNames = mutableListOf()
+
+ 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? = null,
+ @JsonProperty("available_translations") val availableTranslations: List? = null,
+ @JsonProperty("genres") val genres: List? = 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? = null,
+ @JsonProperty("poster") val poster: List? = null,
+ @JsonProperty("logo") val logo: List? = null,
+ @JsonProperty("clearart") val clearart: List? = null,
+ @JsonProperty("banner") val banner: List? = null,
+ @JsonProperty("thumb") val thumb: List? = null,
+ @JsonProperty("screenshot") val screenshot: List? = null,
+ @JsonProperty("headshot") val headshot: List? = null,
+ )
+
+ data class People(
+ @JsonProperty("cast") val cast: List? = null,
+ )
+
+ data class Cast(
+ @JsonProperty("character") val character: String? = null,
+ @JsonProperty("characters") val characters: List? = 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? = 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? = 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,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt
new file mode 100644
index 00000000..3df5197c
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt
@@ -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 LifecycleOwner.observe(liveData: LiveData, 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 LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) {
+ liveData.removeObservers(this)
+ liveData.observe(this) { action(it) }
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt
index c3ec2bbd..f54c8698 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt
@@ -11,10 +11,14 @@ import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
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.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
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.VideoDownloadManager
import kotlinx.coroutines.Dispatchers
@@ -85,13 +89,15 @@ class DownloadChildFragment : Fragment() {
binding?.downloadChildToolbar?.apply {
title = name
- setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
- setNavigationOnClickListener {
- activity?.onBackPressedDispatcher?.onBackPressed()
+ if (isLayout(PHONE or EMULATOR)) {
+ setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
+ setNavigationOnClickListener {
+ activity?.onBackPressedDispatcher?.onBackPressed()
+ }
}
+ setAppBarNoScrollFlagsOnTV()
}
-
val adapter: RecyclerView.Adapter =
DownloadChildAdapter(
ArrayList(),
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
index e08eb772..31790b0f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
@@ -41,6 +41,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
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.VideoDownloadManager
import java.net.URI
@@ -97,6 +98,8 @@ class DownloadFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
hideKeyboard()
+ binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV()
+
observe(downloadsViewModel.noDownloadsText) {
binding?.textNoDownloads?.text = it
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt
index a729f33a..f1031c24 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt
@@ -13,6 +13,8 @@ import androidx.annotation.MainThread
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
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.mvvm.logError
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.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager
+import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES
open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
@@ -167,6 +170,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
this.setPersistentId(card.id)
view.setOnClickListener {
if (isZeroBytes) {
+ removeKey(KEY_RESUME_PACKAGES, card.id.toString())
callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card))
//callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data))
} else {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt
index cfa6682d..0865b220 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt
@@ -1,7 +1,10 @@
package com.lagradost.cloudstream3.ui.player
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.AnimatedVectorDrawable
import android.media.metrics.PlaybackErrorEvent
@@ -24,11 +27,7 @@ import androidx.fragment.app.Fragment
import androidx.media3.common.PlaybackException
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession
-import androidx.media3.ui.AspectRatioFrameLayout
-import androidx.media3.ui.DefaultTimeBar
-import androidx.media3.ui.PlayerView
-import androidx.media3.ui.SubtitleView
-import androidx.media3.ui.TimeBar
+import androidx.media3.ui.*
import androidx.preference.PreferenceManager
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.github.rubensousa.previewseekbar.PreviewBar
@@ -442,6 +441,9 @@ abstract class AbstractPlayerFragment(
is VideoEndedEvent -> {
context?.let { ctx ->
+ // Resets subtitle delay on ended video
+ player.setSubtitleOffset(0)
+
// Only play next episode if autoplay is on (default)
if (PreferenceManager.getDefaultSharedPreferences(ctx)
?.getBoolean(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt
index 210bfdca..31adbc87 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt
@@ -1118,6 +1118,9 @@ class CS3IPlayer : IPlayer {
}
Player.STATE_ENDED -> {
+ // Resets subtitle delay on ended video
+ setSubtitleOffset(0)
+
// Only play next episode if autoplay is on (default)
if (PreferenceManager.getDefaultSharedPreferences(context)
?.getBoolean(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt
index af74cb57..c5de1a1c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt
@@ -10,7 +10,8 @@ enum class LoadType {
InAppDownload,
ExternalApp,
Browser,
- Chromecast
+ Chromecast,
+ Fcast
}
fun LoadType.toSet() : Set {
@@ -29,12 +30,17 @@ fun LoadType.toSet() : Set {
ExtractorLinkType.VIDEO,
ExtractorLinkType.M3U8
)
- LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet()
+ LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.entries.toSet()
LoadType.Chromecast -> setOf(
ExtractorLinkType.VIDEO,
ExtractorLinkType.DASH,
ExtractorLinkType.M3U8
)
+ LoadType.Fcast -> setOf(
+ ExtractorLinkType.VIDEO,
+ ExtractorLinkType.DASH,
+ ExtractorLinkType.M3U8
+ )
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt
index e9e00736..85e20d1c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt
@@ -34,6 +34,9 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchHelper
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.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
@@ -274,8 +277,13 @@ class QuickSearchFragment : Fragment() {
// UIHelper.showInputMethod(view.findFocus())
// }
//}
- binding?.quickSearchBack?.setOnClickListener {
- activity?.popCurrentPage()
+ if (isLayout(PHONE or EMULATOR)) {
+ binding?.quickSearchBack?.apply {
+ isVisible = true
+ setOnClickListener {
+ activity?.popCurrentPage()
+ }
+ }
}
if (isLayout(TV)) {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt
index fad349c8..e4fd0559 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt
@@ -9,9 +9,11 @@ import androidx.core.view.isVisible
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
+import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding
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_LONG_CLICK
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.toPx
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
+import java.text.DateFormat
+import java.text.SimpleDateFormat
import java.util.*
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_MARK_AS_WATCHED = 18
+const val ACTION_FCAST = 19
+
const val TV_EP_SIZE_LARGE = 400
const val TV_EP_SIZE_SMALL = 300
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
@@ -104,7 +110,7 @@ class EpisodeAdapter(
override fun getItemViewType(position: Int): Int {
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)) {
episodePoster.setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
@@ -271,6 +304,7 @@ class EpisodeAdapter(
}
}
}
+
itemView.setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt
index a1574eec..1d3f5a08 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt
@@ -50,6 +50,7 @@ data class ResultEpisode(
val videoWatchState: VideoWatchState,
/** Sum of all previous season episode counts + episode */
val totalEpisodeIndex: Int? = null,
+ val airDate: Long? = null,
)
fun ResultEpisode.getRealPosition(): Long {
@@ -85,6 +86,7 @@ fun buildResultEpisode(
tvType: TvType,
parentId: Int,
totalEpisodeIndex: Int? = null,
+ airDate: Long? = null,
): ResultEpisode {
val posDur = getViewPos(id)
val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None
@@ -107,7 +109,8 @@ fun buildResultEpisode(
tvType,
parentId,
videoWatchState,
- totalEpisodeIndex
+ totalEpisodeIndex,
+ airDate,
)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt
index 6a83f396..13621cda 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt
@@ -783,7 +783,10 @@ class ResultFragmentTv : Fragment() {
// resultEpisodeLoading.isVisible = episodes is Resource.Loading
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() }
if (firstUnwatched != null) {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt
index 37a905a7..a32942f6 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt
@@ -83,6 +83,10 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
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 java.io.File
import java.util.concurrent.TimeUnit
@@ -197,7 +201,11 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
else -> null
}?.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 librarySyncData = it.syncData
+ val yearCheck = year == it.year || year == null || it.year == null
val checks = listOf(
{ imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId },
{ tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId },
{ malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId },
{ aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId },
- { normalizedName == normalizeString(it.name) && year == it.year }
+ { normalizedName == normalizeString(it.name) && yearCheck }
)
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)
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(
click.data,
LoadType.Browser,
@@ -2277,7 +2326,8 @@ class ResultViewModel2 : ViewModel() {
fillers.getOrDefault(episode, false),
loadResponse.type,
mainId,
- totalIndex
+ totalIndex,
+ airDate = i.date
)
val season = eps.seasonIndex ?: 0
@@ -2326,7 +2376,8 @@ class ResultViewModel2 : ViewModel() {
null,
loadResponse.type,
mainId,
- totalIndex
+ totalIndex,
+ airDate = episode.date
)
val season = ep.seasonIndex ?: 0
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt
index dfa84998..8ac17928 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt
@@ -1,13 +1,13 @@
package com.lagradost.cloudstream3.ui.settings
import android.os.Bundle
+import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.annotation.StringRes
import androidx.core.view.children
-import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.preference.Preference
@@ -18,12 +18,15 @@ import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.MainSettingsBinding
import com.lagradost.cloudstream3.mvvm.logError
+import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.ui.home.HomeFragment
import com.lagradost.cloudstream3.ui.result.txt
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.isLayout
+import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
import com.lagradost.cloudstream3.utils.UIHelper.navigate
@@ -82,9 +85,11 @@ class SettingsFragment : Fragment() {
settingsToolbar.apply {
setTitle(title)
- setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
- setNavigationOnClickListener {
- activity?.onBackPressedDispatcher?.onBackPressed()
+ if (isLayout(PHONE or EMULATOR)) {
+ setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
+ setNavigationOnClickListener {
+ activity?.onBackPressedDispatcher?.onBackPressed()
+ }
}
}
UIHelper.fixPaddingStatusbar(settingsToolbar)
@@ -96,10 +101,12 @@ class SettingsFragment : Fragment() {
settingsToolbar.apply {
setTitle(title)
- setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
- children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
- setNavigationOnClickListener {
- activity?.onBackPressedDispatcher?.onBackPressed()
+ if (isLayout(PHONE or EMULATOR)) {
+ setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
+ children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
+ setNavigationOnClickListener {
+ activity?.onBackPressedDispatcher?.onBackPressed()
+ }
}
}
UIHelper.fixPaddingStatusbar(settingsToolbar)
@@ -133,7 +140,6 @@ class SettingsFragment : Fragment() {
val localBinding = MainSettingsBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
- //return inflater.inflate(R.layout.main_settings, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -141,21 +147,44 @@ class SettingsFragment : Fragment() {
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) {
- val login = syncApi.loginInfo()
- val pic = login?.profilePicture ?: continue
- if (binding?.settingsProfilePic?.setImage(
- pic,
- errorImageDrawable = HomeFragment.errorProfilePic
- ) == true
- ) {
- binding?.settingsProfileText?.text = login.name
- binding?.settingsProfile?.isVisible = true
- break
+ fun hasProfilePictureFromAccountManagers(accountManagers: List): Boolean {
+ for (syncApi in accountManagers) {
+ val login = syncApi.loginInfo()
+ val pic = login?.profilePicture ?: continue
+
+ if (binding?.settingsProfilePic?.setImage(
+ pic,
+ errorImageDrawable = HomeFragment.errorProfilePic
+ ) == true
+ ) {
+ 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 {
listOf(
settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt
index fb24c185..4aaa5e12 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt
@@ -35,6 +35,9 @@ import okhttp3.internal.closeQuietly
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStream
+import java.lang.System.currentTimeMillis
+import java.text.SimpleDateFormat
+import java.util.*
class SettingsUpdates : PreferenceFragmentCompat() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -125,12 +128,12 @@ class SettingsUpdates : PreferenceFragmentCompat() {
}
binding.saveBtt.setOnClickListener {
+ val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
var fileStream: OutputStream? = null
try {
- fileStream =
- VideoDownloadManager.setupStream(
+ fileStream = VideoDownloadManager.setupStream(
it.context,
- "logcat",
+ "logcat_${date}",
null,
"txt",
false
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
index 5a845326..61cdd26a 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt
@@ -53,6 +53,7 @@ import com.lagradost.cloudstream3.extractors.FileMoonIn
import com.lagradost.cloudstream3.extractors.FileMoonSx
import com.lagradost.cloudstream3.extractors.Filesim
import com.lagradost.cloudstream3.extractors.Fplayer
+import com.lagradost.cloudstream3.extractors.Geodailymotion
import com.lagradost.cloudstream3.extractors.GMPlayer
import com.lagradost.cloudstream3.extractors.Gdriveplayer
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.Megacloud
import com.lagradost.cloudstream3.extractors.Meownime
+import com.lagradost.cloudstream3.extractors.MetaGnathTuggers
import com.lagradost.cloudstream3.extractors.Minoplres
import com.lagradost.cloudstream3.extractors.MixDrop
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.Sendvid
import com.lagradost.cloudstream3.extractors.ShaveTape
+import com.lagradost.cloudstream3.extractors.Simpulumlamerop
import com.lagradost.cloudstream3.extractors.Solidfiles
import com.lagradost.cloudstream3.extractors.Ssbstream
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.Uqload1
import com.lagradost.cloudstream3.extractors.Uqload2
+import com.lagradost.cloudstream3.extractors.Urochsunloath
import com.lagradost.cloudstream3.extractors.Userload
import com.lagradost.cloudstream3.extractors.Userscloud
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.VidSrcExtractor
import com.lagradost.cloudstream3.extractors.VidSrcExtractor2
+import com.lagradost.cloudstream3.extractors.VidSrcTo
import com.lagradost.cloudstream3.extractors.VideoVard
import com.lagradost.cloudstream3.extractors.VideovardSX
import com.lagradost.cloudstream3.extractors.Vidgomunime
import com.lagradost.cloudstream3.extractors.Vidgomunimesb
+import com.lagradost.cloudstream3.extractors.Vidguardto
import com.lagradost.cloudstream3.extractors.VidhideExtractor
import com.lagradost.cloudstream3.extractors.Vidmoly
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.Wibufile
import com.lagradost.cloudstream3.extractors.XStreamCdn
+import com.lagradost.cloudstream3.extractors.Yipsu
import com.lagradost.cloudstream3.extractors.YourUpload
import com.lagradost.cloudstream3.extractors.YoutubeExtractor
import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor
@@ -301,7 +308,18 @@ enum class ExtractorLinkType {
/** No support at the moment */
TORRENT,
/** 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 {
@@ -871,6 +889,7 @@ val extractorApis: MutableList = arrayListOf(
Streamlare(),
VidSrcExtractor(),
VidSrcExtractor2(),
+ VidSrcTo(),
PlayLtXyz(),
AStreamHub(),
Vidplay(),
@@ -888,7 +907,14 @@ val extractorApis: MutableList = arrayListOf(
StreamWishExtractor(),
EmturbovidExtractor(),
Vtbe(),
- EPlayExtractor()
+ EPlayExtractor(),
+ Vidguardto(),
+ Simpulumlamerop(),
+ Urochsunloath(),
+ Yipsu(),
+ MetaGnathTuggers(),
+ Geodailymotion(),
+
)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt
index 153dbd3e..d9f0b382 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt
@@ -50,7 +50,7 @@ class JsUnpacker(packedJS: String?) {
throw Exception("Unknown p.a.c.k.e.r. encoding")
}
val unbase = Unbase(radix)
- p = Pattern.compile("\\b\\w+\\b")
+ p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""")
m = p.matcher(payload)
val decoded = StringBuilder(payload)
var replaceOffset = 0
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt
index eedb626a..cb527020 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt
@@ -45,6 +45,7 @@ import androidx.core.view.marginBottom
import androidx.core.view.marginLeft
import androidx.core.view.marginRight
import androidx.core.view.marginTop
+import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
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.RequestOptions.bitmapTransform
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.ChipDrawable
import com.google.android.material.chip.ChipGroup
@@ -208,6 +210,14 @@ object UIHelper {
}
}
+ fun View?.setAppBarNoScrollFlagsOnTV() {
+ if (isLayout(Globals.TV or EMULATOR)) {
+ this?.updateLayoutParams {
+ scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL
+ }
+ }
+ }
+
fun Activity.hideKeyboard() {
window?.decorView?.clearFocus()
this.findViewById(android.R.id.content)?.rootView?.let {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt
index 50a8df02..7d4d5d98 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt
@@ -187,7 +187,7 @@ object VideoDownloadManager {
private val DOWNLOAD_BAD_CONFIG =
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"
private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume"
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt
new file mode 100644
index 00000000..9ff5cc08
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt
@@ -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 = 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" }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt
new file mode 100644
index 00000000..1f33bca4
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt
@@ -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 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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt
new file mode 100644
index 00000000..61c00d6e
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt
@@ -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? = 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
+)
diff --git a/app/src/main/res/drawable/hourglass_24.xml b/app/src/main/res/drawable/hourglass_24.xml
new file mode 100644
index 00000000..7bd1ebbd
--- /dev/null
+++ b/app/src/main/res/drawable/hourglass_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/rounded_outline.xml b/app/src/main/res/drawable/rounded_outline.xml
new file mode 100644
index 00000000..b85ace8e
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_outline.xml
@@ -0,0 +1,13 @@
+
+
+
+ -
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml
index fd845ee8..4974a027 100644
--- a/app/src/main/res/layout/download_child_episode.xml
+++ b/app/src/main/res/layout/download_child_episode.xml
@@ -9,6 +9,7 @@
android:layout_height="50dp"
android:layout_marginBottom="5dp"
android:foreground="@drawable/outline_drawable"
+ android:focusable="true"
android:nextFocusLeft="@id/nav_rail_view"
android:nextFocusRight="@id/download_button"
app:cardBackgroundColor="@color/transparent"
@@ -84,7 +85,9 @@
android:layout_height="@dimen/download_size"
android:layout_gravity="center_vertical|end"
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" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml
index 226c1632..21f79ca6 100644
--- a/app/src/main/res/layout/download_header_episode.xml
+++ b/app/src/main/res/layout/download_header_episode.xml
@@ -9,6 +9,8 @@
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:foreground="@drawable/outline_drawable"
+ android:focusable="true"
+ android:nextFocusRight="@id/download_button"
app:cardBackgroundColor="?attr/boxItemBackground"
app:cardCornerRadius="@dimen/rounded_image_radius">
@@ -71,7 +73,9 @@
android:layout_height="@dimen/download_size"
android:layout_gravity="center_vertical|end"
android:layout_marginStart="-50dp"
- android:background="?selectableItemBackgroundBorderless"
+ android:foreground="@drawable/outline_drawable"
+ android:focusable="true"
+ android:nextFocusLeft="@id/episode_holder"
android:padding="10dp" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml
index 2ec2ae0a..893c19ff 100644
--- a/app/src/main/res/layout/fragment_result_tv.xml
+++ b/app/src/main/res/layout/fragment_result_tv.xml
@@ -178,42 +178,40 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:textStyle="bold"
tools:text="The Perfect Run The Perfect Run" />
+
+
-
-
+ android:orientation="horizontal">
+ android:layout_marginEnd="5dp"
+ tools:text="Season 2 Episode 1022 will be released in" />
+ android:scaleType="centerCrop"
+ android:foreground="@drawable/rounded_outline"
+ tools:src="@drawable/profile_bg_orange"
+ android:contentDescription="@string/account"/>
+
+ tools:text="Quick Brown Fox" />
-
-
+ android:layout_height="wrap_content"
+ tools:visibility="visible">
+ tools:src="@drawable/example_poster"
+ tools:visibility="invisible"/>
+ android:src="@drawable/play_button"
+ tools:visibility="invisible"/>
+
+
+
+
%1$s Ep %2$d
Cast: %s
Episode %d will be released in
+ Season %1$d Episode %2$d will be released in
%1$dd %2$dh %3$dm
%1$dh %2$dm
%dm
@@ -292,6 +293,7 @@
Episodes
%1$d-%2$d
%1$d %2$s
+ Upcoming in %s
S
E
No Episodes found
@@ -358,6 +360,7 @@
Download error, check storage permissions
Chromecast episode
Chromecast mirror
+ Cast mirror
Play in app
Play in %s
Play in browser
@@ -635,7 +638,9 @@
VLC
MPV
Web Video Cast
+ Fcast
Web browser
+ Select cast device
App not found
All Languages
Skip %s
diff --git a/build.gradle.kts b/build.gradle.kts
index 801a3c0f..ab1918fe 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,6 +8,8 @@ buildscript {
classpath("com.android.tools.build:gradle:8.2.2")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")
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
}
-tasks.register("clean") {
- delete(rootProject.layout.buildDirectory)
-}
+//tasks.register("clean") {
+// delete(rootProject.layout.buildDirectory)
+//}
diff --git a/library/build.gradle.kts b/library/build.gradle.kts
new file mode 100644
index 00000000..42a8c943
--- /dev/null
+++ b/library/build.gradle.kts
@@ -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 {
+ 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 {
+ groupId = "com.lagradost.api"
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/src/androidMain/AndroidManifest.xml b/library/src/androidMain/AndroidManifest.xml
new file mode 100644
index 00000000..568741e5
--- /dev/null
+++ b/library/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/library/src/androidMain/kotlin/com/lagradost/api/Log.kt b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt
new file mode 100644
index 00000000..12524411
--- /dev/null
+++ b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/api/Log.kt b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt
new file mode 100644
index 00000000..4b8e6329
--- /dev/null
+++ b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt
new file mode 100644
index 00000000..87ee4815
--- /dev/null
+++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt
@@ -0,0 +1,3 @@
+package com.lagradost.cloudstream3
+
+class ErrorLoadingException(message: String? = null) : Exception(message)
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
similarity index 86%
rename from app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
index 817d7db3..d3b4999a 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
+++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
@@ -1,10 +1,7 @@
package com.lagradost.cloudstream3.mvvm
-import android.util.Log
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.LiveData
-import com.bumptech.glide.load.HttpException
-import com.lagradost.cloudstream3.BuildConfig
+import com.lagradost.api.BuildConfig
+import com.lagradost.api.Log
import com.lagradost.cloudstream3.ErrorLoadingException
import kotlinx.coroutines.*
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 LifecycleOwner.observe(liveData: LiveData, 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 LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) {
- liveData.removeObservers(this)
- liveData.observe(this) { action(it) }
-}
-
sealed class Resource {
data class Success(val value: T) : Resource()
data class Failure(
@@ -158,14 +143,14 @@ fun throwAbleToResource(
"Connection Timeout\nPlease try again later."
)
}
- is HttpException -> {
- Resource.Failure(
- false,
- throwable.statusCode,
- null,
- throwable.message ?: "HttpException"
- )
- }
+// is HttpException -> {
+// Resource.Failure(
+// false,
+// throwable.statusCode,
+// null,
+// throwable.message ?: "HttpException"
+// )
+// }
is UnknownHostException -> {
Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}")
}
diff --git a/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt
new file mode 100644
index 00000000..e9a0e6b4
--- /dev/null
+++ b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt
@@ -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")
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 17070047..eabd9f0e 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,3 +1,4 @@
rootProject.name = "CloudStream"
-include(":app")
\ No newline at end of file
+include(":app")
+include(":library")
\ No newline at end of file