Merge remote-tracking branch 'origin/master'

This commit is contained in:
CranberrySoup 2023-09-15 13:13:48 +02:00
commit 45ecc903a2
31 changed files with 1496 additions and 578 deletions

View file

@ -58,8 +58,8 @@ android {
minSdk = 21 minSdk = 21
targetSdk = 29 targetSdk = 29
versionCode = 59 versionCode = 60
versionName = "4.1.8" versionName = "4.1.9"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
@ -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
@ -177,10 +179,17 @@ object APIHolder {
private var trackerCache: HashMap<String, AniSearch> = hashMapOf() private var trackerCache: HashMap<String, AniSearch> = hashMapOf()
/** backwards compatibility, use getTracker4 instead */
suspend fun getTracker(
titles: List<String>,
types: Set<TrackerType>?,
year: Int?,
): Tracker? = getTracker(titles, types, year, false)
/** /**
* 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 +198,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
): 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 +207,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 +1780,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

@ -19,7 +19,7 @@ open class Gofile : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit, subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit callback: (ExtractorLink) -> Unit
) { ) {
val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z]+)").find(url)?.groupValues?.get(1) val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1)
val token = app.get("$mainApi/createAccount").parsedSafe<Account>()?.data?.get("token") val token = app.get("$mainApi/createAccount").parsedSafe<Account>()?.data?.get("token")
val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let { val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let {
Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1) Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1)
@ -59,4 +59,4 @@ open class Gofile : ExtractorApi() {
@JsonProperty("data") val data: Data? = null, @JsonProperty("data") val data: Data? = null,
) )
} }

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

@ -5,7 +5,9 @@ import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
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
@ -13,6 +15,7 @@ import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mapper
import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.debugPrint
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -33,6 +36,9 @@ import java.text.SimpleDateFormat
import java.time.Instant import java.time.Instant
import java.util.Date import java.util.Date
import java.util.TimeZone import java.util.TimeZone
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
class SimklApi(index: Int) : AccountManager(index), SyncAPI { class SimklApi(index: Int) : AccountManager(index), SyncAPI {
override var name = "Simkl" override var name = "Simkl"
@ -59,6 +65,80 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
*/ */
private var lastScoreTime = -1L private var lastScoreTime = -1L
private object SimklCache {
private const val SIMKL_CACHE_KEY = "SIMKL_API_CACHE"
enum class CacheTimes(val value: String) {
OneMonth("30d"),
ThirtyMinutes("30m")
}
private class SimklCacheWrapper<T>(
@JsonProperty("obj") val obj: T?,
@JsonProperty("validUntil") val validUntil: Long,
@JsonProperty("cacheTime") val cacheTime: Long = unixTime,
) {
/** Returns true if cache is newer than cacheDays */
fun isFresh(): Boolean {
return validUntil > unixTime
}
fun remainingTime(): Duration {
val unixTime = unixTime
return if (validUntil > unixTime) {
(validUntil - unixTime).toDuration(DurationUnit.SECONDS)
} else {
Duration.ZERO
}
}
}
fun cleanOldCache() {
getKeys(SIMKL_CACHE_KEY)?.forEach {
val isOld = AcraApplication.getKey<SimklCacheWrapper<Any>>(it)?.isFresh() == false
if (isOld) {
removeKey(it)
}
}
}
fun <T> setKey(path: String, value: T, cacheTime: Duration) {
debugPrint { "Set cache: $SIMKL_CACHE_KEY/$path for ${cacheTime.inWholeDays} days or ${cacheTime.inWholeSeconds} seconds." }
setKey(
SIMKL_CACHE_KEY,
path,
// Storing as plain sting is required to make generics work.
SimklCacheWrapper(value, unixTime + cacheTime.inWholeSeconds).toJson()
)
}
/**
* Gets cached object, if object is not fresh returns null and removes it from cache
*/
inline fun <reified T : Any> getKey(path: String): T? {
// Required for generic otherwise "LinkedHashMap cannot be cast to MediaObject"
val type = mapper.typeFactory.constructParametricType(
SimklCacheWrapper::class.java,
T::class.java
)
val cache = getKey<String>(SIMKL_CACHE_KEY, path)?.let {
mapper.readValue<SimklCacheWrapper<T>>(it, type)
}
return if (cache?.isFresh() == true) {
debugPrint {
"Cache hit at: $SIMKL_CACHE_KEY/$path. " +
"Remains fresh for ${cache.remainingTime().inWholeDays} days or ${cache.remainingTime().inWholeSeconds} seconds."
}
cache.obj
} else {
debugPrint { "Cache miss at: $SIMKL_CACHE_KEY/$path" }
removeKey(SIMKL_CACHE_KEY, path)
null
}
}
}
companion object { companion object {
private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID
private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET
@ -210,18 +290,18 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("img") val img: String? @JsonProperty("img") val img: String?
) { ) {
companion object { companion object {
fun convertToEpisodes(list: List<EpisodeMetadata>?): List<MediaObject.Season.Episode> { fun convertToEpisodes(list: List<EpisodeMetadata>?): List<MediaObject.Season.Episode>? {
return list?.map { return list?.map {
MediaObject.Season.Episode(it.episode) MediaObject.Season.Episode(it.episode)
} ?: emptyList() }
} }
fun convertToSeasons(list: List<EpisodeMetadata>?): List<MediaObject.Season> { fun convertToSeasons(list: List<EpisodeMetadata>?): List<MediaObject.Season>? {
return list?.filter { it.season != null }?.groupBy { return list?.filter { it.season != null }?.groupBy {
it.season it.season
}?.map { (season, episodes) -> }?.mapNotNull { (season, episodes) ->
MediaObject.Season(season!!, convertToEpisodes(episodes)) convertToEpisodes(episodes)?.let { MediaObject.Season(season!!, it) }
} ?: emptyList() }?.ifEmpty { null }
} }
} }
} }
@ -235,11 +315,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("title") val title: String?, @JsonProperty("title") val title: String?,
@JsonProperty("year") val year: Int?, @JsonProperty("year") val year: Int?,
@JsonProperty("ids") val ids: Ids?, @JsonProperty("ids") val ids: Ids?,
@JsonProperty("total_episodes") val total_episodes: Int? = null,
@JsonProperty("status") val status: String? = null,
@JsonProperty("poster") val poster: String? = null, @JsonProperty("poster") val poster: String? = null,
@JsonProperty("type") val type: String? = null, @JsonProperty("type") val type: String? = null,
@JsonProperty("seasons") val seasons: List<Season>? = null, @JsonProperty("seasons") val seasons: List<Season>? = null,
@JsonProperty("episodes") val episodes: List<Season.Episode>? = null @JsonProperty("episodes") val episodes: List<Season.Episode>? = null
) { ) {
fun hasEnded(): Boolean {
return status == "released" || status == "ended"
}
@JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonInclude(JsonInclude.Include.NON_EMPTY)
data class Season( data class Season(
@JsonProperty("number") val number: Int, @JsonProperty("number") val number: Int,
@ -281,6 +367,194 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
class SimklScoreBuilder private constructor() {
data class Builder(
private var url: String? = null,
private var interceptor: Interceptor? = null,
private var ids: MediaObject.Ids? = null,
private var score: Int? = null,
private var status: Int? = null,
private var addEpisodes: Pair<List<MediaObject.Season>?, List<MediaObject.Season.Episode>?>? = null,
private var removeEpisodes: Pair<List<MediaObject.Season>?, List<MediaObject.Season.Episode>?>? = null,
) {
fun interceptor(interceptor: Interceptor) = apply { this.interceptor = interceptor }
fun apiUrl(url: String) = apply { this.url = url }
fun ids(ids: MediaObject.Ids) = apply { this.ids = ids }
fun score(score: Int?, oldScore: Int?) = apply {
if (score != oldScore) {
this.score = score
}
}
fun status(newStatus: Int?, oldStatus: Int?) = apply {
// Only set status if its new
if (newStatus != oldStatus) {
this.status = newStatus
} else {
this.status = null
}
}
fun episodes(
allEpisodes: List<EpisodeMetadata>?,
newEpisodes: Int?,
oldEpisodes: Int?,
) = apply {
if (allEpisodes == null || newEpisodes == null) return@apply
fun getEpisodes(rawEpisodes: List<EpisodeMetadata>) =
if (rawEpisodes.any { it.season != null }) {
EpisodeMetadata.convertToSeasons(rawEpisodes) to null
} else {
null to EpisodeMetadata.convertToEpisodes(rawEpisodes)
}
// Do not add episodes if there is no change
if (newEpisodes > (oldEpisodes ?: 0)) {
this.addEpisodes = getEpisodes(allEpisodes.take(newEpisodes))
}
if ((oldEpisodes ?: 0) > newEpisodes) {
this.removeEpisodes = getEpisodes(allEpisodes.drop(newEpisodes))
}
}
suspend fun execute(): Boolean {
val time = getDateTime(unixTime)
return if (this.status == SimklListStatusType.None.value) {
app.post(
"$url/sync/history/remove",
json = StatusRequest(
shows = listOf(HistoryMediaObject(ids = ids)),
movies = emptyList()
),
interceptor = interceptor
).isSuccessful
} else {
val episodeRemovalResponse = removeEpisodes?.let { (seasons, episodes) ->
app.post(
"${this.url}/sync/history/remove",
json = StatusRequest(
shows = listOf(
HistoryMediaObject(
ids = ids,
seasons = seasons,
episodes = episodes
)
),
movies = emptyList()
),
interceptor = interceptor
).isSuccessful
} ?: true
val historyResponse =
// Only post if there are episodes or score to upload
if (addEpisodes != null || score != null) {
app.post(
"${this.url}/sync/history",
json = StatusRequest(
shows = listOf(
HistoryMediaObject(
null,
null,
ids,
addEpisodes?.first,
addEpisodes?.second,
score,
score?.let { time },
)
), movies = emptyList()
),
interceptor = interceptor
).isSuccessful
} else {
true
}
val statusResponse = status?.let { setStatus ->
val newStatus =
SimklListStatusType.values()
.firstOrNull { it.value == setStatus }?.originalName
?: SimklListStatusType.Watching.originalName!!
app.post(
"${this.url}/sync/add-to-list",
json = StatusRequest(
shows = listOf(
StatusMediaObject(
null,
null,
ids,
newStatus,
)
), movies = emptyList()
),
interceptor = interceptor
).isSuccessful
} ?: true
statusResponse && episodeRemovalResponse && historyResponse
}
}
}
}
suspend fun getEpisodes(
simklId: Int?,
type: String?,
episodes: Int?,
hasEnded: Boolean?
): Array<EpisodeMetadata>? {
if (simklId == null) return null
val cacheKey = "Episodes/$simklId"
val cache = SimklCache.getKey<Array<EpisodeMetadata>>(cacheKey)
// Return cached result if its higher or equal the amount of episodes.
if (cache != null && cache.size >= (episodes ?: 0)) {
return cache
}
// There is always one season in Anime -> no request necessary
if (type == "anime" && episodes != null) {
return episodes.takeIf { it > 0 }?.let {
(1..it).map { episode ->
EpisodeMetadata(
null, null, null, episode, null
)
}.toTypedArray()
}
}
val url = when (type) {
"anime" -> "https://api.simkl.com/anime/episodes/$simklId"
"tv" -> "https://api.simkl.com/tv/episodes/$simklId"
"movie" -> return null
else -> return null
}
debugPrint { "Requesting episodes from $url" }
return app.get(url, params = mapOf("client_id" to clientId))
.parsedSafe<Array<EpisodeMetadata>>()?.also {
val cacheTime =
if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value
// 1 Month cache
SimklCache.setKey(cacheKey, it, Duration.parse(cacheTime))
}
}
@JsonInclude(JsonInclude.Include.NON_EMPTY)
class HistoryMediaObject(
@JsonProperty("title") title: String? = null,
@JsonProperty("year") year: Int? = null,
@JsonProperty("ids") ids: Ids? = null,
@JsonProperty("seasons") seasons: List<Season>? = null,
@JsonProperty("episodes") episodes: List<Season.Episode>? = null,
@JsonProperty("rating") val rating: Int? = null,
@JsonProperty("rated_at") val rated_at: String? = null,
) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes)
@JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonInclude(JsonInclude.Include.NON_EMPTY)
class RatingMediaObject( class RatingMediaObject(
@JsonProperty("title") title: String?, @JsonProperty("title") title: String?,
@ -299,15 +573,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime) @JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime)
) : MediaObject(title, year, ids) ) : MediaObject(title, year, ids)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
class HistoryMediaObject(
@JsonProperty("title") title: String?,
@JsonProperty("year") year: Int?,
@JsonProperty("ids") ids: Ids?,
@JsonProperty("seasons") seasons: List<Season>?,
@JsonProperty("episodes") episodes: List<Season.Episode>?,
) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes)
@JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonInclude(JsonInclude.Include.NON_EMPTY)
data class StatusRequest( data class StatusRequest(
@JsonProperty("movies") val movies: List<MediaObject>, @JsonProperty("movies") val movies: List<MediaObject>,
@ -404,13 +669,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
} }
data class ShowMetadata( data class ShowMetadata(
override val last_watched_at: String?, @JsonProperty("last_watched_at") override val last_watched_at: String?,
override val status: String, @JsonProperty("status") override val status: String,
override val user_rating: Int?, @JsonProperty("user_rating") override val user_rating: Int?,
override val last_watched: String?, @JsonProperty("last_watched") override val last_watched: String?,
override val watched_episodes_count: Int?, @JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?,
override val total_episodes_count: Int?, @JsonProperty("total_episodes_count") override val total_episodes_count: Int?,
val show: Show @JsonProperty("show") val show: Show
) : Metadata { ) : Metadata {
override fun getIds(): Show.Ids { override fun getIds(): Show.Ids {
return this.show.ids return this.show.ids
@ -435,23 +700,23 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
} }
data class Show( data class Show(
val title: String, @JsonProperty("title") val title: String,
val poster: String?, @JsonProperty("poster") val poster: String?,
val year: Int?, @JsonProperty("year") val year: Int?,
val ids: Ids, @JsonProperty("ids") val ids: Ids,
) { ) {
data class Ids( data class Ids(
val simkl: Int, @JsonProperty("simkl") val simkl: Int,
val slug: String?, @JsonProperty("slug") val slug: String?,
val imdb: String?, @JsonProperty("imdb") val imdb: String?,
val zap2it: String?, @JsonProperty("zap2it") val zap2it: String?,
val tmdb: String?, @JsonProperty("tmdb") val tmdb: String?,
val offen: String?, @JsonProperty("offen") val offen: String?,
val tvdb: String?, @JsonProperty("tvdb") val tvdb: String?,
val mal: String?, @JsonProperty("mal") val mal: String?,
val anidb: String?, @JsonProperty("anidb") val anidb: String?,
val anilist: String?, @JsonProperty("anilist") val anilist: String?,
val traktslug: String? @JsonProperty("traktslug") val traktslug: String?
) { ) {
fun matchesId(database: SyncServices, id: String): Boolean { fun matchesId(database: SyncServices, id: String): Boolean {
return when (database) { return when (database) {
@ -491,20 +756,58 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
/**
* Useful to get episodes on demand to prevent unnecessary requests.
*/
class SimklEpisodeConstructor(
private val simklId: Int?,
private val type: String?,
private val totalEpisodeCount: Int?,
private val hasEnded: Boolean?
) {
suspend fun getEpisodes(): Array<EpisodeMetadata>? {
return getEpisodes(simklId, type, totalEpisodeCount, hasEnded)
}
}
class SimklSyncStatus( class SimklSyncStatus(
override var status: Int, override var status: Int,
override var score: Int?, override var score: Int?,
val oldScore: Int?,
override var watchedEpisodes: Int?, override var watchedEpisodes: Int?,
val episodes: Array<EpisodeMetadata>?, val episodeConstructor: SimklEpisodeConstructor,
override var isFavorite: Boolean? = null, override var isFavorite: Boolean? = null,
override var maxEpisodes: Int? = null, override var maxEpisodes: Int? = null,
/** Save seen episodes separately to know the change from old to new. /** Save seen episodes separately to know the change from old to new.
* Required to remove seen episodes if count decreases */ * Required to remove seen episodes if count decreases */
val oldEpisodes: Int, val oldEpisodes: Int,
val oldStatus: String?
) : SyncAPI.AbstractSyncStatus() ) : SyncAPI.AbstractSyncStatus()
override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? {
val realIds = readIdFromString(id) val realIds = readIdFromString(id)
// Key which assumes all ids are the same each time :/
// This could be some sort of reference system to make multiple IDs
// point to the same key.
val idKey =
realIds.toList().map { "${it.first.originalName}=${it.second}" }.sorted().joinToString()
val cachedObject = SimklCache.getKey<MediaObject>(idKey)
val searchResult: MediaObject = cachedObject
?: (searchByIds(realIds)?.firstOrNull()?.also { result ->
val cacheTime =
if (result.hasEnded()) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value
SimklCache.setKey(idKey, result, Duration.parse(cacheTime))
}) ?: return null
val episodeConstructor = SimklEpisodeConstructor(
searchResult.ids?.simkl,
searchResult.type,
searchResult.total_episodes,
searchResult.hasEnded()
)
val foundItem = getSyncListSmart()?.let { list -> val foundItem = getSyncListSmart()?.let { list ->
listOf(list.shows, list.anime, list.movies).flatten().firstOrNull { show -> listOf(list.shows, list.anime, list.movies).flatten().firstOrNull { show ->
realIds.any { (database, id) -> realIds.any { (database, id) ->
@ -513,172 +816,63 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
// Search to get episodes
val searchResult = searchByIds(realIds)?.firstOrNull()
val episodes = getEpisodes(searchResult?.ids?.simkl, searchResult?.type)
if (foundItem != null) { if (foundItem != null) {
return SimklSyncStatus( return SimklSyncStatus(
status = foundItem.status?.let { SimklListStatusType.fromString(it)?.value } status = foundItem.status?.let { SimklListStatusType.fromString(it)?.value }
?: return null, ?: return null,
score = foundItem.user_rating, score = foundItem.user_rating,
watchedEpisodes = foundItem.watched_episodes_count, watchedEpisodes = foundItem.watched_episodes_count,
maxEpisodes = foundItem.total_episodes_count, maxEpisodes = searchResult.total_episodes,
episodes = episodes, episodeConstructor = episodeConstructor,
oldEpisodes = foundItem.watched_episodes_count ?: 0, oldEpisodes = foundItem.watched_episodes_count ?: 0,
oldScore = foundItem.user_rating,
oldStatus = foundItem.status
) )
} else { } else {
return if (searchResult != null) { return SimklSyncStatus(
SimklSyncStatus( status = SimklListStatusType.None.value,
status = SimklListStatusType.None.value, score = 0,
score = 0, watchedEpisodes = 0,
watchedEpisodes = 0, maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes,
maxEpisodes = if (searchResult.type == "movie") 0 else null, episodeConstructor = episodeConstructor,
episodes = episodes, oldEpisodes = 0,
oldEpisodes = 0, oldStatus = null,
) oldScore = null
} else { )
null
}
} }
} }
override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean {
val parsedId = readIdFromString(id) val parsedId = readIdFromString(id)
lastScoreTime = unixTime lastScoreTime = unixTime
if (status.status == SimklListStatusType.None.value) {
return app.post(
"$mainUrl/sync/history/remove",
json = StatusRequest(
shows = listOf(
HistoryMediaObject(
null,
null,
MediaObject.Ids.fromMap(parsedId),
emptyList(),
emptyList()
)
),
movies = emptyList()
),
interceptor = interceptor
).isSuccessful
}
val realScore = status.score
val ratingResponseSuccess = if (realScore != null) {
// Remove rating if score is 0
val ratingsSuffix = if (realScore == 0) "/remove" else ""
debugPrint { "Rate ${this.name} item: rating=$realScore" }
app.post(
"$mainUrl/sync/ratings$ratingsSuffix",
json = StatusRequest(
// Not possible to know if TV or Movie
shows = listOf(
RatingMediaObject(
null,
null,
MediaObject.Ids.fromMap(parsedId),
realScore
)
),
movies = emptyList()
),
interceptor = interceptor
).isSuccessful
} else {
true
}
val simklStatus = status as? SimklSyncStatus val simklStatus = status as? SimklSyncStatus
val builder = SimklScoreBuilder.Builder()
.apiUrl(this.mainUrl)
.score(status.score, simklStatus?.oldScore)
.status(status.status, (status as? SimklSyncStatus)?.oldStatus?.let { oldStatus ->
SimklListStatusType.values().firstOrNull {
it.originalName == oldStatus
}?.value
})
.interceptor(interceptor)
.ids(MediaObject.Ids.fromMap(parsedId))
// Get episodes only when required
val episodes = simklStatus?.episodeConstructor?.getEpisodes()
// All episodes if marked as completed // All episodes if marked as completed
val watchedEpisodes = if (status.status == SimklListStatusType.Completed.value) { val watchedEpisodes = if (status.status == SimklListStatusType.Completed.value) {
simklStatus?.episodes?.size episodes?.size
} else { } else {
status.watchedEpisodes status.watchedEpisodes
} }
// Only post episodes if available episodes and the status is correct builder.episodes(episodes?.toList(), watchedEpisodes, simklStatus?.oldEpisodes)
val episodeResponseSuccess =
if (simklStatus != null && watchedEpisodes != null && !simklStatus.episodes.isNullOrEmpty() && listOf(
SimklListStatusType.Paused.value,
SimklListStatusType.Dropped.value,
SimklListStatusType.Watching.value,
SimklListStatusType.Completed.value,
SimklListStatusType.ReWatching.value
).contains(status.status)
) {
suspend fun postEpisodes(
url: String,
rawEpisodes: List<EpisodeMetadata>
): Boolean {
val (seasons, episodes) = if (rawEpisodes.any { it.season != null }) {
EpisodeMetadata.convertToSeasons(rawEpisodes) to null
} else {
null to EpisodeMetadata.convertToEpisodes(rawEpisodes)
}
debugPrint { "Synced history using $url: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" }
return app.post(
url,
json = StatusRequest(
shows = listOf(
HistoryMediaObject(
null,
null,
MediaObject.Ids.fromMap(parsedId),
seasons,
episodes
)
),
movies = emptyList()
),
interceptor = interceptor
).isSuccessful
}
// If episodes decrease: remove all episodes beyond watched episodes.
val removeResponse = if (simklStatus.oldEpisodes > watchedEpisodes) {
val removeEpisodes = simklStatus.episodes
.drop(watchedEpisodes)
postEpisodes("$mainUrl/sync/history/remove", removeEpisodes)
} else {
true
}
val cutEpisodes = simklStatus.episodes.take(watchedEpisodes)
val addResponse = postEpisodes("$mainUrl/sync/history/", cutEpisodes)
removeResponse && addResponse
} else true
val newStatus =
SimklListStatusType.values().firstOrNull { it.value == status.status }?.originalName
?: SimklListStatusType.Watching.originalName
val statusResponseSuccess = if (newStatus != null) {
debugPrint { "Add to ${this.name} list: status=$newStatus" }
app.post(
"$mainUrl/sync/add-to-list",
json = StatusRequest(
shows = listOf(
StatusMediaObject(
null,
null,
MediaObject.Ids.fromMap(parsedId),
newStatus
)
),
movies = emptyList()
),
interceptor = interceptor
).isSuccessful
} else {
true
}
debugPrint { "All scoring complete: rating=$ratingResponseSuccess, status=$statusResponseSuccess, episode=$episodeResponseSuccess" }
requireLibraryRefresh = true requireLibraryRefresh = true
return ratingResponseSuccess && statusResponseSuccess && episodeResponseSuccess return builder.execute()
} }
@ -694,17 +888,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
).parsedSafe() ).parsedSafe()
} }
suspend fun getEpisodes(simklId: Int?, type: String?): Array<EpisodeMetadata>? {
if (simklId == null) return null
val url = when (type) {
"anime" -> "https://api.simkl.com/anime/episodes/$simklId"
"tv" -> "https://api.simkl.com/tv/episodes/$simklId"
"movie" -> return null
else -> return null
}
return app.get(url, params = mapOf("client_id" to clientId)).parsedSafe()
}
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? { override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
return app.get( return app.get(
"$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name) "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name)
@ -737,16 +920,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
return null return null
} }
private suspend fun getSyncListSince(since: Long?): AllItemsResponse { private suspend fun getSyncListSince(since: Long?): AllItemsResponse? {
val params = getDateTime(since)?.let { val params = getDateTime(since)?.let {
mapOf("date_from" to it) mapOf("date_from" to it)
} ?: emptyMap() } ?: emptyMap()
// Can return null on no change.
return app.get( return app.get(
"$mainUrl/sync/all-items/", "$mainUrl/sync/all-items/",
params = params, params = params,
interceptor = interceptor interceptor = interceptor
).parsed() ).parsedSafe()
} }
private suspend fun getActivities(): ActivitiesResponse? { private suspend fun getActivities(): ActivitiesResponse? {

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 {
@ -446,6 +517,7 @@ abstract class AbstractPlayerFragment(
canEnterPipMode = false canEnterPipMode = false
mMediaSession?.release() mMediaSession?.release()
mMediaSession = null mMediaSession = null
playerView?.player = null
SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged
keepScreenOn(false) keepScreenOn(false)
@ -461,6 +533,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
@ -46,10 +51,12 @@ 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.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
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.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
@ -58,6 +65,7 @@ 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.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
@ -78,6 +86,12 @@ const val toleranceAfterUs = 300_000L
class CS3IPlayer : IPlayer { class CS3IPlayer : IPlayer {
private var isPlaying = false private var isPlaying = false
private var exoPlayer: ExoPlayer? = null private var exoPlayer: ExoPlayer? = null
set(value) {
// If the old value is not null then the player has not been properly released.
debugAssert({ field != null && value != null }, { "Previous player instance should be released!" })
field = value
}
var cacheSize = 0L var cacheSize = 0L
var simpleCacheSize = 0L var simpleCacheSize = 0L
var videoBufferMs = 0L var videoBufferMs = 0L
@ -104,7 +118,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
@ -118,80 +141,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
@ -200,7 +167,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)
} }
@ -254,8 +221,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:
@ -340,6 +308,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)
@ -368,6 +337,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 }
@ -387,6 +357,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
@ -465,6 +436,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)
@ -475,6 +447,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()
@ -504,14 +477,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()
} }
@ -548,6 +521,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 {
@ -555,6 +529,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)
@ -587,6 +562,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)
} }
@ -632,6 +608,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)
@ -663,6 +640,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()
@ -676,6 +654,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>,
@ -710,13 +689,13 @@ class CS3IPlayer : IPlayer {
metadataRendererOutput metadataRendererOutput
).map { ).map {
if (it is TextRenderer) { if (it is TextRenderer) {
currentTextRenderer = CustomTextRenderer( val currentTextRenderer = CustomTextRenderer(
subtitleOffset, subtitleOffset,
textRendererOutput, textRendererOutput,
eventHandler.looper, eventHandler.looper,
CustomSubtitleDecoderFactory() CustomSubtitleDecoderFactory()
) ).also { this.currentTextRenderer = it }
currentTextRenderer!! currentTextRenderer
} else it } else it
}.toTypedArray() }.toTypedArray()
} }
@ -760,15 +739,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
) )
) )
} }
@ -801,43 +798,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()
} }
@ -854,32 +863,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))
} }
} }
@ -918,18 +927,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 {
@ -963,18 +968,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
@ -996,23 +1002,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
@ -1037,7 +1035,7 @@ class CS3IPlayer : IPlayer {
} }
else -> { else -> {
playerError?.invoke(error) event(ErrorEvent(error))
} }
} }
@ -1051,7 +1049,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()
} }
} }
@ -1071,12 +1069,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 -> {
@ -1089,27 +1090,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)
} }
@ -1119,9 +1122,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
@ -1139,7 +1143,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
} }
@ -1148,7 +1152,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 ->
@ -1182,12 +1186,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?,
@ -1243,6 +1248,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 {
@ -1259,7 +1265,7 @@ class CS3IPlayer : IPlayer {
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
} }
val mime = when(link.type) { val mime = when (link.type) {
ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8 ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4 ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4
@ -1267,12 +1273,29 @@ class CS3IPlayer : IPlayer {
ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet 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)
) )
@ -1298,16 +1321,16 @@ 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))
} }
} }
override fun reloadPlayer(context: Context) { override fun reloadPlayer(context: Context) {
Log.i(TAG, "reloadPlayer") Log.i(TAG, "reloadPlayer")
exoPlayer?.release() releasePlayer(false)
currentLink?.let { currentLink?.let {
loadOnlinePlayer(context, it) loadOnlinePlayer(context, it)
} ?: currentDownloadedFile?.let { } ?: currentDownloadedFile?.let {

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

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

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

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

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

View file

@ -1,14 +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 com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.jsoup.Jsoup import org.jsoup.Jsoup
import java.net.URL import java.net.URL
import kotlin.collections.MutableList import java.util.UUID
/** /**
* For use in the ConcatenatingMediaSource. * For use in the ConcatenatingMediaSource.
@ -37,7 +227,6 @@ 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,
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,
@ -99,6 +288,84 @@ private fun inferTypeFromUrl(url: String): ExtractorLinkType {
} }
} }
val INFER_TYPE : ExtractorLinkType? = null 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,
open val name: String, open val name: String,
@ -110,6 +377,9 @@ open class ExtractorLink constructor(
open val extractorData: String? = null, open val extractorData: String? = null,
open val type: ExtractorLinkType, open val type: ExtractorLinkType,
) : VideoDownloadManager.IDownloadableMinimum { ) : VideoDownloadManager.IDownloadableMinimum {
val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8
val isDash : Boolean get() = type == ExtractorLinkType.DASH
constructor( constructor(
source: String, source: String,
name: String, name: String,

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 @@
- Додано журнал змін! Додано журнал змін!