mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
45ecc903a2
31 changed files with 1496 additions and 578 deletions
|
@ -58,8 +58,8 @@ android {
|
|||
minSdk = 21
|
||||
targetSdk = 29
|
||||
|
||||
versionCode = 59
|
||||
versionName = "4.1.8"
|
||||
versionCode = 60
|
||||
versionName = "4.1.9"
|
||||
|
||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||
|
@ -250,9 +250,9 @@ dependencies {
|
|||
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
|
||||
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
|
||||
implementation("com.github.TeamNewPipe:NewPipeExtractor:1f08d28")
|
||||
implementation("com.github.teamnewpipe:NewPipeExtractor:1f08d28")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
|
||||
|
||||
// Library/extensions searching with Levenshtein distance
|
||||
|
|
|
@ -7,9 +7,14 @@ import android.content.Context
|
|||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.util.DisplayMetrics
|
||||
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.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
|
@ -40,7 +45,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
enum class FocusDirection {
|
||||
Start,
|
||||
|
@ -63,6 +70,19 @@ object CommonActivity {
|
|||
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 canShowPipMode: Boolean = false
|
||||
|
@ -328,6 +348,14 @@ object CommonActivity {
|
|||
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 */
|
||||
fun continueGetNextFocus(
|
||||
root: Any?,
|
||||
|
@ -348,16 +376,17 @@ object CommonActivity {
|
|||
} ?: return null
|
||||
|
||||
next = localLook(view, nextId) ?: next
|
||||
val shown = next.hasContent()
|
||||
|
||||
// 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
|
||||
val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent ->
|
||||
parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0
|
||||
} ?: 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 (!next.isShown) {
|
||||
if (!shown) {
|
||||
// we don't want a while true loop, so we let android decide if we find a recursive view
|
||||
if (next == view) return null
|
||||
return getNextFocus(root, next, direction, depth + 1)
|
||||
|
|
|
@ -22,8 +22,10 @@ import com.lagradost.cloudstream3.utils.*
|
|||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||
import com.lagradost.nicehttp.RequestBodyTypes
|
||||
import okhttp3.Interceptor
|
||||
import org.mozilla.javascript.Scriptable
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.absoluteValue
|
||||
|
@ -177,10 +179,17 @@ object APIHolder {
|
|||
|
||||
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.
|
||||
* 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 types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes()
|
||||
|
@ -189,7 +198,8 @@ object APIHolder {
|
|||
suspend fun getTracker(
|
||||
titles: List<String>,
|
||||
types: Set<TrackerType>?,
|
||||
year: Int?
|
||||
year: Int?,
|
||||
lessAccurate: Boolean
|
||||
): Tracker? {
|
||||
return try {
|
||||
require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" }
|
||||
|
@ -197,30 +207,70 @@ object APIHolder {
|
|||
val mainTitle = titles[0]
|
||||
val search =
|
||||
trackerCache[mainTitle]
|
||||
?: app.get("https://api.consumet.org/meta/anilist/$mainTitle")
|
||||
.parsedSafe<AniSearch>()?.also {
|
||||
trackerCache[mainTitle] = it
|
||||
} ?: return null
|
||||
?: searchAnilist(mainTitle)?.also {
|
||||
trackerCache[mainTitle] = it
|
||||
} ?: return null
|
||||
|
||||
val res = search.results?.find { media ->
|
||||
val matchingYears = year == null || media.releaseDate == year
|
||||
val res = search.data?.page?.media?.find { media ->
|
||||
val matchingYears = year == null || media.seasonYear == year
|
||||
val matchingTitles = media.title?.let { title ->
|
||||
titles.any { userTitle ->
|
||||
title.isMatchingTitles(userTitle)
|
||||
}
|
||||
} ?: false
|
||||
|
||||
val matchingTypes = types?.any { it.name.equals(media.type, true) } == true
|
||||
matchingTitles && matchingTypes && matchingYears
|
||||
val matchingTypes = types?.any { it.name.equals(media.format, true) } == true
|
||||
if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears
|
||||
} ?: 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) {
|
||||
logError(t)
|
||||
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> {
|
||||
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
@ -1730,30 +1780,42 @@ data class Tracker(
|
|||
val cover: String? = null,
|
||||
)
|
||||
|
||||
data class Title(
|
||||
@JsonProperty("romaji") val romaji: String? = null,
|
||||
@JsonProperty("english") val english: String? = null,
|
||||
data class AniSearch(
|
||||
@JsonProperty("data") var data: Data? = Data()
|
||||
) {
|
||||
fun isMatchingTitles(title: String?): Boolean {
|
||||
if (title == null) return false
|
||||
return english.equals(title, true) || romaji.equals(title, true)
|
||||
data class Data(
|
||||
@JsonProperty("Page") var page: Page? = Page()
|
||||
) {
|
||||
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
|
||||
**/
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
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.navigationrail.NavigationRailView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.common.collect.Comparators.min
|
||||
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
|
||||
import com.lagradost.cloudstream3.APIHolder.allProviders
|
||||
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.onDialogDismissedEvent
|
||||
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
|
||||
import com.lagradost.cloudstream3.CommonActivity.screenHeight
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.CommonActivity.updateLocale
|
||||
import com.lagradost.cloudstream3.databinding.ActivityMainBinding
|
||||
import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding
|
||||
import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.observeNullable
|
||||
|
@ -832,6 +834,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
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) {
|
||||
focusOutline.get()?.apply {
|
||||
|
@ -874,7 +883,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
if (!exactlyTheSame) {
|
||||
lastView?.removeOnLayoutChangeListener(layoutListener)
|
||||
lastView?.removeOnAttachStateChangeListener(attachListener)
|
||||
(lastView?.parent as? RecyclerView)?.removeOnLayoutChangeListener(layoutListener)
|
||||
(lastView?.parent as? RecyclerView)?.apply {
|
||||
removeOnLayoutChangeListener(layoutListener)
|
||||
//removeOnScrollListener(scrollListener)
|
||||
}
|
||||
}
|
||||
|
||||
val wasGone = focusOutline.isGone
|
||||
|
@ -952,7 +964,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
focusOutline.isVisible = false
|
||||
}
|
||||
if (!exactlyTheSame) {
|
||||
(newFocus.parent as? RecyclerView)?.addOnLayoutChangeListener(layoutListener)
|
||||
(newFocus.parent as? RecyclerView)?.apply {
|
||||
addOnLayoutChangeListener(layoutListener)
|
||||
//addOnScrollListener(scrollListener)
|
||||
}
|
||||
newFocus.addOnLayoutChangeListener(layoutListener)
|
||||
newFocus.addOnAttachStateChangeListener(attachListener)
|
||||
}
|
||||
|
@ -970,8 +985,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
)
|
||||
|
||||
// if they are the same within then snap, aka scrolling
|
||||
val deltaMin = 50.toPx
|
||||
if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMin && (start.y - end.y).absoluteValue < deltaMin) {
|
||||
val deltaMinX = min(end.width / 2, 60.toPx)
|
||||
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()
|
||||
last = start
|
||||
current = end
|
||||
|
@ -1000,7 +1016,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
// animate between a and b
|
||||
animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
|
||||
startDelay = 0
|
||||
duration = 100
|
||||
duration = 200
|
||||
addUpdateListener { animation ->
|
||||
val animatedValue = animation.animatedValue as Float
|
||||
val target = FocusTarget.lerp(last, current, minOf(animatedValue, 1.0f))
|
||||
|
@ -1095,7 +1111,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
|
||||
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, 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)
|
||||
/*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 {
|
||||
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
|
||||
|
|
|
@ -19,7 +19,7 @@ open class Gofile : ExtractorApi() {
|
|||
subtitleCallback: (SubtitleFile) -> 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 websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let {
|
||||
Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1)
|
||||
|
@ -59,4 +59,4 @@ open class Gofile : ExtractorApi() {
|
|||
@JsonProperty("data") val data: Data? = null,
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,13 @@ package com.lagradost.cloudstream3.syncproviders.providers
|
|||
|
||||
import android.util.Log
|
||||
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.removeKey
|
||||
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.subtitles.AbstractSubApi
|
||||
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.InAppAuthAPIManager
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
|
||||
override val idPrefix = "opensubtitles"
|
||||
|
@ -36,6 +37,23 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
|||
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 {
|
||||
return unixTimeMs > currentCoolDown
|
||||
}
|
||||
|
@ -98,13 +116,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
|||
val response = app.post(
|
||||
url = "$host/login",
|
||||
headers = mapOf(
|
||||
"Api-Key" to apiKey,
|
||||
"Content-Type" to "application/json"
|
||||
"Content-Type" to "application/json",
|
||||
),
|
||||
data = mapOf(
|
||||
"username" to username,
|
||||
"password" to password
|
||||
)
|
||||
),
|
||||
interceptor = headerInterceptor
|
||||
)
|
||||
//Log.i(TAG, "Responsecode = ${response.code}")
|
||||
//Log.i(TAG, "Result => ${response.text}")
|
||||
|
@ -149,11 +167,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
|||
// "pt" to "pt-PT",
|
||||
// "pt" to "pt-BR"
|
||||
)
|
||||
private fun fixLanguage(language: String?) : String? {
|
||||
|
||||
private fun fixLanguage(language: String?): String? {
|
||||
return languageExceptions[language] ?: language
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -183,9 +203,9 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
|||
val req = app.get(
|
||||
url = searchQueryUrl,
|
||||
headers = mapOf(
|
||||
Pair("Api-Key", apiKey),
|
||||
Pair("Content-Type", "application/json")
|
||||
)
|
||||
),
|
||||
interceptor = headerInterceptor
|
||||
)
|
||||
Log.i(TAG, "Search Req => ${req.text}")
|
||||
if (!req.isSuccessful) {
|
||||
|
@ -207,7 +227,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
|||
//Use any valid name/title in hierarchy
|
||||
val name = filename ?: featureDetails?.movieName ?: featureDetails?.title
|
||||
?: featureDetails?.parentTitle ?: attr.release ?: query.query
|
||||
val lang = fixLanguageReverse(attr.language)?: ""
|
||||
val lang = fixLanguageReverse(attr.language) ?: ""
|
||||
val resEpNum = featureDetails?.episodeNumber ?: query.epNumber
|
||||
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
|
||||
val year = featureDetails?.year ?: query.year
|
||||
|
@ -251,13 +271,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
|
|||
"Authorization",
|
||||
"Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}"
|
||||
),
|
||||
Pair("Api-Key", apiKey),
|
||||
Pair("Content-Type", "application/json"),
|
||||
Pair("Accept", "*/*")
|
||||
),
|
||||
data = mapOf(
|
||||
Pair("file_id", data.data)
|
||||
)
|
||||
),
|
||||
interceptor = headerInterceptor
|
||||
)
|
||||
Log.i(TAG, "Request result => (${req.code}) ${req.text}")
|
||||
//Log.i(TAG, "Request headers => ${req.headers}")
|
||||
|
|
|
@ -5,7 +5,9 @@ import androidx.core.net.toUri
|
|||
import androidx.fragment.app.FragmentActivity
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.AcraApplication
|
||||
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.removeKey
|
||||
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.TvType
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mapper
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
import com.lagradost.cloudstream3.mvvm.debugPrint
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
|
@ -33,6 +36,9 @@ import java.text.SimpleDateFormat
|
|||
import java.time.Instant
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||
override var name = "Simkl"
|
||||
|
@ -59,6 +65,80 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
*/
|
||||
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 {
|
||||
private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID
|
||||
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?
|
||||
) {
|
||||
companion object {
|
||||
fun convertToEpisodes(list: List<EpisodeMetadata>?): List<MediaObject.Season.Episode> {
|
||||
fun convertToEpisodes(list: List<EpisodeMetadata>?): List<MediaObject.Season.Episode>? {
|
||||
return list?.map {
|
||||
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 {
|
||||
it.season
|
||||
}?.map { (season, episodes) ->
|
||||
MediaObject.Season(season!!, convertToEpisodes(episodes))
|
||||
} ?: emptyList()
|
||||
}?.mapNotNull { (season, episodes) ->
|
||||
convertToEpisodes(episodes)?.let { MediaObject.Season(season!!, it) }
|
||||
}?.ifEmpty { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -235,11 +315,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
@JsonProperty("title") val title: String?,
|
||||
@JsonProperty("year") val year: Int?,
|
||||
@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("type") val type: String? = null,
|
||||
@JsonProperty("seasons") val seasons: List<Season>? = null,
|
||||
@JsonProperty("episodes") val episodes: List<Season.Episode>? = null
|
||||
) {
|
||||
fun hasEnded(): Boolean {
|
||||
return status == "released" || status == "ended"
|
||||
}
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
data class Season(
|
||||
@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)
|
||||
class RatingMediaObject(
|
||||
@JsonProperty("title") title: String?,
|
||||
|
@ -299,15 +573,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
@JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime)
|
||||
) : 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)
|
||||
data class StatusRequest(
|
||||
@JsonProperty("movies") val movies: List<MediaObject>,
|
||||
|
@ -404,13 +669,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
}
|
||||
|
||||
data class ShowMetadata(
|
||||
override val last_watched_at: String?,
|
||||
override val status: String,
|
||||
override val user_rating: Int?,
|
||||
override val last_watched: String?,
|
||||
override val watched_episodes_count: Int?,
|
||||
override val total_episodes_count: Int?,
|
||||
val show: Show
|
||||
@JsonProperty("last_watched_at") override val last_watched_at: String?,
|
||||
@JsonProperty("status") override val status: String,
|
||||
@JsonProperty("user_rating") override val user_rating: Int?,
|
||||
@JsonProperty("last_watched") override val last_watched: String?,
|
||||
@JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?,
|
||||
@JsonProperty("total_episodes_count") override val total_episodes_count: Int?,
|
||||
@JsonProperty("show") val show: Show
|
||||
) : Metadata {
|
||||
override fun getIds(): Show.Ids {
|
||||
return this.show.ids
|
||||
|
@ -435,23 +700,23 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
}
|
||||
|
||||
data class Show(
|
||||
val title: String,
|
||||
val poster: String?,
|
||||
val year: Int?,
|
||||
val ids: Ids,
|
||||
@JsonProperty("title") val title: String,
|
||||
@JsonProperty("poster") val poster: String?,
|
||||
@JsonProperty("year") val year: Int?,
|
||||
@JsonProperty("ids") val ids: Ids,
|
||||
) {
|
||||
data class Ids(
|
||||
val simkl: Int,
|
||||
val slug: String?,
|
||||
val imdb: String?,
|
||||
val zap2it: String?,
|
||||
val tmdb: String?,
|
||||
val offen: String?,
|
||||
val tvdb: String?,
|
||||
val mal: String?,
|
||||
val anidb: String?,
|
||||
val anilist: String?,
|
||||
val traktslug: String?
|
||||
@JsonProperty("simkl") val simkl: Int,
|
||||
@JsonProperty("slug") val slug: String?,
|
||||
@JsonProperty("imdb") val imdb: String?,
|
||||
@JsonProperty("zap2it") val zap2it: String?,
|
||||
@JsonProperty("tmdb") val tmdb: String?,
|
||||
@JsonProperty("offen") val offen: String?,
|
||||
@JsonProperty("tvdb") val tvdb: String?,
|
||||
@JsonProperty("mal") val mal: String?,
|
||||
@JsonProperty("anidb") val anidb: String?,
|
||||
@JsonProperty("anilist") val anilist: String?,
|
||||
@JsonProperty("traktslug") val traktslug: String?
|
||||
) {
|
||||
fun matchesId(database: SyncServices, id: String): Boolean {
|
||||
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(
|
||||
override var status: Int,
|
||||
override var score: Int?,
|
||||
val oldScore: Int?,
|
||||
override var watchedEpisodes: Int?,
|
||||
val episodes: Array<EpisodeMetadata>?,
|
||||
val episodeConstructor: SimklEpisodeConstructor,
|
||||
override var isFavorite: Boolean? = null,
|
||||
override var maxEpisodes: Int? = null,
|
||||
/** Save seen episodes separately to know the change from old to new.
|
||||
* Required to remove seen episodes if count decreases */
|
||||
val oldEpisodes: Int,
|
||||
val oldStatus: String?
|
||||
) : SyncAPI.AbstractSyncStatus()
|
||||
|
||||
override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? {
|
||||
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 ->
|
||||
listOf(list.shows, list.anime, list.movies).flatten().firstOrNull { show ->
|
||||
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) {
|
||||
return SimklSyncStatus(
|
||||
status = foundItem.status?.let { SimklListStatusType.fromString(it)?.value }
|
||||
?: return null,
|
||||
score = foundItem.user_rating,
|
||||
watchedEpisodes = foundItem.watched_episodes_count,
|
||||
maxEpisodes = foundItem.total_episodes_count,
|
||||
episodes = episodes,
|
||||
maxEpisodes = searchResult.total_episodes,
|
||||
episodeConstructor = episodeConstructor,
|
||||
oldEpisodes = foundItem.watched_episodes_count ?: 0,
|
||||
oldScore = foundItem.user_rating,
|
||||
oldStatus = foundItem.status
|
||||
)
|
||||
} else {
|
||||
return if (searchResult != null) {
|
||||
SimklSyncStatus(
|
||||
status = SimklListStatusType.None.value,
|
||||
score = 0,
|
||||
watchedEpisodes = 0,
|
||||
maxEpisodes = if (searchResult.type == "movie") 0 else null,
|
||||
episodes = episodes,
|
||||
oldEpisodes = 0,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return SimklSyncStatus(
|
||||
status = SimklListStatusType.None.value,
|
||||
score = 0,
|
||||
watchedEpisodes = 0,
|
||||
maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes,
|
||||
episodeConstructor = episodeConstructor,
|
||||
oldEpisodes = 0,
|
||||
oldStatus = null,
|
||||
oldScore = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean {
|
||||
val parsedId = readIdFromString(id)
|
||||
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 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
|
||||
val watchedEpisodes = if (status.status == SimklListStatusType.Completed.value) {
|
||||
simklStatus?.episodes?.size
|
||||
episodes?.size
|
||||
} else {
|
||||
status.watchedEpisodes
|
||||
}
|
||||
|
||||
// Only post episodes if available episodes and the status is correct
|
||||
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
|
||||
}
|
||||
builder.episodes(episodes?.toList(), watchedEpisodes, simklStatus?.oldEpisodes)
|
||||
|
||||
// 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
|
||||
return ratingResponseSuccess && statusResponseSuccess && episodeResponseSuccess
|
||||
return builder.execute()
|
||||
}
|
||||
|
||||
|
||||
|
@ -694,17 +888,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
).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>? {
|
||||
return app.get(
|
||||
"$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name)
|
||||
|
@ -737,16 +920,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
|||
return null
|
||||
}
|
||||
|
||||
private suspend fun getSyncListSince(since: Long?): AllItemsResponse {
|
||||
private suspend fun getSyncListSince(since: Long?): AllItemsResponse? {
|
||||
val params = getDateTime(since)?.let {
|
||||
mapOf("date_from" to it)
|
||||
} ?: emptyMap()
|
||||
|
||||
// Can return null on no change.
|
||||
return app.get(
|
||||
"$mainUrl/sync/all-items/",
|
||||
params = params,
|
||||
interceptor = interceptor
|
||||
).parsed()
|
||||
).parsedSafe()
|
||||
}
|
||||
|
||||
private suspend fun getActivities(): ActivitiesResponse? {
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.graphics.drawable.AnimatedVectorDrawable
|
|||
import android.media.metrics.PlaybackErrorEvent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -25,8 +26,10 @@ import androidx.media3.common.PlaybackException
|
|||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.DefaultTimeBar
|
||||
import androidx.media3.ui.PlayerView
|
||||
import androidx.media3.ui.SubtitleView
|
||||
import androidx.media3.ui.TimeBar
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode
|
||||
|
@ -92,11 +95,11 @@ abstract class AbstractPlayerFragment(
|
|||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
open fun playerPositionChanged(posDur: Pair<Long, Long>) {
|
||||
open fun playerPositionChanged(position: Long, duration : Long) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
open fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) {
|
||||
open fun playerDimensionsLoaded(width: Int, height : Int) {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
|
@ -132,8 +135,8 @@ abstract class AbstractPlayerFragment(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateIsPlaying(playing: Pair<CSPlayerLoading, CSPlayerLoading>) {
|
||||
val (wasPlaying, isPlaying) = playing
|
||||
private fun updateIsPlaying(wasPlaying : CSPlayerLoading,
|
||||
isPlaying : CSPlayerLoading) {
|
||||
val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying
|
||||
val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying
|
||||
|
||||
|
@ -206,7 +209,7 @@ abstract class AbstractPlayerFragment(
|
|||
CSPlayerEvent.values()[intent.getIntExtra(
|
||||
EXTRA_CONTROL_TYPE,
|
||||
0
|
||||
)]
|
||||
)], source = PlayerEventSource.UI
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -216,7 +219,7 @@ abstract class AbstractPlayerFragment(
|
|||
val isPlaying = player.getIsPlaying()
|
||||
val isPlayingValue =
|
||||
if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused
|
||||
updateIsPlaying(Pair(isPlayingValue, isPlayingValue))
|
||||
updateIsPlaying(isPlayingValue, isPlayingValue)
|
||||
} else {
|
||||
// Restore the full-screen UI.
|
||||
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) {
|
||||
if (gotoNext && hasNextMirror()) {
|
||||
showToast(
|
||||
|
@ -326,6 +329,7 @@ abstract class AbstractPlayerFragment(
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun playerUpdated(player: Any?) {
|
||||
if (player is ExoPlayer) {
|
||||
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?) {
|
||||
resizeMode = getKey(RESIZE_MODE_KEY) ?: 0
|
||||
resize(resizeMode, false)
|
||||
|
||||
player.releaseCallbacks()
|
||||
player.initCallbacks(
|
||||
playerUpdated = ::playerUpdated,
|
||||
updateIsPlaying = ::updateIsPlaying,
|
||||
playerError = ::playerError,
|
||||
requestAutoFocus = ::requestAudioFocus,
|
||||
nextEpisode = ::nextEpisode,
|
||||
prevEpisode = ::prevEpisode,
|
||||
playerPositionChanged = ::playerPositionChanged,
|
||||
playerDimensionsLoaded = ::playerDimensionsLoaded,
|
||||
eventHandler = ::mainCallback,
|
||||
requestedListeningPercentages = listOf(
|
||||
SKIP_OP_VIDEO_PERCENTAGE,
|
||||
PRELOAD_NEXT_EPISODE_PERCENTAGE,
|
||||
NEXT_WATCH_EPISODE_PERCENTAGE,
|
||||
UPDATE_SYNC_PROGRESS_PERCENTAGE,
|
||||
),
|
||||
subtitlesUpdates = ::subtitlesChanged,
|
||||
embeddedSubtitlesFetched = ::embeddedSubtitlesFetched,
|
||||
onTracksInfoChanged = ::onTracksInfoChanged,
|
||||
onTimestampInvoked = ::onTimestamp,
|
||||
onTimestampSkipped = ::onTimestampSkipped
|
||||
)
|
||||
|
||||
if (player is CS3IPlayer) {
|
||||
|
@ -400,6 +458,19 @@ abstract class AbstractPlayerFragment(
|
|||
subStyle = SubtitlesFragment.getCurrentSavedStyle()
|
||||
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
|
||||
|
||||
try {
|
||||
|
@ -446,6 +517,7 @@ abstract class AbstractPlayerFragment(
|
|||
canEnterPipMode = false
|
||||
mMediaSession?.release()
|
||||
mMediaSession = null
|
||||
playerView?.player = null
|
||||
SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged
|
||||
|
||||
keepScreenOn(false)
|
||||
|
@ -461,6 +533,7 @@ abstract class AbstractPlayerFragment(
|
|||
resize(PlayerResize.values()[resize], showToast)
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
fun resize(resize: PlayerResize, showToast: Boolean) {
|
||||
setKey(RESIZE_MODE_KEY, resize.ordinal)
|
||||
val type = when (resize) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3.ui.player
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
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.ExoPlayer
|
||||
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.ConcatenatingMediaSource
|
||||
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.USER_AGENT
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
|
||||
import com.lagradost.cloudstream3.utils.DrmExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.EpisodeSkip
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
|
||||
|
@ -58,6 +65,7 @@ import com.lagradost.cloudstream3.utils.ExtractorUri
|
|||
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
||||
import java.io.File
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.UUID
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSession
|
||||
|
@ -78,6 +86,12 @@ const val toleranceAfterUs = 300_000L
|
|||
class CS3IPlayer : IPlayer {
|
||||
private var isPlaying = false
|
||||
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 simpleCacheSize = 0L
|
||||
var videoBufferMs = 0L
|
||||
|
@ -104,7 +118,16 @@ class CS3IPlayer : IPlayer {
|
|||
* */
|
||||
data class MediaItemSlice(
|
||||
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
|
||||
|
@ -118,80 +141,24 @@ class CS3IPlayer : IPlayer {
|
|||
* Boolean = if it's active
|
||||
* */
|
||||
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
|
||||
|
||||
/** Fired when seeking the player or on requestedListeningPercentages,
|
||||
* used to make things appear on que
|
||||
* position, duration */
|
||||
private var playerPositionChanged: ((Pair<Long, Long>) -> Unit)? = null
|
||||
private var eventHandler: ((PlayerEvent) -> Unit)? = null
|
||||
|
||||
private var nextEpisode: (() -> Unit)? = null
|
||||
private var prevEpisode: (() -> Unit)? = null
|
||||
|
||||
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
|
||||
fun event(event: PlayerEvent) {
|
||||
eventHandler?.invoke(event)
|
||||
}
|
||||
|
||||
override fun releaseCallbacks() {
|
||||
playerUpdated = 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
|
||||
eventHandler = null
|
||||
}
|
||||
|
||||
override fun initCallbacks(
|
||||
playerUpdated: (Any?) -> Unit,
|
||||
updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)?,
|
||||
requestAutoFocus: (() -> Unit)?,
|
||||
playerError: ((Exception) -> Unit)?,
|
||||
playerDimensionsLoaded: ((Pair<Int, Int>) -> Unit)?,
|
||||
eventHandler: ((PlayerEvent) -> Unit),
|
||||
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.playerPositionChanged = playerPositionChanged
|
||||
this.nextEpisode = nextEpisode
|
||||
this.prevEpisode = prevEpisode
|
||||
this.subtitlesUpdates = subtitlesUpdates
|
||||
this.embeddedSubtitlesFetched = embeddedSubtitlesFetched
|
||||
this.onTracksInfoChanged = onTracksInfoChanged
|
||||
this.onTimestampInvoked = onTimestampInvoked
|
||||
this.onTimestampSkipped = onTimestampSkipped
|
||||
this.eventHandler = eventHandler
|
||||
}
|
||||
|
||||
// I know, this is not a perfect solution, however it works for fixing subs
|
||||
|
@ -200,7 +167,7 @@ class CS3IPlayer : IPlayer {
|
|||
try {
|
||||
Handler(it).post {
|
||||
try {
|
||||
seekTime(1L)
|
||||
seekTime(1L, source = PlayerEventSource.Player)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
@ -254,8 +221,9 @@ class CS3IPlayer : IPlayer {
|
|||
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>? {
|
||||
if (id == null) return null
|
||||
// This beast of an expression does:
|
||||
|
@ -340,6 +308,7 @@ class CS3IPlayer : IPlayer {
|
|||
}.flatten()
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun Tracks.Group.getFormats(): List<Pair<Format, Int>> {
|
||||
return (0 until this.mediaTrackGroup.length).mapNotNull { i ->
|
||||
if (this.isSupported)
|
||||
|
@ -368,6 +337,7 @@ class CS3IPlayer : IPlayer {
|
|||
)
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
override fun getVideoTracks(): CurrentTracks {
|
||||
val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList()
|
||||
val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO }
|
||||
|
@ -387,6 +357,7 @@ class CS3IPlayer : IPlayer {
|
|||
/**
|
||||
* @return True if the player should be reloaded
|
||||
* */
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean {
|
||||
Log.i(TAG, "setPreferredSubtitles init $subtitle")
|
||||
currentSubtitles = subtitle
|
||||
|
@ -465,6 +436,7 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
override fun getAspectRatio(): Rational? {
|
||||
return exoPlayer?.videoFormat?.let { format ->
|
||||
Rational(format.width, format.height)
|
||||
|
@ -475,6 +447,7 @@ class CS3IPlayer : IPlayer {
|
|||
subtitleHelper.setSubStyle(style)
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
override fun saveData() {
|
||||
Log.i(TAG, "saveData")
|
||||
updatedTime()
|
||||
|
@ -504,14 +477,14 @@ class CS3IPlayer : IPlayer {
|
|||
Log.i(TAG, "onStop")
|
||||
|
||||
saveData()
|
||||
exoPlayer?.pause()
|
||||
handleEvent(CSPlayerEvent.Pause, PlayerEventSource.Player)
|
||||
//releasePlayer()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
Log.i(TAG, "onPause")
|
||||
saveData()
|
||||
exoPlayer?.pause()
|
||||
handleEvent(CSPlayerEvent.Pause, PlayerEventSource.Player)
|
||||
//releasePlayer()
|
||||
}
|
||||
|
||||
|
@ -548,6 +521,7 @@ class CS3IPlayer : IPlayer {
|
|||
|
||||
var requestSubtitleUpdate: (() -> Unit)? = null
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun createOnlineSource(headers: Map<String, String>): HttpDataSource.Factory {
|
||||
val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT)
|
||||
return source.apply {
|
||||
|
@ -555,6 +529,7 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory {
|
||||
val provider = getApiFromNameNull(link.source)
|
||||
val interceptor = provider?.getVideoInterceptor(link)
|
||||
|
@ -587,6 +562,7 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun Context.createOfflineSource(): DataSource.Factory {
|
||||
return DefaultDataSourceFactory(this, USER_AGENT)
|
||||
}
|
||||
|
@ -632,6 +608,7 @@ class CS3IPlayer : IPlayer {
|
|||
return Pair(subSources, activeSubtitles)
|
||||
}*/
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun getCache(context: Context, cacheSize: Long): SimpleCache? {
|
||||
return try {
|
||||
val databaseProvider = StandaloneDatabaseProvider(context)
|
||||
|
@ -663,6 +640,7 @@ class CS3IPlayer : IPlayer {
|
|||
return getMediaItemBuilder(mimeType).setUri(url).build()
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector {
|
||||
val trackSelector = DefaultTrackSelector(context)
|
||||
trackSelector.parameters = trackSelector.buildUponParameters()
|
||||
|
@ -676,6 +654,7 @@ class CS3IPlayer : IPlayer {
|
|||
|
||||
var currentTextRenderer: CustomTextRenderer? = null
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun buildExoPlayer(
|
||||
context: Context,
|
||||
mediaItemSlices: List<MediaItemSlice>,
|
||||
|
@ -710,13 +689,13 @@ class CS3IPlayer : IPlayer {
|
|||
metadataRendererOutput
|
||||
).map {
|
||||
if (it is TextRenderer) {
|
||||
currentTextRenderer = CustomTextRenderer(
|
||||
val currentTextRenderer = CustomTextRenderer(
|
||||
subtitleOffset,
|
||||
textRendererOutput,
|
||||
eventHandler.looper,
|
||||
CustomSubtitleDecoderFactory()
|
||||
)
|
||||
currentTextRenderer!!
|
||||
).also { this.currentTextRenderer = it }
|
||||
currentTextRenderer
|
||||
} else it
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
@ -760,15 +739,33 @@ class CS3IPlayer : IPlayer {
|
|||
|
||||
// If there is only one item then treat it as normal, if multiple: concatenate the items.
|
||||
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 {
|
||||
val source = ConcatenatingMediaSource()
|
||||
mediaItemSlices.map {
|
||||
mediaItemSlices.map { item ->
|
||||
source.addMediaSource(
|
||||
// The duration MUST be known for it to work properly, see https://github.com/google/ExoPlayer/issues/4727
|
||||
ClippingMediaSource(
|
||||
factory.createMediaSource(it.mediaItem),
|
||||
it.durationUs
|
||||
factory.createMediaSource(item.mediaItem),
|
||||
item.durationUs
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -801,43 +798,55 @@ class CS3IPlayer : IPlayer {
|
|||
return null
|
||||
}
|
||||
|
||||
fun updatedTime(writePosition: Long? = null) {
|
||||
fun updatedTime(
|
||||
writePosition: Long? = null,
|
||||
source: PlayerEventSource = PlayerEventSource.Player
|
||||
) {
|
||||
val position = writePosition ?: exoPlayer?.currentPosition
|
||||
|
||||
getCurrentTimestamp(position)?.let { timestamp ->
|
||||
onTimestampInvoked?.invoke(timestamp)
|
||||
event(TimestampInvokedEvent(timestamp, source))
|
||||
}
|
||||
|
||||
val duration = exoPlayer?.contentDuration
|
||||
if (duration != null && position != null) {
|
||||
playerPositionChanged?.invoke(Pair(position, duration))
|
||||
event(
|
||||
PositionEvent(
|
||||
source,
|
||||
fromMs = exoPlayer?.currentPosition ?: 0,
|
||||
position,
|
||||
duration
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun seekTime(time: Long) {
|
||||
exoPlayer?.seekTime(time)
|
||||
override fun seekTime(time: Long, source: PlayerEventSource) {
|
||||
exoPlayer?.seekTime(time, source)
|
||||
}
|
||||
|
||||
override fun seekTo(time: Long) {
|
||||
updatedTime(time)
|
||||
override fun seekTo(time: Long, source: PlayerEventSource) {
|
||||
updatedTime(time, source)
|
||||
exoPlayer?.seekTo(time)
|
||||
}
|
||||
|
||||
private fun ExoPlayer.seekTime(time: Long) {
|
||||
updatedTime(currentPosition + time)
|
||||
private fun ExoPlayer.seekTime(time: Long, source: PlayerEventSource) {
|
||||
updatedTime(currentPosition + time, source)
|
||||
seekTo(currentPosition + time)
|
||||
}
|
||||
|
||||
override fun handleEvent(event: CSPlayerEvent) {
|
||||
override fun handleEvent(event: CSPlayerEvent, source: PlayerEventSource) {
|
||||
Log.i(TAG, "handleEvent ${event.name}")
|
||||
try {
|
||||
exoPlayer?.apply {
|
||||
when (event) {
|
||||
CSPlayerEvent.Play -> {
|
||||
event(PlayEvent(source))
|
||||
play()
|
||||
}
|
||||
|
||||
CSPlayerEvent.Pause -> {
|
||||
event(PauseEvent(source))
|
||||
pause()
|
||||
}
|
||||
|
||||
|
@ -854,32 +863,32 @@ class CS3IPlayer : IPlayer {
|
|||
|
||||
CSPlayerEvent.PlayPauseToggle -> {
|
||||
if (isPlaying) {
|
||||
pause()
|
||||
handleEvent(CSPlayerEvent.Pause, source)
|
||||
} else {
|
||||
play()
|
||||
handleEvent(CSPlayerEvent.Play, source)
|
||||
}
|
||||
}
|
||||
|
||||
CSPlayerEvent.SeekForward -> seekTime(seekActionTime)
|
||||
CSPlayerEvent.SeekBack -> seekTime(-seekActionTime)
|
||||
CSPlayerEvent.NextEpisode -> nextEpisode?.invoke()
|
||||
CSPlayerEvent.PrevEpisode -> prevEpisode?.invoke()
|
||||
CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source)
|
||||
CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source)
|
||||
CSPlayerEvent.NextEpisode -> event(EpisodeSeekEvent(offset = 1, source = source))
|
||||
CSPlayerEvent.PrevEpisode -> event(EpisodeSeekEvent(offset = -1, source = source))
|
||||
CSPlayerEvent.SkipCurrentChapter -> {
|
||||
//val dur = this@CS3IPlayer.getDuration() ?: return@apply
|
||||
getCurrentTimestamp()?.let { lastTimeStamp ->
|
||||
if (lastTimeStamp.skipToNextEpisode) {
|
||||
handleEvent(CSPlayerEvent.NextEpisode)
|
||||
handleEvent(CSPlayerEvent.NextEpisode, source)
|
||||
} else {
|
||||
seekTo(lastTimeStamp.endMs + 1L)
|
||||
}
|
||||
onTimestampSkipped?.invoke(lastTimeStamp)
|
||||
event(TimestampSkippedEvent(timestamp = lastTimeStamp, source = source))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "handleEvent error", e)
|
||||
playerError?.invoke(e)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "handleEvent error", t)
|
||||
event(ErrorEvent(t))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -918,18 +927,14 @@ class CS3IPlayer : IPlayer {
|
|||
|
||||
requestSubtitleUpdate = ::reloadSubs
|
||||
|
||||
playerUpdated?.invoke(exoPlayer)
|
||||
event(PlayerAttachedEvent(exoPlayer))
|
||||
exoPlayer?.prepare()
|
||||
|
||||
exoPlayer?.let { exo ->
|
||||
updateIsPlaying?.invoke(
|
||||
Pair(
|
||||
CSPlayerLoading.IsBuffering,
|
||||
CSPlayerLoading.IsBuffering
|
||||
)
|
||||
)
|
||||
event(StatusEvent(CSPlayerLoading.IsBuffering, CSPlayerLoading.IsBuffering))
|
||||
isPlaying = exo.isPlaying
|
||||
}
|
||||
|
||||
exoPlayer?.addListener(object : Player.Listener {
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
normalSafeApiCall {
|
||||
|
@ -963,18 +968,19 @@ class CS3IPlayer : IPlayer {
|
|||
)
|
||||
}
|
||||
|
||||
embeddedSubtitlesFetched?.invoke(exoPlayerReportedTracks)
|
||||
onTracksInfoChanged?.invoke()
|
||||
subtitlesUpdates?.invoke()
|
||||
event(EmbeddedSubtitlesFetchedEvent(tracks = exoPlayerReportedTracks))
|
||||
event(TracksChangedEvent())
|
||||
event(SubtitlesUpdatedEvent())
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||
exoPlayer?.let { exo ->
|
||||
updateIsPlaying?.invoke(
|
||||
Pair(
|
||||
if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused,
|
||||
if (playbackState == Player.STATE_BUFFERING) CSPlayerLoading.IsBuffering else if (exo.isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused
|
||||
event(
|
||||
StatusEvent(
|
||||
wasPlaying = if (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
|
||||
|
@ -996,23 +1002,15 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
|
||||
Player.STATE_ENDED -> {
|
||||
// Only play next episode if autoplay is on (default)
|
||||
if (PreferenceManager.getDefaultSharedPreferences(context)
|
||||
?.getBoolean(
|
||||
context.getString(com.lagradost.cloudstream3.R.string.autoplay_next_key),
|
||||
true
|
||||
) == true
|
||||
) {
|
||||
handleEvent(CSPlayerEvent.NextEpisode)
|
||||
}
|
||||
event(VideoEndedEvent())
|
||||
}
|
||||
|
||||
Player.STATE_BUFFERING -> {
|
||||
updatedTime()
|
||||
updatedTime(source = PlayerEventSource.Player)
|
||||
}
|
||||
|
||||
Player.STATE_IDLE -> {
|
||||
// IDLE
|
||||
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
|
@ -1037,7 +1035,7 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
|
||||
else -> {
|
||||
playerError?.invoke(error)
|
||||
event(ErrorEvent(error))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1051,7 +1049,7 @@ class CS3IPlayer : IPlayer {
|
|||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
super.onIsPlayingChanged(isPlaying)
|
||||
if (isPlaying) {
|
||||
requestAutoFocus?.invoke()
|
||||
event(RequestAudioFocusEvent())
|
||||
onRenderFirst()
|
||||
}
|
||||
}
|
||||
|
@ -1071,12 +1069,15 @@ class CS3IPlayer : IPlayer {
|
|||
true
|
||||
) == true
|
||||
) {
|
||||
handleEvent(CSPlayerEvent.NextEpisode)
|
||||
handleEvent(
|
||||
CSPlayerEvent.NextEpisode,
|
||||
source = PlayerEventSource.Player
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Player.STATE_BUFFERING -> {
|
||||
updatedTime()
|
||||
updatedTime(source = PlayerEventSource.Player)
|
||||
}
|
||||
|
||||
Player.STATE_IDLE -> {
|
||||
|
@ -1089,27 +1090,29 @@ class CS3IPlayer : IPlayer {
|
|||
|
||||
override fun onVideoSizeChanged(videoSize: VideoSize) {
|
||||
super.onVideoSizeChanged(videoSize)
|
||||
playerDimensionsLoaded?.invoke(Pair(videoSize.width, videoSize.height))
|
||||
event(ResizedEvent(height = videoSize.height, width = videoSize.width))
|
||||
}
|
||||
|
||||
override fun onRenderedFirstFrame() {
|
||||
super.onRenderedFirstFrame()
|
||||
onRenderFirst()
|
||||
updatedTime()
|
||||
updatedTime(source = PlayerEventSource.Player)
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "loadExo error", e)
|
||||
playerError?.invoke(e)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "loadExo error", t)
|
||||
event(ErrorEvent(t))
|
||||
}
|
||||
}
|
||||
|
||||
private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList()
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) {
|
||||
lastTimeStamps = timeStamps
|
||||
timeStamps.forEach { timestamp ->
|
||||
exoPlayer?.createMessage { _, _ ->
|
||||
updatedTime()
|
||||
updatedTime(source = PlayerEventSource.Player)
|
||||
//if (payload is EpisodeSkip.SkipStamp) // this should always be true
|
||||
// onTimestampInvoked?.invoke(payload)
|
||||
}
|
||||
|
@ -1119,9 +1122,10 @@ class CS3IPlayer : IPlayer {
|
|||
?.setDeleteAfterDelivery(false)
|
||||
?.send()
|
||||
}
|
||||
updatedTime()
|
||||
updatedTime(source = PlayerEventSource.Player)
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
fun onRenderFirst() {
|
||||
if (hasUsedFirstRender) { // this insures that we only call this once per player load
|
||||
return
|
||||
|
@ -1139,7 +1143,7 @@ class CS3IPlayer : IPlayer {
|
|||
|
||||
if (invalid) {
|
||||
releasePlayer(saveTime = false)
|
||||
playerError?.invoke(InvalidFileException("Too short playback"))
|
||||
event(ErrorEvent(InvalidFileException("Too short playback")))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1148,7 +1152,7 @@ class CS3IPlayer : IPlayer {
|
|||
val width = format?.width
|
||||
val height = format?.height
|
||||
if (height != null && width != null) {
|
||||
playerDimensionsLoaded?.invoke(Pair(width, height))
|
||||
event(ResizedEvent(width = width, height = height))
|
||||
updatedTime()
|
||||
exoPlayer?.apply {
|
||||
requestedListeningPercentages?.forEach { percentage ->
|
||||
|
@ -1182,12 +1186,13 @@ class CS3IPlayer : IPlayer {
|
|||
|
||||
subtitleHelper.setActiveSubtitles(activeSubtitles.toSet())
|
||||
loadExo(context, listOf(MediaItemSlice(mediaItem, Long.MIN_VALUE)), subSources)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "loadOfflinePlayer error", e)
|
||||
playerError?.invoke(e)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "loadOfflinePlayer error", t)
|
||||
event(ErrorEvent(t))
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun getSubSources(
|
||||
onlineSourceFactory: HttpDataSource.Factory?,
|
||||
offlineSourceFactory: DataSource.Factory?,
|
||||
|
@ -1243,6 +1248,7 @@ class CS3IPlayer : IPlayer {
|
|||
return exoPlayer != null
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
private fun loadOnlinePlayer(context: Context, link: ExtractorLink) {
|
||||
Log.i(TAG, "loadOnlinePlayer $link")
|
||||
try {
|
||||
|
@ -1259,7 +1265,7 @@ class CS3IPlayer : IPlayer {
|
|||
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
|
||||
}
|
||||
|
||||
val mime = when(link.type) {
|
||||
val mime = when (link.type) {
|
||||
ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
||||
ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
|
||||
ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4
|
||||
|
@ -1267,12 +1273,29 @@ class CS3IPlayer : IPlayer {
|
|||
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)
|
||||
}
|
||||
} 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
|
||||
MediaItemSlice(getMediaItem(mime, link.url), Long.MIN_VALUE)
|
||||
)
|
||||
|
@ -1298,16 +1321,16 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
|
||||
loadExo(context, mediaItems, subSources, cacheFactory)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "loadOnlinePlayer error", e)
|
||||
playerError?.invoke(e)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "loadOnlinePlayer error", t)
|
||||
event(ErrorEvent(t))
|
||||
}
|
||||
}
|
||||
|
||||
override fun reloadPlayer(context: Context) {
|
||||
Log.i(TAG, "reloadPlayer")
|
||||
|
||||
exoPlayer?.release()
|
||||
releasePlayer(false)
|
||||
currentLink?.let {
|
||||
loadOnlinePlayer(context, it)
|
||||
} ?: currentDownloadedFile?.let {
|
||||
|
|
|
@ -38,6 +38,8 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.keyEventListener
|
||||
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.databinding.PlayerCustomLayoutBinding
|
||||
import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding
|
||||
|
@ -126,19 +128,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
protected var useTrueSystemBrightness = true
|
||||
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 navigationBarHeight: Int? = null
|
||||
|
||||
|
@ -874,7 +863,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
currentTouch
|
||||
)?.let { seekTo ->
|
||||
if (abs(seekTo - startTime) > MINIMUM_SEEK_TIME) {
|
||||
player.seekTo(seekTo)
|
||||
player.seekTo(seekTo, PlayerEventSource.UI)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -909,7 +898,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
|||
}
|
||||
|
||||
else -> {
|
||||
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
|
||||
player.handleEvent(CSPlayerEvent.PlayPauseToggle, PlayerEventSource.UI)
|
||||
}
|
||||
}
|
||||
} else if (doubleTapEnabled && isFullScreenPlayer) {
|
||||
|
|
|
@ -551,7 +551,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
//println("CURRENT SELECTED :$currentSelectedSubtitles of $currentSubs")
|
||||
context?.let { ctx ->
|
||||
val isPlaying = player.getIsPlaying()
|
||||
player.handleEvent(CSPlayerEvent.Pause)
|
||||
player.handleEvent(CSPlayerEvent.Pause, PlayerEventSource.UI)
|
||||
val currentSubtitles = sortSubs(currentSubs)
|
||||
|
||||
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")
|
||||
super.playerError(exception)
|
||||
}
|
||||
|
@ -945,14 +945,13 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
var maxEpisodeSet: Int? = null
|
||||
var hasRequestedStamps: Boolean = false
|
||||
override fun playerPositionChanged(posDur: Pair<Long, Long>) {
|
||||
override fun playerPositionChanged(position: Long, duration : Long) {
|
||||
// Don't save livestream data
|
||||
if ((currentMeta as? ResultEpisode)?.tvType?.isLiveStream() == true) return
|
||||
|
||||
// Don't save NSFW data
|
||||
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 (!hasRequestedStamps) {
|
||||
hasRequestedStamps = true
|
||||
|
@ -1209,8 +1208,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) {
|
||||
setPlayerDimen(widthHeight)
|
||||
override fun playerDimensionsLoaded(width: Int, height : Int) {
|
||||
setPlayerDimen(width to height)
|
||||
}
|
||||
|
||||
private fun unwrapBundle(savedInstanceState: Bundle?) {
|
||||
|
|
|
@ -45,9 +45,120 @@ enum class CSPlayerLoading {
|
|||
IsPaused,
|
||||
IsPlaying,
|
||||
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 {
|
||||
/**
|
||||
|
@ -108,27 +219,16 @@ interface IPlayer {
|
|||
fun getDuration(): Long?
|
||||
fun getPosition(): Long?
|
||||
|
||||
fun seekTime(time: Long)
|
||||
fun seekTo(time: Long)
|
||||
fun seekTime(time: Long, source: PlayerEventSource = PlayerEventSource.UI)
|
||||
fun seekTo(time: Long, source: PlayerEventSource = PlayerEventSource.UI)
|
||||
|
||||
fun getSubtitleOffset(): Long // in ms
|
||||
fun setSubtitleOffset(offset: Long) // in ms
|
||||
|
||||
fun initCallbacks(
|
||||
playerUpdated: (Any?) -> Unit, // attach player to view
|
||||
updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)? = null, // (wasPlaying, isPlaying)
|
||||
requestAutoFocus: (() -> Unit)? = null, // current player starts, asking for all other programs to shut the fuck up
|
||||
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)
|
||||
eventHandler: ((PlayerEvent) -> Unit),
|
||||
/** this is used to request when the player should report back view percentage */
|
||||
requestedListeningPercentages: List<Int>? = null,
|
||||
)
|
||||
|
||||
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 getCurrentPreferredSubtitle(): SubtitleData?
|
||||
|
||||
fun handleEvent(event: CSPlayerEvent)
|
||||
fun handleEvent(event: CSPlayerEvent, source: PlayerEventSource = PlayerEventSource.UI)
|
||||
|
||||
fun onStop()
|
||||
fun onPause()
|
||||
|
|
|
@ -67,9 +67,8 @@ class LinkGenerator(
|
|||
link.name ?: link.url,
|
||||
unshortenLinkSafe(link.url), // unshorten because it might be a raw link
|
||||
referer ?: "",
|
||||
Qualities.Unknown.value, isM3u8 ?: normalSafeApiCall {
|
||||
URI(link.url).path?.substringAfterLast(".")?.contains("m3u")
|
||||
} ?: false
|
||||
Qualities.Unknown.value,
|
||||
type = INFER_TYPE,
|
||||
) to null
|
||||
)
|
||||
}
|
||||
|
|
|
@ -130,8 +130,8 @@ open class ResultFragmentPhone : FullScreenPlayer() {
|
|||
return currentTrailerIndex + 1 < currentTrailers.size
|
||||
}
|
||||
|
||||
override fun playerError(exception: Exception) {
|
||||
if (player.getIsPlaying()) { // because we dont want random toasts in player
|
||||
override fun playerError(exception: Throwable) {
|
||||
if (player.getIsPlaying()) { // because we don't want random toasts in player
|
||||
super.playerError(exception)
|
||||
} else {
|
||||
nextMirror()
|
||||
|
|
|
@ -3,15 +3,17 @@ package com.lagradost.cloudstream3.ui.result
|
|||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isGone
|
||||
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.ui.player.CSPlayerEvent
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerEventSource
|
||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||
import com.lagradost.cloudstream3.utils.IOnBackPressed
|
||||
|
||||
|
@ -32,7 +34,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed {
|
|||
|
||||
override fun prevEpisode() {}
|
||||
|
||||
override fun playerPositionChanged(posDur: Pair<Long, Long>) {}
|
||||
override fun playerPositionChanged(position: Long, duration : Long) {}
|
||||
|
||||
override fun nextMirror() {}
|
||||
|
||||
|
@ -99,8 +101,8 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed {
|
|||
}
|
||||
}
|
||||
|
||||
override fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) {
|
||||
playerWidthHeight = widthHeight
|
||||
override fun playerDimensionsLoaded(width: Int, height : Int) {
|
||||
playerWidthHeight = width to height
|
||||
fixPlayerSize()
|
||||
}
|
||||
|
||||
|
@ -164,7 +166,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed {
|
|||
|
||||
playerBinding?.playerIntroPlay?.setOnClickListener {
|
||||
playerBinding?.playerIntroPlay?.isGone = true
|
||||
player.handleEvent(CSPlayerEvent.Play)
|
||||
player.handleEvent(CSPlayerEvent.Play, PlayerEventSource.UI)
|
||||
updateUIVisibility()
|
||||
fixPlayerSize()
|
||||
}
|
||||
|
|
|
@ -1538,7 +1538,11 @@ class ResultViewModel2 : ViewModel() {
|
|||
this.name,
|
||||
this.japName
|
||||
).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),
|
||||
this.year
|
||||
)
|
||||
|
|
|
@ -1,14 +1,204 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
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.extractors.*
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jsoup.Jsoup
|
||||
import java.net.URL
|
||||
import kotlin.collections.MutableList
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* For use in the ConcatenatingMediaSource.
|
||||
|
@ -37,7 +227,6 @@ data class ExtractorLinkPlayList(
|
|||
val playlist: List<PlayListItem>,
|
||||
override val referer: String,
|
||||
override val quality: Int,
|
||||
val isM3u8: Boolean = false,
|
||||
override val headers: Map<String, String> = mapOf(),
|
||||
/** Used for getExtractorVerifierJob() */
|
||||
override val extractorData: String? = null,
|
||||
|
@ -99,6 +288,84 @@ private fun inferTypeFromUrl(url: String): ExtractorLinkType {
|
|||
}
|
||||
}
|
||||
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 val source: String,
|
||||
open val name: String,
|
||||
|
@ -110,6 +377,9 @@ open class ExtractorLink constructor(
|
|||
open val extractorData: String? = null,
|
||||
open val type: ExtractorLinkType,
|
||||
) : VideoDownloadManager.IDownloadableMinimum {
|
||||
val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8
|
||||
val isDash : Boolean get() = type == ExtractorLinkType.DASH
|
||||
|
||||
constructor(
|
||||
source: String,
|
||||
name: String,
|
||||
|
|
|
@ -128,7 +128,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
|||
<FrameLayout
|
||||
android:id="@+id/background_poster_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:layout_height="150dp"
|
||||
android:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
|
@ -147,8 +147,6 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
|||
android:src="@drawable/background_shadow">
|
||||
|
||||
</ImageView>
|
||||
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
|
@ -411,7 +409,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
|||
android:padding="5dp"
|
||||
android:requiresFadingEdge="vertical"
|
||||
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. " />
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
|
@ -537,18 +535,32 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
|
|||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<ImageView
|
||||
<FrameLayout
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:id="@+id/episodes_shadow"
|
||||
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"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
android:layout_height="match_parent">
|
||||
<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"/>
|
||||
<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
|
||||
android:id="@+id/episode_holder_tv"
|
||||
|
|
|
@ -49,7 +49,8 @@
|
|||
android:layout_marginTop="10dp"
|
||||
android:text="@string/hls_playlist"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="16sp" />
|
||||
android:textSize="16sp"
|
||||
android:visibility="invisible" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
|
|
|
@ -200,4 +200,69 @@
|
|||
<string name="use">استخدم</string>
|
||||
<string name="unable_to_inflate">%sتعذر إنشاء واجهة المستخدم بشكل صحيح، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور</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>
|
||||
|
|
|
@ -157,7 +157,7 @@
|
|||
<string name="show_fillers_settings">Mostrar episódios de Filler em anime</string>
|
||||
<string name="show_trailers_settings">Mostrar trailers</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="updates_settings">Mostrar atualizações do app</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="tv_series_singular">Série</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="torrent_singular">Torrent</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_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_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_summary">Útil para burlar bloqueios de provedores de internet</string>
|
||||
<string name="add_site_pref">Clonar 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="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="resize_fit">Ajustar para a Tela</string>
|
||||
<string name="resize_fill">Esticar</string>
|
||||
|
@ -338,7 +338,7 @@
|
|||
<string name="subtitles_shadow">Sombreado</string>
|
||||
<string name="subtitles_raised">Em Relevo</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_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>
|
||||
|
@ -382,9 +382,9 @@
|
|||
<string name="resolution_and_title">Resolução e título</string>
|
||||
<string name="title">Título</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_url">URL invalido</string>
|
||||
<string name="error_invalid_url">URL inválida</string>
|
||||
<string name="error">Erro</string>
|
||||
<string name="subtitles_remove_captions">Remover legendas ocultas(CC) 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_deleted">Plugin Apagado</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_finish_format" formatted="true">Transferido %d %s com sucesso</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</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="plugin_singular">Plugin</string>
|
||||
|
@ -444,7 +444,7 @@
|
|||
<string name="browser">Navegador</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="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="play_livestream_button">Reproduzir Livestream</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>
|
||||
<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>
|
||||
<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="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="mobile_data">Dados móveis</string>
|
||||
<string name="profile_number">Perfil %d</string>
|
||||
|
@ -550,4 +550,21 @@
|
|||
<string name="audio_tracks">Faixas de áudio</string>
|
||||
<string name="sort_updated_new">Adicionado em (novo para antigo)</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>
|
||||
|
|
|
@ -5,19 +5,19 @@
|
|||
<string name="next_episode_format" formatted="true">Episode %d wird veröffentlicht in</string>
|
||||
<string name="result_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="backup_success">Daten erfolgreich gesichert</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="episode_action_chromecast_mirror">Chromecast-Mirror</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_intro">Intro</string>
|
||||
<string name="clear_history">Verlauf löschen</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="episode_poster_img_des">Episodenvorschaubild</string>
|
||||
<string name="home_main_poster_img_des">Medienvorschaubild</string>
|
||||
|
@ -34,7 +34,7 @@
|
|||
<string name="app_name">CloudStream</string>
|
||||
<string name="play_with_app_name">Mit CloudStream abspielen</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_settings">Einstellungen</string>
|
||||
<string name="search_hint">Suchen…</string>
|
||||
|
@ -44,8 +44,8 @@
|
|||
<string name="next_episode">Nächste Episode</string>
|
||||
<string name="result_tags">Genres</string>
|
||||
<string name="result_share">Teilen</string>
|
||||
<string name="result_open_in_browser">In Browser öffnen</string>
|
||||
<string name="skip_loading">Puffern überspringen</string>
|
||||
<string name="result_open_in_browser">Im Browser öffnen</string>
|
||||
<string name="skip_loading">Laden überspringen</string>
|
||||
<string name="loading">Lädt…</string>
|
||||
<string name="type_watching">Am schauen</string>
|
||||
<string name="type_on_hold">Pausiert</string>
|
||||
|
@ -79,7 +79,7 @@
|
|||
<string name="popup_play_file">Datei abspielen</string>
|
||||
<string name="popup_resume_download">Download fortsetzen</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_expanded_hide">Verstecken</string>
|
||||
<string name="home_play">Abspielen</string>
|
||||
|
@ -106,8 +106,8 @@
|
|||
<string name="subs_font_size">Schriftgröße</string>
|
||||
<string name="search_provider_text_providers">Suche anhand Anbietern</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_none">Noch keine Benenes verteilt</string>
|
||||
<string name="benene_count_text">%d Benenes an die Devs geschenkt</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_download_languages">Sprachen herunterladen</string>
|
||||
<string name="subs_subtitle_languages">Untertitelsprache</string>
|
||||
|
@ -117,8 +117,8 @@
|
|||
<string name="action_open_watching">Mehr Infos</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_torrent">Dieser Anbieter bietet Torrents an, ein VPN wird 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="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 nicht auf der Website vorhanden sind.</string>
|
||||
<string name="torrent_plot">Beschreibung</string>
|
||||
<string name="normal_no_plot">Keine Handlung 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_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_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_des">Systemhelligkeit anstelle eines dunklen Overlay im Player verwenden</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_trailers_settings">Trailer 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="updates_settings">App-Updates anzeigen</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="lightnovel">Light Novel App von denselben Entwicklern</string>
|
||||
<string name="anim">Anime App von denselben Entwicklern</string>
|
||||
<string name="discord">Discord beitreten</string>
|
||||
<string name="benene">Eine Benene an die Devs verteilen</string>
|
||||
<string name="benene_des">Verteilte Benenes</string>
|
||||
<string name="discord">Trete dem Discord Server bei</string>
|
||||
<string name="benene">Eine Benene an die Devs schenken</string>
|
||||
<string name="benene_des">Geschenkte Benenes</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="play_episode_toast">Episode abspielen</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="render_error">Renderfehler</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_play_in_format">In %s 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="poster_ui_settings">UI-Elemente auf Vorschaubild umschalten</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_aspect_ratio_resize">Skalieren</string>
|
||||
<string name="video_source">Quelle</string>
|
||||
|
@ -270,16 +270,16 @@
|
|||
<string name="video_buffer_length_settings">Videopufferlänge</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_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_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_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. 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_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="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="download_path_pref">Downloadpfad</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_fill">Strecken</string>
|
||||
<string name="resize_zoom">Vergrößern</string>
|
||||
|
@ -308,7 +308,7 @@
|
|||
<string name="example_ip">127.0.0.1</string>
|
||||
<string name="example_site_name">MeineCooleSeite</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="account">Account</string>
|
||||
<string name="logout">Ausloggen</string>
|
||||
|
@ -317,13 +317,13 @@
|
|||
<string name="add_account">Account hinzufügen</string>
|
||||
<string name="create_account">Account erstellen</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="sync_score">Bewertung</string>
|
||||
<string name="sync_score_format" formatted="true">%d / 10</string>
|
||||
<string name="sync_total_episodes_none">/\?\?</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="none">Keine</string>
|
||||
<string name="normal">Normal</string>
|
||||
|
@ -335,10 +335,10 @@
|
|||
<string name="subtitles_shadow">Schatten</string>
|
||||
<string name="subtitles_raised">Erhöht</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_extra_hint_later_format">Verwenden, wenn die Untertitel %dms 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_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 %d ms zu spät angezeigt werden</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="recommended">Empfohlen</string>
|
||||
|
@ -359,7 +359,7 @@
|
|||
<string name="quality_hd">HD</string>
|
||||
<string name="quality_ts">TS</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_dvd">DVD</string>
|
||||
<string name="quality_4k">4K</string>
|
||||
|
@ -408,7 +408,7 @@
|
|||
<string name="plugin">Plugins</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="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_disabled" formatted="true">Deaktiviert: %d</string>
|
||||
<string name="plugins_not_downloaded" formatted="true">Nicht heruntergeladen: %d</string>
|
||||
|
@ -416,7 +416,7 @@
|
|||
\n
|
||||
\nAufgrund eines hirnlosen DMCA-Takedowns durch Sky UK Limited 🤮 können wir die Repository-Site nicht in der App verlinken.
|
||||
\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_short">Öffentliche Liste</string>
|
||||
<string name="uppercase_all_subtitles">Alle Untertitel in Großbuchstaben</string>
|
||||
|
@ -427,7 +427,7 @@
|
|||
<string name="video_tracks">Videospuren</string>
|
||||
<string name="apply_on_restart">Bei Neustart anwenden</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="extension_rating" formatted="true">Bewertung: %s</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="redo_setup_process">Einrichtungsvorgang wiederholen</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="pref_category_links">Links</string>
|
||||
<string name="pref_category_app_updates">App-Updates</string>
|
||||
|
@ -482,7 +482,7 @@
|
|||
<string name="no">Nein</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_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_package_installer">PackageInstaller</string>
|
||||
<string name="update_started">Aktualisierung gestartet</string>
|
||||
|
@ -493,18 +493,18 @@
|
|||
<string name="browser">Browser</string>
|
||||
<string name="sort_by">Sortieren nach</string>
|
||||
<string name="sort">Sortieren</string>
|
||||
<string name="sort_rating_desc">Bewertung (gut bis schlecht)</string>
|
||||
<string name="sort_rating_asc">Bewertung (schlecht bis gut)</string>
|
||||
<string name="sort_updated_new">Aktualisiert (neu bis alt)</string>
|
||||
<string name="sort_updated_old">Aktualisiert (alt bis neu)</string>
|
||||
<string name="sort_alphabetical_a">Alphabetisch (A bis Z)</string>
|
||||
<string name="sort_alphabetical_z">Alphabetisch (Z bis A)</string>
|
||||
<string name="sort_rating_desc">Bewertung (gut zu schlecht)</string>
|
||||
<string name="sort_rating_asc">Bewertung (schlecht zu gut)</string>
|
||||
<string name="sort_updated_new">Aktualisiert (neu zu alt)</string>
|
||||
<string name="sort_updated_old">Aktualisiert (alt zu neu)</string>
|
||||
<string name="sort_alphabetical_a">Alphabetisch (A zu Z)</string>
|
||||
<string name="sort_alphabetical_z">Alphabetisch (Z zu A)</string>
|
||||
<string name="select_library">Bibliothek auswählen</string>
|
||||
<string name="open_with">Öffnen mit</string>
|
||||
<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>
|
||||
<string name="empty_library_logged_in_message">Diese Liste ist leer. Versuche zu einer anderen Liste zu wechseln.</string>
|
||||
<string name="safe_mode_file">Datei für abgesicherten Modus gefunden!
|
||||
<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 den abgesicherten Modus gefunden!
|
||||
\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_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="already_voted">Es wurde bereits abgestimmt</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="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="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="disable">Deaktivieren</string>
|
||||
</resources>
|
||||
|
|
|
@ -540,7 +540,7 @@
|
|||
\n
|
||||
\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_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="set_default">Définir par défaut</string>
|
||||
<string name="use">Utiliser</string>
|
||||
|
@ -552,4 +552,6 @@
|
|||
<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="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>
|
||||
|
|
|
@ -566,4 +566,15 @@
|
|||
<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="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>
|
||||
|
|
|
@ -219,7 +219,7 @@
|
|||
<string name="status_ongoing">Folyamatban levő</string>
|
||||
<string name="year">Év</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="remote_error">Távoli 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="backup_settings">Biztonsági mentés</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_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>
|
||||
|
@ -510,4 +510,5 @@
|
|||
<string name="tv_layout">TV elrendezés</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="automatic_plugin_download_mode_title">Válassza ki a módot a pluginek letöltésének szűréséhez</string>
|
||||
</resources>
|
||||
|
|
|
@ -153,4 +153,9 @@
|
|||
<string name="updates_settings">ଆପ୍ ଅଦ୍ୟତନ ଦେଖାଇବା</string>
|
||||
<string name="update_started">ଅଦ୍ୟତନ ଆରମ୍ଭ ହୋଇଛି</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>
|
||||
|
|
|
@ -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="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="already_voted">Ați votat deja</string>
|
||||
</resources>
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
<string name="title_home">முகப்பு</string>
|
||||
<string name="title_search">தேடு</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="next_episode">அடுத்த எபிசோட்</string>
|
||||
<string name="next_episode">அடுத்த அத்தியாயம்</string>
|
||||
<string name="result_tags">வகைகள்</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="type_watching">பார்த்து கொண்டிருப்பது</string>
|
||||
<string name="type_on_hold">நிறுத்தி வைக்கப்பட்டுள்ளது</string>
|
||||
|
@ -21,9 +21,9 @@
|
|||
<string name="play_torrent_button">ஸ்ட்ரீம் டோரண்ட்</string>
|
||||
<string name="pick_subtitle">வசன வரிகள்</string>
|
||||
<string name="go_back">பின் செல்</string>
|
||||
<string name="play_episode">எபிசோடை இயக்கு</string>
|
||||
<string name="play_episode">அத்தியாயத்தை இயக்கு</string>
|
||||
<string name="download">எபிசோட் பதிவிற்கான அனுமதி கொடுக்கவும்</string>
|
||||
<string name="downloaded">பதிவிறக்கம் செய்யப்பட்டது</string>
|
||||
<string name="downloaded">பதிவிறக்கப்பட்டது</string>
|
||||
<string name="downloading">பதிவிறக்குகிறது</string>
|
||||
<string name="download_paused">பதிவிறக்கம் இடைநிறுத்தப்பட்டது</string>
|
||||
<string name="download_started">பதிவிறக்கம் தொடங்கியது</string>
|
||||
|
@ -67,10 +67,10 @@
|
|||
<string name="loading">ஏற்றுகிறது…</string>
|
||||
<string name="type_dropped">கைவிடப்பட்டது</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_livestream_button">லைவ்ஸ்ட்ரீம் இயக்கு</string>
|
||||
<string name="play_trailer_button">டிரெய்லரை இயக்கவும்</string>
|
||||
<string name="play_trailer_button">டிரெய்லரை இயக்கு</string>
|
||||
<string name="pick_source">மூலம்</string>
|
||||
<string name="error_loading_links_toast">இணைப்புகளை ஏற்றுவதில் பிழை</string>
|
||||
<string name="home_play">இயக்கு</string>
|
||||
|
@ -107,4 +107,14 @@
|
|||
<string name="double_tap_to_pause_settings">இடைநிறுத்துவதற்கு இருமுறை தட்டவும்</string>
|
||||
<string name="chromecast_subtitles_settings_des">Chromecast வசன அமைப்புகள்</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->%s</string>
|
||||
<string name="filler" formatted="true">நிரப்பி</string>
|
||||
</resources>
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<string name="preview_background_img_des">Попередній перегляд фону</string>
|
||||
<string name="player_speed_text_format" formatted="true">Швидкість (%.2fx)</string>
|
||||
<string name="new_update_format" formatted="true">Знайдено нове оновлення!
|
||||
\n%s -> %s</string>
|
||||
\n%s –> %s</string>
|
||||
<string name="title_search">Пошук</string>
|
||||
<string name="title_downloads">Завантаження</string>
|
||||
<string name="duration_format" formatted="true">%d хв</string>
|
||||
|
@ -37,7 +37,7 @@
|
|||
<string name="type_dropped">Покинуто</string>
|
||||
<string name="play_movie_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="go_back">Назад</string>
|
||||
<string name="play_episode">Переглянути епізод</string>
|
||||
|
@ -75,7 +75,7 @@
|
|||
<string name="continue_watching">Продовжити перегляд</string>
|
||||
<string name="action_remove_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="normal_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_des">Налаштування субтитрів Chromecast</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="autoplay_next_settings_des">Відтворювати наступний епізод після закінчення поточного</string>
|
||||
<string name="autoplay_next_settings_des">Відтворює наступний епізод після закінчення поточного</string>
|
||||
<string name="title_home">Головна</string>
|
||||
<string name="app_name">CloudStream</string>
|
||||
<string name="filler" formatted="true">Філер</string>
|
||||
|
@ -130,7 +130,7 @@
|
|||
<string name="picture_in_picture">Картинка в картинці</string>
|
||||
<string name="player_subtitles_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_pause_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="use_system_brightness_settings_des">Використовуйте системну яскравість у плеєрі замість темної накладки</string>
|
||||
<string name="restore_success">Завантажено файл резервної копії</string>
|
||||
<string name="torrent">Торренти</string>
|
||||
<string name="torrent">Торенти</string>
|
||||
<string name="episode_sync_settings_des">Автоматична синхронізація прогресу поточного епізоду</string>
|
||||
<string name="backup_failed">Відсутні дозволи на зберігання. Будь ласка, спробуйте ще раз.</string>
|
||||
<string name="kitsu_settings">Показувати постери від Kitsu</string>
|
||||
|
@ -256,7 +256,7 @@
|
|||
<string name="nsfw">NSFW</string>
|
||||
<string name="movies_singular">Фільм</string>
|
||||
<string name="ova_singular">OVA</string>
|
||||
<string name="torrent_singular">Торрент</string>
|
||||
<string name="torrent_singular">Торент</string>
|
||||
<string name="show_hd">Мітка якості</string>
|
||||
<string name="nsfw_singular">NSFW</string>
|
||||
<string name="episode_action_play_in_browser">Переглянути в браузері</string>
|
||||
|
@ -294,9 +294,9 @@
|
|||
<string name="primary_color_settings">Основний колір</string>
|
||||
<string name="app_theme_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_username">Моє круте ім\'я</string>
|
||||
<string name="example_username">Моє круте ім’я</string>
|
||||
<string name="example_email">hello@world.com</string>
|
||||
<string name="example_site_name">Мій крутий сайт</string>
|
||||
<string name="example_lang_name">Код мови (uk)</string>
|
||||
|
@ -348,9 +348,9 @@
|
|||
<string name="limit_title_rez">Роздільна здатність відеоплеєра</string>
|
||||
<string name="video_buffer_length_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="video_disk_description">Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об\'ємом вільної пам\'яті, наприклад Android TV.</string>
|
||||
<string name="video_disk_description">Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об’ємом вільної пам’яті, наприклад Android TV.</string>
|
||||
<string name="dns_pref">DNS через HTTPS</string>
|
||||
<string name="download_path_pref">Шлях завантаження</string>
|
||||
<string name="add_site_summary">Додайте клон існуючого сайту, з іншою URL-адресою</string>
|
||||
|
@ -374,10 +374,10 @@
|
|||
<string name="sync_score">Оцінений</string>
|
||||
<string name="player_load_subtitles">Завантажити з файлу</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_extra_hint_later_format">Використовуйте цей параметр, якщо субтитри з\'являються на %d мс занадто рано</string>
|
||||
<string name="subtitle_offset_extra_hint_before_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="player_loaded_subtitles" formatted="true">Завантажено %s</string>
|
||||
<string name="actor_supporting">Підтримка</string>
|
||||
<string name="actor_background">Фон</string>
|
||||
|
@ -507,8 +507,8 @@
|
|||
<string name="safe_mode_file">Файл безпечного режиму знайдено!
|
||||
\nРозширеня не завантажуються під час запуску, доки файл не буде видалено.</string>
|
||||
<string name="pref_category_android_tv">Android TV</string>
|
||||
<string name="android_tv_interface_off_seek_settings">Плеєр сховано - обсяг перемотки</string>
|
||||
<string name="android_tv_interface_on_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_summary">Обсяг перемотки, який використовується, коли плеєр видимий</string>
|
||||
<string name="android_tv_interface_off_seek_settings_summary">Обсяг перемотки, який використовується, коли плеєр прихований</string>
|
||||
<string name="test_failed">Тест провалено</string>
|
||||
|
@ -532,7 +532,7 @@
|
|||
<string name="set_default">Встановити за замовчуванням</string>
|
||||
<string name="profiles">Профілі</string>
|
||||
<string name="help">Допомога</string>
|
||||
<string name="quality_profile_help">Тут ви можете змінити порядок джерел. Якщо відео має вищий пріоритет, воно з\'явиться вище у списку джерел. Сума пріоритету джерела та пріоритету якості є пріоритетом відео.
|
||||
<string name="quality_profile_help">Тут ви можете змінити порядок джерел. Якщо відео має вищий пріоритет, воно з’явиться вище у списку джерел. Сума пріоритету джерела та пріоритету якості є пріоритетом відео.
|
||||
\n
|
||||
\nДжерело A: 3
|
||||
\nЯкість B: 7
|
||||
|
|
1
fastlane/metadata/android/or/changelogs/2.txt
Normal file
1
fastlane/metadata/android/or/changelogs/2.txt
Normal file
|
@ -0,0 +1 @@
|
|||
- ପରିବର୍ତ୍ତନ ପୋଥି ଯୋଡ଼ାଗଲା!
|
|
@ -1 +1 @@
|
|||
- Додано журнал змін!
|
||||
– Додано журнал змін!
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue