Merge branch 'recloudstream:master' into master

This commit is contained in:
self-similarity 2023-09-13 20:32:25 +00:00 committed by GitHub
commit cc288d2e8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1522 additions and 661 deletions

View file

@ -51,12 +51,12 @@ android {
} }
compileSdk = 33 compileSdk = 33
buildToolsVersion = "30.0.3" buildToolsVersion = "34.0.0"
defaultConfig { defaultConfig {
applicationId = "com.lagradost.cloudstream3" applicationId = "com.lagradost.cloudstream3"
minSdk = 21 minSdk = 21
targetSdk = 29 targetSdk = 33
versionCode = 59 versionCode = 59
versionName = "4.1.8" versionName = "4.1.8"
@ -233,7 +233,7 @@ dependencies {
// To fix SSL fuckery on android 9 // To fix SSL fuckery on android 9
implementation("org.conscrypt:conscrypt-android:2.2.1") implementation("org.conscrypt:conscrypt-android:2.2.1")
// Util to skip the URI file fuckery 🙏 // Util to skip the URI file fuckery 🙏
implementation("com.github.LagradOst:SafeFile:0.0.2") implementation("com.github.LagradOst:SafeFile:0.0.5")
// API because cba maintaining it myself // API because cba maintaining it myself
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
@ -250,9 +250,9 @@ dependencies {
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet // used for subtitle decoding https://github.com/albfernandez/juniversalchardet
implementation("com.github.albfernandez:juniversalchardet:2.4.0") implementation("com.github.albfernandez:juniversalchardet:2.4.0")
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204 // newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev
// this should be updated frequently to avoid trailer fu*kery // this should be updated frequently to avoid trailer fu*kery
implementation("com.github.TeamNewPipe:NewPipeExtractor:1f08d28") implementation("com.github.teamnewpipe:NewPipeExtractor:1f08d28")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
// Library/extensions searching with Levenshtein distance // Library/extensions searching with Levenshtein distance

View file

@ -7,9 +7,14 @@ import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.os.Build import android.os.Build
import android.util.DisplayMetrics
import android.util.Log import android.util.Log
import android.view.* import android.view.Gravity
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.View.NO_ID import android.view.View.NO_ID
import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
@ -40,7 +45,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.NewPipe
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.Locale
import kotlin.math.max
import kotlin.math.min
enum class FocusDirection { enum class FocusDirection {
Start, Start,
@ -63,6 +70,19 @@ object CommonActivity {
return (this as MainActivity?)?.mSessionManager?.currentCastSession return (this as MainActivity?)?.mSessionManager?.currentCastSession
} }
val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics
// screenWidth and screenHeight does always
// refer to the screen while in landscape mode
val screenWidth: Int
get() {
return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
}
val screenHeight: Int
get() {
return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
}
var canEnterPipMode: Boolean = false var canEnterPipMode: Boolean = false
var canShowPipMode: Boolean = false var canShowPipMode: Boolean = false
@ -328,6 +348,14 @@ object CommonActivity {
currentLook = currentLook.parent as? View ?: break currentLook = currentLook.parent as? View ?: break
}*/ }*/
private fun View.hasContent() : Boolean {
return isShown && when(this) {
//is RecyclerView -> this.childCount > 0
is ViewGroup -> this.childCount > 0
else -> true
}
}
/** skips the initial stage of searching for an id using the view, see getNextFocus for specification */ /** skips the initial stage of searching for an id using the view, see getNextFocus for specification */
fun continueGetNextFocus( fun continueGetNextFocus(
root: Any?, root: Any?,
@ -348,16 +376,17 @@ object CommonActivity {
} ?: return null } ?: return null
next = localLook(view, nextId) ?: next next = localLook(view, nextId) ?: next
val shown = next.hasContent()
// if cant focus but visible then break and let android decide // if cant focus but visible then break and let android decide
// the exception if is the view is a parent and has children that wants focus // the exception if is the view is a parent and has children that wants focus
val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent -> val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent ->
parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0 parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0
} ?: false } ?: false
if (!next.isFocusable && next.isShown && !hasChildrenThatWantsFocus) return null if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null
// if not shown then continue because we will "skip" over views to get to a replacement // if not shown then continue because we will "skip" over views to get to a replacement
if (!next.isShown) { if (!shown) {
// we don't want a while true loop, so we let android decide if we find a recursive view // we don't want a while true loop, so we let android decide if we find a recursive view
if (next == view) return null if (next == view) return null
return getNextFocus(root, next, direction, depth + 1) return getNextFocus(root, next, direction, depth + 1)

View file

@ -22,8 +22,10 @@ import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.mainWork
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.nicehttp.RequestBodyTypes
import okhttp3.Interceptor import okhttp3.Interceptor
import org.mozilla.javascript.Scriptable import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -180,7 +182,7 @@ object APIHolder {
/** /**
* Get anime tracker information based on title, year and type. * Get anime tracker information based on title, year and type.
* Both titles are attempted to be matched with both Romaji and English title. * Both titles are attempted to be matched with both Romaji and English title.
* Uses the consumet api. * Uses the anilist api.
* *
* @param titles uses first index to search, but if you have multiple titles and want extra guarantee to match you can also have that * @param titles uses first index to search, but if you have multiple titles and want extra guarantee to match you can also have that
* @param types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes() * @param types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes()
@ -189,7 +191,8 @@ object APIHolder {
suspend fun getTracker( suspend fun getTracker(
titles: List<String>, titles: List<String>,
types: Set<TrackerType>?, types: Set<TrackerType>?,
year: Int? year: Int?,
lessAccurate: Boolean = false
): Tracker? { ): Tracker? {
return try { return try {
require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" } require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" }
@ -197,30 +200,70 @@ object APIHolder {
val mainTitle = titles[0] val mainTitle = titles[0]
val search = val search =
trackerCache[mainTitle] trackerCache[mainTitle]
?: app.get("https://api.consumet.org/meta/anilist/$mainTitle") ?: searchAnilist(mainTitle)?.also {
.parsedSafe<AniSearch>()?.also { trackerCache[mainTitle] = it
trackerCache[mainTitle] = it } ?: return null
} ?: return null
val res = search.results?.find { media -> val res = search.data?.page?.media?.find { media ->
val matchingYears = year == null || media.releaseDate == year val matchingYears = year == null || media.seasonYear == year
val matchingTitles = media.title?.let { title -> val matchingTitles = media.title?.let { title ->
titles.any { userTitle -> titles.any { userTitle ->
title.isMatchingTitles(userTitle) title.isMatchingTitles(userTitle)
} }
} ?: false } ?: false
val matchingTypes = types?.any { it.name.equals(media.type, true) } == true val matchingTypes = types?.any { it.name.equals(media.format, true) } == true
matchingTitles && matchingTypes && matchingYears if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears
} ?: return null } ?: return null
Tracker(res.malId, res.aniId, res.image, res.cover) Tracker(res.idMal, res.id.toString(), res.coverImage?.extraLarge ?: res.coverImage?.large, res.bannerImage)
} catch (t: Throwable) { } catch (t: Throwable) {
logError(t) logError(t)
null null
} }
} }
private suspend fun searchAnilist(
title: String?,
): AniSearch? {
val query = """
query (
${'$'}page: Int = 1
${'$'}search: String
${'$'}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]
${'$'}type: MediaType
) {
Page(page: ${'$'}page, perPage: 20) {
media(
search: ${'$'}search
sort: ${'$'}sort
type: ${'$'}type
) {
id
idMal
title { romaji english }
coverImage { extraLarge large }
bannerImage
seasonYear
format
}
}
}
""".trimIndent().trim()
val data = mapOf(
"query" to query,
"variables" to mapOf(
"search" to title,
"sort" to "SEARCH_MATCH",
"type" to "ANIME",
)
).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
return app.post("https://graphql.anilist.co", requestBody = data)
.parsedSafe()
}
fun Context.getApiSettings(): HashSet<String> { fun Context.getApiSettings(): HashSet<String> {
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
@ -1730,30 +1773,42 @@ data class Tracker(
val cover: String? = null, val cover: String? = null,
) )
data class Title( data class AniSearch(
@JsonProperty("romaji") val romaji: String? = null, @JsonProperty("data") var data: Data? = Data()
@JsonProperty("english") val english: String? = null,
) { ) {
fun isMatchingTitles(title: String?): Boolean { data class Data(
if (title == null) return false @JsonProperty("Page") var page: Page? = Page()
return english.equals(title, true) || romaji.equals(title, true) ) {
data class Page(
@JsonProperty("media") var media: ArrayList<Media> = arrayListOf()
) {
data class Media(
@JsonProperty("title") var title: Title? = null,
@JsonProperty("id") var id: Int? = null,
@JsonProperty("idMal") var idMal: Int? = null,
@JsonProperty("seasonYear") var seasonYear: Int? = null,
@JsonProperty("format") var format: String? = null,
@JsonProperty("coverImage") var coverImage: CoverImage? = null,
@JsonProperty("bannerImage") var bannerImage: String? = null,
) {
data class CoverImage(
@JsonProperty("extraLarge") var extraLarge: String? = null,
@JsonProperty("large") var large: String? = null,
)
data class Title(
@JsonProperty("romaji") var romaji: String? = null,
@JsonProperty("english") var english: String? = null,
) {
fun isMatchingTitles(title: String?): Boolean {
if (title == null) return false
return english.equals(title, true) || romaji.equals(title, true)
}
}
}
}
} }
} }
data class Results(
@JsonProperty("id") val aniId: String? = null,
@JsonProperty("malId") val malId: Int? = null,
@JsonProperty("title") val title: Title? = null,
@JsonProperty("releaseDate") val releaseDate: Int? = null,
@JsonProperty("type") val type: String? = null,
@JsonProperty("image") val image: String? = null,
@JsonProperty("cover") val cover: String? = null,
)
data class AniSearch(
@JsonProperty("results") val results: ArrayList<Results>? = arrayListOf()
)
/** /**
* used for the getTracker() method * used for the getTracker() method
**/ **/

View file

@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -52,6 +53,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.common.collect.Comparators.min
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.apis
@ -64,13 +66,13 @@ import com.lagradost.cloudstream3.CommonActivity.loadThemes
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.screenHeight
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.databinding.ActivityMainBinding import com.lagradost.cloudstream3.databinding.ActivityMainBinding
import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding
import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.mvvm.observeNullable
@ -832,6 +834,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
focusOutline.get()?.isVisible = false focusOutline.get()?.isVisible = false
} }
} }
/*private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
current = current.copy(x = current.x + dx, y = current.y + dy)
setTargetPosition(current)
}
}*/
private fun setTargetPosition(target: FocusTarget) { private fun setTargetPosition(target: FocusTarget) {
focusOutline.get()?.apply { focusOutline.get()?.apply {
@ -874,7 +883,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (!exactlyTheSame) { if (!exactlyTheSame) {
lastView?.removeOnLayoutChangeListener(layoutListener) lastView?.removeOnLayoutChangeListener(layoutListener)
lastView?.removeOnAttachStateChangeListener(attachListener) lastView?.removeOnAttachStateChangeListener(attachListener)
(lastView?.parent as? RecyclerView)?.removeOnLayoutChangeListener(layoutListener) (lastView?.parent as? RecyclerView)?.apply {
removeOnLayoutChangeListener(layoutListener)
//removeOnScrollListener(scrollListener)
}
} }
val wasGone = focusOutline.isGone val wasGone = focusOutline.isGone
@ -952,7 +964,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
focusOutline.isVisible = false focusOutline.isVisible = false
} }
if (!exactlyTheSame) { if (!exactlyTheSame) {
(newFocus.parent as? RecyclerView)?.addOnLayoutChangeListener(layoutListener) (newFocus.parent as? RecyclerView)?.apply {
addOnLayoutChangeListener(layoutListener)
//addOnScrollListener(scrollListener)
}
newFocus.addOnLayoutChangeListener(layoutListener) newFocus.addOnLayoutChangeListener(layoutListener)
newFocus.addOnAttachStateChangeListener(attachListener) newFocus.addOnAttachStateChangeListener(attachListener)
} }
@ -970,8 +985,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
) )
// if they are the same within then snap, aka scrolling // if they are the same within then snap, aka scrolling
val deltaMin = 50.toPx val deltaMinX = min(end.width / 2, 60.toPx)
if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMin && (start.y - end.y).absoluteValue < deltaMin) { val deltaMinY = min(end.height / 2, 60.toPx)
if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMinX && (start.y - end.y).absoluteValue < deltaMinY) {
animator?.cancel() animator?.cancel()
last = start last = start
current = end current = end
@ -1000,7 +1016,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// animate between a and b // animate between a and b
animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
startDelay = 0 startDelay = 0
duration = 100 duration = 200
addUpdateListener { animation -> addUpdateListener { animation ->
val animatedValue = animation.animatedValue as Float val animatedValue = animation.animatedValue as Float
val target = FocusTarget.lerp(last, current, minOf(animatedValue, 1.0f)) val target = FocusTarget.lerp(last, current, minOf(animatedValue, 1.0f))
@ -1095,7 +1111,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
// println("refocus $oldFocus -> $newFocus") // println("refocus $oldFocus -> $newFocus")
try {
val r = Rect(0,0,0,0)
newFocus.getDrawingRect(r)
val x = r.centerX()
val y = r.centerY()
val dx = 0 //screenWidth / 2
val dy = screenHeight / 2
val r2 = Rect(x-dx,y-dy,x+dx,y+dy)
newFocus.requestRectangleOnScreen(r2, false)
// TvFocus.current =TvFocus.current.copy(y=y.toFloat())
} catch (_ : Throwable) { }
TvFocus.updateFocusView(newFocus) TvFocus.updateFocusView(newFocus)
/*var focus = newFocus
while(focus != null) {
if(focus is ScrollingView && focus.canScrollVertically()) {
focus.scrollBy()
}
when(focus.parent) {
is View -> focus = newFocus
else -> break
}
}*/
} }
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)

View file

@ -2,15 +2,12 @@ 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.utils.AppUtils 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.Qualities import com.lagradost.cloudstream3.utils.Qualities
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
class Moviesapi : Chillx() { class Moviesapi : Chillx() {
override val name = "Moviesapi" override val name = "Moviesapi"
@ -32,7 +29,7 @@ open class Chillx : ExtractorApi() {
override val requiresReferer = true override val requiresReferer = true
companion object { companion object {
private const val KEY = "11x&W5UBrcqn\$9Yl" private const val KEY = "m4H6D9%0\$N&F6rQ&"
} }
override suspend fun getUrl( override suspend fun getUrl(
@ -47,8 +44,7 @@ open class Chillx : ExtractorApi() {
referer = referer referer = referer
).text ).text
)?.groupValues?.get(1) )?.groupValues?.get(1)
val encData = AppUtils.tryParseJson<AESData>(base64Decode(master ?: return)) val decrypt = cryptoAESHandler(master ?: return, KEY.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
val decrypt = cryptoAESHandler(encData ?: return, KEY, false)
val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1) val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1)
@ -86,52 +82,6 @@ open class Chillx : ExtractorApi() {
} }
} }
private fun cryptoAESHandler(
data: AESData,
pass: String,
encrypt: Boolean = true
): String {
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")
val spec = PBEKeySpec(
pass.toCharArray(),
data.salt?.hexToByteArray(),
data.iterations?.toIntOrNull() ?: 1,
256
)
val key = factory.generateSecret(spec)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
return if (!encrypt) {
cipher.init(
Cipher.DECRYPT_MODE,
SecretKeySpec(key.encoded, "AES"),
IvParameterSpec(data.iv?.hexToByteArray())
)
String(cipher.doFinal(base64DecodeArray(data.ciphertext.toString())))
} else {
cipher.init(
Cipher.ENCRYPT_MODE,
SecretKeySpec(key.encoded, "AES"),
IvParameterSpec(data.iv?.hexToByteArray())
)
base64Encode(cipher.doFinal(data.ciphertext?.toByteArray()))
}
}
private fun String.hexToByteArray(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
data class AESData(
@JsonProperty("ciphertext") val ciphertext: String? = null,
@JsonProperty("iv") val iv: String? = null,
@JsonProperty("salt") val salt: String? = null,
@JsonProperty("iterations") val iterations: String? = null,
)
data class Tracks( data class Tracks(
@JsonProperty("file") val file: String? = null, @JsonProperty("file") val file: String? = null,
@JsonProperty("label") val label: String? = null, @JsonProperty("label") val label: String? = null,

View file

@ -2,14 +2,10 @@ 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.AesHelper.cryptoAESHandler
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.security.DigestException
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class DatabaseGdrive2 : Gdriveplayer() { class DatabaseGdrive2 : Gdriveplayer() {
override var mainUrl = "https://databasegdriveplayer.co" override var mainUrl = "https://databasegdriveplayer.co"
@ -65,78 +61,6 @@ open class Gdriveplayer : ExtractorApi() {
?.data()?.let { getAndUnpack(it) } ?.data()?.let { getAndUnpack(it) }
} }
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
// https://stackoverflow.com/a/41434590/8166854
private fun GenerateKeyAndIv(
password: ByteArray,
salt: ByteArray,
hashAlgorithm: String = "MD5",
keyLength: Int = 32,
ivLength: Int = 16,
iterations: Int = 1
): List<ByteArray>? {
val md = MessageDigest.getInstance(hashAlgorithm)
val digestLength = md.digestLength
val targetKeySize = keyLength + ivLength
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
val generatedData = ByteArray(requiredLength)
var generatedLength = 0
try {
md.reset()
while (generatedLength < targetKeySize) {
if (generatedLength > 0)
md.update(
generatedData,
generatedLength - digestLength,
digestLength
)
md.update(password)
md.update(salt, 0, 8)
md.digest(generatedData, generatedLength, digestLength)
for (i in 1 until iterations) {
md.update(generatedData, generatedLength, digestLength)
md.digest(generatedData, generatedLength, digestLength)
}
generatedLength += digestLength
}
return listOf(
generatedData.copyOfRange(0, keyLength),
generatedData.copyOfRange(keyLength, targetKeySize)
)
} catch (e: DigestException) {
return null
}
}
private fun cryptoAESHandler(
data: AesData,
pass: ByteArray,
encrypt: Boolean = true
): String? {
val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
String(cipher.doFinal(base64DecodeArray(data.ct)))
} else {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
base64Encode(cipher.doFinal(data.ct.toByteArray()))
}
}
private fun Regex.first(str: String): String? { private fun Regex.first(str: String): String? {
return find(str)?.groupValues?.getOrNull(1) return find(str)?.groupValues?.getOrNull(1)
} }
@ -154,14 +78,14 @@ open class Gdriveplayer : ExtractorApi() {
val document = app.get(url).document val document = app.get(url).document
val eval = unpackJs(document)?.replace("\\", "") ?: return val eval = unpackJs(document)?.replace("\\", "") ?: return
val data = tryParseJson<AesData>(Regex("data='(\\S+?)'").first(eval)) ?: return val data = Regex("data='(\\S+?)'").first(eval) ?: return
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval) val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
?.split(Regex("\\D+")) ?.split(Regex("\\D+"))
?.joinToString("") { ?.joinToString("") {
Char(it.toInt()).toString() Char(it.toInt()).toString()
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() } }.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
?: throw ErrorLoadingException("can't find password") ?: throw ErrorLoadingException("can't find password")
val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "") val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "")
val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],") val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],") val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
@ -194,12 +118,6 @@ open class Gdriveplayer : ExtractorApi() {
} }
data class AesData(
@JsonProperty("ct") val ct: String,
@JsonProperty("iv") val iv: String,
@JsonProperty("s") val s: String
)
data class Tracks( data class Tracks(
@JsonProperty("file") val file: String, @JsonProperty("file") val file: String,
@JsonProperty("kind") val kind: String, @JsonProperty("kind") val kind: String,

View file

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

View file

@ -7,15 +7,22 @@ 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 SpeedoStream2 : SpeedoStream() {
override val mainUrl = "https://speedostream.mom"
}
class SpeedoStream1 : SpeedoStream() { class SpeedoStream1 : SpeedoStream() {
override val mainUrl = "https://speedostream.pm" override val mainUrl = "https://speedostream.pm"
} }
open class SpeedoStream : ExtractorApi() { open class SpeedoStream : ExtractorApi() {
override val name = "SpeedoStream" override val name = "SpeedoStream"
override val mainUrl = "https://speedostream.mom" override val mainUrl = "https://speedostream.bond"
override val requiresReferer = true override val requiresReferer = true
// .bond, .pm, .mom redirect to .bond
private val hostUrl = "https://speedostream.bond"
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>() val sources = mutableListOf<ExtractorLink>()
app.get(url, referer = referer).document.select("script").map { script -> app.get(url, referer = referer).document.select("script").map { script ->
@ -26,7 +33,7 @@ open class SpeedoStream : ExtractorApi() {
M3u8Helper.generateM3u8( M3u8Helper.generateM3u8(
name, name,
it.file, it.file,
"$mainUrl/", "$hostUrl/",
).forEach { m3uData -> sources.add(m3uData) } ).forEach { m3uData -> sources.add(m3uData) }
} }
} }
@ -37,6 +44,4 @@ open class SpeedoStream : ExtractorApi() {
private data class File( private data class File(
@JsonProperty("file") val file: String, @JsonProperty("file") val file: String,
) )
} }

View file

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

View file

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

View file

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

View file

@ -0,0 +1,95 @@
package com.lagradost.cloudstream3.extractors.helper
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.base64DecodeArray
import com.lagradost.cloudstream3.base64Encode
import com.lagradost.cloudstream3.utils.AppUtils
import java.security.DigestException
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object AesHelper {
private const val HASH = "AES/CBC/PKCS5PADDING"
private const val KDF = "MD5"
fun cryptoAESHandler(
data: String,
pass: ByteArray,
encrypt: Boolean = true,
padding: String = HASH,
): String? {
val parse = AppUtils.tryParseJson<AesData>(data) ?: return null
val (key, iv) = generateKeyAndIv(pass, parse.s.hexToByteArray()) ?: return null
val cipher = Cipher.getInstance(padding)
return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
String(cipher.doFinal(base64DecodeArray(parse.ct)))
} else {
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
base64Encode(cipher.doFinal(parse.ct.toByteArray()))
}
}
// https://stackoverflow.com/a/41434590/8166854
fun generateKeyAndIv(
password: ByteArray,
salt: ByteArray,
hashAlgorithm: String = KDF,
keyLength: Int = 32,
ivLength: Int = 16,
iterations: Int = 1
): Pair<ByteArray,ByteArray>? {
val md = MessageDigest.getInstance(hashAlgorithm)
val digestLength = md.digestLength
val targetKeySize = keyLength + ivLength
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
val generatedData = ByteArray(requiredLength)
var generatedLength = 0
try {
md.reset()
while (generatedLength < targetKeySize) {
if (generatedLength > 0)
md.update(
generatedData,
generatedLength - digestLength,
digestLength
)
md.update(password)
md.update(salt, 0, 8)
md.digest(generatedData, generatedLength, digestLength)
for (i in 1 until iterations) {
md.update(generatedData, generatedLength, digestLength)
md.digest(generatedData, generatedLength, digestLength)
}
generatedLength += digestLength
}
return generatedData.copyOfRange(0, keyLength) to generatedData.copyOfRange(keyLength, targetKeySize)
} catch (e: DigestException) {
return null
}
}
fun String.hexToByteArray(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
private data class AesData(
@JsonProperty("ct") val ct: String,
@JsonProperty("iv") val iv: String,
@JsonProperty("s") val s: String
)
}

View file

@ -2,12 +2,13 @@ package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Log import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.google.common.collect.BiMap
import com.google.common.collect.HashBiMap
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.subtitles.AbstractSubApi import com.lagradost.cloudstream3.subtitles.AbstractSubApi
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
@ -15,8 +16,8 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils
import java.net.URLEncoder import okhttp3.Interceptor
import java.nio.charset.StandardCharsets import okhttp3.Response
class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi { class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
override val idPrefix = "opensubtitles" override val idPrefix = "opensubtitles"
@ -36,6 +37,23 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
var currentSession: SubtitleOAuthEntity? = null var currentSession: SubtitleOAuthEntity? = null
} }
private val headerInterceptor = OpenSubtitleInterceptor()
/** Automatically adds required api headers */
private class OpenSubtitleInterceptor : Interceptor {
/** Required user agent! */
private val userAgent = "Cloudstream3 v0.1"
override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(
chain.request().newBuilder()
.removeHeader("user-agent")
.addHeader("user-agent", userAgent)
.addHeader("Api-Key", apiKey)
.build()
)
}
}
private fun canDoRequest(): Boolean { private fun canDoRequest(): Boolean {
return unixTimeMs > currentCoolDown return unixTimeMs > currentCoolDown
} }
@ -98,13 +116,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
val response = app.post( val response = app.post(
url = "$host/login", url = "$host/login",
headers = mapOf( headers = mapOf(
"Api-Key" to apiKey, "Content-Type" to "application/json",
"Content-Type" to "application/json"
), ),
data = mapOf( data = mapOf(
"username" to username, "username" to username,
"password" to password "password" to password
) ),
interceptor = headerInterceptor
) )
//Log.i(TAG, "Responsecode = ${response.code}") //Log.i(TAG, "Responsecode = ${response.code}")
//Log.i(TAG, "Result => ${response.text}") //Log.i(TAG, "Result => ${response.text}")
@ -149,11 +167,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
// "pt" to "pt-PT", // "pt" to "pt-PT",
// "pt" to "pt-BR" // "pt" to "pt-BR"
) )
private fun fixLanguage(language: String?) : String? {
private fun fixLanguage(language: String?): String? {
return languageExceptions[language] ?: language return languageExceptions[language] ?: language
} }
// O(n) but good enough, BiMap did not want to work properly // O(n) but good enough, BiMap did not want to work properly
private fun fixLanguageReverse(language: String?) : String? { private fun fixLanguageReverse(language: String?): String? {
return languageExceptions.entries.firstOrNull { it.value == language }?.key ?: language return languageExceptions.entries.firstOrNull { it.value == language }?.key ?: language
} }
@ -183,9 +203,9 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
val req = app.get( val req = app.get(
url = searchQueryUrl, url = searchQueryUrl,
headers = mapOf( headers = mapOf(
Pair("Api-Key", apiKey),
Pair("Content-Type", "application/json") Pair("Content-Type", "application/json")
) ),
interceptor = headerInterceptor
) )
Log.i(TAG, "Search Req => ${req.text}") Log.i(TAG, "Search Req => ${req.text}")
if (!req.isSuccessful) { if (!req.isSuccessful) {
@ -207,7 +227,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
//Use any valid name/title in hierarchy //Use any valid name/title in hierarchy
val name = filename ?: featureDetails?.movieName ?: featureDetails?.title val name = filename ?: featureDetails?.movieName ?: featureDetails?.title
?: featureDetails?.parentTitle ?: attr.release ?: query.query ?: featureDetails?.parentTitle ?: attr.release ?: query.query
val lang = fixLanguageReverse(attr.language)?: "" val lang = fixLanguageReverse(attr.language) ?: ""
val resEpNum = featureDetails?.episodeNumber ?: query.epNumber val resEpNum = featureDetails?.episodeNumber ?: query.epNumber
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
val year = featureDetails?.year ?: query.year val year = featureDetails?.year ?: query.year
@ -251,13 +271,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
"Authorization", "Authorization",
"Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}" "Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}"
), ),
Pair("Api-Key", apiKey),
Pair("Content-Type", "application/json"), Pair("Content-Type", "application/json"),
Pair("Accept", "*/*") Pair("Accept", "*/*")
), ),
data = mapOf( data = mapOf(
Pair("file_id", data.data) Pair("file_id", data.data)
) ),
interceptor = headerInterceptor
) )
Log.i(TAG, "Request result => (${req.code}) ${req.text}") Log.i(TAG, "Request result => (${req.code}) ${req.text}")
//Log.i(TAG, "Request headers => ${req.headers}") //Log.i(TAG, "Request headers => ${req.headers}")

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import android.graphics.drawable.AnimatedVectorDrawable
import android.media.metrics.PlaybackErrorEvent import android.media.metrics.PlaybackErrorEvent
import android.os.Build import android.os.Build
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
@ -25,8 +26,10 @@ 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.AspectRatioFrameLayout
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import androidx.media3.ui.SubtitleView import androidx.media3.ui.SubtitleView
import androidx.media3.ui.TimeBar
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode
@ -92,11 +95,11 @@ abstract class AbstractPlayerFragment(
throw NotImplementedError() throw NotImplementedError()
} }
open fun playerPositionChanged(posDur: Pair<Long, Long>) { open fun playerPositionChanged(position: Long, duration : Long) {
throw NotImplementedError() throw NotImplementedError()
} }
open fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) { open fun playerDimensionsLoaded(width: Int, height : Int) {
throw NotImplementedError() throw NotImplementedError()
} }
@ -132,8 +135,8 @@ abstract class AbstractPlayerFragment(
} }
} }
private fun updateIsPlaying(playing: Pair<CSPlayerLoading, CSPlayerLoading>) { private fun updateIsPlaying(wasPlaying : CSPlayerLoading,
val (wasPlaying, isPlaying) = playing isPlaying : CSPlayerLoading) {
val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying
val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying
@ -206,7 +209,7 @@ abstract class AbstractPlayerFragment(
CSPlayerEvent.values()[intent.getIntExtra( CSPlayerEvent.values()[intent.getIntExtra(
EXTRA_CONTROL_TYPE, EXTRA_CONTROL_TYPE,
0 0
)] )], source = PlayerEventSource.UI
) )
} }
} }
@ -216,7 +219,7 @@ abstract class AbstractPlayerFragment(
val isPlaying = player.getIsPlaying() val isPlaying = player.getIsPlaying()
val isPlayingValue = val isPlayingValue =
if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused
updateIsPlaying(Pair(isPlayingValue, isPlayingValue)) updateIsPlaying(isPlayingValue, isPlayingValue)
} else { } else {
// Restore the full-screen UI. // Restore the full-screen UI.
piphide?.isVisible = true piphide?.isVisible = true
@ -249,7 +252,7 @@ abstract class AbstractPlayerFragment(
} }
} }
open fun playerError(exception: Exception) { open fun playerError(exception: Throwable) {
fun showToast(message: String, gotoNext: Boolean = false) { fun showToast(message: String, gotoNext: Boolean = false) {
if (gotoNext && hasNextMirror()) { if (gotoNext && hasNextMirror()) {
showToast( showToast(
@ -326,6 +329,7 @@ abstract class AbstractPlayerFragment(
} }
} }
@SuppressLint("UnsafeOptInUsageError")
private fun playerUpdated(player: Any?) { private fun playerUpdated(player: Any?) {
if (player is ExoPlayer) { if (player is ExoPlayer) {
context?.let { ctx -> context?.let { ctx ->
@ -366,33 +370,87 @@ abstract class AbstractPlayerFragment(
// } // }
//} //}
/** This receives the events from the player, if you want to append functionality you do it here,
* do note that this only receives events for UI changes,
* and returning early WONT stop it from changing in eg the player time or pause status */
open fun mainCallback(event : PlayerEvent) {
Log.i(TAG, "Handle event: $event")
when(event) {
is ResizedEvent -> {
playerDimensionsLoaded(event.width, event.height)
}
is PlayerAttachedEvent -> {
playerUpdated(event.player)
}
is SubtitlesUpdatedEvent -> {
subtitlesChanged()
}
is TimestampSkippedEvent -> {
onTimestampSkipped(event.timestamp)
}
is TimestampInvokedEvent -> {
onTimestamp(event.timestamp)
}
is TracksChangedEvent -> {
onTracksInfoChanged()
}
is EmbeddedSubtitlesFetchedEvent -> {
embeddedSubtitlesFetched(event.tracks)
}
is ErrorEvent -> {
playerError(event.error)
}
is RequestAudioFocusEvent -> {
requestAudioFocus()
}
is EpisodeSeekEvent -> {
when(event.offset) {
-1 -> prevEpisode()
1 -> nextEpisode()
else -> {}
}
}
is StatusEvent -> {
updateIsPlaying(wasPlaying = event.wasPlaying, isPlaying = event.isPlaying)
}
is PositionEvent -> {
playerPositionChanged(position = event.toMs, duration = event.durationMs)
}
is VideoEndedEvent -> {
context?.let { ctx ->
// Only play next episode if autoplay is on (default)
if (PreferenceManager.getDefaultSharedPreferences(ctx)
?.getBoolean(
ctx.getString(R.string.autoplay_next_key),
true
) == true
) {
player.handleEvent(
CSPlayerEvent.NextEpisode,
source = PlayerEventSource.Player
)
}
}
}
is PauseEvent -> Unit
is PlayEvent -> Unit
}
}
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n", "UnsafeOptInUsageError")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
resizeMode = getKey(RESIZE_MODE_KEY) ?: 0 resizeMode = getKey(RESIZE_MODE_KEY) ?: 0
resize(resizeMode, false) resize(resizeMode, false)
player.releaseCallbacks() player.releaseCallbacks()
player.initCallbacks( player.initCallbacks(
playerUpdated = ::playerUpdated, eventHandler = ::mainCallback,
updateIsPlaying = ::updateIsPlaying,
playerError = ::playerError,
requestAutoFocus = ::requestAudioFocus,
nextEpisode = ::nextEpisode,
prevEpisode = ::prevEpisode,
playerPositionChanged = ::playerPositionChanged,
playerDimensionsLoaded = ::playerDimensionsLoaded,
requestedListeningPercentages = listOf( requestedListeningPercentages = listOf(
SKIP_OP_VIDEO_PERCENTAGE, SKIP_OP_VIDEO_PERCENTAGE,
PRELOAD_NEXT_EPISODE_PERCENTAGE, PRELOAD_NEXT_EPISODE_PERCENTAGE,
NEXT_WATCH_EPISODE_PERCENTAGE, NEXT_WATCH_EPISODE_PERCENTAGE,
UPDATE_SYNC_PROGRESS_PERCENTAGE, UPDATE_SYNC_PROGRESS_PERCENTAGE,
), ),
subtitlesUpdates = ::subtitlesChanged,
embeddedSubtitlesFetched = ::embeddedSubtitlesFetched,
onTracksInfoChanged = ::onTracksInfoChanged,
onTimestampInvoked = ::onTimestamp,
onTimestampSkipped = ::onTimestampSkipped
) )
if (player is CS3IPlayer) { if (player is CS3IPlayer) {
@ -400,6 +458,19 @@ abstract class AbstractPlayerFragment(
subStyle = SubtitlesFragment.getCurrentSavedStyle() subStyle = SubtitlesFragment.getCurrentSavedStyle()
player.initSubtitles(subView, subtitleHolder, subStyle) player.initSubtitles(subView, subtitleHolder, subStyle)
/** this might seam a bit fucky and that is because it is, the seek event is captured twice, once by the player
* and once by the UI even if it should only be registered once by the UI */
playerView?.findViewById<DefaultTimeBar>(R.id.exo_progress)?.addListener(object : TimeBar.OnScrubListener {
override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit
override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
if (canceled) return
val playerDuration = player.getDuration() ?: return
val playerPosition = player.getPosition() ?: return
mainCallback(PositionEvent(source = PlayerEventSource.UI, durationMs = playerDuration, fromMs = playerPosition, toMs = position))
}
})
SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged
try { try {
@ -461,6 +532,7 @@ abstract class AbstractPlayerFragment(
resize(PlayerResize.values()[resize], showToast) resize(PlayerResize.values()[resize], showToast)
} }
@SuppressLint("UnsafeOptInUsageError")
fun resize(resize: PlayerResize, showToast: Boolean) { fun resize(resize: PlayerResize, showToast: Boolean) {
setKey(RESIZE_MODE_KEY, resize.ordinal) setKey(RESIZE_MODE_KEY, resize.ordinal)
val type = when (resize) { val type = when (resize) {

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.player package com.lagradost.cloudstream3.ui.player
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Handler import android.os.Handler
@ -31,6 +32,10 @@ import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.SeekParameters import androidx.media3.exoplayer.SeekParameters
import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager
import androidx.media3.exoplayer.drm.FrameworkMediaDrm
import androidx.media3.exoplayer.drm.LocalMediaDrmCallback
import androidx.media3.exoplayer.source.ClippingMediaSource import androidx.media3.exoplayer.source.ClippingMediaSource
import androidx.media3.exoplayer.source.ConcatenatingMediaSource import androidx.media3.exoplayer.source.ConcatenatingMediaSource
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
@ -50,12 +55,16 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.DrmExtractorLink
import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.ExtractorUri
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import java.io.File import java.io.File
import java.lang.IllegalArgumentException
import java.util.UUID
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSession import javax.net.ssl.SSLSession
@ -102,7 +111,16 @@ class CS3IPlayer : IPlayer {
* */ * */
data class MediaItemSlice( data class MediaItemSlice(
val mediaItem: MediaItem, val mediaItem: MediaItem,
val durationUs: Long val durationUs: Long,
val drm: DrmMetadata? = null
)
data class DrmMetadata(
val kid: String,
val key: String,
val uuid: UUID,
val kty: String,
val keyRequestParameters: HashMap<String, String>,
) )
override fun getDuration(): Long? = exoPlayer?.duration override fun getDuration(): Long? = exoPlayer?.duration
@ -116,80 +134,24 @@ class CS3IPlayer : IPlayer {
* Boolean = if it's active * Boolean = if it's active
* */ * */
private var playerSelectedSubtitleTracks = listOf<Pair<String, Boolean>>() private var playerSelectedSubtitleTracks = listOf<Pair<String, Boolean>>()
/** isPlaying */
private var updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)? = null
private var requestAutoFocus: (() -> Unit)? = null
private var playerError: ((Exception) -> Unit)? = null
private var subtitlesUpdates: (() -> Unit)? = null
/** width x height */
private var playerDimensionsLoaded: ((Pair<Int, Int>) -> Unit)? = null
/** used for playerPositionChanged */
private var requestedListeningPercentages: List<Int>? = null private var requestedListeningPercentages: List<Int>? = null
/** Fired when seeking the player or on requestedListeningPercentages, private var eventHandler: ((PlayerEvent) -> Unit)? = null
* used to make things appear on que
* position, duration */
private var playerPositionChanged: ((Pair<Long, Long>) -> Unit)? = null
private var nextEpisode: (() -> Unit)? = null fun event(event: PlayerEvent) {
private var prevEpisode: (() -> Unit)? = null eventHandler?.invoke(event)
}
private var playerUpdated: ((Any?) -> Unit)? = null
private var embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)? = null
private var onTracksInfoChanged: (() -> Unit)? = null
private var onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)? = null
private var onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)? = null
override fun releaseCallbacks() { override fun releaseCallbacks() {
playerUpdated = null eventHandler = null
updateIsPlaying = null
requestAutoFocus = null
playerError = null
playerDimensionsLoaded = null
requestedListeningPercentages = null
playerPositionChanged = null
nextEpisode = null
prevEpisode = null
subtitlesUpdates = null
onTracksInfoChanged = null
onTimestampInvoked = null
requestSubtitleUpdate = null
onTimestampSkipped = null
} }
override fun initCallbacks( override fun initCallbacks(
playerUpdated: (Any?) -> Unit, eventHandler: ((PlayerEvent) -> Unit),
updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)?,
requestAutoFocus: (() -> Unit)?,
playerError: ((Exception) -> Unit)?,
playerDimensionsLoaded: ((Pair<Int, Int>) -> Unit)?,
requestedListeningPercentages: List<Int>?, requestedListeningPercentages: List<Int>?,
playerPositionChanged: ((Pair<Long, Long>) -> Unit)?,
nextEpisode: (() -> Unit)?,
prevEpisode: (() -> Unit)?,
subtitlesUpdates: (() -> Unit)?,
embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)?,
onTracksInfoChanged: (() -> Unit)?,
onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)?,
onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)?,
) { ) {
this.playerUpdated = playerUpdated
this.updateIsPlaying = updateIsPlaying
this.requestAutoFocus = requestAutoFocus
this.playerError = playerError
this.playerDimensionsLoaded = playerDimensionsLoaded
this.requestedListeningPercentages = requestedListeningPercentages this.requestedListeningPercentages = requestedListeningPercentages
this.playerPositionChanged = playerPositionChanged this.eventHandler = eventHandler
this.nextEpisode = nextEpisode
this.prevEpisode = prevEpisode
this.subtitlesUpdates = subtitlesUpdates
this.embeddedSubtitlesFetched = embeddedSubtitlesFetched
this.onTracksInfoChanged = onTracksInfoChanged
this.onTimestampInvoked = onTimestampInvoked
this.onTimestampSkipped = onTimestampSkipped
} }
// I know, this is not a perfect solution, however it works for fixing subs // I know, this is not a perfect solution, however it works for fixing subs
@ -198,7 +160,7 @@ class CS3IPlayer : IPlayer {
try { try {
Handler(it).post { Handler(it).post {
try { try {
seekTime(1L) seekTime(1L, source = PlayerEventSource.Player)
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }
@ -252,8 +214,9 @@ class CS3IPlayer : IPlayer {
subtitleHelper.setAllSubtitles(subtitles) subtitleHelper.setAllSubtitles(subtitles)
} }
var currentSubtitles: SubtitleData? = null private var currentSubtitles: SubtitleData? = null
@SuppressLint("UnsafeOptInUsageError")
private fun List<Tracks.Group>.getTrack(id: String?): Pair<TrackGroup, Int>? { private fun List<Tracks.Group>.getTrack(id: String?): Pair<TrackGroup, Int>? {
if (id == null) return null if (id == null) return null
// This beast of an expression does: // This beast of an expression does:
@ -338,6 +301,7 @@ class CS3IPlayer : IPlayer {
}.flatten() }.flatten()
} }
@SuppressLint("UnsafeOptInUsageError")
private fun Tracks.Group.getFormats(): List<Pair<Format, Int>> { private fun Tracks.Group.getFormats(): List<Pair<Format, Int>> {
return (0 until this.mediaTrackGroup.length).mapNotNull { i -> return (0 until this.mediaTrackGroup.length).mapNotNull { i ->
if (this.isSupported) if (this.isSupported)
@ -366,6 +330,7 @@ class CS3IPlayer : IPlayer {
) )
} }
@SuppressLint("UnsafeOptInUsageError")
override fun getVideoTracks(): CurrentTracks { override fun getVideoTracks(): CurrentTracks {
val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList() val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList()
val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO } val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO }
@ -385,6 +350,7 @@ class CS3IPlayer : IPlayer {
/** /**
* @return True if the player should be reloaded * @return True if the player should be reloaded
* */ * */
@SuppressLint("UnsafeOptInUsageError")
override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean {
Log.i(TAG, "setPreferredSubtitles init $subtitle") Log.i(TAG, "setPreferredSubtitles init $subtitle")
currentSubtitles = subtitle currentSubtitles = subtitle
@ -463,6 +429,7 @@ class CS3IPlayer : IPlayer {
} }
} }
@SuppressLint("UnsafeOptInUsageError")
override fun getAspectRatio(): Rational? { override fun getAspectRatio(): Rational? {
return exoPlayer?.videoFormat?.let { format -> return exoPlayer?.videoFormat?.let { format ->
Rational(format.width, format.height) Rational(format.width, format.height)
@ -473,6 +440,7 @@ class CS3IPlayer : IPlayer {
subtitleHelper.setSubStyle(style) subtitleHelper.setSubStyle(style)
} }
@SuppressLint("UnsafeOptInUsageError")
override fun saveData() { override fun saveData() {
Log.i(TAG, "saveData") Log.i(TAG, "saveData")
updatedTime() updatedTime()
@ -502,14 +470,14 @@ class CS3IPlayer : IPlayer {
Log.i(TAG, "onStop") Log.i(TAG, "onStop")
saveData() saveData()
exoPlayer?.pause() handleEvent(CSPlayerEvent.Pause, PlayerEventSource.Player)
//releasePlayer() //releasePlayer()
} }
override fun onPause() { override fun onPause() {
Log.i(TAG, "onPause") Log.i(TAG, "onPause")
saveData() saveData()
exoPlayer?.pause() handleEvent(CSPlayerEvent.Pause, PlayerEventSource.Player)
//releasePlayer() //releasePlayer()
} }
@ -546,6 +514,7 @@ class CS3IPlayer : IPlayer {
var requestSubtitleUpdate: (() -> Unit)? = null var requestSubtitleUpdate: (() -> Unit)? = null
@SuppressLint("UnsafeOptInUsageError")
private fun createOnlineSource(headers: Map<String, String>): HttpDataSource.Factory { private fun createOnlineSource(headers: Map<String, String>): HttpDataSource.Factory {
val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT) val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT)
return source.apply { return source.apply {
@ -553,6 +522,7 @@ class CS3IPlayer : IPlayer {
} }
} }
@SuppressLint("UnsafeOptInUsageError")
private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory { private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory {
val provider = getApiFromNameNull(link.source) val provider = getApiFromNameNull(link.source)
val interceptor = provider?.getVideoInterceptor(link) val interceptor = provider?.getVideoInterceptor(link)
@ -585,6 +555,7 @@ class CS3IPlayer : IPlayer {
} }
} }
@SuppressLint("UnsafeOptInUsageError")
private fun Context.createOfflineSource(): DataSource.Factory { private fun Context.createOfflineSource(): DataSource.Factory {
return DefaultDataSourceFactory(this, USER_AGENT) return DefaultDataSourceFactory(this, USER_AGENT)
} }
@ -630,6 +601,7 @@ class CS3IPlayer : IPlayer {
return Pair(subSources, activeSubtitles) return Pair(subSources, activeSubtitles)
}*/ }*/
@SuppressLint("UnsafeOptInUsageError")
private fun getCache(context: Context, cacheSize: Long): SimpleCache? { private fun getCache(context: Context, cacheSize: Long): SimpleCache? {
return try { return try {
val databaseProvider = StandaloneDatabaseProvider(context) val databaseProvider = StandaloneDatabaseProvider(context)
@ -661,6 +633,7 @@ class CS3IPlayer : IPlayer {
return getMediaItemBuilder(mimeType).setUri(url).build() return getMediaItemBuilder(mimeType).setUri(url).build()
} }
@SuppressLint("UnsafeOptInUsageError")
private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector { private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector {
val trackSelector = DefaultTrackSelector(context) val trackSelector = DefaultTrackSelector(context)
trackSelector.parameters = trackSelector.buildUponParameters() trackSelector.parameters = trackSelector.buildUponParameters()
@ -674,6 +647,7 @@ class CS3IPlayer : IPlayer {
var currentTextRenderer: CustomTextRenderer? = null var currentTextRenderer: CustomTextRenderer? = null
@SuppressLint("UnsafeOptInUsageError")
private fun buildExoPlayer( private fun buildExoPlayer(
context: Context, context: Context,
mediaItemSlices: List<MediaItemSlice>, mediaItemSlices: List<MediaItemSlice>,
@ -758,15 +732,33 @@ class CS3IPlayer : IPlayer {
// If there is only one item then treat it as normal, if multiple: concatenate the items. // If there is only one item then treat it as normal, if multiple: concatenate the items.
val videoMediaSource = if (mediaItemSlices.size == 1) { val videoMediaSource = if (mediaItemSlices.size == 1) {
factory.createMediaSource(mediaItemSlices.first().mediaItem) val item = mediaItemSlices.first()
item.drm?.let { drm ->
val drmCallback =
LocalMediaDrmCallback("{\"keys\":[{\"kty\":\"${drm.kty}\",\"k\":\"${drm.key}\",\"kid\":\"${drm.kid}\"}],\"type\":\"temporary\"}".toByteArray())
val manager = DefaultDrmSessionManager.Builder()
.setPlayClearSamplesWithoutKeys(true)
.setMultiSession(false)
.setKeyRequestParameters(drm.keyRequestParameters)
.setUuidAndExoMediaDrmProvider(drm.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
.build(drmCallback)
val manifestDataSourceFactory = DefaultHttpDataSource.Factory()
DashMediaSource.Factory(manifestDataSourceFactory)
.setDrmSessionManagerProvider { manager }
.createMediaSource(item.mediaItem)
} ?: run {
factory.createMediaSource(item.mediaItem)
}
} else { } else {
val source = ConcatenatingMediaSource() val source = ConcatenatingMediaSource()
mediaItemSlices.map { mediaItemSlices.map { item ->
source.addMediaSource( source.addMediaSource(
// The duration MUST be known for it to work properly, see https://github.com/google/ExoPlayer/issues/4727 // The duration MUST be known for it to work properly, see https://github.com/google/ExoPlayer/issues/4727
ClippingMediaSource( ClippingMediaSource(
factory.createMediaSource(it.mediaItem), factory.createMediaSource(item.mediaItem),
it.durationUs item.durationUs
) )
) )
} }
@ -799,43 +791,55 @@ class CS3IPlayer : IPlayer {
return null return null
} }
fun updatedTime(writePosition: Long? = null) { fun updatedTime(
writePosition: Long? = null,
source: PlayerEventSource = PlayerEventSource.Player
) {
val position = writePosition ?: exoPlayer?.currentPosition val position = writePosition ?: exoPlayer?.currentPosition
getCurrentTimestamp(position)?.let { timestamp -> getCurrentTimestamp(position)?.let { timestamp ->
onTimestampInvoked?.invoke(timestamp) event(TimestampInvokedEvent(timestamp, source))
} }
val duration = exoPlayer?.contentDuration val duration = exoPlayer?.contentDuration
if (duration != null && position != null) { if (duration != null && position != null) {
playerPositionChanged?.invoke(Pair(position, duration)) event(
PositionEvent(
source,
fromMs = exoPlayer?.currentPosition ?: 0,
position,
duration
)
)
} }
} }
override fun seekTime(time: Long) { override fun seekTime(time: Long, source: PlayerEventSource) {
exoPlayer?.seekTime(time) exoPlayer?.seekTime(time, source)
} }
override fun seekTo(time: Long) { override fun seekTo(time: Long, source: PlayerEventSource) {
updatedTime(time) updatedTime(time, source)
exoPlayer?.seekTo(time) exoPlayer?.seekTo(time)
} }
private fun ExoPlayer.seekTime(time: Long) { private fun ExoPlayer.seekTime(time: Long, source: PlayerEventSource) {
updatedTime(currentPosition + time) updatedTime(currentPosition + time, source)
seekTo(currentPosition + time) seekTo(currentPosition + time)
} }
override fun handleEvent(event: CSPlayerEvent) { override fun handleEvent(event: CSPlayerEvent, source: PlayerEventSource) {
Log.i(TAG, "handleEvent ${event.name}") Log.i(TAG, "handleEvent ${event.name}")
try { try {
exoPlayer?.apply { exoPlayer?.apply {
when (event) { when (event) {
CSPlayerEvent.Play -> { CSPlayerEvent.Play -> {
event(PlayEvent(source))
play() play()
} }
CSPlayerEvent.Pause -> { CSPlayerEvent.Pause -> {
event(PauseEvent(source))
pause() pause()
} }
@ -852,32 +856,32 @@ class CS3IPlayer : IPlayer {
CSPlayerEvent.PlayPauseToggle -> { CSPlayerEvent.PlayPauseToggle -> {
if (isPlaying) { if (isPlaying) {
pause() handleEvent(CSPlayerEvent.Pause, source)
} else { } else {
play() handleEvent(CSPlayerEvent.Play, source)
} }
} }
CSPlayerEvent.SeekForward -> seekTime(seekActionTime) CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source)
CSPlayerEvent.SeekBack -> seekTime(-seekActionTime) CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source)
CSPlayerEvent.NextEpisode -> nextEpisode?.invoke() CSPlayerEvent.NextEpisode -> event(EpisodeSeekEvent(offset = 1, source = source))
CSPlayerEvent.PrevEpisode -> prevEpisode?.invoke() CSPlayerEvent.PrevEpisode -> event(EpisodeSeekEvent(offset = -1, source = source))
CSPlayerEvent.SkipCurrentChapter -> { CSPlayerEvent.SkipCurrentChapter -> {
//val dur = this@CS3IPlayer.getDuration() ?: return@apply //val dur = this@CS3IPlayer.getDuration() ?: return@apply
getCurrentTimestamp()?.let { lastTimeStamp -> getCurrentTimestamp()?.let { lastTimeStamp ->
if (lastTimeStamp.skipToNextEpisode) { if (lastTimeStamp.skipToNextEpisode) {
handleEvent(CSPlayerEvent.NextEpisode) handleEvent(CSPlayerEvent.NextEpisode, source)
} else { } else {
seekTo(lastTimeStamp.endMs + 1L) seekTo(lastTimeStamp.endMs + 1L)
} }
onTimestampSkipped?.invoke(lastTimeStamp) event(TimestampSkippedEvent(timestamp = lastTimeStamp, source = source))
} }
} }
} }
} }
} catch (e: Exception) { } catch (t: Throwable) {
Log.e(TAG, "handleEvent error", e) Log.e(TAG, "handleEvent error", t)
playerError?.invoke(e) event(ErrorEvent(t))
} }
} }
@ -916,18 +920,14 @@ class CS3IPlayer : IPlayer {
requestSubtitleUpdate = ::reloadSubs requestSubtitleUpdate = ::reloadSubs
playerUpdated?.invoke(exoPlayer) event(PlayerAttachedEvent(exoPlayer))
exoPlayer?.prepare() exoPlayer?.prepare()
exoPlayer?.let { exo -> exoPlayer?.let { exo ->
updateIsPlaying?.invoke( event(StatusEvent(CSPlayerLoading.IsBuffering, CSPlayerLoading.IsBuffering))
Pair(
CSPlayerLoading.IsBuffering,
CSPlayerLoading.IsBuffering
)
)
isPlaying = exo.isPlaying isPlaying = exo.isPlaying
} }
exoPlayer?.addListener(object : Player.Listener { exoPlayer?.addListener(object : Player.Listener {
override fun onTracksChanged(tracks: Tracks) { override fun onTracksChanged(tracks: Tracks) {
normalSafeApiCall { normalSafeApiCall {
@ -961,18 +961,19 @@ class CS3IPlayer : IPlayer {
) )
} }
embeddedSubtitlesFetched?.invoke(exoPlayerReportedTracks) event(EmbeddedSubtitlesFetchedEvent(tracks = exoPlayerReportedTracks))
onTracksInfoChanged?.invoke() event(TracksChangedEvent())
subtitlesUpdates?.invoke() event(SubtitlesUpdatedEvent())
} }
} }
@SuppressLint("UnsafeOptInUsageError")
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
exoPlayer?.let { exo -> exoPlayer?.let { exo ->
updateIsPlaying?.invoke( event(
Pair( StatusEvent(
if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused, wasPlaying = if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused,
if (playbackState == Player.STATE_BUFFERING) CSPlayerLoading.IsBuffering else if (exo.isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused isPlaying = if (playbackState == Player.STATE_BUFFERING) CSPlayerLoading.IsBuffering else if (exo.isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused
) )
) )
isPlaying = exo.isPlaying isPlaying = exo.isPlaying
@ -994,23 +995,15 @@ class CS3IPlayer : IPlayer {
} }
Player.STATE_ENDED -> { Player.STATE_ENDED -> {
// Only play next episode if autoplay is on (default) event(VideoEndedEvent())
if (PreferenceManager.getDefaultSharedPreferences(context)
?.getBoolean(
context.getString(com.lagradost.cloudstream3.R.string.autoplay_next_key),
true
) == true
) {
handleEvent(CSPlayerEvent.NextEpisode)
}
} }
Player.STATE_BUFFERING -> { Player.STATE_BUFFERING -> {
updatedTime() updatedTime(source = PlayerEventSource.Player)
} }
Player.STATE_IDLE -> { Player.STATE_IDLE -> {
// IDLE
} }
else -> Unit else -> Unit
@ -1035,7 +1028,7 @@ class CS3IPlayer : IPlayer {
} }
else -> { else -> {
playerError?.invoke(error) event(ErrorEvent(error))
} }
} }
@ -1049,7 +1042,7 @@ class CS3IPlayer : IPlayer {
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying) super.onIsPlayingChanged(isPlaying)
if (isPlaying) { if (isPlaying) {
requestAutoFocus?.invoke() event(RequestAudioFocusEvent())
onRenderFirst() onRenderFirst()
} }
} }
@ -1069,12 +1062,15 @@ class CS3IPlayer : IPlayer {
true true
) == true ) == true
) { ) {
handleEvent(CSPlayerEvent.NextEpisode) handleEvent(
CSPlayerEvent.NextEpisode,
source = PlayerEventSource.Player
)
} }
} }
Player.STATE_BUFFERING -> { Player.STATE_BUFFERING -> {
updatedTime() updatedTime(source = PlayerEventSource.Player)
} }
Player.STATE_IDLE -> { Player.STATE_IDLE -> {
@ -1087,27 +1083,29 @@ class CS3IPlayer : IPlayer {
override fun onVideoSizeChanged(videoSize: VideoSize) { override fun onVideoSizeChanged(videoSize: VideoSize) {
super.onVideoSizeChanged(videoSize) super.onVideoSizeChanged(videoSize)
playerDimensionsLoaded?.invoke(Pair(videoSize.width, videoSize.height)) event(ResizedEvent(height = videoSize.height, width = videoSize.width))
} }
override fun onRenderedFirstFrame() { override fun onRenderedFirstFrame() {
super.onRenderedFirstFrame() super.onRenderedFirstFrame()
onRenderFirst() onRenderFirst()
updatedTime() updatedTime(source = PlayerEventSource.Player)
} }
}) })
} catch (e: Exception) { } catch (t: Throwable) {
Log.e(TAG, "loadExo error", e) Log.e(TAG, "loadExo error", t)
playerError?.invoke(e) event(ErrorEvent(t))
} }
} }
private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList() private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList()
@SuppressLint("UnsafeOptInUsageError")
override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) { override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) {
lastTimeStamps = timeStamps lastTimeStamps = timeStamps
timeStamps.forEach { timestamp -> timeStamps.forEach { timestamp ->
exoPlayer?.createMessage { _, _ -> exoPlayer?.createMessage { _, _ ->
updatedTime() updatedTime(source = PlayerEventSource.Player)
//if (payload is EpisodeSkip.SkipStamp) // this should always be true //if (payload is EpisodeSkip.SkipStamp) // this should always be true
// onTimestampInvoked?.invoke(payload) // onTimestampInvoked?.invoke(payload)
} }
@ -1117,9 +1115,10 @@ class CS3IPlayer : IPlayer {
?.setDeleteAfterDelivery(false) ?.setDeleteAfterDelivery(false)
?.send() ?.send()
} }
updatedTime() updatedTime(source = PlayerEventSource.Player)
} }
@SuppressLint("UnsafeOptInUsageError")
fun onRenderFirst() { fun onRenderFirst() {
if (hasUsedFirstRender) { // this insures that we only call this once per player load if (hasUsedFirstRender) { // this insures that we only call this once per player load
return return
@ -1137,7 +1136,7 @@ class CS3IPlayer : IPlayer {
if (invalid) { if (invalid) {
releasePlayer(saveTime = false) releasePlayer(saveTime = false)
playerError?.invoke(InvalidFileException("Too short playback")) event(ErrorEvent(InvalidFileException("Too short playback")))
return return
} }
@ -1146,7 +1145,7 @@ class CS3IPlayer : IPlayer {
val width = format?.width val width = format?.width
val height = format?.height val height = format?.height
if (height != null && width != null) { if (height != null && width != null) {
playerDimensionsLoaded?.invoke(Pair(width, height)) event(ResizedEvent(width = width, height = height))
updatedTime() updatedTime()
exoPlayer?.apply { exoPlayer?.apply {
requestedListeningPercentages?.forEach { percentage -> requestedListeningPercentages?.forEach { percentage ->
@ -1180,12 +1179,13 @@ class CS3IPlayer : IPlayer {
subtitleHelper.setActiveSubtitles(activeSubtitles.toSet()) subtitleHelper.setActiveSubtitles(activeSubtitles.toSet())
loadExo(context, listOf(MediaItemSlice(mediaItem, Long.MIN_VALUE)), subSources) loadExo(context, listOf(MediaItemSlice(mediaItem, Long.MIN_VALUE)), subSources)
} catch (e: Exception) { } catch (t: Throwable) {
Log.e(TAG, "loadOfflinePlayer error", e) Log.e(TAG, "loadOfflinePlayer error", t)
playerError?.invoke(e) event(ErrorEvent(t))
} }
} }
@SuppressLint("UnsafeOptInUsageError")
private fun getSubSources( private fun getSubSources(
onlineSourceFactory: HttpDataSource.Factory?, onlineSourceFactory: HttpDataSource.Factory?,
offlineSourceFactory: DataSource.Factory?, offlineSourceFactory: DataSource.Factory?,
@ -1241,6 +1241,7 @@ class CS3IPlayer : IPlayer {
return exoPlayer != null return exoPlayer != null
} }
@SuppressLint("UnsafeOptInUsageError")
private fun loadOnlinePlayer(context: Context, link: ExtractorLink) { private fun loadOnlinePlayer(context: Context, link: ExtractorLink) {
Log.i(TAG, "loadOnlinePlayer $link") Log.i(TAG, "loadOnlinePlayer $link")
try { try {
@ -1257,18 +1258,37 @@ class CS3IPlayer : IPlayer {
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
} }
val mime = when { val mime = when (link.type) {
link.isM3u8 -> MimeTypes.APPLICATION_M3U8 ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
link.isDash -> MimeTypes.APPLICATION_MPD ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
else -> MimeTypes.VIDEO_MP4 ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4
ExtractorLinkType.TORRENT -> throw IllegalArgumentException("No torrent support")
ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support")
} }
val mediaItems = if (link is ExtractorLinkPlayList) {
link.playlist.map { val mediaItems = when (link) {
is ExtractorLinkPlayList -> link.playlist.map {
MediaItemSlice(getMediaItem(mime, it.url), it.durationUs) MediaItemSlice(getMediaItem(mime, it.url), it.durationUs)
} }
} else {
listOf( is DrmExtractorLink -> {
listOf(
// Single sliced list with unset length
MediaItemSlice(
getMediaItem(mime, link.url), Long.MIN_VALUE,
drm = DrmMetadata(
kid = link.kid,
key = link.key,
uuid = link.uuid,
kty = link.kty,
keyRequestParameters = link.keyRequestParameters
)
)
)
}
else -> listOf(
// Single sliced list with unset length // Single sliced list with unset length
MediaItemSlice(getMediaItem(mime, link.url), Long.MIN_VALUE) MediaItemSlice(getMediaItem(mime, link.url), Long.MIN_VALUE)
) )
@ -1294,9 +1314,9 @@ class CS3IPlayer : IPlayer {
} }
loadExo(context, mediaItems, subSources, cacheFactory) loadExo(context, mediaItems, subSources, cacheFactory)
} catch (e: Exception) { } catch (t: Throwable) {
Log.e(TAG, "loadOnlinePlayer error", e) Log.e(TAG, "loadOnlinePlayer error", t)
playerError?.invoke(e) event(ErrorEvent(t))
} }
} }

View file

@ -50,47 +50,60 @@ class DownloadFileGenerator(
return null return null
} }
fun cleanDisplayName(name: String): String {
return name.substringBeforeLast('.').trim()
}
override suspend fun generateLinks( override suspend fun generateLinks(
clearCache: Boolean, clearCache: Boolean,
isCasting: Boolean, type: LoadType,
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit, callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
subtitleCallback: (SubtitleData) -> Unit, subtitleCallback: (SubtitleData) -> Unit,
offset: Int, offset: Int
): Boolean { ): Boolean {
val meta = episodes[currentIndex + offset] val meta = episodes[currentIndex + offset]
callback(Pair(null, meta)) callback(null to meta)
context?.let { ctx -> val ctx = context ?: return true
val relative = meta.relativePath val relative = meta.relativePath ?: return true
val display = meta.displayName val display = meta.displayName ?: return true
if (display == null || relative == null) { val cleanDisplay = cleanDisplayName(display)
return@let
VideoDownloadManager.getFolder(ctx, relative, meta.basePath)
?.forEach { (name, uri) ->
// only these files are allowed, so no videos as subtitles
if (listOf(
".vtt",
".srt",
".txt",
".ass",
".ttml",
".sbv",
".dfxp"
).none { name.contains(it, true) }
) return@forEach
// cant have the exact same file as a subtitle
if (name.equals(display, true)) return@forEach
val cleanName = cleanDisplayName(name)
// we only want files with the approx same name
if (!cleanName.startsWith(cleanDisplay, true)) return@forEach
val realName = cleanName.removePrefix(cleanDisplay)
subtitleCallback(
SubtitleData(
realName.ifBlank { ctx.getString(R.string.default_subtitles) },
uri.toString(),
SubtitleOrigin.DOWNLOADED_FILE,
name.toSubtitleMimeType(),
emptyMap()
)
)
} }
VideoDownloadManager.getFolder(ctx, relative, meta.basePath)
?.forEach { file ->
val name = display.removeSuffix(".mp4")
if (file.first != meta.displayName && file.first.startsWith(name)) {
val realName = file.first.removePrefix(name)
.removeSuffix(".vtt")
.removeSuffix(".srt")
.removeSuffix(".txt")
.trim()
.removePrefix("(")
.removeSuffix(")")
subtitleCallback(
SubtitleData(
realName.ifBlank { ctx.getString(R.string.default_subtitles) },
file.second.toString(),
SubtitleOrigin.DOWNLOADED_FILE,
name.toSubtitleMimeType(),
emptyMap()
)
)
}
}
}
return true return true
} }

View file

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

View file

@ -38,6 +38,8 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.keyEventListener
import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener
import com.lagradost.cloudstream3.CommonActivity.screenHeight
import com.lagradost.cloudstream3.CommonActivity.screenWidth
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding
@ -126,19 +128,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
protected var useTrueSystemBrightness = true protected var useTrueSystemBrightness = true
private val fullscreenNotch = true //TODO SETTING private val fullscreenNotch = true //TODO SETTING
protected val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics
// screenWidth and screenHeight does always
// refer to the screen while in landscape mode
protected val screenWidth: Int
get() {
return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
}
protected val screenHeight: Int
get() {
return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
}
private var statusBarHeight: Int? = null private var statusBarHeight: Int? = null
private var navigationBarHeight: Int? = null private var navigationBarHeight: Int? = null
@ -874,7 +863,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
currentTouch currentTouch
)?.let { seekTo -> )?.let { seekTo ->
if (abs(seekTo - startTime) > MINIMUM_SEEK_TIME) { if (abs(seekTo - startTime) > MINIMUM_SEEK_TIME) {
player.seekTo(seekTo) player.seekTo(seekTo, PlayerEventSource.UI)
} }
} }
} }
@ -909,7 +898,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
else -> { else -> {
player.handleEvent(CSPlayerEvent.PlayPauseToggle) player.handleEvent(CSPlayerEvent.PlayPauseToggle, PlayerEventSource.UI)
} }
} }
} else if (doubleTapEnabled && isFullScreenPlayer) { } else if (doubleTapEnabled && isFullScreenPlayer) {

View file

@ -551,7 +551,7 @@ class GeneratorPlayer : FullScreenPlayer() {
//println("CURRENT SELECTED :$currentSelectedSubtitles of $currentSubs") //println("CURRENT SELECTED :$currentSelectedSubtitles of $currentSubs")
context?.let { ctx -> context?.let { ctx ->
val isPlaying = player.getIsPlaying() val isPlaying = player.getIsPlaying()
player.handleEvent(CSPlayerEvent.Pause) player.handleEvent(CSPlayerEvent.Pause, PlayerEventSource.UI)
val currentSubtitles = sortSubs(currentSubs) val currentSubtitles = sortSubs(currentSubs)
val sourceDialog = Dialog(ctx, R.style.AlertDialogCustomBlack) val sourceDialog = Dialog(ctx, R.style.AlertDialogCustomBlack)
@ -883,7 +883,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
override fun playerError(exception: Exception) { override fun playerError(exception: Throwable) {
Log.i(TAG, "playerError = $currentSelectedLink") Log.i(TAG, "playerError = $currentSelectedLink")
super.playerError(exception) super.playerError(exception)
} }
@ -945,14 +945,13 @@ class GeneratorPlayer : FullScreenPlayer() {
var maxEpisodeSet: Int? = null var maxEpisodeSet: Int? = null
var hasRequestedStamps: Boolean = false var hasRequestedStamps: Boolean = false
override fun playerPositionChanged(posDur: Pair<Long, Long>) { override fun playerPositionChanged(position: Long, duration : Long) {
// Don't save livestream data // Don't save livestream data
if ((currentMeta as? ResultEpisode)?.tvType?.isLiveStream() == true) return if ((currentMeta as? ResultEpisode)?.tvType?.isLiveStream() == true) return
// Don't save NSFW data // Don't save NSFW data
if ((currentMeta as? ResultEpisode)?.tvType == TvType.NSFW) return if ((currentMeta as? ResultEpisode)?.tvType == TvType.NSFW) return
val (position, duration) = posDur
if (duration <= 0L) return // idk how you achieved this, but div by zero crash if (duration <= 0L) return // idk how you achieved this, but div by zero crash
if (!hasRequestedStamps) { if (!hasRequestedStamps) {
hasRequestedStamps = true hasRequestedStamps = true
@ -1209,8 +1208,8 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
} }
override fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) { override fun playerDimensionsLoaded(width: Int, height : Int) {
setPlayerDimen(widthHeight) setPlayerDimen(width to height)
} }
private fun unwrapBundle(savedInstanceState: Bundle?) { private fun unwrapBundle(savedInstanceState: Bundle?) {

View file

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

View file

@ -45,9 +45,120 @@ enum class CSPlayerLoading {
IsPaused, IsPaused,
IsPlaying, IsPlaying,
IsBuffering, IsBuffering,
//IsDone,
} }
enum class PlayerEventSource {
/** This event was invoked from the user pressing some button or selecting something */
UI,
/** This event was invoked automatically */
Player,
/** This event was invoked from a external sync tool like WatchTogether */
Sync,
}
abstract class PlayerEvent {
abstract val source: PlayerEventSource
}
/** this is used to update UI based of the current time,
* using requestedListeningPercentages as well as saving time */
data class PositionEvent(
override val source: PlayerEventSource,
val fromMs: Long,
val toMs: Long,
/** duration of the entire video */
val durationMs: Long,
) : PlayerEvent() {
/** how many ms (+-) we have skipped */
val seekMs : Long get() = toMs - fromMs
}
/** player error when rendering or misc, used to display toast or log */
data class ErrorEvent(
val error: Throwable,
override val source: PlayerEventSource = PlayerEventSource.Player,
) : PlayerEvent()
/** Event when timestamps appear, null when it should disappear */
data class TimestampInvokedEvent(
val timestamp: EpisodeSkip.SkipStamp,
override val source: PlayerEventSource = PlayerEventSource.Player,
) : PlayerEvent()
/** Event for when a chapter is skipped, aka when event is handled (or for future use when skip automatically ads/sponsor) */
data class TimestampSkippedEvent(
val timestamp: EpisodeSkip.SkipStamp,
override val source: PlayerEventSource = PlayerEventSource.Player,
) : PlayerEvent()
/** this is used by the player to load the next or prev episode */
data class EpisodeSeekEvent(
/** -1 = prev, 1 = next, will never be 0, atm the user cant seek more than +-1 */
val offset: Int,
override val source: PlayerEventSource = PlayerEventSource.Player,
) : PlayerEvent() {
init {
assert(offset != 0)
}
}
/** Event when the video is resized aka changed resolution or mirror */
data class ResizedEvent(
val height: Int,
val width: Int,
override val source: PlayerEventSource = PlayerEventSource.Player,
) : PlayerEvent()
/** Event when the player status update, along with the previous status (for animation)*/
data class StatusEvent(
val wasPlaying: CSPlayerLoading,
val isPlaying: CSPlayerLoading,
override val source: PlayerEventSource = PlayerEventSource.Player
) : PlayerEvent()
/** Event when tracks are changed, used for UI changes */
data class TracksChangedEvent(
override val source: PlayerEventSource = PlayerEventSource.Player
) : PlayerEvent()
/** Event from player to give all embedded subtitles */
data class EmbeddedSubtitlesFetchedEvent(
val tracks: List<SubtitleData>,
override val source: PlayerEventSource = PlayerEventSource.Player
) : PlayerEvent()
/** on attach player to view */
data class PlayerAttachedEvent(
val player: Any?,
override val source: PlayerEventSource = PlayerEventSource.Player
) : PlayerEvent()
/** Event from player to inform that subtitles have updated in some way */
data class SubtitlesUpdatedEvent(
override val source: PlayerEventSource = PlayerEventSource.Player
) : PlayerEvent()
/** current player starts, asking for all other programs to shut the fuck up */
data class RequestAudioFocusEvent(
override val source: PlayerEventSource = PlayerEventSource.Player
) : PlayerEvent()
/** Pause event, separate from StatusEvent */
data class PauseEvent(
override val source: PlayerEventSource = PlayerEventSource.Player
) : PlayerEvent()
/** Play event, separate from StatusEvent */
data class PlayEvent(
override val source: PlayerEventSource = PlayerEventSource.Player
) : PlayerEvent()
/** Event when the player video has ended, up to the settings on what to do when that happens */
data class VideoEndedEvent(
override val source: PlayerEventSource = PlayerEventSource.Player
) : PlayerEvent()
interface Track { interface Track {
/** /**
@ -108,27 +219,16 @@ interface IPlayer {
fun getDuration(): Long? fun getDuration(): Long?
fun getPosition(): Long? fun getPosition(): Long?
fun seekTime(time: Long) fun seekTime(time: Long, source: PlayerEventSource = PlayerEventSource.UI)
fun seekTo(time: Long) fun seekTo(time: Long, source: PlayerEventSource = PlayerEventSource.UI)
fun getSubtitleOffset(): Long // in ms fun getSubtitleOffset(): Long // in ms
fun setSubtitleOffset(offset: Long) // in ms fun setSubtitleOffset(offset: Long) // in ms
fun initCallbacks( fun initCallbacks(
playerUpdated: (Any?) -> Unit, // attach player to view eventHandler: ((PlayerEvent) -> Unit),
updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)? = null, // (wasPlaying, isPlaying) /** this is used to request when the player should report back view percentage */
requestAutoFocus: (() -> Unit)? = null, // current player starts, asking for all other programs to shut the fuck up requestedListeningPercentages: List<Int>? = null,
playerError: ((Exception) -> Unit)? = null, // player error when rendering or misc, used to display toast or log
playerDimensionsLoaded: ((Pair<Int, Int>) -> Unit)? = null, // (with, height), for UI
requestedListeningPercentages: List<Int>? = null, // this is used to request when the player should report back view percentage
playerPositionChanged: ((Pair<Long, Long>) -> Unit)? = null,// (position, duration) this is used to update UI based of the current time
nextEpisode: (() -> Unit)? = null, // this is used by the player to load the next episode
prevEpisode: (() -> Unit)? = null, // this is used by the player to load the previous episode
subtitlesUpdates: (() -> Unit)? = null, // callback from player to inform that subtitles have updated in some way
embeddedSubtitlesFetched: ((List<SubtitleData>) -> Unit)? = null, // callback from player to give all embedded subtitles
onTracksInfoChanged: (() -> Unit)? = null, // Callback when tracks are changed, used for UI changes
onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)? = null, // Callback when timestamps appear, null when it should disappear
onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)? = null, // callback for when a chapter is skipped, aka when event is handled (or for future use when skip automatically ads/sponsor)
) )
fun releaseCallbacks() fun releaseCallbacks()
@ -155,7 +255,7 @@ interface IPlayer {
fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean // returns true if the player requires a reload, null for nothing fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean // returns true if the player requires a reload, null for nothing
fun getCurrentPreferredSubtitle(): SubtitleData? fun getCurrentPreferredSubtitle(): SubtitleData?
fun handleEvent(event: CSPlayerEvent) fun handleEvent(event: CSPlayerEvent, source: PlayerEventSource = PlayerEventSource.UI)
fun onStop() fun onStop()
fun onPause() fun onPause()

View file

@ -48,7 +48,7 @@ class LinkGenerator(
override suspend fun generateLinks( override suspend fun generateLinks(
clearCache: Boolean, clearCache: Boolean,
isCasting: Boolean, type: LoadType,
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit, callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
subtitleCallback: (SubtitleData) -> Unit, subtitleCallback: (SubtitleData) -> Unit,
offset: Int offset: Int
@ -67,9 +67,8 @@ class LinkGenerator(
link.name ?: link.url, link.name ?: link.url,
unshortenLinkSafe(link.url), // unshorten because it might be a raw link unshortenLinkSafe(link.url), // unshorten because it might be a raw link
referer ?: "", referer ?: "",
Qualities.Unknown.value, isM3u8 ?: normalSafeApiCall { Qualities.Unknown.value,
URI(link.url).path?.substringAfterLast(".")?.contains("m3u") type = INFER_TYPE,
} ?: false
) to null ) to null
) )
} }

View file

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

View file

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

View file

@ -130,8 +130,8 @@ open class ResultFragmentPhone : FullScreenPlayer() {
return currentTrailerIndex + 1 < currentTrailers.size return currentTrailerIndex + 1 < currentTrailers.size
} }
override fun playerError(exception: Exception) { override fun playerError(exception: Throwable) {
if (player.getIsPlaying()) { // because we dont want random toasts in player if (player.getIsPlaying()) { // because we don't want random toasts in player
super.playerError(exception) super.playerError(exception)
} else { } else {
nextMirror() nextMirror()

View file

@ -3,15 +3,17 @@ package com.lagradost.cloudstream3.ui.result
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.lagradost.cloudstream3.CommonActivity.screenHeight
import com.lagradost.cloudstream3.CommonActivity.screenWidth
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
import com.lagradost.cloudstream3.ui.player.PlayerEventSource
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.IOnBackPressed import com.lagradost.cloudstream3.utils.IOnBackPressed
@ -32,7 +34,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed {
override fun prevEpisode() {} override fun prevEpisode() {}
override fun playerPositionChanged(posDur: Pair<Long, Long>) {} override fun playerPositionChanged(position: Long, duration : Long) {}
override fun nextMirror() {} override fun nextMirror() {}
@ -99,8 +101,8 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed {
} }
} }
override fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) { override fun playerDimensionsLoaded(width: Int, height : Int) {
playerWidthHeight = widthHeight playerWidthHeight = width to height
fixPlayerSize() fixPlayerSize()
} }
@ -164,7 +166,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed {
playerBinding?.playerIntroPlay?.setOnClickListener { playerBinding?.playerIntroPlay?.setOnClickListener {
playerBinding?.playerIntroPlay?.isGone = true playerBinding?.playerIntroPlay?.isGone = true
player.handleEvent(CSPlayerEvent.Play) player.handleEvent(CSPlayerEvent.Play, PlayerEventSource.UI)
updateUIVisibility() updateUIVisibility()
fixPlayerSize() fixPlayerSize()
} }

View file

@ -36,6 +36,7 @@ import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.player.IGenerator import com.lagradost.cloudstream3.ui.player.IGenerator
import com.lagradost.cloudstream3.ui.player.LoadType
import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction
@ -745,7 +746,7 @@ class ResultViewModel2 : ViewModel() {
val generator = RepoLinkGenerator(listOf(episode)) val generator = RepoLinkGenerator(listOf(episode))
val currentLinks = mutableSetOf<ExtractorLink>() val currentLinks = mutableSetOf<ExtractorLink>()
val currentSubs = mutableSetOf<SubtitleData>() val currentSubs = mutableSetOf<SubtitleData>()
generator.generateLinks(clearCache = false, isCasting = false, callback = { generator.generateLinks(clearCache = false, LoadType.Chromecast, callback = {
it.first?.let { link -> it.first?.let { link ->
currentLinks.add(link) currentLinks.add(link)
} }
@ -825,7 +826,7 @@ class ResultViewModel2 : ViewModel() {
isVisible: Boolean = true isVisible: Boolean = true
) { ) {
if (activity == null) return if (activity == null) return
loadLinks(result, isVisible = isVisible, isCasting = true) { data -> loadLinks(result, isVisible = isVisible, LoadType.Chromecast) { data ->
startChromecast(activity, result, data.links, data.subs, 0) startChromecast(activity, result, data.links, data.subs, 0)
} }
} }
@ -936,7 +937,7 @@ class ResultViewModel2 : ViewModel() {
private fun loadLinks( private fun loadLinks(
result: ResultEpisode, result: ResultEpisode,
isVisible: Boolean, isVisible: Boolean,
isCasting: Boolean, type: LoadType,
clearCache: Boolean = false, clearCache: Boolean = false,
work: suspend (CoroutineScope.(LinkLoadingResult) -> Unit) work: suspend (CoroutineScope.(LinkLoadingResult) -> Unit)
) { ) {
@ -945,7 +946,7 @@ class ResultViewModel2 : ViewModel() {
val links = loadLinks( val links = loadLinks(
result, result,
isVisible = isVisible, isVisible = isVisible,
isCasting = isCasting, type = type,
clearCache = clearCache clearCache = clearCache
) )
if (!this.isActive) return@ioSafe if (!this.isActive) return@ioSafe
@ -956,11 +957,11 @@ class ResultViewModel2 : ViewModel() {
private var currentLoadLinkJob: Job? = null private var currentLoadLinkJob: Job? = null
private fun acquireSingleLink( private fun acquireSingleLink(
result: ResultEpisode, result: ResultEpisode,
isCasting: Boolean, type: LoadType,
text: UiText, text: UiText,
callback: (Pair<LinkLoadingResult, Int>) -> Unit, callback: (Pair<LinkLoadingResult, Int>) -> Unit,
) { ) {
loadLinks(result, isVisible = true, isCasting = isCasting) { links -> loadLinks(result, isVisible = true, type) { links ->
postPopup( postPopup(
text, text,
links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) {
@ -971,11 +972,10 @@ class ResultViewModel2 : ViewModel() {
private fun acquireSingleSubtitle( private fun acquireSingleSubtitle(
result: ResultEpisode, result: ResultEpisode,
isCasting: Boolean,
text: UiText, text: UiText,
callback: (Pair<LinkLoadingResult, Int>) -> Unit, callback: (Pair<LinkLoadingResult, Int>) -> Unit,
) { ) {
loadLinks(result, isVisible = true, isCasting = isCasting) { links -> loadLinks(result, isVisible = true, type = LoadType.Unknown) { links ->
postPopup( postPopup(
text, text,
links.subs.map { txt(it.name) }) links.subs.map { txt(it.name) })
@ -988,7 +988,7 @@ class ResultViewModel2 : ViewModel() {
private suspend fun CoroutineScope.loadLinks( private suspend fun CoroutineScope.loadLinks(
result: ResultEpisode, result: ResultEpisode,
isVisible: Boolean, isVisible: Boolean,
isCasting: Boolean, type: LoadType,
clearCache: Boolean = false, clearCache: Boolean = false,
): LinkLoadingResult { ): LinkLoadingResult {
val tempGenerator = RepoLinkGenerator(listOf(result)) val tempGenerator = RepoLinkGenerator(listOf(result))
@ -1002,7 +1002,7 @@ class ResultViewModel2 : ViewModel() {
} }
try { try {
updatePage() updatePage()
tempGenerator.generateLinks(clearCache, isCasting, { (link, _) -> tempGenerator.generateLinks(clearCache, type, { (link, _) ->
if (link != null) { if (link != null) {
links += link links += link
updatePage() updatePage()
@ -1272,7 +1272,6 @@ class ResultViewModel2 : ViewModel() {
acquireSingleSubtitle( acquireSingleSubtitle(
click.data, click.data,
false,
txt(R.string.episode_action_download_subtitle) txt(R.string.episode_action_download_subtitle)
) { (links, index) -> ) { (links, index) ->
downloadSubtitle( downloadSubtitle(
@ -1317,7 +1316,7 @@ class ResultViewModel2 : ViewModel() {
val response = currentResponse ?: return val response = currentResponse ?: return
acquireSingleLink( acquireSingleLink(
click.data, click.data,
false, LoadType.InAppDownload,
txt(R.string.episode_action_download_mirror) txt(R.string.episode_action_download_mirror)
) { (result, index) -> ) { (result, index) ->
ioSafe { ioSafe {
@ -1347,7 +1346,7 @@ class ResultViewModel2 : ViewModel() {
loadLinks( loadLinks(
click.data, click.data,
isVisible = false, isVisible = false,
isCasting = false, type = LoadType.InApp,
clearCache = true clearCache = true
) )
} }
@ -1356,7 +1355,7 @@ class ResultViewModel2 : ViewModel() {
ACTION_CHROME_CAST_MIRROR -> { ACTION_CHROME_CAST_MIRROR -> {
acquireSingleLink( acquireSingleLink(
click.data, click.data,
isCasting = true, LoadType.Chromecast,
txt(R.string.episode_action_chromecast_mirror) txt(R.string.episode_action_chromecast_mirror)
) { (result, index) -> ) { (result, index) ->
startChromecast(activity, click.data, result.links, result.subs, index) startChromecast(activity, click.data, result.links, result.subs, index)
@ -1365,7 +1364,7 @@ class ResultViewModel2 : ViewModel() {
ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink(
click.data, click.data,
isCasting = true, LoadType.Browser,
txt(R.string.episode_action_play_in_browser) txt(R.string.episode_action_play_in_browser)
) { (result, index) -> ) { (result, index) ->
try { try {
@ -1380,7 +1379,7 @@ class ResultViewModel2 : ViewModel() {
ACTION_COPY_LINK -> { ACTION_COPY_LINK -> {
acquireSingleLink( acquireSingleLink(
click.data, click.data,
isCasting = true, LoadType.ExternalApp,
txt(R.string.episode_action_copy_link) txt(R.string.episode_action_copy_link)
) { (result, index) -> ) { (result, index) ->
val act = activity ?: return@acquireSingleLink val act = activity ?: return@acquireSingleLink
@ -1399,7 +1398,7 @@ class ResultViewModel2 : ViewModel() {
} }
ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> { ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> {
loadLinks(click.data, isVisible = true, isCasting = true) { links -> loadLinks(click.data, isVisible = true, LoadType.ExternalApp) { links ->
if (links.links.isEmpty()) { if (links.links.isEmpty()) {
showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
return@loadLinks return@loadLinks
@ -1415,7 +1414,7 @@ class ResultViewModel2 : ViewModel() {
ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink( ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink(
click.data, click.data,
isCasting = true, LoadType.Chromecast,
txt( txt(
R.string.episode_action_play_in_format, R.string.episode_action_play_in_format,
txt(R.string.player_settings_play_in_web) txt(R.string.player_settings_play_in_web)
@ -1432,7 +1431,7 @@ class ResultViewModel2 : ViewModel() {
ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink( ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink(
click.data, click.data,
isCasting = true, LoadType.Chromecast,
txt( txt(
R.string.episode_action_play_in_format, R.string.episode_action_play_in_format,
txt(R.string.player_settings_play_in_mpv) txt(R.string.player_settings_play_in_mpv)
@ -1461,7 +1460,6 @@ class ResultViewModel2 : ViewModel() {
if (index >= 0) if (index >= 0)
it.goto(index) it.goto(index)
} }
} ?: return, list } ?: return, list
) )
) )
@ -1540,7 +1538,11 @@ class ResultViewModel2 : ViewModel() {
this.name, this.name,
this.japName this.japName
).filter { it.length > 2 } ).filter { it.length > 2 }
.distinct(), // the reason why we filter is due to not wanting smth like " " or "?" .distinct().map {
// this actually would be nice if we improved a bit as 3rd season == season 3 == III ect
// right now it just removes the dubbed status
it.lowercase().replace(Regex("""\(?[ds]ub(bed)?\)?(\s|$)""") , "").trim()
},
TrackerType.getTypes(this.type), TrackerType.getTypes(this.type),
this.year this.year
) )
@ -2173,7 +2175,7 @@ class ResultViewModel2 : ViewModel() {
trailerData.extractorUrl, trailerData.extractorUrl,
trailerData.referer ?: "", trailerData.referer ?: "",
Qualities.Unknown.value, Qualities.Unknown.value,
trailerData.extractorUrl.contains(".m3u8") type = INFER_TYPE
) )
) to arrayListOf() ) to arrayListOf()
} else { } else {

View file

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

View file

@ -1,12 +1,204 @@
package com.lagradost.cloudstream3.utils package com.lagradost.cloudstream3.utils
import android.net.Uri import android.net.Uri
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.extractors.AStreamHub
import com.lagradost.cloudstream3.extractors.Acefile
import com.lagradost.cloudstream3.extractors.Ahvsh
import com.lagradost.cloudstream3.extractors.Aico
import com.lagradost.cloudstream3.extractors.AsianLoad
import com.lagradost.cloudstream3.extractors.Bestx
import com.lagradost.cloudstream3.extractors.Blogger
import com.lagradost.cloudstream3.extractors.BullStream
import com.lagradost.cloudstream3.extractors.ByteShare
import com.lagradost.cloudstream3.extractors.Cda
import com.lagradost.cloudstream3.extractors.Cdnplayer
import com.lagradost.cloudstream3.extractors.Chillx
import com.lagradost.cloudstream3.extractors.CineGrabber
import com.lagradost.cloudstream3.extractors.Cinestart
import com.lagradost.cloudstream3.extractors.DBfilm
import com.lagradost.cloudstream3.extractors.Dailymotion
import com.lagradost.cloudstream3.extractors.DatabaseGdrive
import com.lagradost.cloudstream3.extractors.DatabaseGdrive2
import com.lagradost.cloudstream3.extractors.DesuArcg
import com.lagradost.cloudstream3.extractors.DesuDrive
import com.lagradost.cloudstream3.extractors.DesuOdchan
import com.lagradost.cloudstream3.extractors.DesuOdvip
import com.lagradost.cloudstream3.extractors.Dokicloud
import com.lagradost.cloudstream3.extractors.DoodCxExtractor
import com.lagradost.cloudstream3.extractors.DoodLaExtractor
import com.lagradost.cloudstream3.extractors.DoodPmExtractor
import com.lagradost.cloudstream3.extractors.DoodShExtractor
import com.lagradost.cloudstream3.extractors.DoodSoExtractor
import com.lagradost.cloudstream3.extractors.DoodToExtractor
import com.lagradost.cloudstream3.extractors.DoodWatchExtractor
import com.lagradost.cloudstream3.extractors.DoodWfExtractor
import com.lagradost.cloudstream3.extractors.DoodWsExtractor
import com.lagradost.cloudstream3.extractors.DoodYtExtractor
import com.lagradost.cloudstream3.extractors.Dooood
import com.lagradost.cloudstream3.extractors.Embedgram
import com.lagradost.cloudstream3.extractors.Evoload
import com.lagradost.cloudstream3.extractors.Evoload1
import com.lagradost.cloudstream3.extractors.FEmbed
import com.lagradost.cloudstream3.extractors.FEnet
import com.lagradost.cloudstream3.extractors.Fastream
import com.lagradost.cloudstream3.extractors.FeHD
import com.lagradost.cloudstream3.extractors.Fembed9hd
import com.lagradost.cloudstream3.extractors.FileMoon
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.GMPlayer
import com.lagradost.cloudstream3.extractors.Gdriveplayer
import com.lagradost.cloudstream3.extractors.Gdriveplayerapi
import com.lagradost.cloudstream3.extractors.Gdriveplayerapp
import com.lagradost.cloudstream3.extractors.Gdriveplayerbiz
import com.lagradost.cloudstream3.extractors.Gdriveplayerco
import com.lagradost.cloudstream3.extractors.Gdriveplayerfun
import com.lagradost.cloudstream3.extractors.Gdriveplayerio
import com.lagradost.cloudstream3.extractors.Gdriveplayerme
import com.lagradost.cloudstream3.extractors.Gdriveplayerorg
import com.lagradost.cloudstream3.extractors.Gdriveplayerus
import com.lagradost.cloudstream3.extractors.Gofile
import com.lagradost.cloudstream3.extractors.GuardareStream
import com.lagradost.cloudstream3.extractors.Guccihide
import com.lagradost.cloudstream3.extractors.Hxfile
import com.lagradost.cloudstream3.extractors.JWPlayer
import com.lagradost.cloudstream3.extractors.Jawcloud
import com.lagradost.cloudstream3.extractors.Jeniusplay
import com.lagradost.cloudstream3.extractors.Keephealth
import com.lagradost.cloudstream3.extractors.KotakAnimeid
import com.lagradost.cloudstream3.extractors.Kotakajair
import com.lagradost.cloudstream3.extractors.Krakenfiles
import com.lagradost.cloudstream3.extractors.LayarKaca
import com.lagradost.cloudstream3.extractors.Linkbox
import com.lagradost.cloudstream3.extractors.Luxubu
import com.lagradost.cloudstream3.extractors.Lvturbo
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.MixDrop
import com.lagradost.cloudstream3.extractors.MixDropBz
import com.lagradost.cloudstream3.extractors.MixDropCh
import com.lagradost.cloudstream3.extractors.MixDropTo
import com.lagradost.cloudstream3.extractors.Movhide
import com.lagradost.cloudstream3.extractors.Moviehab
import com.lagradost.cloudstream3.extractors.MoviehabNet
import com.lagradost.cloudstream3.extractors.Moviesapi
import com.lagradost.cloudstream3.extractors.Moviesm4u
import com.lagradost.cloudstream3.extractors.Mp4Upload
import com.lagradost.cloudstream3.extractors.Mvidoo
import com.lagradost.cloudstream3.extractors.MwvnVizcloudInfo
import com.lagradost.cloudstream3.extractors.Neonime7n
import com.lagradost.cloudstream3.extractors.Neonime8n
import com.lagradost.cloudstream3.extractors.OkRu
import com.lagradost.cloudstream3.extractors.OkRuHttps
import com.lagradost.cloudstream3.extractors.Okrulink
import com.lagradost.cloudstream3.extractors.Pixeldrain
import com.lagradost.cloudstream3.extractors.PlayLtXyz
import com.lagradost.cloudstream3.extractors.PlayerVoxzer
import com.lagradost.cloudstream3.extractors.Rabbitstream
import com.lagradost.cloudstream3.extractors.Rasacintaku
import com.lagradost.cloudstream3.extractors.SBfull
import com.lagradost.cloudstream3.extractors.Sbasian
import com.lagradost.cloudstream3.extractors.Sbface
import com.lagradost.cloudstream3.extractors.Sbflix
import com.lagradost.cloudstream3.extractors.Sblona
import com.lagradost.cloudstream3.extractors.Sblongvu
import com.lagradost.cloudstream3.extractors.Sbnet
import com.lagradost.cloudstream3.extractors.Sbrapid
import com.lagradost.cloudstream3.extractors.Sbsonic
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.Solidfiles
import com.lagradost.cloudstream3.extractors.SpeedoStream
import com.lagradost.cloudstream3.extractors.SpeedoStream1
import com.lagradost.cloudstream3.extractors.SpeedoStream2
import com.lagradost.cloudstream3.extractors.Ssbstream
import com.lagradost.cloudstream3.extractors.StreamM4u
import com.lagradost.cloudstream3.extractors.StreamSB
import com.lagradost.cloudstream3.extractors.StreamSB1
import com.lagradost.cloudstream3.extractors.StreamSB10
import com.lagradost.cloudstream3.extractors.StreamSB11
import com.lagradost.cloudstream3.extractors.StreamSB2
import com.lagradost.cloudstream3.extractors.StreamSB3
import com.lagradost.cloudstream3.extractors.StreamSB4
import com.lagradost.cloudstream3.extractors.StreamSB5
import com.lagradost.cloudstream3.extractors.StreamSB6
import com.lagradost.cloudstream3.extractors.StreamSB7
import com.lagradost.cloudstream3.extractors.StreamSB8
import com.lagradost.cloudstream3.extractors.StreamSB9
import com.lagradost.cloudstream3.extractors.StreamTape
import com.lagradost.cloudstream3.extractors.StreamTapeNet
import com.lagradost.cloudstream3.extractors.StreamhideCom
import com.lagradost.cloudstream3.extractors.StreamhideTo
import com.lagradost.cloudstream3.extractors.Streamhub2
import com.lagradost.cloudstream3.extractors.Streamlare
import com.lagradost.cloudstream3.extractors.StreamoUpload
import com.lagradost.cloudstream3.extractors.Streamplay
import com.lagradost.cloudstream3.extractors.Streamsss
import com.lagradost.cloudstream3.extractors.Supervideo
import com.lagradost.cloudstream3.extractors.Tantifilm
import com.lagradost.cloudstream3.extractors.Tomatomatela
import com.lagradost.cloudstream3.extractors.TomatomatelalClub
import com.lagradost.cloudstream3.extractors.Tubeless
import com.lagradost.cloudstream3.extractors.Upstream
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.Userload
import com.lagradost.cloudstream3.extractors.Userscloud
import com.lagradost.cloudstream3.extractors.Uservideo
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.VideoVard
import com.lagradost.cloudstream3.extractors.VideovardSX
import com.lagradost.cloudstream3.extractors.Vidgomunime
import com.lagradost.cloudstream3.extractors.Vidgomunimesb
import com.lagradost.cloudstream3.extractors.Vidmoly
import com.lagradost.cloudstream3.extractors.Vidmolyme
import com.lagradost.cloudstream3.extractors.Vido
import com.lagradost.cloudstream3.extractors.Vidstreamz
import com.lagradost.cloudstream3.extractors.Vizcloud
import com.lagradost.cloudstream3.extractors.Vizcloud2
import com.lagradost.cloudstream3.extractors.VizcloudCloud
import com.lagradost.cloudstream3.extractors.VizcloudDigital
import com.lagradost.cloudstream3.extractors.VizcloudInfo
import com.lagradost.cloudstream3.extractors.VizcloudLive
import com.lagradost.cloudstream3.extractors.VizcloudOnline
import com.lagradost.cloudstream3.extractors.VizcloudSite
import com.lagradost.cloudstream3.extractors.VizcloudXyz
import com.lagradost.cloudstream3.extractors.Voe
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.YourUpload
import com.lagradost.cloudstream3.extractors.YoutubeExtractor
import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor
import com.lagradost.cloudstream3.extractors.YoutubeNoCookieExtractor
import com.lagradost.cloudstream3.extractors.YoutubeShortLinkExtractor
import com.lagradost.cloudstream3.extractors.Yufiles
import com.lagradost.cloudstream3.extractors.Zorofile
import com.lagradost.cloudstream3.extractors.Zplayer
import com.lagradost.cloudstream3.extractors.ZplayerV2
import com.lagradost.cloudstream3.extractors.Ztreamhub
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.extractors.* import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.jsoup.Jsoup import org.jsoup.Jsoup
import kotlin.collections.MutableList import java.net.URL
import java.util.UUID
/** /**
* For use in the ConcatenatingMediaSource. * For use in the ConcatenatingMediaSource.
@ -35,22 +227,144 @@ data class ExtractorLinkPlayList(
val playlist: List<PlayListItem>, val playlist: List<PlayListItem>,
override val referer: String, override val referer: String,
override val quality: Int, override val quality: Int,
override val isM3u8: Boolean = false,
override val headers: Map<String, String> = mapOf(), override val headers: Map<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */ /** Used for getExtractorVerifierJob() */
override val extractorData: String? = null, override val extractorData: String? = null,
override val type: ExtractorLinkType,
) : ExtractorLink( ) : ExtractorLink(
source, source = source,
name, name = name,
// Blank as un-used url = "",
"", referer = referer,
referer, quality = quality,
quality, headers = headers,
isM3u8, extractorData = extractorData,
headers, type = type
extractorData ) {
) constructor(
source: String,
name: String,
playlist: List<PlayListItem>,
referer: String,
quality: Int,
isM3u8: Boolean = false,
headers: Map<String, String> = mapOf(),
extractorData: String? = null,
) : this(
source = source,
name = name,
playlist = playlist,
referer = referer,
quality = quality,
type = if (isM3u8) ExtractorLinkType.M3U8 else ExtractorLinkType.VIDEO,
headers = headers,
extractorData = extractorData,
)
}
/** Metadata about the file type used for downloads and exoplayer hint,
* if you respond with the wrong one the file will fail to download or be played */
enum class ExtractorLinkType {
/** Single stream of bytes no matter the actual file type */
VIDEO,
/** Split into several .ts files, has support for encrypted m3u8s */
M3U8,
/** Like m3u8 but uses xml, currently no download support */
DASH,
/** No support at the moment */
TORRENT,
/** No support at the moment */
MAGNET,
}
private fun inferTypeFromUrl(url: String): ExtractorLinkType {
val path = normalSafeApiCall { URL(url).path }
return when {
path?.endsWith(".m3u8") == true -> ExtractorLinkType.M3U8
path?.endsWith(".mpd") == true -> ExtractorLinkType.DASH
path?.endsWith(".torrent") == true -> ExtractorLinkType.TORRENT
url.startsWith("magnet:") -> ExtractorLinkType.MAGNET
else -> ExtractorLinkType.VIDEO
}
}
val INFER_TYPE : ExtractorLinkType? = null
/**
* UUID for the ClearKey DRM scheme.
*
*
* ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up.
*/
val CLEARKEY_UUID = UUID(-0x1d8e62a7567a4c37L, 0x781AB030AF78D30EL)
/**
* UUID for the Widevine DRM scheme.
*
*
* Widevine is supported on Android devices running Android 4.3 (API Level 18) and up.
*/
val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L)
/**
* UUID for the PlayReady DRM scheme.
*
*
* PlayReady is supported on all AndroidTV devices. Note that most other Android devices do not
* provide PlayReady support.
*/
val PLAYREADY_UUID = UUID(-0x65fb0f8667bfbd7aL, -0x546d19a41f77a06bL)
open class DrmExtractorLink private constructor(
override val source: String,
override val name: String,
override val url: String,
override val referer: String,
override val quality: Int,
override val headers: Map<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */
override val extractorData: String? = null,
override val type: ExtractorLinkType,
open val kid : String,
open val key : String,
open val uuid : UUID,
open val kty : String,
open val keyRequestParameters : HashMap<String, String>
) : ExtractorLink(
source, name, url, referer, quality, type, headers, extractorData
) {
constructor(
source: String,
name: String,
url: String,
referer: String,
quality: Int,
/** the type of the media, use INFER_TYPE if you want to auto infer the type from the url */
type: ExtractorLinkType?,
headers: Map<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */
extractorData: String? = null,
kid : String,
key : String,
uuid : UUID = CLEARKEY_UUID,
kty : String = "oct",
keyRequestParameters : HashMap<String, String> = hashMapOf(),
) : this(
source = source,
name = name,
url = url,
referer = referer,
quality = quality,
headers = headers,
extractorData = extractorData,
type = type ?: inferTypeFromUrl(url),
kid = kid,
key = key,
uuid = uuid,
keyRequestParameters = keyRequestParameters,
kty = kty,
)
}
open class ExtractorLink constructor( open class ExtractorLink constructor(
open val source: String, open val source: String,
@ -58,12 +372,36 @@ open class ExtractorLink constructor(
override val url: String, override val url: String,
override val referer: String, override val referer: String,
open val quality: Int, open val quality: Int,
open val isM3u8: Boolean = false,
override val headers: Map<String, String> = mapOf(), override val headers: Map<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */ /** Used for getExtractorVerifierJob() */
open val extractorData: String? = null, open val extractorData: String? = null,
open val isDash: Boolean = false, open val type: ExtractorLinkType,
) : VideoDownloadManager.IDownloadableMinimum { ) : VideoDownloadManager.IDownloadableMinimum {
val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8
val isDash : Boolean get() = type == ExtractorLinkType.DASH
constructor(
source: String,
name: String,
url: String,
referer: String,
quality: Int,
/** the type of the media, use INFER_TYPE if you want to auto infer the type from the url */
type: ExtractorLinkType?,
headers: Map<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */
extractorData: String? = null,
) : this(
source = source,
name = name,
url = url,
referer = referer,
quality = quality,
headers = headers,
extractorData = extractorData,
type = type ?: inferTypeFromUrl(url)
)
/** /**
* Old constructor without isDash, allows for backwards compatibility with extensions. * Old constructor without isDash, allows for backwards compatibility with extensions.
* Should be removed after all extensions have updated their cloudstream.jar * Should be removed after all extensions have updated their cloudstream.jar
@ -80,8 +418,30 @@ open class ExtractorLink constructor(
extractorData: String? = null extractorData: String? = null
) : this(source, name, url, referer, quality, isM3u8, headers, extractorData, false) ) : this(source, name, url, referer, quality, isM3u8, headers, extractorData, false)
constructor(
source: String,
name: String,
url: String,
referer: String,
quality: Int,
isM3u8: Boolean = false,
headers: Map<String, String> = mapOf(),
/** Used for getExtractorVerifierJob() */
extractorData: String? = null,
isDash: Boolean,
) : this(
source = source,
name = name,
url = url,
referer = referer,
quality = quality,
headers = headers,
extractorData = extractorData,
type = if (isDash) ExtractorLinkType.DASH else if (isM3u8) ExtractorLinkType.M3U8 else ExtractorLinkType.VIDEO
)
override fun toString(): String { override fun toString(): String {
return "ExtractorLink(name=$name, url=$url, referer=$referer, isM3u8=$isM3u8)" return "ExtractorLink(name=$name, url=$url, referer=$referer, type=$type)"
} }
} }
@ -135,6 +495,7 @@ enum class Qualities(var value: Int, val defaultPriority: Int) {
else -> "${qual}p" else -> "${qual}p"
} }
} }
fun getStringByIntFull(quality: Int): String { fun getStringByIntFull(quality: Int): String {
return when (quality) { return when (quality) {
0 -> "Auto" 0 -> "Auto"
@ -389,6 +750,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Acefile(), Acefile(),
SpeedoStream(), SpeedoStream(),
SpeedoStream1(), SpeedoStream1(),
SpeedoStream2(),
Zorofile(), Zorofile(),
Embedgram(), Embedgram(),
Mvidoo(), Mvidoo(),

View file

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

View file

@ -128,7 +128,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
<FrameLayout <FrameLayout
android:id="@+id/background_poster_holder" android:id="@+id/background_poster_holder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="200dp" android:layout_height="150dp"
android:visibility="visible"> android:visibility="visible">
<ImageView <ImageView
@ -147,8 +147,6 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:src="@drawable/background_shadow"> android:src="@drawable/background_shadow">
</ImageView> </ImageView>
</FrameLayout> </FrameLayout>
@ -411,7 +409,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:padding="5dp" android:padding="5dp"
android:requiresFadingEdge="vertical" android:requiresFadingEdge="vertical"
android:textColor="?attr/textColor" android:textColor="?attr/textColor"
android:textSize="12sp" android:textSize="16sp"
tools:text="Ryan Quicksave Romano is an eccentric adventurer with a strange power: he can create a save-point in time and redo his life whenever he dies. Arriving in New Rome, the glitzy capital of sin of a rebuilding Europe, he finds the city torn between mega-corporations, sponsored heroes, superpowered criminals, and true monsters. It's a time of chaos, where potions can grant the power to rule the world and dangers lurk everywhere. " /> tools:text="Ryan Quicksave Romano is an eccentric adventurer with a strange power: he can create a save-point in time and redo his life whenever he dies. Arriving in New Rome, the glitzy capital of sin of a rebuilding Europe, he finds the city torn between mega-corporations, sponsored heroes, superpowered criminals, and true monsters. It's a time of chaos, where potions can grant the power to rule the world and dangers lurk everywhere. " />
<com.google.android.material.chip.ChipGroup <com.google.android.material.chip.ChipGroup
@ -537,18 +535,32 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
<ImageView <FrameLayout
android:visibility="gone"
tools:visibility="visible"
android:id="@+id/episodes_shadow" android:id="@+id/episodes_shadow"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:layout_gravity="end" <ImageView
android:clickable="false" android:layout_width="match_parent"
android:focusable="false" android:layout_height="match_parent"
android:focusableInTouchMode="false" android:layout_gravity="end"
android:importantForAccessibility="no" android:clickable="false"
android:src="@drawable/episodes_shadow" android:focusable="false"
android:visibility="gone" android:focusableInTouchMode="false"
tools:visibility="visible" /> android:importantForAccessibility="no"
android:src="@drawable/episodes_shadow"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="end"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:importantForAccessibility="no"
android:src="@drawable/episodes_shadow"/>
</FrameLayout>
<LinearLayout <LinearLayout
android:id="@+id/episode_holder_tv" android:id="@+id/episode_holder_tv"

View file

@ -49,7 +49,8 @@
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:text="@string/hls_playlist" android:text="@string/hls_playlist"
android:textColor="?attr/textColor" android:textColor="?attr/textColor"
android:textSize="16sp" /> android:textSize="16sp"
android:visibility="invisible" />
</LinearLayout> </LinearLayout>

View file

@ -200,4 +200,69 @@
<string name="use">استخدم</string> <string name="use">استخدم</string>
<string name="unable_to_inflate">%sتعذر إنشاء واجهة المستخدم بشكل صحيح، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور</string> <string name="unable_to_inflate">%sتعذر إنشاء واجهة المستخدم بشكل صحيح، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور</string>
<string name="qualities">الصفات</string> <string name="qualities">الصفات</string>
<string name="subs_edge_type">نوع الحافة</string>
<string name="home_play">العب</string>
<string name="error_loading_links_toast">حدث خطأ أثناء تحميل الروابط</string>
<string name="download_storage_text">التخزين الداخلي</string>
<string name="app_subbed_text">الترجمة</string>
<string name="popup_resume_download">استئناف تحميل</string>
<string name="home_info">معلومات</string>
<string name="popup_pause_download">وقفة التحميل</string>
<string name="sort_cancel">الغي</string>
<string name="sort_save">احفظ</string>
<string name="subtitles_settings">إعدادات الترجمة</string>
<string name="subs_text_color">لون الخط</string>
<string name="subs_outline_color">لون المخطط التفصيلي</string>
<string name="sort_close">اقفل</string>
<string name="sort_clear">امسح</string>
<string name="player_speed">سرعة اللاعب</string>
<string name="subs_background_color">لون الخلفية</string>
<string name="subs_window_color">لون النافذة</string>
<string name="subs_subtitle_elevation">ارتفاع الترجمة</string>
<string name="popup_delete_file">حذف ملف</string>
<string name="pref_disable_acra">تعطيل الإبلاغ التلقائي عن الأخطاء</string>
<string name="update_started">بدأ التحديث</string>
<string name="sort_copy">انسخ</string>
<string name="stream">بث</string>
<string name="popup_play_file">ملف اللعب</string>
<string name="home_more_info">مزيد من المعلومات</string>
<string name="filter_bookmarks">تصفية الإشارات المرجعية</string>
<string name="error_bookmarks_text">إشارات مرجعية</string>
<string name="action_remove_from_bookmarks">زيل</string>
<string name="action_add_to_bookmarks">ضبط حالة المشاهدة</string>
<string name="app_dubbed_text">مدبلجة</string>
<string name="home_expanded_hide">اخفي</string>
<string name="sort_apply">قدم</string>
<string name="torrent_plot">وصف</string>
<string name="picture_in_picture_des">يستمر التشغيل في مشغل مصغر فوق التطبيقات الأخرى</string>
<string name="delete_message" formatted="true">نهائيا %sسيؤدي هذا الى حذف
\nهل أنت متأكد؟</string>
<string name="subs_font">الخط</string>
<string name="subs_font_size">حجم الخط</string>
<string name="action_remove_watching">زيل</string>
<string name="vpn_torrent">هذا المزود عبارة عن تورنت، ويوصى باستخدام فيبيان</string>
<string name="provider_info_meta">لا يتم توفير البيانات الوصفية بواسطة الموقع، وسيفشل تحميل الفيديو إذا لم يكن موجودًا في الموقع.</string>
<string name="status_ongoing">جاري التنفيذ</string>
<string name="status_completed">مكتمل</string>
<string name="status">حالة</string>
<string name="subs_auto_select_language">التحديد التلقائي للغة</string>
<string name="player_size_settings">زر تغيير حجم المشغل</string>
<string name="continue_watching">مواصلة المشاهدة</string>
<string name="action_open_watching">مزيد من المعلومات</string>
<string name="search_provider_text_providers">البحث باستخدام مقدمي الخدمات</string>
<string name="search_provider_text_types">البحث باستخدام الأنواع</string>
<string name="benene_count_text">بنيني الى المطورين %d تم منح</string>
<string name="benene_count_text_none">لم يتم تقديم بنيني</string>
<string name="subs_download_languages">تحميل اللغات</string>
<string name="subs_subtitle_languages">لغة الترجمة</string>
<string name="subs_hold_to_reset_to_default">اضغط لإعادة التعيين إلى الوضع الافتراضي</string>
<string name="subs_import_text" formatted="true">%s قم باستيراد الخطوط بوضعها في</string>
<string name="vpn_might_be_needed">قد تكون هناك حاجة إلى فيبيان حتى يعمل هذا المزود بشكل صحيح</string>
<string name="normal_no_plot">لم يتم العثور على قطعة أرض</string>
<string name="torrent_no_plot">لم يتم العثور على وصف</string>
<string name="show_log_cat">🐈عرض لوجكات</string>
<string name="test_log">سجل</string>
<string name="picture_in_picture">صور في صور</string>
<string name="resume_time_left" formatted="true">%d
\nباقي</string>
</resources> </resources>

View file

@ -157,7 +157,7 @@
<string name="show_fillers_settings">Mostrar episódios de Filler em anime</string> <string name="show_fillers_settings">Mostrar episódios de Filler em anime</string>
<string name="show_trailers_settings">Mostrar trailers</string> <string name="show_trailers_settings">Mostrar trailers</string>
<string name="kitsu_settings">Mostrar posters do Kitsu</string> <string name="kitsu_settings">Mostrar posters do Kitsu</string>
<string name="pref_filter_search_quality">Esconder qualidades de vídeo selecionadas nos resultados da Pesquisa</string> <string name="pref_filter_search_quality">Esconder qualidades de vídeo selecionadas nos resultados da pesquisa</string>
<string name="automatic_plugin_updates">Atualizações de plugin automáticas</string> <string name="automatic_plugin_updates">Atualizações de plugin automáticas</string>
<string name="updates_settings">Mostrar atualizações do app</string> <string name="updates_settings">Mostrar atualizações do app</string>
<string name="updates_settings_des">Automaticamente procurar por novas atualizações ao abrir</string> <string name="updates_settings_des">Automaticamente procurar por novas atualizações ao abrir</string>
@ -222,7 +222,7 @@
<string name="movies_singular">Filme</string> <string name="movies_singular">Filme</string>
<string name="tv_series_singular">Série</string> <string name="tv_series_singular">Série</string>
<string name="cartoons_singular">Desenho Animado</string> <string name="cartoons_singular">Desenho Animado</string>
<string name="anime_singular">@string/anime</string> <string name="anime_singular">Anime</string>
<string name="ova_singular">@string/ova</string> <string name="ova_singular">@string/ova</string>
<string name="torrent_singular">Torrent</string> <string name="torrent_singular">Torrent</string>
<string name="documentaries_singular">Documentário</string> <string name="documentaries_singular">Documentário</string>
@ -265,14 +265,14 @@
<string name="video_buffer_disk_settings">Cache do vídeo em disco</string> <string name="video_buffer_disk_settings">Cache do vídeo em disco</string>
<string name="video_buffer_clear_settings">Limpar cache de vídeo e imagem</string> <string name="video_buffer_clear_settings">Limpar cache de vídeo e imagem</string>
<string name="video_ram_description">Causará travamentos aleatórios se definido muito alto. Não mude caso tiver pouca memória RAM, como um Android TV ou um telefone antigo</string> <string name="video_ram_description">Causará travamentos aleatórios se definido muito alto. Não mude caso tiver pouca memória RAM, como um Android TV ou um telefone antigo</string>
<string name="video_disk_description">Pode causar problemas em sistemas com pouco espaço de armazenamento se definido muito alto, como em dispositivos Android TV</string> <string name="video_disk_description">Causa problemas em sistemas com pouco espaço de armazenamento se definido muito alto, como em dispositivos Android TV.</string>
<string name="dns_pref">DNS sobre HTTPS</string> <string name="dns_pref">DNS sobre HTTPS</string>
<string name="dns_pref_summary">Útil para burlar bloqueios de provedores de internet</string> <string name="dns_pref_summary">Útil para burlar bloqueios de provedores de internet</string>
<string name="add_site_pref">Clonar site</string> <string name="add_site_pref">Clonar site</string>
<string name="remove_site_pref">Remover site</string> <string name="remove_site_pref">Remover site</string>
<string name="add_site_summary">Adiciona um clone de um site existente, com uma URL diferente</string> <string name="add_site_summary">Adiciona um clone de um site existente, com uma URL diferente</string>
<string name="download_path_pref">Caminho para Download</string> <string name="download_path_pref">Caminho para Download</string>
<string name="nginx_url_pref">Url do servidor Nginx</string> <string name="nginx_url_pref">URL do servidor NGINX</string>
<string name="display_subbed_dubbed_settings">Mostrar Anime Dublado/Legendado</string> <string name="display_subbed_dubbed_settings">Mostrar Anime Dublado/Legendado</string>
<string name="resize_fit">Ajustar para a Tela</string> <string name="resize_fit">Ajustar para a Tela</string>
<string name="resize_fill">Esticar</string> <string name="resize_fill">Esticar</string>
@ -338,7 +338,7 @@
<string name="subtitles_shadow">Sombreado</string> <string name="subtitles_shadow">Sombreado</string>
<string name="subtitles_raised">Em Relevo</string> <string name="subtitles_raised">Em Relevo</string>
<string name="subtitle_offset">Sincronizar legendas</string> <string name="subtitle_offset">Sincronizar legendas</string>
<string name="subtitle_offset_hint">1000ms</string> <string name="subtitle_offset_hint">1000 ms</string>
<string name="subtitle_offset_title">Atraso de legenda</string> <string name="subtitle_offset_title">Atraso de legenda</string>
<string name="subtitle_offset_extra_hint_later_format">Use isto se as legendas forem mostradas %dms adiantadas</string> <string name="subtitle_offset_extra_hint_later_format">Use isto se as legendas forem mostradas %dms adiantadas</string>
<string name="subtitle_offset_extra_hint_before_format">Use isto se as legendas forem mostradas %dms atrasadas</string> <string name="subtitle_offset_extra_hint_before_format">Use isto se as legendas forem mostradas %dms atrasadas</string>
@ -382,9 +382,9 @@
<string name="resolution_and_title">Resolução e título</string> <string name="resolution_and_title">Resolução e título</string>
<string name="title">Título</string> <string name="title">Título</string>
<string name="resolution">Resolução</string> <string name="resolution">Resolução</string>
<string name="error_invalid_id">Id invalida</string> <string name="error_invalid_id">ID inválido</string>
<string name="error_invalid_data">Dado invalido</string> <string name="error_invalid_data">Dado invalido</string>
<string name="error_invalid_url">URL invalido</string> <string name="error_invalid_url">URL inválida</string>
<string name="error">Erro</string> <string name="error">Erro</string>
<string name="subtitles_remove_captions">Remover legendas ocultas(CC) das legendas</string> <string name="subtitles_remove_captions">Remover legendas ocultas(CC) das legendas</string>
<string name="subtitles_remove_bloat">Remover bloat das legendas</string> <string name="subtitles_remove_bloat">Remover bloat das legendas</string>
@ -406,8 +406,8 @@
<string name="plugin_loaded">Plugin Carregado</string> <string name="plugin_loaded">Plugin Carregado</string>
<string name="plugin_deleted">Plugin Apagado</string> <string name="plugin_deleted">Plugin Apagado</string>
<string name="plugin_load_fail" formatted="true">Falha ao carregar %s</string> <string name="plugin_load_fail" formatted="true">Falha ao carregar %s</string>
<string name="batch_download_start_format" formatted="true">Iniciada a transferência %d %s</string> <string name="batch_download_start_format" formatted="true">Iniciada a transferência %d %s</string>
<string name="batch_download_finish_format" formatted="true">Transferido %d %s com sucesso</string> <string name="batch_download_finish_format" formatted="true">Transferido %d %s</string>
<string name="batch_download_nothing_to_download_format" formatted="true">Tudo %s já transferido</string> <string name="batch_download_nothing_to_download_format" formatted="true">Tudo %s já transferido</string>
<string name="batch_download">Transferência em batch</string> <string name="batch_download">Transferência em batch</string>
<string name="plugin_singular">Plugin</string> <string name="plugin_singular">Plugin</string>
@ -444,7 +444,7 @@
<string name="browser">Navegador</string> <string name="browser">Navegador</string>
<string name="pref_category_backup">Copia de Segurança</string> <string name="pref_category_backup">Copia de Segurança</string>
<string name="android_tv_interface_off_seek_settings_summary">A Barra de Progresso pode ser usada quando o player estiver oculto</string> <string name="android_tv_interface_off_seek_settings_summary">A Barra de Progresso pode ser usada quando o player estiver oculto</string>
<string name="subscription_list_name">Inscrever</string> <string name="subscription_list_name">Inscrito</string>
<string name="empty_library_logged_in_message">Essa lista está vazia. Tente mudar para outra.</string> <string name="empty_library_logged_in_message">Essa lista está vazia. Tente mudar para outra.</string>
<string name="play_livestream_button">Reproduzir Livestream</string> <string name="play_livestream_button">Reproduzir Livestream</string>
<string name="test_log">Log do Teste</string> <string name="test_log">Log do Teste</string>
@ -493,10 +493,10 @@
\nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado!</string> \nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado!</string>
<string name="safe_mode_file">Arquivo de modo de segurança encontrado! <string name="safe_mode_file">Arquivo de modo de segurança encontrado!
\nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido.</string> \nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido.</string>
<string name="subscription_new">Inscrevel em %d</string> <string name="subscription_new">Inscrito em %s</string>
<string name="subscription_episode_released">Episódio %d Lançado</string> <string name="subscription_episode_released">Episódio %d Lançado</string>
<string name="set_default">Selecionar padrão</string> <string name="set_default">Selecionar padrão</string>
<string name="subscription_deleted">Disinscrevel em %d</string> <string name="subscription_deleted">Desinscrito de %s</string>
<string name="apk_installer_settings_des">Alguns aparelhos não possuem suporte para este pacote de instalação. Tente a opção legada se a atualização não instalar.</string> <string name="apk_installer_settings_des">Alguns aparelhos não possuem suporte para este pacote de instalação. Tente a opção legada se a atualização não instalar.</string>
<string name="mobile_data">Dados móveis</string> <string name="mobile_data">Dados móveis</string>
<string name="profile_number">Perfil %d</string> <string name="profile_number">Perfil %d</string>
@ -550,4 +550,21 @@
<string name="audio_tracks">Faixas de áudio</string> <string name="audio_tracks">Faixas de áudio</string>
<string name="sort_updated_new">Adicionado em (novo para antigo)</string> <string name="sort_updated_new">Adicionado em (novo para antigo)</string>
<string name="video_tracks">Faixas de video</string> <string name="video_tracks">Faixas de video</string>
<string name="pref_category_subtitles">Legendas</string>
<string name="player_settings_play_in_browser">Navegador</string>
<string name="is_adult">18+</string>
<string name="pref_category_links">Links</string>
<string name="pref_category_player_features">Funcionalidades do Player</string>
<string name="apk_installer_settings">Instalador APK</string>
<string name="pref_category_looks">Aparência</string>
<string name="disable">Desativar</string>
<string name="use">Usar</string>
<string name="network_adress_example">Link da stream</string>
<string name="pref_category_gestures">Gestos</string>
<string name="plugin_downloaded">Plugin baixado</string>
<string name="jsdelivr_enabled">Não foi possível se conectar ao GitHub. Ativando proxy jsDelivr…</string>
<string name="pref_category_cache">Cache</string>
<string name="other_singular">Vídeo</string>
<string name="pref_category_android_tv">Android TV</string>
<string name="wifi">Wi-Fi</string>
</resources> </resources>

View file

@ -5,19 +5,19 @@
<string name="next_episode_format" formatted="true">Episode %d wird veröffentlicht in</string> <string name="next_episode_format" formatted="true">Episode %d wird veröffentlicht in</string>
<string name="result_poster_img_des">Vorschaubild</string> <string name="result_poster_img_des">Vorschaubild</string>
<string name="search_poster_img_des">Vorschaubild</string> <string name="search_poster_img_des">Vorschaubild</string>
<string name="subs_hold_to_reset_to_default">Halten, um auf die Standardeinstellungen zurückzusetzen</string> <string name="subs_hold_to_reset_to_default">Halten, um auf Standardeinstellungen zurückzusetzen</string>
<string name="restore_failed_format" formatted="true">Wiederherstellung der Daten aus der Datei %s fehlgeschlagen</string> <string name="restore_failed_format" formatted="true">Wiederherstellung der Daten aus der Datei %s fehlgeschlagen</string>
<string name="backup_success">Daten erfolgreich gesichert</string> <string name="backup_success">Daten erfolgreich gesichert</string>
<string name="backup_failed_error_format">Fehler beim Sichern von %s</string> <string name="backup_failed_error_format">Fehler beim Sichern von %s</string>
<string name="no_chromecast_support_toast">Dieser Anbieter hat keine Chromecast-Unterstützung</string> <string name="no_chromecast_support_toast">Dieser Anbieter hat keine Chromecast-Unterstützung</string>
<string name="episode_action_chromecast_mirror">Chromecast-Mirror</string> <string name="episode_action_chromecast_mirror">Chromecast-Mirror</string>
<string name="episode_action_play_in_app">In App wiedergeben</string> <string name="episode_action_play_in_app">In App wiedergeben</string>
<string name="skip_type_mixed_op">Vermischte Openings</string> <string name="skip_type_mixed_op">Gemischte Openings</string>
<string name="skip_type_creddits">Abspann</string> <string name="skip_type_creddits">Abspann</string>
<string name="skip_type_intro">Intro</string> <string name="skip_type_intro">Intro</string>
<string name="clear_history">Verlauf löschen</string> <string name="clear_history">Verlauf löschen</string>
<string name="history">Verlauf</string> <string name="history">Verlauf</string>
<string name="enable_skip_op_from_database_des">Überspringen Knopf für Openings/Endings anzeigen</string> <string name="enable_skip_op_from_database_des">Button zum Überspringen für Openings/Endings anzeigen</string>
<string name="clipboard_too_large">Zu viel Text. Kann nicht in der Zwischenablage gespeichert werden.</string> <string name="clipboard_too_large">Zu viel Text. Kann nicht in der Zwischenablage gespeichert werden.</string>
<string name="episode_poster_img_des">Episodenvorschaubild</string> <string name="episode_poster_img_des">Episodenvorschaubild</string>
<string name="home_main_poster_img_des">Medienvorschaubild</string> <string name="home_main_poster_img_des">Medienvorschaubild</string>
@ -34,7 +34,7 @@
<string name="app_name">CloudStream</string> <string name="app_name">CloudStream</string>
<string name="play_with_app_name">Mit CloudStream abspielen</string> <string name="play_with_app_name">Mit CloudStream abspielen</string>
<string name="title_home">Startseite</string> <string name="title_home">Startseite</string>
<string name="title_search">Suchen</string> <string name="title_search">Suche</string>
<string name="title_downloads">Downloads</string> <string name="title_downloads">Downloads</string>
<string name="title_settings">Einstellungen</string> <string name="title_settings">Einstellungen</string>
<string name="search_hint">Suchen…</string> <string name="search_hint">Suchen…</string>
@ -44,8 +44,8 @@
<string name="next_episode">Nächste Episode</string> <string name="next_episode">Nächste Episode</string>
<string name="result_tags">Genres</string> <string name="result_tags">Genres</string>
<string name="result_share">Teilen</string> <string name="result_share">Teilen</string>
<string name="result_open_in_browser">In Browser öffnen</string> <string name="result_open_in_browser">Im Browser öffnen</string>
<string name="skip_loading">Puffern überspringen</string> <string name="skip_loading">Laden überspringen</string>
<string name="loading">Lädt…</string> <string name="loading">Lädt…</string>
<string name="type_watching">Am schauen</string> <string name="type_watching">Am schauen</string>
<string name="type_on_hold">Pausiert</string> <string name="type_on_hold">Pausiert</string>
@ -79,7 +79,7 @@
<string name="popup_play_file">Datei abspielen</string> <string name="popup_play_file">Datei abspielen</string>
<string name="popup_resume_download">Download fortsetzen</string> <string name="popup_resume_download">Download fortsetzen</string>
<string name="popup_pause_download">Download pausieren</string> <string name="popup_pause_download">Download pausieren</string>
<string name="pref_disable_acra">Automatische Fehlerberichterstattung deaktivieren</string> <string name="pref_disable_acra">Automatische Fehlerberichtserstattung deaktivieren</string>
<string name="home_more_info">Mehr Infos</string> <string name="home_more_info">Mehr Infos</string>
<string name="home_expanded_hide">Verstecken</string> <string name="home_expanded_hide">Verstecken</string>
<string name="home_play">Abspielen</string> <string name="home_play">Abspielen</string>
@ -106,8 +106,8 @@
<string name="subs_font_size">Schriftgröße</string> <string name="subs_font_size">Schriftgröße</string>
<string name="search_provider_text_providers">Suche anhand Anbietern</string> <string name="search_provider_text_providers">Suche anhand Anbietern</string>
<string name="search_provider_text_types">Suche anhand Typen</string> <string name="search_provider_text_types">Suche anhand Typen</string>
<string name="benene_count_text">%d Benenes an die Devs verteilt</string> <string name="benene_count_text">%d Benenes an die Devs geschenkt</string>
<string name="benene_count_text_none">Noch keine Benenes verteilt</string> <string name="benene_count_text_none">Noch keine Benenes verschenkt</string>
<string name="subs_auto_select_language">Sprache automatisch wählen</string> <string name="subs_auto_select_language">Sprache automatisch wählen</string>
<string name="subs_download_languages">Sprachen herunterladen</string> <string name="subs_download_languages">Sprachen herunterladen</string>
<string name="subs_subtitle_languages">Untertitelsprache</string> <string name="subs_subtitle_languages">Untertitelsprache</string>
@ -117,8 +117,8 @@
<string name="action_open_watching">Mehr Infos</string> <string name="action_open_watching">Mehr Infos</string>
<string name="action_open_play">@string/home_play</string> <string name="action_open_play">@string/home_play</string>
<string name="vpn_might_be_needed">Damit dieser Anbieter korrekt funktioniert, ist möglicherweise ein VPN erforderlich</string> <string name="vpn_might_be_needed">Damit dieser Anbieter korrekt funktioniert, ist möglicherweise ein VPN erforderlich</string>
<string name="vpn_torrent">Dieser Anbieter bietet Torrents an, ein VPN wird dringend empfohlen</string> <string name="vpn_torrent">Dieser Anbieter bietet Torrents an, ein VPN wird deswegen dringend empfohlen</string>
<string name="provider_info_meta">Metadaten werden nicht von der Website bereitgestellt, das Laden des Videos schlägt fehl, wenn sie auf der Website nicht vorhanden sind.</string> <string name="provider_info_meta">Metadaten werden nicht von der Website bereitgestellt, das Laden des Videos schlägt fehl, wenn sie nicht auf der Website vorhanden sind.</string>
<string name="torrent_plot">Beschreibung</string> <string name="torrent_plot">Beschreibung</string>
<string name="normal_no_plot">Keine Handlung gefunden</string> <string name="normal_no_plot">Keine Handlung gefunden</string>
<string name="torrent_no_plot">Keine Beschreibung gefunden</string> <string name="torrent_no_plot">Keine Beschreibung gefunden</string>
@ -143,7 +143,7 @@
<string name="double_tap_to_pause_settings">Doppeltippen zum Pausieren</string> <string name="double_tap_to_pause_settings">Doppeltippen zum Pausieren</string>
<string name="double_tap_to_seek_amount_settings">Zeit für vor- und zurückspulen im Player (Sekunden)</string> <string name="double_tap_to_seek_amount_settings">Zeit für vor- und zurückspulen im Player (Sekunden)</string>
<string name="double_tap_to_seek_settings_des">Zweimal auf die rechte oder linke Seite tippen, um vor- oder zurückzuspulen</string> <string name="double_tap_to_seek_settings_des">Zweimal auf die rechte oder linke Seite tippen, um vor- oder zurückzuspulen</string>
<string name="double_tap_to_pause_settings_des">Doppelt in die Mitte tippen, um zu pausieren</string> <string name="double_tap_to_pause_settings_des">Zweimal in die Mitte tippen, um zu pausieren</string>
<string name="use_system_brightness_settings">Systemhelligkeit verwenden</string> <string name="use_system_brightness_settings">Systemhelligkeit verwenden</string>
<string name="use_system_brightness_settings_des">Systemhelligkeit anstelle eines dunklen Overlay im Player verwenden</string> <string name="use_system_brightness_settings_des">Systemhelligkeit anstelle eines dunklen Overlay im Player verwenden</string>
<string name="episode_sync_settings">Episodenfortschritt aktualisieren</string> <string name="episode_sync_settings">Episodenfortschritt aktualisieren</string>
@ -163,7 +163,7 @@
<string name="show_fillers_settings">Füller-Episoden für Animes anzeigen</string> <string name="show_fillers_settings">Füller-Episoden für Animes anzeigen</string>
<string name="show_trailers_settings">Trailer anzeigen</string> <string name="show_trailers_settings">Trailer anzeigen</string>
<string name="kitsu_settings">Vorschaubilder von Kitsu anzeigen</string> <string name="kitsu_settings">Vorschaubilder von Kitsu anzeigen</string>
<string name="pref_filter_search_quality">Ausgewählte Videoqualität bei Suchergebnissen ausblenden</string> <string name="pref_filter_search_quality">Ausgewählte Videoqualität in den Suchergebnissen ausblenden</string>
<string name="automatic_plugin_updates">Automatische Plugin-Updates</string> <string name="automatic_plugin_updates">Automatische Plugin-Updates</string>
<string name="updates_settings">App-Updates anzeigen</string> <string name="updates_settings">App-Updates anzeigen</string>
<string name="updates_settings_des">Automatisches Suchen nach neuen Updates nach dem Start.</string> <string name="updates_settings_des">Automatisches Suchen nach neuen Updates nach dem Start.</string>
@ -172,11 +172,11 @@
<string name="github">Github</string> <string name="github">Github</string>
<string name="lightnovel">Light Novel App von denselben Entwicklern</string> <string name="lightnovel">Light Novel App von denselben Entwicklern</string>
<string name="anim">Anime App von denselben Entwicklern</string> <string name="anim">Anime App von denselben Entwicklern</string>
<string name="discord">Discord beitreten</string> <string name="discord">Trete dem Discord Server bei</string>
<string name="benene">Eine Benene an die Devs verteilen</string> <string name="benene">Eine Benene an die Devs schenken</string>
<string name="benene_des">Verteilte Benenes</string> <string name="benene_des">Geschenkte Benenes</string>
<string name="app_language">App-Sprache</string> <string name="app_language">App-Sprache</string>
<string name="no_links_found_toast">Keine Verlinkung gefunden</string> <string name="no_links_found_toast">Keine Links gefunden</string>
<string name="copy_link_toast">Link in die Zwischenablage kopiert</string> <string name="copy_link_toast">Link in die Zwischenablage kopiert</string>
<string name="play_episode_toast">Episode abspielen</string> <string name="play_episode_toast">Episode abspielen</string>
<string name="subs_default_reset_toast">Auf Standardwert zurücksetzen</string> <string name="subs_default_reset_toast">Auf Standardwert zurücksetzen</string>
@ -240,7 +240,7 @@
<string name="remote_error">Remote-Fehler</string> <string name="remote_error">Remote-Fehler</string>
<string name="render_error">Renderfehler</string> <string name="render_error">Renderfehler</string>
<string name="unexpected_error">Unerwarteter Playerfehler</string> <string name="unexpected_error">Unerwarteter Playerfehler</string>
<string name="storage_error">Downloadfehler, Speicherberechtigungen prüfen</string> <string name="storage_error">Downloadfehler, bitte überprüfen sie die Speicherberechtigungen</string>
<string name="episode_action_chromecast_episode">Chromecast-Episode</string> <string name="episode_action_chromecast_episode">Chromecast-Episode</string>
<string name="episode_action_play_in_format">In %s wiedergeben</string> <string name="episode_action_play_in_format">In %s wiedergeben</string>
<string name="episode_action_play_in_browser">In Browser wiedergeben</string> <string name="episode_action_play_in_browser">In Browser wiedergeben</string>
@ -255,7 +255,7 @@
<string name="show_title">Titel</string> <string name="show_title">Titel</string>
<string name="poster_ui_settings">UI-Elemente auf Vorschaubild umschalten</string> <string name="poster_ui_settings">UI-Elemente auf Vorschaubild umschalten</string>
<string name="no_update_found">Kein Update gefunden</string> <string name="no_update_found">Kein Update gefunden</string>
<string name="check_for_update">Auf Update prüfen</string> <string name="check_for_update">Auf Updates prüfen</string>
<string name="video_lock">Sperren</string> <string name="video_lock">Sperren</string>
<string name="video_aspect_ratio_resize">Skalieren</string> <string name="video_aspect_ratio_resize">Skalieren</string>
<string name="video_source">Quelle</string> <string name="video_source">Quelle</string>
@ -270,16 +270,16 @@
<string name="video_buffer_length_settings">Videopufferlänge</string> <string name="video_buffer_length_settings">Videopufferlänge</string>
<string name="video_buffer_disk_settings">Video-Cache in Speicher</string> <string name="video_buffer_disk_settings">Video-Cache in Speicher</string>
<string name="video_buffer_clear_settings">Video- und Bild-Cache leeren</string> <string name="video_buffer_clear_settings">Video- und Bild-Cache leeren</string>
<string name="video_ram_description">Verursacht Abstürze, wenn zu hoch eingestellt. Nicht ändern, wenn wenig Arbeitsspeicher verfügbar ist, wie z.B. ein Android TV oder ein altes Telefon.</string> <string name="video_ram_description">Verursacht Abstürze, wenn zu hoch eingestellt. Nicht ändern, wenn wenig Arbeitsspeicher verfügbar ist, wie z.B. auf einem Android TV oder auf einem alten Smartphone.</string>
<string name="video_disk_description">Kann auf Systemen mit geringem Speicherplatz, wie z. B. Android TV-Geräten, zu Problemen führen, wenn der Wert zu hoch eingestellt ist.</string> <string name="video_disk_description">Kann auf Systemen mit geringem Speicherplatz, wie z. B. auf Android TV-Geräten, zu Problemen führen, wenn der Wert zu hoch eingestellt ist.</string>
<string name="dns_pref">DNS über HTTPS</string> <string name="dns_pref">DNS über HTTPS</string>
<string name="dns_pref_summary">Nützlich für die Umgehung von ISP-Sperren</string> <string name="dns_pref_summary">Nützlich zur Umgehung von ISP-Sperren</string>
<string name="add_site_pref">Website klonen</string> <string name="add_site_pref">Website klonen</string>
<string name="remove_site_pref">Website entfernen</string> <string name="remove_site_pref">Website entfernen</string>
<string name="add_site_summary">Einen Klon einer bestehenden Website mit einer anderen URL hinzufügen</string> <string name="add_site_summary">Einen Klon einer bestehenden Website mit einer anderen URL hinzufügen</string>
<string name="download_path_pref">Downloadpfad</string> <string name="download_path_pref">Downloadpfad</string>
<string name="nginx_url_pref">Nginx-Server-URL</string> <string name="nginx_url_pref">Nginx-Server-URL</string>
<string name="display_subbed_dubbed_settings">Dubbed/Subbed Anime anzeigen (Synchronisiert/Untertitelt)</string> <string name="display_subbed_dubbed_settings">Dubbed/Subbed Anime anzeigen</string>
<string name="resize_fit">An Bildschirm anpassen</string> <string name="resize_fit">An Bildschirm anpassen</string>
<string name="resize_fill">Strecken</string> <string name="resize_fill">Strecken</string>
<string name="resize_zoom">Vergrößern</string> <string name="resize_zoom">Vergrößern</string>
@ -308,7 +308,7 @@
<string name="example_ip">127.0.0.1</string> <string name="example_ip">127.0.0.1</string>
<string name="example_site_name">MeineCooleSeite</string> <string name="example_site_name">MeineCooleSeite</string>
<string name="example_site_url">example.com</string> <string name="example_site_url">example.com</string>
<string name="example_lang_name">Sprachcode (en)</string> <string name="example_lang_name">Sprachencode (en)</string>
<string name="login_format" formatted="true">%s %s</string> <string name="login_format" formatted="true">%s %s</string>
<string name="account">Account</string> <string name="account">Account</string>
<string name="logout">Ausloggen</string> <string name="logout">Ausloggen</string>
@ -317,13 +317,13 @@
<string name="add_account">Account hinzufügen</string> <string name="add_account">Account hinzufügen</string>
<string name="create_account">Account erstellen</string> <string name="create_account">Account erstellen</string>
<string name="add_sync">Synchronisation hinzufügen</string> <string name="add_sync">Synchronisation hinzufügen</string>
<string name="added_sync_format" formatted="true">Hinzugefügt %s</string> <string name="added_sync_format" formatted="true">%s hinzugefügt</string>
<string name="upload_sync">Sync</string> <string name="upload_sync">Sync</string>
<string name="sync_score">Bewertung</string> <string name="sync_score">Bewertung</string>
<string name="sync_score_format" formatted="true">%d / 10</string> <string name="sync_score_format" formatted="true">%d / 10</string>
<string name="sync_total_episodes_none">/\?\?</string> <string name="sync_total_episodes_none">/\?\?</string>
<string name="sync_total_episodes_some" formatted="true">/%d</string> <string name="sync_total_episodes_some" formatted="true">/%d</string>
<string name="authenticated_user" formatted="true">Authentifiziert %s</string> <string name="authenticated_user" formatted="true">%s authentifiziert</string>
<string name="authenticated_user_fail" formatted="true">Die Authentifizierung bei %s ist fehlgeschlagen</string> <string name="authenticated_user_fail" formatted="true">Die Authentifizierung bei %s ist fehlgeschlagen</string>
<string name="none">Keine</string> <string name="none">Keine</string>
<string name="normal">Normal</string> <string name="normal">Normal</string>
@ -335,10 +335,10 @@
<string name="subtitles_shadow">Schatten</string> <string name="subtitles_shadow">Schatten</string>
<string name="subtitles_raised">Erhöht</string> <string name="subtitles_raised">Erhöht</string>
<string name="subtitle_offset">Untertitel synchronisieren</string> <string name="subtitle_offset">Untertitel synchronisieren</string>
<string name="subtitle_offset_hint">1000ms</string> <string name="subtitle_offset_hint">1000 ms</string>
<string name="subtitle_offset_title">Untertitelverzögerung</string> <string name="subtitle_offset_title">Untertitelverzögerung</string>
<string name="subtitle_offset_extra_hint_later_format">Verwenden, wenn die Untertitel %dms zu früh angezeigt werden</string> <string name="subtitle_offset_extra_hint_later_format">Verwenden, wenn die Untertitel %d ms zu früh angezeigt werden</string>
<string name="subtitle_offset_extra_hint_before_format">Verwenden, wenn die Untertitel %dms zu spät angezeigt werden</string> <string name="subtitle_offset_extra_hint_before_format">Verwenden, wenn die Untertitel %d ms zu spät angezeigt werden</string>
<string name="subtitle_offset_extra_hint_none_format">Keine Untertitelverzögerung</string> <string name="subtitle_offset_extra_hint_none_format">Keine Untertitelverzögerung</string>
<string name="subtitles_example_text">Vogel Quax zwickt Johnys Pferd Bim</string> <string name="subtitles_example_text">Vogel Quax zwickt Johnys Pferd Bim</string>
<string name="recommended">Empfohlen</string> <string name="recommended">Empfohlen</string>
@ -359,7 +359,7 @@
<string name="quality_hd">HD</string> <string name="quality_hd">HD</string>
<string name="quality_ts">TS</string> <string name="quality_ts">TS</string>
<string name="quality_tc">TC</string> <string name="quality_tc">TC</string>
<string name="quality_blueray">BlueRay</string> <string name="quality_blueray">Blue-ray</string>
<string name="quality_workprint">WP</string> <string name="quality_workprint">WP</string>
<string name="quality_dvd">DVD</string> <string name="quality_dvd">DVD</string>
<string name="quality_4k">4K</string> <string name="quality_4k">4K</string>
@ -408,7 +408,7 @@
<string name="plugin">Plugins</string> <string name="plugin">Plugins</string>
<string name="delete_repository_plugins">Dadurch werden auch alle Repository-Plugins gelöscht</string> <string name="delete_repository_plugins">Dadurch werden auch alle Repository-Plugins gelöscht</string>
<string name="delete_repository">Repository löschen</string> <string name="delete_repository">Repository löschen</string>
<string name="setup_extensions_subtext">Lade eine Liste der Websiten herunter, welche du verwenden möchtest</string> <string name="setup_extensions_subtext">Lade eine Liste der Websites herunter, welche du verwenden möchtest</string>
<string name="plugins_downloaded" formatted="true">Heruntergeladen: %d</string> <string name="plugins_downloaded" formatted="true">Heruntergeladen: %d</string>
<string name="plugins_disabled" formatted="true">Deaktiviert: %d</string> <string name="plugins_disabled" formatted="true">Deaktiviert: %d</string>
<string name="plugins_not_downloaded" formatted="true">Nicht heruntergeladen: %d</string> <string name="plugins_not_downloaded" formatted="true">Nicht heruntergeladen: %d</string>
@ -416,7 +416,7 @@
\n \n
\nAufgrund eines hirnlosen DMCA-Takedowns durch Sky UK Limited 🤮 können wir die Repository-Site nicht in der App verlinken. \nAufgrund eines hirnlosen DMCA-Takedowns durch Sky UK Limited 🤮 können wir die Repository-Site nicht in der App verlinken.
\n \n
\nTrete unserem Discord bei oder suche online.</string> \nTrete unserem Discord Server bei oder suche online.</string>
<string name="view_public_repositories_button">Community-Repositories anzeigen</string> <string name="view_public_repositories_button">Community-Repositories anzeigen</string>
<string name="view_public_repositories_button_short">Öffentliche Liste</string> <string name="view_public_repositories_button_short">Öffentliche Liste</string>
<string name="uppercase_all_subtitles">Alle Untertitel in Großbuchstaben</string> <string name="uppercase_all_subtitles">Alle Untertitel in Großbuchstaben</string>
@ -427,7 +427,7 @@
<string name="video_tracks">Videospuren</string> <string name="video_tracks">Videospuren</string>
<string name="apply_on_restart">Bei Neustart anwenden</string> <string name="apply_on_restart">Bei Neustart anwenden</string>
<string name="safe_mode_title">Abgesicherter Modus aktiviert</string> <string name="safe_mode_title">Abgesicherter Modus aktiviert</string>
<string name="safe_mode_description">Alle Erweiterungen wurden aufgrund eines Absturzes deaktiviert, damit Sie diejenige finden können, die Probleme verursacht.</string> <string name="safe_mode_description">Alle Erweiterungen wurden aufgrund eines Absturzes deaktiviert, damit Sie diejenige finden können, welche Probleme verursacht.</string>
<string name="safe_mode_crash_info">Absturzinfo ansehen</string> <string name="safe_mode_crash_info">Absturzinfo ansehen</string>
<string name="extension_rating" formatted="true">Bewertung: %s</string> <string name="extension_rating" formatted="true">Bewertung: %s</string>
<string name="extension_description">Beschreibung</string> <string name="extension_description">Beschreibung</string>
@ -460,7 +460,7 @@
<string name="automatic_plugin_download_summary">Automatische Installation aller noch nicht installierten Plugins aus hinzugefügten Repositories.</string> <string name="automatic_plugin_download_summary">Automatische Installation aller noch nicht installierten Plugins aus hinzugefügten Repositories.</string>
<string name="redo_setup_process">Einrichtungsvorgang wiederholen</string> <string name="redo_setup_process">Einrichtungsvorgang wiederholen</string>
<string name="apk_installer_settings">APK-Installer</string> <string name="apk_installer_settings">APK-Installer</string>
<string name="apk_installer_settings_des">Einige Telefone unterstützen den neuen Package-Installer nicht. Benutze die Legacy-Option, wenn sich die Updates nicht installieren lassen.</string> <string name="apk_installer_settings_des">Einige Smartphones unterstützen den neuen Package-Installer nicht. Benutze die Legacy-Option, wenn sich die Updates nicht installieren lassen.</string>
<string name="season_format">%s %d%s</string> <string name="season_format">%s %d%s</string>
<string name="pref_category_links">Links</string> <string name="pref_category_links">Links</string>
<string name="pref_category_app_updates">App-Updates</string> <string name="pref_category_app_updates">App-Updates</string>
@ -482,7 +482,7 @@
<string name="no">Nein</string> <string name="no">Nein</string>
<string name="update_notification_downloading">App-Update wird heruntergeladen…</string> <string name="update_notification_downloading">App-Update wird heruntergeladen…</string>
<string name="update_notification_installing">App-Update wird installiert…</string> <string name="update_notification_installing">App-Update wird installiert…</string>
<string name="update_notification_failed">Konnte die neue Version der App nicht installieren</string> <string name="update_notification_failed">Die neue Version der App konnte nicht installieren werden</string>
<string name="apk_installer_legacy">Legacy</string> <string name="apk_installer_legacy">Legacy</string>
<string name="apk_installer_package_installer">PackageInstaller</string> <string name="apk_installer_package_installer">PackageInstaller</string>
<string name="update_started">Aktualisierung gestartet</string> <string name="update_started">Aktualisierung gestartet</string>
@ -493,18 +493,18 @@
<string name="browser">Browser</string> <string name="browser">Browser</string>
<string name="sort_by">Sortieren nach</string> <string name="sort_by">Sortieren nach</string>
<string name="sort">Sortieren</string> <string name="sort">Sortieren</string>
<string name="sort_rating_desc">Bewertung (gut bis schlecht)</string> <string name="sort_rating_desc">Bewertung (gut zu schlecht)</string>
<string name="sort_rating_asc">Bewertung (schlecht bis gut)</string> <string name="sort_rating_asc">Bewertung (schlecht zu gut)</string>
<string name="sort_updated_new">Aktualisiert (neu bis alt)</string> <string name="sort_updated_new">Aktualisiert (neu zu alt)</string>
<string name="sort_updated_old">Aktualisiert (alt bis neu)</string> <string name="sort_updated_old">Aktualisiert (alt zu neu)</string>
<string name="sort_alphabetical_a">Alphabetisch (A bis Z)</string> <string name="sort_alphabetical_a">Alphabetisch (A zu Z)</string>
<string name="sort_alphabetical_z">Alphabetisch (Z bis A)</string> <string name="sort_alphabetical_z">Alphabetisch (Z zu A)</string>
<string name="select_library">Bibliothek auswählen</string> <string name="select_library">Bibliothek auswählen</string>
<string name="open_with">Öffnen mit</string> <string name="open_with">Öffnen mit</string>
<string name="empty_library_no_accounts_message">Deine Bibliothek ist leer :( <string name="empty_library_no_accounts_message">Deine Bibliothek ist leer :(
\nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu.</string> \nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu.</string>
<string name="empty_library_logged_in_message">Diese Liste ist leer. Versuche zu einer anderen Liste zu wechseln.</string> <string name="empty_library_logged_in_message">Diese Liste ist leer. Versuch zu einer anderen Liste zu wechseln.</string>
<string name="safe_mode_file">Datei für abgesicherten Modus gefunden! <string name="safe_mode_file">Datei für den abgesicherten Modus gefunden!
\nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird.</string> \nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird.</string>
<string name="android_tv_interface_off_seek_settings">Player ausgeblendet - Betrag zum vor- und zurückspulen</string> <string name="android_tv_interface_off_seek_settings">Player ausgeblendet - Betrag zum vor- und zurückspulen</string>
<string name="android_tv_interface_on_seek_settings_summary">Der Betrag, welcher verwendet wird, wenn der Player eingeblendet ist</string> <string name="android_tv_interface_on_seek_settings_summary">Der Betrag, welcher verwendet wird, wenn der Player eingeblendet ist</string>
@ -549,7 +549,7 @@
<string name="automatic_plugin_download_mode_title">Filtermodus für Plugin-Downloads auswählen</string> <string name="automatic_plugin_download_mode_title">Filtermodus für Plugin-Downloads auswählen</string>
<string name="already_voted">Es wurde bereits abgestimmt</string> <string name="already_voted">Es wurde bereits abgestimmt</string>
<string name="no_plugins_found_error">Keine Plugins im Repository gefunden</string> <string name="no_plugins_found_error">Keine Plugins im Repository gefunden</string>
<string name="no_repository_found_error">Repository nicht gefunden, überprüfe die URL und probiere eine VPN</string> <string name="no_repository_found_error">Repository nicht gefunden, überprüf die URL und versuch ein VPN</string>
<string name="unable_to_inflate">Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein schwerwiegender Fehler und sollte sofort gemeldet werden. %s</string> <string name="unable_to_inflate">Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein SCHWERWIEGENDER FEHLER und sollte sofort gemeldet werden. %s</string>
<string name="disable">Deaktivieren</string> <string name="disable">Deaktivieren</string>
</resources> </resources>

View file

@ -540,7 +540,7 @@
\n \n
\nREMARQUE: Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé!</string> \nREMARQUE: Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé!</string>
<string name="no_plugins_found_error">Aucun plugin trouvé dans ce dossier</string> <string name="no_plugins_found_error">Aucun plugin trouvé dans ce dossier</string>
<string name="no_repository_found_error">Dossier non trouvé, vérifiez l\'url et essayé un VPN</string> <string name="no_repository_found_error">Dépôt introuvable, vérifiez l\'URL et essayez avec un VPN</string>
<string name="mobile_data">Données mobiles</string> <string name="mobile_data">Données mobiles</string>
<string name="set_default">Définir par défaut</string> <string name="set_default">Définir par défaut</string>
<string name="use">Utiliser</string> <string name="use">Utiliser</string>
@ -552,4 +552,6 @@
<string name="qualities">Qualités</string> <string name="qualities">Qualités</string>
<string name="unable_to_inflate">L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s</string> <string name="unable_to_inflate">L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s</string>
<string name="automatic_plugin_download_mode_title">Sélectionnez le mode pour filtrer le téléchargement des plugins</string> <string name="automatic_plugin_download_mode_title">Sélectionnez le mode pour filtrer le téléchargement des plugins</string>
<string name="profile_background_des">Fond de profil</string>
<string name="default_account">@string/default_subtitles</string>
</resources> </resources>

View file

@ -566,4 +566,15 @@
<string name="profile_background_des">Pozadina profila</string> <string name="profile_background_des">Pozadina profila</string>
<string name="unable_to_inflate">Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s</string> <string name="unable_to_inflate">Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s</string>
<string name="automatic_plugin_download_mode_title">Odaberi modus za filtriranje preuzimanja dodataka</string> <string name="automatic_plugin_download_mode_title">Odaberi modus za filtriranje preuzimanja dodataka</string>
<string name="disable">Onemogući</string>
<string name="default_account">@string/default_subtitles</string>
<string name="no_plugins_found_error">U repozitoriju nisu pronađeni dodaci</string>
<string name="no_repository_found_error">Repozitorij nije pronađen, provjerite URL i pokušajte koristiti VPN</string>
<string name="quality_profile_help">Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je video prioritet.
\n
\nIzvor A: 3
\nKvaliteta B: 7
\nImat će kombinirani prioritet videozapisa od 10.
\n
\nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita!</string>
</resources> </resources>

View file

@ -219,7 +219,7 @@
<string name="status_ongoing">Folyamatban levő</string> <string name="status_ongoing">Folyamatban levő</string>
<string name="year">Év</string> <string name="year">Év</string>
<string name="site">Webhely</string> <string name="site">Webhely</string>
<string name="synopsis">Szinopszis</string> <string name="synopsis">Összegzés</string>
<string name="no_subtitles">Nincsenek feliratok</string> <string name="no_subtitles">Nincsenek feliratok</string>
<string name="remote_error">Távoli hiba</string> <string name="remote_error">Távoli hiba</string>
<string name="render_error">Render hiba</string> <string name="render_error">Render hiba</string>
@ -237,7 +237,7 @@
<string name="swipe_to_change_settings_des">Csúsztassa felfelé vagy lefelé a bal vagy jobb oldalon a fényerő vagy a hangerő megváltoztatásához</string> <string name="swipe_to_change_settings_des">Csúsztassa felfelé vagy lefelé a bal vagy jobb oldalon a fényerő vagy a hangerő megváltoztatásához</string>
<string name="backup_settings">Biztonsági mentés</string> <string name="backup_settings">Biztonsági mentés</string>
<string name="benene_count_text_none">0 Banán a fejlesztőknek</string> <string name="benene_count_text_none">0 Banán a fejlesztőknek</string>
<string name="swipe_to_seek_settings">Húzás a kereséshez</string> <string name="swipe_to_seek_settings">Húzd el, hogy beless</string>
<string name="autoplay_next_settings">Következő epizód automatikus lejátszása</string> <string name="autoplay_next_settings">Következő epizód automatikus lejátszása</string>
<string name="autoplay_next_settings_des">Következő epizód lejátszása amikor az aktuális epizód véget ér</string> <string name="autoplay_next_settings_des">Következő epizód lejátszása amikor az aktuális epizód véget ér</string>
<string name="double_tap_to_seek_settings">Dupla koppintás a kereséshez</string> <string name="double_tap_to_seek_settings">Dupla koppintás a kereséshez</string>
@ -510,4 +510,5 @@
<string name="tv_layout">TV elrendezés</string> <string name="tv_layout">TV elrendezés</string>
<string name="automatic">Automatikus</string> <string name="automatic">Automatikus</string>
<string name="android_tv_interface_on_seek_settings_summary">Az átugrás mértéke, amikor a lejátszó látható</string> <string name="android_tv_interface_on_seek_settings_summary">Az átugrás mértéke, amikor a lejátszó látható</string>
<string name="automatic_plugin_download_mode_title">Válassza ki a módot a pluginek letöltésének szűréséhez</string>
</resources> </resources>

View file

@ -153,4 +153,9 @@
<string name="updates_settings">ଆପ୍ ଅଦ୍ୟତନ ଦେଖାଇବା</string> <string name="updates_settings">ଆପ୍ ଅଦ୍ୟତନ ଦେଖାଇବା</string>
<string name="update_started">ଅଦ୍ୟତନ ଆରମ୍ଭ ହୋଇଛି</string> <string name="update_started">ଅଦ୍ୟତନ ଆରମ୍ଭ ହୋଇଛି</string>
<string name="search_hint">ସନ୍ଧାନ କରିବା…</string> <string name="search_hint">ସନ୍ଧାନ କରିବା…</string>
<string name="skip_type_recap">ସଂକ୍ଷିପ୍ତବୃତ୍ତି</string>
<string name="play_movie_button">ଚଳଚ୍ଚିତ୍ର ଚଲାଅ</string>
<string name="search_hint_site" formatted="true">%s ସନ୍ଧାନ କରିବା…</string>
<string name="next_episode">ପରବର୍ତ୍ତୀ ଅଧ୍ୟାୟ</string>
<string name="no_data">କୌଣସି ତଥ୍ୟ ନାହିଁ</string>
</resources> </resources>

View file

@ -570,4 +570,5 @@
<string name="unable_to_inflate">UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s</string> <string name="unable_to_inflate">UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s</string>
<string name="automatic_plugin_download_mode_title">Selectați modul de filtrare a descărcării plugin-urilor</string> <string name="automatic_plugin_download_mode_title">Selectați modul de filtrare a descărcării plugin-urilor</string>
<string name="default_account">@string/default_subtitles</string> <string name="default_account">@string/default_subtitles</string>
<string name="already_voted">Ați votat deja</string>
</resources> </resources>

View file

@ -6,12 +6,12 @@
<string name="title_home">முகப்பு</string> <string name="title_home">முகப்பு</string>
<string name="title_search">தேடு</string> <string name="title_search">தேடு</string>
<string name="title_downloads">பதிவிறக்கம்</string> <string name="title_downloads">பதிவிறக்கம்</string>
<string name="no_data">கவல் எதுவும் இல்லை</string> <string name="no_data">ரவு இல்லை</string>
<string name="episode_more_options_des">மேலும் விருப்பங்கள்</string> <string name="episode_more_options_des">மேலும் விருப்பங்கள்</string>
<string name="next_episode">அடுத்த எபிசோட</string> <string name="next_episode">அடுத்த அத்தியாயம</string>
<string name="result_tags">வகைகள்</string> <string name="result_tags">வகைகள்</string>
<string name="result_share">பகிர்</string> <string name="result_share">பகிர்</string>
<string name="result_open_in_browser">Browser இல் திற</string> <string name="result_open_in_browser">உலாவியில் திற</string>
<string name="skip_loading">ஏற்றுவதைத் தவிர்</string> <string name="skip_loading">ஏற்றுவதைத் தவிர்</string>
<string name="type_watching">பார்த்து கொண்டிருப்பது</string> <string name="type_watching">பார்த்து கொண்டிருப்பது</string>
<string name="type_on_hold">நிறுத்தி வைக்கப்பட்டுள்ளது</string> <string name="type_on_hold">நிறுத்தி வைக்கப்பட்டுள்ளது</string>
@ -21,9 +21,9 @@
<string name="play_torrent_button">ஸ்ட்ரீம் டோரண்ட்</string> <string name="play_torrent_button">ஸ்ட்ரீம் டோரண்ட்</string>
<string name="pick_subtitle">வசன வரிகள்</string> <string name="pick_subtitle">வசன வரிகள்</string>
<string name="go_back">பின் செல்</string> <string name="go_back">பின் செல்</string>
<string name="play_episode">எபிசோடை இயக்கு</string> <string name="play_episode">அத்தியாயத்தை இயக்கு</string>
<string name="download">எபிசோட் பதிவிற்கான அனுமதி கொடுக்கவும்</string> <string name="download">எபிசோட் பதிவிற்கான அனுமதி கொடுக்கவும்</string>
<string name="downloaded">பதிவிறக்கம் செய்யப்பட்டது</string> <string name="downloaded">பதிவிறக்கப்பட்டது</string>
<string name="downloading">பதிவிறக்குகிறது</string> <string name="downloading">பதிவிறக்குகிறது</string>
<string name="download_paused">பதிவிறக்கம் இடைநிறுத்தப்பட்டது</string> <string name="download_paused">பதிவிறக்கம் இடைநிறுத்தப்பட்டது</string>
<string name="download_started">பதிவிறக்கம் தொடங்கியது</string> <string name="download_started">பதிவிறக்கம் தொடங்கியது</string>
@ -67,10 +67,10 @@
<string name="loading">ஏற்றுகிறது…</string> <string name="loading">ஏற்றுகிறது…</string>
<string name="type_dropped">கைவிடப்பட்டது</string> <string name="type_dropped">கைவிடப்பட்டது</string>
<string name="download_done">பதிவிறக்கம் முடிந்தது</string> <string name="download_done">பதிவிறக்கம் முடிந்தது</string>
<string name="reload_error">இணைப்பை மீண்டும் முயற்சிக்கவும்…</string> <string name="reload_error">இணைப்பை மீண்டும் முயவும்…</string>
<string name="play_movie_button">திரைப்படத்தை இயக்கு</string> <string name="play_movie_button">திரைப்படத்தை இயக்கு</string>
<string name="play_livestream_button">லைவ்ஸ்ட்ரீம் இயக்கு</string> <string name="play_livestream_button">லைவ்ஸ்ட்ரீம் இயக்கு</string>
<string name="play_trailer_button">டிரெய்லரை இயக்கவும்</string> <string name="play_trailer_button">டிரெய்லரை இயக்க</string>
<string name="pick_source">மூலம்</string> <string name="pick_source">மூலம்</string>
<string name="error_loading_links_toast">இணைப்புகளை ஏற்றுவதில் பிழை</string> <string name="error_loading_links_toast">இணைப்புகளை ஏற்றுவதில் பிழை</string>
<string name="home_play">இயக்கு</string> <string name="home_play">இயக்கு</string>
@ -107,4 +107,14 @@
<string name="double_tap_to_pause_settings">இடைநிறுத்துவதற்கு இருமுறை தட்டவும்</string> <string name="double_tap_to_pause_settings">இடைநிறுத்துவதற்கு இருமுறை தட்டவும்</string>
<string name="chromecast_subtitles_settings_des">Chromecast வசன அமைப்புகள்</string> <string name="chromecast_subtitles_settings_des">Chromecast வசன அமைப்புகள்</string>
<string name="use_system_brightness_settings_des">இருண்ட மேலடுக்குக்குப் பதிலாக ஆப் பிளேயரில் சிஸ்டம் பிரகாசத்தைப் பயன்படுத்தவும்</string> <string name="use_system_brightness_settings_des">இருண்ட மேலடுக்குக்குப் பதிலாக ஆப் பிளேயரில் சிஸ்டம் பிரகாசத்தைப் பயன்படுத்தவும்</string>
<string name="next_episode_format" formatted="true">அத்தியாயம் %d-இன் வெளியீட்டு நேரம்</string>
<string name="next_episode_time_hour_format" formatted="true">%dம %dநி</string>
<string name="next_episode_time_min_format" formatted="true">%dநி</string>
<string name="home_next_random_img_des">அடுத்து ஏதாவது</string>
<string name="browser">உலாவி</string>
<string name="duration_format" formatted="true">%d நிமி</string>
<string name="play_with_app_name">CloudStream-உடன் இயக்கு</string>
<string name="new_update_format" formatted="true">புதிய புதுப்பிப்பு உள்ளது
\n%s-&gt;%s</string>
<string name="filler" formatted="true">நிரப்பி</string>
</resources> </resources>

View file

@ -18,7 +18,7 @@
<string name="preview_background_img_des">Попередній перегляд фону</string> <string name="preview_background_img_des">Попередній перегляд фону</string>
<string name="player_speed_text_format" formatted="true">Швидкість (%.2fx)</string> <string name="player_speed_text_format" formatted="true">Швидкість (%.2fx)</string>
<string name="new_update_format" formatted="true">Знайдено нове оновлення! <string name="new_update_format" formatted="true">Знайдено нове оновлення!
\n%s -&gt; %s</string> \n%s &gt; %s</string>
<string name="title_search">Пошук</string> <string name="title_search">Пошук</string>
<string name="title_downloads">Завантаження</string> <string name="title_downloads">Завантаження</string>
<string name="duration_format" formatted="true">%d хв</string> <string name="duration_format" formatted="true">%d хв</string>
@ -37,7 +37,7 @@
<string name="type_dropped">Покинуто</string> <string name="type_dropped">Покинуто</string>
<string name="play_movie_button">Переглянути фільм</string> <string name="play_movie_button">Переглянути фільм</string>
<string name="play_trailer_button">Переглянути трейлер</string> <string name="play_trailer_button">Переглянути трейлер</string>
<string name="play_torrent_button">Трансляція через торрент</string> <string name="play_torrent_button">Трансляція через торент</string>
<string name="reload_error">Повторити підключення…</string> <string name="reload_error">Повторити підключення…</string>
<string name="go_back">Назад</string> <string name="go_back">Назад</string>
<string name="play_episode">Переглянути епізод</string> <string name="play_episode">Переглянути епізод</string>
@ -75,7 +75,7 @@
<string name="continue_watching">Продовжити перегляд</string> <string name="continue_watching">Продовжити перегляд</string>
<string name="action_remove_watching">Вилучити</string> <string name="action_remove_watching">Вилучити</string>
<string name="action_open_watching">Детальніше</string> <string name="action_open_watching">Детальніше</string>
<string name="vpn_torrent">Цей постачальник є торрентом, рекомендується VPN</string> <string name="vpn_torrent">Цей постачальник є торентом, рекомендується використовувати VPN</string>
<string name="torrent_plot">Опис</string> <string name="torrent_plot">Опис</string>
<string name="normal_no_plot">Сюжет не знайдено</string> <string name="normal_no_plot">Сюжет не знайдено</string>
<string name="torrent_no_plot">Опис не знайдено</string> <string name="torrent_no_plot">Опис не знайдено</string>
@ -86,9 +86,9 @@
<string name="chromecast_subtitles_settings">Субтитри Chromecast</string> <string name="chromecast_subtitles_settings">Субтитри Chromecast</string>
<string name="chromecast_subtitles_settings_des">Налаштування субтитрів Chromecast</string> <string name="chromecast_subtitles_settings_des">Налаштування субтитрів Chromecast</string>
<string name="eigengraumode_settings">Режим Eigengravy</string> <string name="eigengraumode_settings">Режим Eigengravy</string>
<string name="swipe_to_change_settings">Проведіть пальцем, щоб змінити налаштування</string> <string name="swipe_to_change_settings">Проведіть, щоб змінити налаштування</string>
<string name="swipe_to_change_settings_des">Проведіть вгору або вниз з лівого або правого боку, щоб змінити яскравість чи гучність</string> <string name="swipe_to_change_settings_des">Проведіть вгору або вниз з лівого або правого боку, щоб змінити яскравість чи гучність</string>
<string name="autoplay_next_settings_des">Відтворювати наступний епізод після закінчення поточного</string> <string name="autoplay_next_settings_des">Відтворює наступний епізод після закінчення поточного</string>
<string name="title_home">Головна</string> <string name="title_home">Головна</string>
<string name="app_name">CloudStream</string> <string name="app_name">CloudStream</string>
<string name="filler" formatted="true">Філер</string> <string name="filler" formatted="true">Філер</string>
@ -130,7 +130,7 @@
<string name="picture_in_picture">Картинка в картинці</string> <string name="picture_in_picture">Картинка в картинці</string>
<string name="player_subtitles_settings_des">Налаштування субтитрів плеєра</string> <string name="player_subtitles_settings_des">Налаштування субтитрів плеєра</string>
<string name="eigengraumode_settings_des">Додає опцію керування швидкістю в плеєрі</string> <string name="eigengraumode_settings_des">Додає опцію керування швидкістю в плеєрі</string>
<string name="swipe_to_seek_settings">Проведіть пальцем, щоб перемотати</string> <string name="swipe_to_seek_settings">Проведіть, щоб перемотати</string>
<string name="double_tap_to_seek_settings">Двічі торкніться, щоб перемотати</string> <string name="double_tap_to_seek_settings">Двічі торкніться, щоб перемотати</string>
<string name="double_tap_to_pause_settings">Двічі торкніться для паузи</string> <string name="double_tap_to_pause_settings">Двічі торкніться для паузи</string>
<string name="double_tap_to_seek_amount_settings">Крок перемотки (секунди)</string> <string name="double_tap_to_seek_amount_settings">Крок перемотки (секунди)</string>
@ -224,7 +224,7 @@
<string name="double_tap_to_seek_settings_des">Двічі торкніться праворуч або ліворуч, щоб перемотати відео вперед або назад</string> <string name="double_tap_to_seek_settings_des">Двічі торкніться праворуч або ліворуч, щоб перемотати відео вперед або назад</string>
<string name="use_system_brightness_settings_des">Використовуйте системну яскравість у плеєрі замість темної накладки</string> <string name="use_system_brightness_settings_des">Використовуйте системну яскравість у плеєрі замість темної накладки</string>
<string name="restore_success">Завантажено файл резервної копії</string> <string name="restore_success">Завантажено файл резервної копії</string>
<string name="torrent">Торренти</string> <string name="torrent">Торенти</string>
<string name="episode_sync_settings_des">Автоматична синхронізація прогресу поточного епізоду</string> <string name="episode_sync_settings_des">Автоматична синхронізація прогресу поточного епізоду</string>
<string name="backup_failed">Відсутні дозволи на зберігання. Будь ласка, спробуйте ще раз.</string> <string name="backup_failed">Відсутні дозволи на зберігання. Будь ласка, спробуйте ще раз.</string>
<string name="kitsu_settings">Показувати постери від Kitsu</string> <string name="kitsu_settings">Показувати постери від Kitsu</string>
@ -256,7 +256,7 @@
<string name="nsfw">NSFW</string> <string name="nsfw">NSFW</string>
<string name="movies_singular">Фільм</string> <string name="movies_singular">Фільм</string>
<string name="ova_singular">OVA</string> <string name="ova_singular">OVA</string>
<string name="torrent_singular">Торрент</string> <string name="torrent_singular">Торент</string>
<string name="show_hd">Мітка якості</string> <string name="show_hd">Мітка якості</string>
<string name="nsfw_singular">NSFW</string> <string name="nsfw_singular">NSFW</string>
<string name="episode_action_play_in_browser">Переглянути в браузері</string> <string name="episode_action_play_in_browser">Переглянути в браузері</string>
@ -294,9 +294,9 @@
<string name="primary_color_settings">Основний колір</string> <string name="primary_color_settings">Основний колір</string>
<string name="app_theme_settings">Тема застосунку</string> <string name="app_theme_settings">Тема застосунку</string>
<string name="bottom_title_settings">Розташування назви постера</string> <string name="bottom_title_settings">Розташування назви постера</string>
<string name="bottom_title_settings_des">Розмістіть назву під постером</string> <string name="bottom_title_settings_des">Розмістити назву під постером</string>
<string name="example_password">Пароль123</string> <string name="example_password">Пароль123</string>
<string name="example_username">Моє круте ім\'я</string> <string name="example_username">Моє круте імя</string>
<string name="example_email">hello@world.com</string> <string name="example_email">hello@world.com</string>
<string name="example_site_name">Мій крутий сайт</string> <string name="example_site_name">Мій крутий сайт</string>
<string name="example_lang_name">Код мови (uk)</string> <string name="example_lang_name">Код мови (uk)</string>
@ -348,9 +348,9 @@
<string name="limit_title_rez">Роздільна здатність відеоплеєра</string> <string name="limit_title_rez">Роздільна здатність відеоплеєра</string>
<string name="video_buffer_length_settings">Довжина буфера відео</string> <string name="video_buffer_length_settings">Довжина буфера відео</string>
<string name="video_buffer_clear_settings">Очистити кеш відео та зображень</string> <string name="video_buffer_clear_settings">Очистити кеш відео та зображень</string>
<string name="video_ram_description">Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об\'ємом пам\'яті, наприклад Android TV.</string> <string name="video_ram_description">Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об’ємом пам’яті, наприклад Android TV.</string>
<string name="dns_pref_summary">Корисно для обходу блокувань провайдера</string> <string name="dns_pref_summary">Корисно для обходу блокувань провайдера</string>
<string name="video_disk_description">Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об\'ємом вільної пам\'яті, наприклад Android TV.</string> <string name="video_disk_description">Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об’ємом вільної пам’яті, наприклад Android TV.</string>
<string name="dns_pref">DNS через HTTPS</string> <string name="dns_pref">DNS через HTTPS</string>
<string name="download_path_pref">Шлях завантаження</string> <string name="download_path_pref">Шлях завантаження</string>
<string name="add_site_summary">Додайте клон існуючого сайту, з іншою URL-адресою</string> <string name="add_site_summary">Додайте клон існуючого сайту, з іншою URL-адресою</string>
@ -374,10 +374,10 @@
<string name="sync_score">Оцінений</string> <string name="sync_score">Оцінений</string>
<string name="player_load_subtitles">Завантажити з файлу</string> <string name="player_load_subtitles">Завантажити з файлу</string>
<string name="max">Макс.</string> <string name="max">Макс.</string>
<string name="subtitles_example_text">Щастям б\'єш жук їх глицю в фон й ґедзь пріч</string> <string name="subtitles_example_text">Щастям бєш жук їх глицю в фон й ґедзь пріч</string>
<string name="subtitle_offset_hint">1000 мс</string> <string name="subtitle_offset_hint">1000 мс</string>
<string name="subtitle_offset_extra_hint_later_format">Використовуйте цей параметр, якщо субтитри з\'являються на %d мс занадто рано</string> <string name="subtitle_offset_extra_hint_later_format">Використовуйте цей параметр, якщо субтитри зявляються на %d мс занадто рано</string>
<string name="subtitle_offset_extra_hint_before_format">Використовуйте це, якщо субтитри з\'являються із запізненням на %d мс</string> <string name="subtitle_offset_extra_hint_before_format">Використовуйте це, якщо субтитри зявляються із запізненням на %d мс</string>
<string name="player_loaded_subtitles" formatted="true">Завантажено %s</string> <string name="player_loaded_subtitles" formatted="true">Завантажено %s</string>
<string name="actor_supporting">Підтримка</string> <string name="actor_supporting">Підтримка</string>
<string name="actor_background">Фон</string> <string name="actor_background">Фон</string>
@ -507,8 +507,8 @@
<string name="safe_mode_file">Файл безпечного режиму знайдено! <string name="safe_mode_file">Файл безпечного режиму знайдено!
\nРозширеня не завантажуються під час запуску, доки файл не буде видалено.</string> \nРозширеня не завантажуються під час запуску, доки файл не буде видалено.</string>
<string name="pref_category_android_tv">Android TV</string> <string name="pref_category_android_tv">Android TV</string>
<string name="android_tv_interface_off_seek_settings">Плеєр сховано - обсяг перемотки</string> <string name="android_tv_interface_off_seek_settings">Плеєр сховано обсяг перемотки</string>
<string name="android_tv_interface_on_seek_settings">Плеєр показано - обсяг перемотки</string> <string name="android_tv_interface_on_seek_settings">Плеєр показано обсяг перемотки</string>
<string name="android_tv_interface_on_seek_settings_summary">Обсяг перемотки, який використовується, коли плеєр видимий</string> <string name="android_tv_interface_on_seek_settings_summary">Обсяг перемотки, який використовується, коли плеєр видимий</string>
<string name="android_tv_interface_off_seek_settings_summary">Обсяг перемотки, який використовується, коли плеєр прихований</string> <string name="android_tv_interface_off_seek_settings_summary">Обсяг перемотки, який використовується, коли плеєр прихований</string>
<string name="test_failed">Тест провалено</string> <string name="test_failed">Тест провалено</string>
@ -532,7 +532,7 @@
<string name="set_default">Встановити за замовчуванням</string> <string name="set_default">Встановити за замовчуванням</string>
<string name="profiles">Профілі</string> <string name="profiles">Профілі</string>
<string name="help">Допомога</string> <string name="help">Допомога</string>
<string name="quality_profile_help">Тут ви можете змінити порядок джерел. Якщо відео має вищий пріоритет, воно з\'явиться вище у списку джерел. Сума пріоритету джерела та пріоритету якості є пріоритетом відео. <string name="quality_profile_help">Тут ви можете змінити порядок джерел. Якщо відео має вищий пріоритет, воно зявиться вище у списку джерел. Сума пріоритету джерела та пріоритету якості є пріоритетом відео.
\n \n
\nДжерело A: 3 \nДжерело A: 3
\nЯкість B: 7 \nЯкість B: 7

View file

@ -0,0 +1 @@
- ପରିବର୍ତ୍ତନ ପୋଥି ଯୋଡ଼ାଗଲା!

View file

@ -1 +1 @@
- Додано журнал змін! Додано журнал змін!