mirror of
https://github.com/recloudstream/cloudstream.git
synced 2026-08-23 08:33:16 +00:00
Compare commits
2 commits
master
...
migration-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
137d6173b4 |
||
|
|
abd73b4f12 |
140 changed files with 2137 additions and 5891 deletions
|
|
@ -276,8 +276,6 @@ dependencies {
|
|||
implementation(libs.jackson.module.kotlin) // JSON Parser
|
||||
implementation(libs.zipline)
|
||||
|
||||
// Temp/deprecated; will be removed once extensions have time to migrate from using it
|
||||
implementation("com.google.code.gson:gson:2.11.0")
|
||||
// Deprecated; will be removed once extensions have time to migrate from using it
|
||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
||||
|
||||
|
|
@ -325,9 +323,11 @@ tasks.withType<KotlinJvmCompile> {
|
|||
compilerOptions {
|
||||
jvmTarget.set(javaTarget)
|
||||
jvmDefault.set(JvmDefaultMode.ENABLE)
|
||||
freeCompilerArgs.add("-Xannotation-default-target=param-property")
|
||||
optIn.addAll(
|
||||
"com.lagradost.cloudstream3.InternalAPI",
|
||||
"com.lagradost.cloudstream3.Prerelease",
|
||||
"kotlin.uuid.ExperimentalUuidApi",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
package com.lagradost.cloudstream3.utils.serializers
|
||||
|
||||
import android.net.Uri
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.KeepGeneratedSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@KeepGeneratedSerializer
|
||||
@Serializable(with = NonEmptyData.Serializer::class)
|
||||
data class NonEmptyData(
|
||||
val title: String = "",
|
||||
val tags: List<String> = emptyList(),
|
||||
val meta: Map<String, String> = emptyMap(),
|
||||
val name: String = "hello",
|
||||
) {
|
||||
object Serializer : NonEmptySerializer<NonEmptyData>(NonEmptyData.generatedSerializer())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@KeepGeneratedSerializer
|
||||
@Serializable(with = WriteOnlyData.Serializer::class)
|
||||
data class WriteOnlyData(
|
||||
val fieldA: String = "",
|
||||
val fieldB: String = "",
|
||||
) {
|
||||
object Serializer : WriteOnlySerializer<WriteOnlyData>(
|
||||
WriteOnlyData.generatedSerializer(),
|
||||
setOf("fieldB"),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@KeepGeneratedSerializer
|
||||
@Serializable(with = MultiWriteOnly.Serializer::class)
|
||||
data class MultiWriteOnly(
|
||||
val fieldA: String = "",
|
||||
val fieldB: String = "",
|
||||
val fieldC: String = "",
|
||||
) {
|
||||
object Serializer : WriteOnlySerializer<MultiWriteOnly>(
|
||||
MultiWriteOnly.generatedSerializer(),
|
||||
setOf("fieldB", "fieldC"),
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UriData(
|
||||
@Serializable(with = UriSerializer::class)
|
||||
val uri: Uri = Uri.EMPTY,
|
||||
)
|
||||
|
||||
class SerializerTest {
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerOmitsEmptyStrings() {
|
||||
val data = NonEmptyData(title = "", name = "hello")
|
||||
val result = data.toJson()
|
||||
assertFalse(result.contains("title"))
|
||||
assertTrue(result.contains("name"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerOmitsEmptyLists() {
|
||||
val data = NonEmptyData(tags = emptyList(), name = "hello")
|
||||
val result = data.toJson()
|
||||
assertFalse(result.contains("tags"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerOmitsEmptyMaps() {
|
||||
val data = NonEmptyData(meta = emptyMap(), name = "hello")
|
||||
val result = data.toJson()
|
||||
assertFalse(result.contains("meta"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerKeepsNonEmptyFields() {
|
||||
val data = NonEmptyData(title = "hello", tags = listOf("a"), meta = mapOf("k" to "v"))
|
||||
val result = data.toJson()
|
||||
assertTrue(result.contains("title"))
|
||||
assertTrue(result.contains("tags"))
|
||||
assertTrue(result.contains("meta"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerDoesNotAffectDeserialization() {
|
||||
val input = """{"title":"hello","tags":["a"],"meta":{"k":"v"},"name":"world"}"""
|
||||
val result = parseJson<NonEmptyData>(input)
|
||||
assertEquals("hello", result.title)
|
||||
assertEquals(listOf("a"), result.tags)
|
||||
assertEquals(mapOf("k" to "v"), result.meta)
|
||||
assertEquals("world", result.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOnlySerializerOmitsFieldOnSerialize() {
|
||||
val data = WriteOnlyData(fieldA = "hello", fieldB = "secret")
|
||||
val result = data.toJson()
|
||||
assertTrue(result.contains("fieldA"))
|
||||
assertFalse(result.contains("fieldB"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOnlySerializerDeserializesNormally() {
|
||||
val input = """{"fieldA":"hello","fieldB":"secret"}"""
|
||||
val result = parseJson<WriteOnlyData>(input)
|
||||
assertEquals("hello", result.fieldA)
|
||||
assertEquals("secret", result.fieldB)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOnlySerializerDeserializesMissingAsDefault() {
|
||||
val input = """{"fieldA":"hello"}"""
|
||||
val result = parseJson<WriteOnlyData>(input)
|
||||
assertEquals("hello", result.fieldA)
|
||||
assertEquals("", result.fieldB)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOnlySerializerHandlesMultipleKeys() {
|
||||
val data = MultiWriteOnly(fieldA = "hello", fieldB = "secret1", fieldC = "secret2")
|
||||
val result = data.toJson()
|
||||
assertTrue(result.contains("fieldA"))
|
||||
assertFalse(result.contains("fieldB"))
|
||||
assertFalse(result.contains("fieldC"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uriSerializerSerializesUriToString() {
|
||||
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
|
||||
val result = data.toJson()
|
||||
assertTrue(result.contains("https://example.com/path?query=1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uriSerializerDeserializesStringToUri() {
|
||||
val input = """{"uri":"https://example.com/path?query=1"}"""
|
||||
val result = parseJson<UriData>(input)
|
||||
assertEquals(Uri.parse("https://example.com/path?query=1"), result.uri)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uriSerializerRoundtripsCorrectly() {
|
||||
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
|
||||
val encoded = data.toJson()
|
||||
val decoded = parseJson<UriData>(encoded)
|
||||
assertEquals(data.uri, decoded.uri)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package com.lagradost.cloudstream3.utils.serializers
|
||||
|
||||
import android.net.Uri
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
@Serializable
|
||||
data class UriData(
|
||||
@Serializable(with = UriSerializer::class)
|
||||
@SerialName("uri") val uri: Uri = Uri.EMPTY,
|
||||
)
|
||||
|
||||
class UriSerializerTest {
|
||||
|
||||
@Test
|
||||
fun uriSerializerSerializesUriToString() {
|
||||
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
|
||||
val result = data.toJson()
|
||||
assertTrue(result.contains("https://example.com/path?query=1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uriSerializerDeserializesStringToUri() {
|
||||
val input = """{"uri":"https://example.com/path?query=1"}"""
|
||||
val result = parseJson<UriData>(input)
|
||||
assertEquals(Uri.parse("https://example.com/path?query=1"), result.uri)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uriSerializerRoundtripsCorrectly() {
|
||||
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
|
||||
val encoded = data.toJson()
|
||||
val decoded = parseJson<UriData>(encoded)
|
||||
assertEquals(data.uri, decoded.uri)
|
||||
}
|
||||
}
|
||||
|
|
@ -113,7 +113,7 @@ class CloudStreamApp : Application(), SingletonImageLoader.Factory {
|
|||
get() = _context?.get()
|
||||
private set(value) {
|
||||
_context = WeakReference(value)
|
||||
setContext(value)
|
||||
setContext(WeakReference(value))
|
||||
}
|
||||
|
||||
fun <T : Any> getKeyClass(path: String, valueType: Class<T>): T? {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ import com.lagradost.cloudstream3.APIHolder
|
|||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.base64Encode
|
||||
import com.lagradost.cloudstream3.splitUrlParameters
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
|
||||
import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
import java.net.URI
|
||||
import java.security.SecureRandom
|
||||
|
||||
data class AuthLoginPage(
|
||||
|
|
@ -171,8 +172,10 @@ abstract class AuthAPI {
|
|||
get() = unixTimeMS
|
||||
|
||||
fun splitRedirectUrl(redirectUrl: String): Map<String, String> {
|
||||
return splitUrlParameters(
|
||||
redirectUrl.replace(APP_STRING, "https").replace("/#", "?")
|
||||
return splitQuery(
|
||||
URI(
|
||||
redirectUrl.replace(APP_STRING, "https").replace("/#", "?")
|
||||
).toURL()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import android.widget.ImageView
|
|||
import android.widget.LinearLayout
|
||||
import android.widget.ListView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.google.android.gms.cast.MediaLoadOptions
|
||||
import com.google.android.gms.cast.MediaQueueItem
|
||||
import com.google.android.gms.cast.MediaSeekOptions
|
||||
|
|
@ -35,24 +34,35 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData
|
|||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment
|
||||
import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks
|
||||
import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.json.JSONObject
|
||||
|
||||
/*class SkipOpController(val view: ImageView) : UIController() {
|
||||
init {
|
||||
view.setImageResource(R.drawable.exo_controls_fastforward)
|
||||
view.setOnClickListener {
|
||||
remoteMediaClient?.let {
|
||||
val options = MediaSeekOptions.Builder()
|
||||
.setPosition(it.approximateStreamPosition + 85000)
|
||||
it.seek(options.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
private fun RemoteMediaClient.getItemIndex(): Int? {
|
||||
return try {
|
||||
val index = this.mediaQueue.itemIds.indexOf(this.currentItem?.itemId ?: 0)
|
||||
if (index < 0) null else index
|
||||
} catch (_: Exception) {
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -79,41 +89,48 @@ class SkipNextEpisodeController(val view: ImageView) : UIController() {
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MetadataHolder(
|
||||
@JsonProperty("apiName") @SerialName("apiName") val apiName: String,
|
||||
@JsonProperty("isMovie") @SerialName("isMovie") val isMovie: Boolean,
|
||||
@JsonProperty("title") @SerialName("title") val title: String?,
|
||||
@JsonProperty("poster") @SerialName("poster") val poster: String?,
|
||||
@JsonProperty("currentEpisodeIndex") @SerialName("currentEpisodeIndex") val currentEpisodeIndex: Int,
|
||||
@JsonProperty("episodes") @SerialName("episodes") val episodes: List<ResultEpisode>,
|
||||
@JsonProperty("currentLinks") @SerialName("currentLinks") val currentLinks: List<ExtractorLink>,
|
||||
@JsonProperty("currentSubtitles") @SerialName("currentSubtitles") val currentSubtitles: List<SubtitleData>,
|
||||
val apiName: String,
|
||||
val isMovie: Boolean,
|
||||
val title: String?,
|
||||
val poster: String?,
|
||||
val currentEpisodeIndex: Int,
|
||||
val episodes: List<ResultEpisode>,
|
||||
val currentLinks: List<ExtractorLink>,
|
||||
val currentSubtitles: List<SubtitleData>
|
||||
)
|
||||
|
||||
class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : UIController() {
|
||||
class SelectSourceController(val view: ImageView, val activity: ControllerActivity) :
|
||||
UIController() {
|
||||
init {
|
||||
view.setImageResource(R.drawable.ic_baseline_playlist_play_24)
|
||||
view.setOnClickListener {
|
||||
// lateinit var dialog: AlertDialog
|
||||
val holder = getCurrentMetaData()
|
||||
|
||||
if (holder != null) {
|
||||
val items = holder.currentLinks
|
||||
if (items.isNotEmpty() && remoteMediaClient?.currentItem != null) {
|
||||
val subTracks = remoteMediaClient?.mediaInfo?.mediaTracks?.filter { it.type == MediaTrack.TYPE_TEXT }
|
||||
?: ArrayList()
|
||||
val subTracks =
|
||||
remoteMediaClient?.mediaInfo?.mediaTracks?.filter { it.type == MediaTrack.TYPE_TEXT }
|
||||
?: ArrayList()
|
||||
|
||||
val bottomSheetDialogBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack)
|
||||
val bottomSheetDialogBuilder =
|
||||
AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack)
|
||||
bottomSheetDialogBuilder.setView(R.layout.sort_bottom_sheet)
|
||||
|
||||
val bottomSheetDialog = bottomSheetDialogBuilder.create()
|
||||
bottomSheetDialog.show()
|
||||
|
||||
val providerList = bottomSheetDialog.findViewById<ListView>(R.id.sort_providers)!!
|
||||
val subtitleList = bottomSheetDialog.findViewById<ListView>(R.id.sort_subtitles)!!
|
||||
// bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet)
|
||||
val providerList =
|
||||
bottomSheetDialog.findViewById<ListView>(R.id.sort_providers)!!
|
||||
val subtitleList =
|
||||
bottomSheetDialog.findViewById<ListView>(R.id.sort_subtitles)!!
|
||||
if (subTracks.isEmpty()) {
|
||||
bottomSheetDialog.findViewById<LinearLayout>(R.id.sort_subtitles_holder)?.visibility = GONE
|
||||
bottomSheetDialog.findViewById<LinearLayout>(R.id.sort_subtitles_holder)?.visibility =
|
||||
GONE
|
||||
} else {
|
||||
val arrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
|
||||
val arrayAdapter =
|
||||
ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
|
||||
arrayAdapter.add(view.context.getString(R.string.no_subtitles))
|
||||
arrayAdapter.addAll(subTracks.mapNotNull { it.name })
|
||||
|
||||
|
|
@ -121,8 +138,10 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
subtitleList.adapter = arrayAdapter
|
||||
|
||||
val currentTracks = remoteMediaClient?.mediaStatus?.activeTrackIds
|
||||
val subtitleIndex = if (currentTracks == null) 0 else subTracks.map { it.id }
|
||||
.indexOfFirst { currentTracks.contains(it) } + 1
|
||||
|
||||
val subtitleIndex =
|
||||
if (currentTracks == null) 0 else subTracks.map { it.id }
|
||||
.indexOfFirst { currentTracks.contains(it) } + 1
|
||||
|
||||
subtitleList.setSelection(subtitleIndex)
|
||||
subtitleList.setItemChecked(subtitleIndex, true)
|
||||
|
|
@ -134,7 +153,9 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
ChromecastSubtitlesFragment.getCurrentSavedStyle().apply {
|
||||
val font = TextTrackStyle()
|
||||
font.setFontFamily(fontFamily ?: "Google Sans")
|
||||
fontGenericFamily?.let { font.fontGenericFamily = it }
|
||||
fontGenericFamily?.let {
|
||||
font.fontGenericFamily = it
|
||||
}
|
||||
font.windowColor = windowColor
|
||||
font.backgroundColor = backgroundColor
|
||||
|
||||
|
|
@ -151,7 +172,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
if (!it.status.isSuccess) {
|
||||
Log.e(
|
||||
"CHROMECAST", "Failed with status code:" +
|
||||
it.status.statusCode + " > " + it.status.statusMessage
|
||||
it.status.statusCode + " > " + it.status.statusMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -160,15 +181,17 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
}
|
||||
}
|
||||
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages.MediaInformation
|
||||
//https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages.MediaInformation
|
||||
val contentUrl = (remoteMediaClient?.currentItem?.media?.contentUrl
|
||||
?: remoteMediaClient?.currentItem?.media?.contentId)
|
||||
|
||||
val sortingMethods = items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" }
|
||||
.toTypedArray()
|
||||
val sortingMethods =
|
||||
items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" }
|
||||
.toTypedArray()
|
||||
val sotringIndex = items.indexOfFirst { it.url == contentUrl }
|
||||
|
||||
val arrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
|
||||
val arrayAdapter =
|
||||
ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
|
||||
arrayAdapter.addAll(sortingMethods.toMutableList())
|
||||
|
||||
providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
|
|
@ -178,8 +201,10 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
|
||||
providerList.setOnItemClickListener { _, _, which, _ ->
|
||||
val epData = holder.episodes[holder.currentEpisodeIndex]
|
||||
|
||||
fun loadMirror(index: Int) {
|
||||
if (holder.currentLinks.size <= index) return
|
||||
|
||||
val mediaItem = getMediaInfo(
|
||||
epData,
|
||||
holder,
|
||||
|
|
@ -189,21 +214,25 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
)
|
||||
|
||||
val startAt = remoteMediaClient?.approximateStreamPosition ?: 0
|
||||
|
||||
//remoteMediaClient.load(mediaItem, true, startAt)
|
||||
try { // THIS IS VERY IMPORTANT BECAUSE WE NEVER WANT TO AUTOLOAD THE NEXT EPISODE
|
||||
val currentIdIndex = remoteMediaClient?.getItemIndex()
|
||||
|
||||
val nextId = remoteMediaClient?.mediaQueue?.itemIds?.get(
|
||||
currentIdIndex?.plus(1) ?: 0
|
||||
)
|
||||
|
||||
if (currentIdIndex == null && nextId != null) {
|
||||
awaitLinks(
|
||||
remoteMediaClient?.queueInsertAndPlayItem(
|
||||
MediaQueueItem.Builder(mediaItem).build(),
|
||||
nextId,
|
||||
startAt,
|
||||
JSONObject(),
|
||||
JSONObject()
|
||||
)
|
||||
) { loadMirror(index + 1) }
|
||||
) {
|
||||
loadMirror(index + 1)
|
||||
}
|
||||
} else {
|
||||
val mediaLoadOptions =
|
||||
MediaLoadOptions.Builder()
|
||||
|
|
@ -215,9 +244,11 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
mediaItem,
|
||||
mediaLoadOptions
|
||||
)
|
||||
) { loadMirror(index + 1) }
|
||||
) {
|
||||
loadMirror(index + 1)
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
} catch (e: Exception) {
|
||||
val mediaLoadOptions =
|
||||
MediaLoadOptions.Builder()
|
||||
.setPlayPosition(startAt)
|
||||
|
|
@ -228,8 +259,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadMirror(which)
|
||||
|
||||
bottomSheetDialog.dismissSafe(activity)
|
||||
}
|
||||
}
|
||||
|
|
@ -239,19 +270,23 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
|
||||
private fun getCurrentMetaData(): MetadataHolder? {
|
||||
return try {
|
||||
val data = remoteMediaClient?.mediaInfo?.customData?.toString() ?: return null
|
||||
parseJson<MetadataHolder>(data)
|
||||
} catch (_: Exception) {
|
||||
val data = remoteMediaClient?.mediaInfo?.customData?.toString()
|
||||
data?.toKotlinObject()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
var isLoadingMore = false
|
||||
|
||||
|
||||
override fun onMediaStatusUpdated() {
|
||||
super.onMediaStatusUpdated()
|
||||
val meta = getCurrentMetaData()
|
||||
view.visibility = if ((meta?.currentLinks?.size ?: 0) > 1) VISIBLE else INVISIBLE
|
||||
|
||||
view.visibility = if ((meta?.currentLinks?.size
|
||||
?: 0) > 1
|
||||
) VISIBLE else INVISIBLE
|
||||
try {
|
||||
if (meta != null && meta.episodes.size > meta.currentEpisodeIndex + 1) {
|
||||
val currentIdIndex = remoteMediaClient?.getItemIndex() ?: return
|
||||
|
|
@ -268,7 +303,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
currentPosition,
|
||||
currentDuration,
|
||||
epData,
|
||||
meta.episodes.getOrNull(index + 1),
|
||||
meta.episodes.getOrNull(index + 1)
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
|
|
@ -279,7 +314,9 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
ioSafe {
|
||||
val currentLinks = mutableSetOf<ExtractorLink>()
|
||||
val currentSubs = mutableSetOf<SubtitleData>()
|
||||
|
||||
val generator = RepoLinkGenerator(listOf(epData))
|
||||
|
||||
val isSuccessful = safeApiCall {
|
||||
generator.generateLinks(
|
||||
clearCache = false,
|
||||
|
|
@ -292,7 +329,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
currentSubs.add(it)
|
||||
},
|
||||
offset = 0,
|
||||
isCasting = true,
|
||||
isCasting = true
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -303,18 +340,32 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
val jsonCopy = meta.copy(
|
||||
currentLinks = sortedLinks,
|
||||
currentSubtitles = sortedSubs,
|
||||
currentEpisodeIndex = index,
|
||||
currentEpisodeIndex = index
|
||||
)
|
||||
|
||||
val done = JSONObject(jsonCopy.toJson())
|
||||
val done =
|
||||
JSONObject(jsonCopy.toJson())
|
||||
|
||||
val mediaInfo = getMediaInfo(
|
||||
epData,
|
||||
jsonCopy,
|
||||
0,
|
||||
done,
|
||||
sortedSubs,
|
||||
sortedSubs
|
||||
)
|
||||
|
||||
/*fun loadIndex(index: Int) {
|
||||
println("LOAD INDEX::::: $index")
|
||||
if (meta.currentLinks.size <= index) return
|
||||
val info = getMediaInfo(
|
||||
epData,
|
||||
meta,
|
||||
index,
|
||||
done)
|
||||
awaitLinks(remoteMediaClient?.load(info, true, 0)) {
|
||||
loadIndex(index + 1)
|
||||
}
|
||||
}*/
|
||||
activity.runOnUiThread {
|
||||
awaitLinks(
|
||||
remoteMediaClient?.queueAppendItem(
|
||||
|
|
@ -323,6 +374,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
)
|
||||
) {
|
||||
println("FAILED TO LOAD NEXT ITEM")
|
||||
// loadIndex(1)
|
||||
}
|
||||
isLoadingMore = false
|
||||
}
|
||||
|
|
@ -345,7 +397,10 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
|||
|
||||
class SkipTimeController(val view: ImageView, forwards: Boolean) : UIController() {
|
||||
init {
|
||||
//val settingsManager = PreferenceManager.getDefaultSharedPreferences()
|
||||
//val time = settingsManager?.getInt("chromecast_tap_time", 30) ?: 30
|
||||
val time = 30
|
||||
//view.setImageResource(if (forwards) R.drawable.netflix_skip_forward else R.drawable.netflix_skip_back)
|
||||
view.setImageResource(if (forwards) R.drawable.go_forward_30 else R.drawable.go_back_30)
|
||||
view.setOnClickListener {
|
||||
remoteMediaClient?.let {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lagradost.api.Log
|
||||
import com.lagradost.cloudstream3.CloudStreamApp
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.isEpisodeBased
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
|
|
@ -37,7 +36,6 @@ import com.lagradost.cloudstream3.utils.ResourceLiveData
|
|||
import com.lagradost.cloudstream3.utils.downloader.DownloadObjects
|
||||
import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager
|
||||
import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.deleteFilesAndUpdateSettings
|
||||
import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.downloadDeleteEvent
|
||||
import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.getDownloadFileInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -69,17 +67,6 @@ class DownloadViewModel : ViewModel() {
|
|||
private val _selectedItemIds = ConsistentLiveData<Set<Int>?>(null)
|
||||
val selectedItemIds: LiveData<Set<Int>?> = _selectedItemIds
|
||||
|
||||
init {
|
||||
// Keep the Downloads list in sync when a download is deleted/cancelled from
|
||||
// anywhere in the app (result page button, queue, notification, etc.). See
|
||||
// onDownloadDeleted for the rationale (issue #1227).
|
||||
downloadDeleteEvent += ::onDownloadDeleted
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
downloadDeleteEvent -= ::onDownloadDeleted
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun cancelSelection() {
|
||||
updateSelectedItems { null }
|
||||
|
|
@ -402,18 +389,6 @@ class DownloadViewModel : ViewModel() {
|
|||
postChildren(_childCards.success?.filter { it.data.id !in idsToRemove })
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the Downloads screen in real time when a download is deleted/cancelled.
|
||||
*/
|
||||
private fun onDownloadDeleted(id: Int) {
|
||||
// Keep multi-select state consistent: forget the removed id if it was selected.
|
||||
updateSelectedItems { it?.minus(id) }
|
||||
|
||||
val context = CloudStreamApp.context ?: return
|
||||
updateHeaderList(context)
|
||||
postChildren(_childCards.success?.filterNot { it.data.id == id })
|
||||
}
|
||||
|
||||
private fun updateStorageStats(visual: List<VisualDownloadCached.Header>) {
|
||||
try {
|
||||
val stat = StatFs(Environment.getExternalStorageDirectory().path)
|
||||
|
|
@ -609,4 +584,4 @@ class DownloadViewModel : ViewModel() {
|
|||
val names: List<String>,
|
||||
val parentName: String?
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -164,11 +164,9 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
|||
}
|
||||
}
|
||||
|
||||
fun downloadDeleteEvent(data: Int) {
|
||||
if (data == persistentId) {
|
||||
resetView()
|
||||
}
|
||||
}
|
||||
/*fun downloadDeleteEvent(data: Int) {
|
||||
|
||||
}*/
|
||||
|
||||
/*fun downloadEvent(data: Pair<Int, VideoDownloadManager.DownloadActionType>) {
|
||||
val (id, action) = data
|
||||
|
|
@ -187,7 +185,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
|||
|
||||
override fun onAttachedToWindow() {
|
||||
VideoDownloadManager.downloadStatusEvent += ::downloadStatusEvent
|
||||
VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent
|
||||
// VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent
|
||||
// VideoDownloadManager.downloadEvent += ::downloadEvent
|
||||
VideoDownloadManager.downloadProgressEvent += ::downloadProgressEvent
|
||||
|
||||
|
|
@ -202,7 +200,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
|
|||
|
||||
override fun onDetachedFromWindow() {
|
||||
VideoDownloadManager.downloadStatusEvent -= ::downloadStatusEvent
|
||||
VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent
|
||||
// VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent
|
||||
// VideoDownloadManager.downloadEvent -= ::downloadEvent
|
||||
VideoDownloadManager.downloadProgressEvent -= ::downloadProgressEvent
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ import androidx.core.view.isVisible
|
|||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.chip.Chip
|
||||
|
|
@ -45,7 +43,6 @@ import com.lagradost.cloudstream3.mvvm.Resource
|
|||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.mvvm.observeNullable
|
||||
import com.lagradost.cloudstream3.plugins.Plugin
|
||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
|
||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
|
||||
import com.lagradost.cloudstream3.ui.BaseFragment
|
||||
|
|
@ -427,30 +424,11 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(
|
|||
.inflate(R.layout.sort_bottom_single_provider_choice, parent, false)
|
||||
val titleText = view.findViewById<TextView>(R.id.text1)
|
||||
val pinIcon = view.findViewById<ImageView>(R.id.pinicon)
|
||||
val settingsIcon = view.findViewById<ImageView>(R.id.action_settings)
|
||||
|
||||
val name = getItem(position)
|
||||
titleText?.text = name
|
||||
val providerApi = currentValidApis[position]
|
||||
val isPinned =
|
||||
pinnedphashset.contains(providerApi.name)
|
||||
pinnedphashset.contains(currentValidApis[position].name)
|
||||
pinIcon.visibility = if (isPinned) View.VISIBLE else View.GONE
|
||||
|
||||
val pluginInstance = providerApi.sourcePlugin?.let { PluginManager.plugins[it] } as? Plugin
|
||||
val isDownloadedPluginWithSettings = pluginInstance?.openSettings != null && !isLayout(TV)
|
||||
|
||||
settingsIcon.visibility = if (isDownloadedPluginWithSettings) View.VISIBLE else View.GONE
|
||||
if (isDownloadedPluginWithSettings) {
|
||||
settingsIcon.setOnClickListener {
|
||||
try {
|
||||
val activityContext = it.context.getActivity() ?: it.context
|
||||
pluginInstance.openSettings?.invoke(activityContext)
|
||||
} catch (e: Throwable) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
|
@ -473,14 +451,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(
|
|||
arrayAdapter.clear()
|
||||
val sortedApis = validAPIs
|
||||
.filter {
|
||||
val isPinned = pinnedphashset.contains(it.name)
|
||||
|
||||
// Hide pinned NSFW when NSFW not selected. NSFW is distracting when not chosen.
|
||||
if (isPinned && !preSelectedTypes.contains(TvType.NSFW)) {
|
||||
if (it.supportedTypes.all { type -> type == TvType.NSFW }) return@filter false
|
||||
}
|
||||
|
||||
it.hasMainPage && (isPinned || it.supportedTypes.any(
|
||||
it.hasMainPage && (pinnedphashset.contains(it.name) || it.supportedTypes.any(
|
||||
preSelectedTypes::contains
|
||||
))
|
||||
}
|
||||
|
|
@ -694,6 +665,7 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(
|
|||
fromUI = true
|
||||
)
|
||||
showToast(R.string.action_reload, Toast.LENGTH_SHORT)
|
||||
true
|
||||
}
|
||||
|
||||
homePreviewSearchButton.setOnClickListener { _ ->
|
||||
|
|
@ -701,23 +673,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(
|
|||
homeViewModel.queryTextSubmit("")
|
||||
}
|
||||
|
||||
// Load value for toggling Tv layout real time clock. Hide by default at startup
|
||||
// set visibility first, to apply a scroll effect later
|
||||
context?.let {
|
||||
if (isLayout(TV)) {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it)
|
||||
val toggleClock =
|
||||
settingsManager.getBoolean(
|
||||
getString(R.string.tv_layout_clock_key),
|
||||
false
|
||||
)
|
||||
binding.homeClock.isVisible = toggleClock
|
||||
} else {
|
||||
binding.homeClock.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
if (isLayout(PHONE)) {
|
||||
|
|
@ -757,17 +712,6 @@ class HomeFragment : BaseFragment<FragmentHomeBinding>(
|
|||
view.getLocationInWindow(rect)
|
||||
scrollParent.isVisible = true
|
||||
scrollParent.translationY = rect[1].toFloat() - 60.toPx
|
||||
|
||||
// Move the TV layout real time clock out of the way too
|
||||
// We check if we have the correct layout and if the clock is enabled
|
||||
if(isLayout(TV) && binding.homeClock.isVisible) {
|
||||
val scrollParent = binding.homeClock
|
||||
|
||||
val rect = IntArray(2)
|
||||
view.getLocationInWindow(rect)
|
||||
scrollParent.isVisible = true
|
||||
scrollParent.translationY = rect[1].toFloat() - 60.toPx
|
||||
}
|
||||
}
|
||||
}
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class HomeScrollAdapter(
|
|||
|
||||
when (binding) {
|
||||
is HomeScrollViewBinding -> {
|
||||
binding.homeScrollPreview.loadImage(posterUrl, item.posterHeaders)
|
||||
binding.homeScrollPreview.loadImage(posterUrl)
|
||||
binding.homeScrollPreviewTags.apply {
|
||||
text = item.tags?.joinToString(" • ") ?: ""
|
||||
isGone = item.tags.isNullOrEmpty()
|
||||
|
|
@ -79,8 +79,8 @@ class HomeScrollAdapter(
|
|||
binding.homeScrollPreview.setOnClickListener { view ->
|
||||
callback.invoke(view ?: return@setOnClickListener, position, item)
|
||||
}
|
||||
binding.homeScrollPreview.loadImage(posterUrl, item.posterHeaders)
|
||||
binding.homeScrollPreview.loadImage(posterUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -307,7 +307,6 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
|
|||
playerVideoTitleRez,
|
||||
playerVideoInfo,
|
||||
playerGoBackHolder,
|
||||
playerVideoClock,
|
||||
).forEach {
|
||||
it.animateY(titleMove)
|
||||
}
|
||||
|
|
@ -773,7 +772,7 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
|
|||
val showPlayerEpisodes = !isGone && isThereEpisodes()
|
||||
playerEpisodesButtonRoot.isVisible = showPlayerEpisodes
|
||||
playerEpisodesButton.isVisible = showPlayerEpisodes
|
||||
playerVideoTitleHolder.isGone = togglePlayerTitleGone || playerVideoTitle.text.isBlank()
|
||||
playerVideoTitleHolder.isGone = togglePlayerTitleGone
|
||||
playerVideoTitleRez.isGone = isGone || playerVideoTitleRez.text.isBlank()
|
||||
playerEpisodeFiller.isGone = isGone
|
||||
playerCenterMenu.isGone = isGone
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Spanned
|
||||
|
|
@ -80,7 +79,6 @@ import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioT
|
|||
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.LinkSource
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.ProfileSettings
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getLinkPriority
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog
|
||||
|
|
@ -134,7 +132,6 @@ import kotlinx.coroutines.delay
|
|||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.Serializable
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Calendar
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
|
@ -513,7 +510,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
showDownloadProgress(DownloadEvent(0, 0, 0, null))
|
||||
|
||||
// uiReset() // Removed due to UX
|
||||
|
||||
|
||||
currentSelectedLink = link
|
||||
// setEpisodes(viewModel.getAllMeta() ?: emptyList())
|
||||
setPlayerDimen(null)
|
||||
|
|
@ -1115,29 +1112,21 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
var sourceIndex = 0
|
||||
var startSource = 0
|
||||
// Filtered and sorted links
|
||||
var currentHiddenFooter: View? = null
|
||||
var filteredLinks: List<DisplayLink> = emptyList()
|
||||
var sortedUrls = emptyList<Pair<ExtractorLink?, ExtractorUri?>>()
|
||||
|
||||
fun refreshLinks(qualityProfile: Int) {
|
||||
val currentLinkUsed = currentSelectedLink
|
||||
// Always display current linkFooter
|
||||
val sortedLinks = viewModel.state.sortLinks(qualityProfile)
|
||||
|
||||
filteredLinks = sortedLinks.filter { it.shouldUseLink || it.link == currentLinkUsed }
|
||||
|
||||
if (sortedLinks.isEmpty()) {
|
||||
sortedUrls = viewModel.state.sortLinks(qualityProfile)
|
||||
if (sortedUrls.isEmpty()) {
|
||||
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone =
|
||||
true
|
||||
} else {
|
||||
startSource = filteredLinks.indexOfFirst { it.link == currentLinkUsed }
|
||||
startSource = sortedUrls.indexOf(currentSelectedLink)
|
||||
sourceIndex = startSource
|
||||
|
||||
val sourcesArrayAdapter =
|
||||
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
|
||||
sourcesArrayAdapter.addAll(filteredLinks.map { displayLink ->
|
||||
val (link, uri) = displayLink.link
|
||||
sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) ->
|
||||
val name = link?.name ?: uri?.name ?: "NULL"
|
||||
"$name ${Qualities.getStringByInt(link?.quality)}"
|
||||
})
|
||||
|
|
@ -1153,7 +1142,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
providerList.setOnItemLongClickListener { _, _, position, _ ->
|
||||
sortedLinks.getOrNull(position)?.link?.first?.url?.let {
|
||||
sortedUrls.getOrNull(position)?.first?.url?.let {
|
||||
clipboardHelper(
|
||||
txt(R.string.video_source),
|
||||
it
|
||||
|
|
@ -1161,25 +1150,6 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
true
|
||||
}
|
||||
|
||||
val hiddenLinks = sortedLinks.size - filteredLinks.size
|
||||
providerList.removeFooterView(currentHiddenFooter)
|
||||
|
||||
if (hiddenLinks > 0) {
|
||||
val hiddenLinksFooter: TextView = layoutInflater.inflate(
|
||||
R.layout.sort_bottom_footer_add_choice, null
|
||||
) as TextView
|
||||
|
||||
providerList.addFooterView(hiddenLinksFooter, null, false)
|
||||
currentHiddenFooter = hiddenLinksFooter
|
||||
|
||||
val hiddenLinksText =
|
||||
ctx.resources.getQuantityString(R.plurals.links_hidden, hiddenLinks)
|
||||
.format(hiddenLinks)
|
||||
hiddenLinksFooter.text = hiddenLinksText
|
||||
hiddenLinksFooter.setCompoundDrawables(null, null, null, null)
|
||||
hiddenLinksFooter.setTypeface(null, Typeface.ITALIC)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1393,8 +1363,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
if (init) {
|
||||
filteredLinks.getOrNull(sourceIndex)?.let {
|
||||
loadLink(it.link, true)
|
||||
sortedUrls.getOrNull(sourceIndex)?.let {
|
||||
loadLink(it, true)
|
||||
}
|
||||
}
|
||||
sourceDialog.dismissSafe(activity)
|
||||
|
|
@ -1562,10 +1532,6 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
override fun playerError(exception: Throwable) {
|
||||
currentSelectedLink?.let { link ->
|
||||
viewModel.modifyState { this.addError(link) }
|
||||
}
|
||||
|
||||
val currentUrl =
|
||||
currentSelectedLink?.let { it.first?.url ?: it.second?.uri?.toString() } ?: "unknown"
|
||||
val headers = currentSelectedLink?.first?.headers?.toString() ?: "none"
|
||||
|
|
@ -1588,22 +1554,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
private fun noLinksFound() {
|
||||
viewModel.forceClearCache = true
|
||||
val hiddenLinks = viewModel.state.sortLinks(currentQualityProfile).count { !it.shouldUseLink }
|
||||
|
||||
context?.let { ctx ->
|
||||
// Display that there are hidden links to the user.
|
||||
if (hiddenLinks > 0) {
|
||||
val noLinksString = ctx.getString(R.string.no_links_found_toast)
|
||||
val hiddenString =
|
||||
ctx.resources.getQuantityString(R.plurals.links_hidden, hiddenLinks)
|
||||
.format(hiddenLinks)
|
||||
val toastText = "$noLinksString\n($hiddenString)"
|
||||
showToast(toastText, Toast.LENGTH_SHORT)
|
||||
} else {
|
||||
showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
|
||||
activity?.popCurrentPage()
|
||||
}
|
||||
|
||||
|
|
@ -1614,9 +1566,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
val links = viewModel.state.sortLinks(currentQualityProfile)
|
||||
|
||||
val firstAvailableLink = links.firstOrNull { it.shouldUseLink }?.link
|
||||
if (firstAvailableLink == null) {
|
||||
if (links.isEmpty()) {
|
||||
noLinksFound()
|
||||
return
|
||||
}
|
||||
|
|
@ -1624,7 +1574,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
if (!isPlayerActive.compareAndSet(false, true)) {
|
||||
return
|
||||
}
|
||||
loadLink(firstAvailableLink, false)
|
||||
loadLink(links.first(), false)
|
||||
showPlayerMetadata()
|
||||
}
|
||||
|
||||
|
|
@ -1691,26 +1641,25 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getNextLink(): DisplayLink? {
|
||||
val links = viewModel.state.sortLinks(currentQualityProfile)
|
||||
val currentIndex = links.indexOfFirst { it.link == currentSelectedLink }
|
||||
val nextPotentialLink =
|
||||
links.withIndex().firstOrNull { it.index > currentIndex && it.value.shouldUseLink }
|
||||
return nextPotentialLink?.value
|
||||
}
|
||||
|
||||
override fun hasNextMirror(): Boolean {
|
||||
return getNextLink() != null
|
||||
val links = viewModel.state.sortLinks(currentQualityProfile)
|
||||
return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size
|
||||
}
|
||||
|
||||
override fun nextMirror() {
|
||||
val nextLink = getNextLink()
|
||||
if (nextLink == null) {
|
||||
val links = viewModel.state.sortLinks(currentQualityProfile)
|
||||
if (links.isEmpty()) {
|
||||
noLinksFound()
|
||||
return
|
||||
}
|
||||
|
||||
loadLink(nextLink.link, true)
|
||||
val newIndex = links.indexOf(currentSelectedLink) + 1
|
||||
if (newIndex >= links.size) {
|
||||
noLinksFound()
|
||||
return
|
||||
}
|
||||
|
||||
loadLink(links[newIndex], true)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
|
@ -2217,7 +2166,6 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
isPlayerActive.set(false)
|
||||
binding?.overlayLoadingSkipButton?.isVisible = false
|
||||
binding?.playerLoadingOverlay?.isVisible = true
|
||||
viewModel.modifyState { setError(emptyList()) }
|
||||
uiReset()
|
||||
}
|
||||
|
||||
|
|
@ -2271,14 +2219,6 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
fromTagToEnglishLanguageName(it)?.lowercase() ?: return@mapNotNull null
|
||||
} ?: listOf()
|
||||
}
|
||||
|
||||
// Set up TV clock visibility
|
||||
if (isLayout(TV)) {
|
||||
val showTvClock = settingsManager.getBoolean(ctx.getString(R.string.tv_layout_clock_key), false)
|
||||
playerBinding?.playerVideoClock?.isVisible = showTvClock
|
||||
} else {
|
||||
playerBinding?.playerVideoClock?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
unwrapBundle(savedInstanceState)
|
||||
|
|
@ -2357,23 +2297,19 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
|
||||
observe(viewModel.currentLinks) { (_, instance) ->
|
||||
observe(viewModel.currentLinks) { (links, instance) ->
|
||||
if (instance != viewModel.state.instance) return@observe // Outdated observe
|
||||
|
||||
val sortedLinks = viewModel.state.sortLinks(currentQualityProfile)
|
||||
val usableLinks = sortedLinks.count { link -> link.shouldUseLink }
|
||||
|
||||
val turnVisible = usableLinks > 0 && viewModel.generator?.canSkipLoading == true
|
||||
val turnVisible = links.isNotEmpty() && viewModel.generator?.canSkipLoading == true
|
||||
val wasGone = binding.overlayLoadingSkipButton.isGone
|
||||
|
||||
binding.overlayLoadingSkipButton.apply {
|
||||
isVisible = turnVisible
|
||||
|
||||
if (usableLinks == 0) {
|
||||
if (links.isEmpty()) {
|
||||
setText(R.string.skip_loading)
|
||||
} else {
|
||||
@SuppressLint("SetTextI18n")
|
||||
text = "${context.getString(R.string.skip_loading)} (${usableLinks})"
|
||||
text = "${context.getString(R.string.skip_loading)} (${links.size})"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import com.lagradost.cloudstream3.mvvm.Resource
|
|||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.ProfileSettings
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getLinkPriority
|
||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
|
|
@ -42,20 +40,12 @@ data class GeneratorState(
|
|||
val id: Int?,
|
||||
)
|
||||
|
||||
data class DisplayLink(
|
||||
val link: VideoLink,
|
||||
// If the link should be displayed and used by the player
|
||||
val shouldUseLink: Boolean,
|
||||
val priority: Int
|
||||
)
|
||||
|
||||
/** Immutable state of all current links relevant to displaying the video */
|
||||
// @MustUseReturnValues
|
||||
// @Immutable
|
||||
data class VideoState(
|
||||
val subtitles: PersistentSet<SubtitleData> = persistentSetOf(),
|
||||
val links: PersistentSet<VideoLink> = persistentSetOf(),
|
||||
val erroredLinks: PersistentSet<VideoLink> = persistentSetOf(),
|
||||
val stamps: PersistentList<VideoSkipStamp> = persistentListOf(),
|
||||
val loading: Resource<Unit> = Resource.Loading(),
|
||||
val generatorState: GeneratorState? = null,
|
||||
|
|
@ -66,52 +56,19 @@ data class VideoState(
|
|||
*
|
||||
* sortedBy is not exactly expensive, but each hasNextMirror does it again, so this alleviates unnecessary recomputation
|
||||
* */
|
||||
private val sortedLinks: ConcurrentHashMap<Int, List<DisplayLink>> = ConcurrentHashMap()
|
||||
private val sortedLinks: ConcurrentHashMap<Int, List<VideoLink>> = ConcurrentHashMap()
|
||||
|
||||
/**
|
||||
* The cache is guaranteed to be up to date link-wise due to the immutable links.
|
||||
* However, hideNegativeSources and hideErrorSources could be updated, which requires clearing the cache.
|
||||
*/
|
||||
fun clearSortedLinksCache() = sortedLinks.clear()
|
||||
|
||||
private fun hasLinkErrored(link: VideoLink): Boolean {
|
||||
return erroredLinks.any { it == link }
|
||||
}
|
||||
|
||||
private fun VideoLink.toDisplayLink(
|
||||
qualityProfile: Int,
|
||||
hideNegativeSources: Boolean,
|
||||
hideErrorSources: Boolean
|
||||
): DisplayLink {
|
||||
val priority = getLinkPriority(qualityProfile, this.first)
|
||||
val shouldHideLink =
|
||||
(hideNegativeSources && priority < 0) || (hideErrorSources && hasLinkErrored(this))
|
||||
val displayLink = DisplayLink(this, !shouldHideLink, priority)
|
||||
|
||||
return displayLink
|
||||
}
|
||||
|
||||
// Modifying sortedLinks is not considered a "visible" side effect, and rerunning it does not change the result
|
||||
// It is by all standards, idempotent and by extension also pure as it has no "visible" side effect
|
||||
/** Returns .links in the sorted order according to the qualityProfile.
|
||||
* Use .links if order is not needed */
|
||||
@Contract(pure = true)
|
||||
fun sortLinks(qualityProfile: Int): List<DisplayLink> {
|
||||
sortedLinks[qualityProfile]?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
val hideNegativeSources =
|
||||
QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideNegativeSources)
|
||||
val hideErrorSources =
|
||||
QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideErrorSources)
|
||||
|
||||
return links.map { link ->
|
||||
fun sortLinks(qualityProfile: Int): List<VideoLink> {
|
||||
return sortedLinks[qualityProfile] ?: links.sortedBy { link ->
|
||||
// negative because we want to sort highest quality first
|
||||
link.toDisplayLink(qualityProfile, hideNegativeSources, hideErrorSources)
|
||||
}.sortedBy {
|
||||
// negative because we want to sort highest quality first
|
||||
-it.priority
|
||||
-getLinkPriority(qualityProfile, link.first)
|
||||
}.also { value -> sortedLinks[qualityProfile] = value }
|
||||
}
|
||||
|
||||
|
|
@ -156,12 +113,6 @@ data class VideoState(
|
|||
@JvmName("setVideoSkipStamp")
|
||||
@Contract(pure = true)
|
||||
fun set(items: Collection<VideoSkipStamp>): VideoState = copy(stamps = items.toPersistentList())
|
||||
|
||||
@Contract(pure = true)
|
||||
fun addError(item: VideoLink): VideoState = copy(erroredLinks = erroredLinks.add(item))
|
||||
|
||||
@Contract(pure = true)
|
||||
fun setError(items: Collection<VideoLink>): VideoState = copy(erroredLinks = items.toPersistentSet())
|
||||
}
|
||||
|
||||
data class VideoLive<T>(
|
||||
|
|
@ -190,8 +141,9 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
var state = VideoState(instance = 0)
|
||||
private set
|
||||
|
||||
private val _currentLinks = MutableLiveData<VideoLive<Set<VideoLink>>>(null)
|
||||
val currentLinks: LiveData<VideoLive<Set<VideoLink>>> = _currentLinks
|
||||
private val _currentLinks =
|
||||
MutableLiveData<VideoLive<Set<Pair<ExtractorLink?, ExtractorUri?>>>>(null)
|
||||
val currentLinks: LiveData<VideoLive<Set<Pair<ExtractorLink?, ExtractorUri?>>>> = _currentLinks
|
||||
|
||||
private val _currentSubtitles = MutableLiveData<VideoLive<Set<SubtitleData>>>(null)
|
||||
val currentSubtitles: LiveData<VideoLive<Set<SubtitleData>>> = _currentSubtitles
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import com.lagradost.cloudstream3.mvvm.logError
|
|||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import torrServer.TorrServer
|
||||
import java.io.File
|
||||
import java.net.ConnectException
|
||||
|
|
@ -34,14 +32,14 @@ object Torrent {
|
|||
|
||||
/** Returns true if the server is up */
|
||||
private suspend fun echo(): Boolean {
|
||||
if (TORRENT_SERVER_URL.isEmpty()) {
|
||||
if(TORRENT_SERVER_URL.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
return try {
|
||||
app.get(
|
||||
"$TORRENT_SERVER_URL/echo",
|
||||
).text.isNotEmpty()
|
||||
} catch (_: ConnectException) {
|
||||
} catch (e: ConnectException) {
|
||||
// `Failed to connect to /127.0.0.1:8090` if the server is down
|
||||
false
|
||||
} catch (t: Throwable) {
|
||||
|
|
@ -54,7 +52,7 @@ object Torrent {
|
|||
/** Gracefully shutdown the server.
|
||||
* should not be used because I am unable to start it again, and the stopTorrentServer() crashes the app */
|
||||
suspend fun shutdown(): Boolean {
|
||||
if (TORRENT_SERVER_URL.isEmpty()) {
|
||||
if(TORRENT_SERVER_URL.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
return try {
|
||||
|
|
@ -70,7 +68,7 @@ object Torrent {
|
|||
/** Lists all torrents by the server */
|
||||
@Throws
|
||||
private suspend fun list(): Array<TorrentStatus> {
|
||||
if (TORRENT_SERVER_URL.isEmpty()) {
|
||||
if(TORRENT_SERVER_URL.isEmpty()) {
|
||||
throw ErrorLoadingException("Not initialized")
|
||||
}
|
||||
return app.post(
|
||||
|
|
@ -85,7 +83,7 @@ object Torrent {
|
|||
|
||||
/** Drops a single torrent, (I think) this means closing the stream. Returns returns if it is successful */
|
||||
private suspend fun drop(hash: String): Boolean {
|
||||
if (TORRENT_SERVER_URL.isEmpty()) {
|
||||
if(TORRENT_SERVER_URL.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
return try {
|
||||
|
|
@ -106,7 +104,7 @@ object Torrent {
|
|||
|
||||
/** Removes a single torrent from the server registry */
|
||||
private suspend fun rem(hash: String): Boolean {
|
||||
if (TORRENT_SERVER_URL.isEmpty()) {
|
||||
if(TORRENT_SERVER_URL.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
return try {
|
||||
|
|
@ -128,7 +126,7 @@ object Torrent {
|
|||
|
||||
/** Removes all torrents from the server, and returns if it is successful */
|
||||
suspend fun clearAll(): Boolean {
|
||||
if (TORRENT_SERVER_URL.isEmpty()) {
|
||||
if(TORRENT_SERVER_URL.isEmpty()) {
|
||||
return true
|
||||
}
|
||||
return try {
|
||||
|
|
@ -166,8 +164,10 @@ object Torrent {
|
|||
/** Gets all the metadata of a torrent, will throw if that hash does not exists
|
||||
* https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/c18f58e51b6738f053261bc863177078aa9c1c98/web/api/torrents.go#L126 */
|
||||
@Throws
|
||||
suspend fun get(hash: String): TorrentStatus {
|
||||
if (TORRENT_SERVER_URL.isEmpty()) {
|
||||
suspend fun get(
|
||||
hash: String,
|
||||
): TorrentStatus {
|
||||
if(TORRENT_SERVER_URL.isEmpty()) {
|
||||
throw ErrorLoadingException("Not initialized")
|
||||
}
|
||||
return app.post(
|
||||
|
|
@ -184,7 +184,7 @@ object Torrent {
|
|||
/** Adds a torrent to the server, this is needed for us to get the hash for further modification, as well as start streaming it*/
|
||||
@Throws
|
||||
private suspend fun add(url: String): TorrentStatus {
|
||||
if (TORRENT_SERVER_URL.isEmpty()) {
|
||||
if(TORRENT_SERVER_URL.isEmpty()) {
|
||||
throw ErrorLoadingException("Not initialized")
|
||||
}
|
||||
return app.post(
|
||||
|
|
@ -204,7 +204,7 @@ object Torrent {
|
|||
return true
|
||||
}
|
||||
val port = TorrServer.startTorrentServer(dir, 0)
|
||||
if (port < 0) {
|
||||
if(port < 0) {
|
||||
return false
|
||||
}
|
||||
TORRENT_SERVER_URL = "http://127.0.0.1:$port"
|
||||
|
|
@ -278,55 +278,94 @@ object Torrent {
|
|||
|
||||
// https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/c18f58e51b6738f053261bc863177078aa9c1c98/web/api/torrents.go#L18
|
||||
// https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/main/web/api/route.go#L7
|
||||
@Serializable
|
||||
data class TorrentRequest(
|
||||
@JsonProperty("action") @SerialName("action") val action: String,
|
||||
@JsonProperty("hash") @SerialName("hash") val hash: String = "",
|
||||
@JsonProperty("link") @SerialName("link") val link: String = "",
|
||||
@JsonProperty("title") @SerialName("title") val title: String = "",
|
||||
@JsonProperty("poster") @SerialName("poster") val poster: String = "",
|
||||
@JsonProperty("data") @SerialName("data") val data: String = "",
|
||||
@JsonProperty("save_to_db") @SerialName("save_to_db") val saveToDB: Boolean = false,
|
||||
@JsonProperty("action")
|
||||
val action: String,
|
||||
@JsonProperty("hash")
|
||||
val hash: String = "",
|
||||
@JsonProperty("link")
|
||||
val link: String = "",
|
||||
@JsonProperty("title")
|
||||
val title: String = "",
|
||||
@JsonProperty("poster")
|
||||
val poster: String = "",
|
||||
@JsonProperty("data")
|
||||
val data: String = "",
|
||||
@JsonProperty("save_to_db")
|
||||
val saveToDB: Boolean = false,
|
||||
)
|
||||
|
||||
// https://github.com/Diegopyl1209/torrentserver-aniyomi/blob/c18f58e51b6738f053261bc863177078aa9c1c98/torr/state/state.go#L33
|
||||
// omitempty = nullable
|
||||
@Serializable
|
||||
data class TorrentStatus(
|
||||
@JsonProperty("title") @SerialName("title") var title: String,
|
||||
@JsonProperty("poster") @SerialName("poster") var poster: String,
|
||||
@JsonProperty("data") @SerialName("data") var data: String?,
|
||||
@JsonProperty("timestamp") @SerialName("timestamp") var timestamp: Long,
|
||||
@JsonProperty("name") @SerialName("name") var name: String?,
|
||||
@JsonProperty("hash") @SerialName("hash") var hash: String?,
|
||||
@JsonProperty("stat") @SerialName("stat") var stat: Int,
|
||||
@JsonProperty("stat_string") @SerialName("stat_string") var statString: String,
|
||||
@JsonProperty("loaded_size") @SerialName("loaded_size") var loadedSize: Long?,
|
||||
@JsonProperty("torrent_size") @SerialName("torrent_size") var torrentSize: Long?,
|
||||
@JsonProperty("preloaded_bytes") @SerialName("preloaded_bytes") var preloadedBytes: Long?,
|
||||
@JsonProperty("preload_size") @SerialName("preload_size") var preloadSize: Long?,
|
||||
@JsonProperty("download_speed") @SerialName("download_speed") var downloadSpeed: Double?,
|
||||
@JsonProperty("upload_speed") @SerialName("upload_speed") var uploadSpeed: Double?,
|
||||
@JsonProperty("total_peers") @SerialName("total_peers") var totalPeers: Int?,
|
||||
@JsonProperty("pending_peers") @SerialName("pending_peers") var pendingPeers: Int?,
|
||||
@JsonProperty("active_peers") @SerialName("active_peers") var activePeers: Int?,
|
||||
@JsonProperty("connected_seeders") @SerialName("connected_seeders") var connectedSeeders: Int?,
|
||||
@JsonProperty("half_open_peers") @SerialName("half_open_peers") var halfOpenPeers: Int?,
|
||||
@JsonProperty("bytes_written") @SerialName("bytes_written") var bytesWritten: Long?,
|
||||
@JsonProperty("bytes_written_data") @SerialName("bytes_written_data") var bytesWrittenData: Long?,
|
||||
@JsonProperty("bytes_read") @SerialName("bytes_read") var bytesRead: Long?,
|
||||
@JsonProperty("bytes_read_data") @SerialName("bytes_read_data") var bytesReadData: Long?,
|
||||
@JsonProperty("bytes_read_useful_data") @SerialName("bytes_read_useful_data") var bytesReadUsefulData: Long?,
|
||||
@JsonProperty("chunks_written") @SerialName("chunks_written") var chunksWritten: Long?,
|
||||
@JsonProperty("chunks_read") @SerialName("chunks_read") var chunksRead: Long?,
|
||||
@JsonProperty("chunks_read_useful") @SerialName("chunks_read_useful") var chunksReadUseful: Long?,
|
||||
@JsonProperty("chunks_read_wasted") @SerialName("chunks_read_wasted") var chunksReadWasted: Long?,
|
||||
@JsonProperty("pieces_dirtied_good") @SerialName("pieces_dirtied_good") var piecesDirtiedGood: Long?,
|
||||
@JsonProperty("pieces_dirtied_bad") @SerialName("pieces_dirtied_bad") var piecesDirtiedBad: Long?,
|
||||
@JsonProperty("duration_seconds") @SerialName("duration_seconds") var durationSeconds: Double?,
|
||||
@JsonProperty("bit_rate") @SerialName("bit_rate") var bitRate: String?,
|
||||
@JsonProperty("file_stats") @SerialName("file_stats") var fileStats: List<TorrentFileStat>?,
|
||||
@JsonProperty("trackers") @SerialName("trackers") var trackers: List<String>?,
|
||||
@JsonProperty("title")
|
||||
var title: String,
|
||||
@JsonProperty("poster")
|
||||
var poster: String,
|
||||
@JsonProperty("data")
|
||||
var data: String?,
|
||||
@JsonProperty("timestamp")
|
||||
var timestamp: Long,
|
||||
@JsonProperty("name")
|
||||
var name: String?,
|
||||
@JsonProperty("hash")
|
||||
var hash: String?,
|
||||
@JsonProperty("stat")
|
||||
var stat: Int,
|
||||
@JsonProperty("stat_string")
|
||||
var statString: String,
|
||||
@JsonProperty("loaded_size")
|
||||
var loadedSize: Long?,
|
||||
@JsonProperty("torrent_size")
|
||||
var torrentSize: Long?,
|
||||
@JsonProperty("preloaded_bytes")
|
||||
var preloadedBytes: Long?,
|
||||
@JsonProperty("preload_size")
|
||||
var preloadSize: Long?,
|
||||
@JsonProperty("download_speed")
|
||||
var downloadSpeed: Double?,
|
||||
@JsonProperty("upload_speed")
|
||||
var uploadSpeed: Double?,
|
||||
@JsonProperty("total_peers")
|
||||
var totalPeers: Int?,
|
||||
@JsonProperty("pending_peers")
|
||||
var pendingPeers: Int?,
|
||||
@JsonProperty("active_peers")
|
||||
var activePeers: Int?,
|
||||
@JsonProperty("connected_seeders")
|
||||
var connectedSeeders: Int?,
|
||||
@JsonProperty("half_open_peers")
|
||||
var halfOpenPeers: Int?,
|
||||
@JsonProperty("bytes_written")
|
||||
var bytesWritten: Long?,
|
||||
@JsonProperty("bytes_written_data")
|
||||
var bytesWrittenData: Long?,
|
||||
@JsonProperty("bytes_read")
|
||||
var bytesRead: Long?,
|
||||
@JsonProperty("bytes_read_data")
|
||||
var bytesReadData: Long?,
|
||||
@JsonProperty("bytes_read_useful_data")
|
||||
var bytesReadUsefulData: Long?,
|
||||
@JsonProperty("chunks_written")
|
||||
var chunksWritten: Long?,
|
||||
@JsonProperty("chunks_read")
|
||||
var chunksRead: Long?,
|
||||
@JsonProperty("chunks_read_useful")
|
||||
var chunksReadUseful: Long?,
|
||||
@JsonProperty("chunks_read_wasted")
|
||||
var chunksReadWasted: Long?,
|
||||
@JsonProperty("pieces_dirtied_good")
|
||||
var piecesDirtiedGood: Long?,
|
||||
@JsonProperty("pieces_dirtied_bad")
|
||||
var piecesDirtiedBad: Long?,
|
||||
@JsonProperty("duration_seconds")
|
||||
var durationSeconds: Double?,
|
||||
@JsonProperty("bit_rate")
|
||||
var bitRate: String?,
|
||||
@JsonProperty("file_stats")
|
||||
var fileStats: List<TorrentFileStat>?,
|
||||
@JsonProperty("trackers")
|
||||
var trackers: List<String>?,
|
||||
) {
|
||||
fun streamUrl(url: String): String {
|
||||
val fileName =
|
||||
|
|
@ -342,10 +381,12 @@ object Torrent {
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class TorrentFileStat(
|
||||
@JsonProperty("id") @SerialName("id") val id: Int?,
|
||||
@JsonProperty("path") @SerialName("path") val path: String?,
|
||||
@JsonProperty("length") @SerialName("length") val length: Long?,
|
||||
@JsonProperty("id")
|
||||
val id: Int?,
|
||||
@JsonProperty("path")
|
||||
val path: String?,
|
||||
@JsonProperty("length")
|
||||
val length: Long?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,16 +12,12 @@ import com.lagradost.cloudstream3.utils.txt
|
|||
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import java.util.EnumMap
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.also
|
||||
import kotlin.math.abs
|
||||
|
||||
object QualityDataHelper {
|
||||
private const val VIDEO_SOURCE_PRIORITY = "video_source_priority"
|
||||
private const val VIDEO_PROFILE_NAME = "video_profile_name"
|
||||
private const val VIDEO_QUALITY_PRIORITY = "video_quality_priority"
|
||||
const val VIDEO_PROFILE_SETTINGS = "video_profile_settings"
|
||||
|
||||
// Old key only supporting one type per profile
|
||||
@Deprecated("Changed to support multiple types per profile")
|
||||
|
|
@ -57,21 +53,13 @@ object QualityDataHelper {
|
|||
val types: Set<QualityProfileType>
|
||||
)
|
||||
|
||||
|
||||
// Map profile and name to priority
|
||||
val sourcePriorityCache: ConcurrentHashMap<Int, HashMap<String, Int>> = ConcurrentHashMap()
|
||||
|
||||
fun getSourcePriority(profile: Int, name: String?): Int {
|
||||
if (name == null) return DEFAULT_SOURCE_PRIORITY
|
||||
|
||||
return sourcePriorityCache[profile]?.get(name) ?: (getKey<Int>(
|
||||
return getKey<Int>(
|
||||
"$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile",
|
||||
name,
|
||||
DEFAULT_SOURCE_PRIORITY
|
||||
) ?: DEFAULT_SOURCE_PRIORITY).also {
|
||||
sourcePriorityCache.getOrPut(profile) { hashMapOf() }
|
||||
sourcePriorityCache[profile]?.set(name, it)
|
||||
}
|
||||
) ?: DEFAULT_SOURCE_PRIORITY
|
||||
}
|
||||
|
||||
fun getAllSourcePriorityNames(profile: Int): List<String> {
|
||||
|
|
@ -89,8 +77,6 @@ object QualityDataHelper {
|
|||
} else {
|
||||
setKey(folder, name, priority)
|
||||
}
|
||||
|
||||
sourcePriorityCache[profile]?.set(name, priority)
|
||||
}
|
||||
|
||||
fun setProfileName(profile: Int, name: String?) {
|
||||
|
|
@ -107,17 +93,12 @@ object QualityDataHelper {
|
|||
?: txt(R.string.profile_number, profile)
|
||||
}
|
||||
|
||||
// Map profile and quality to priority
|
||||
val qualityPriorityCache: ConcurrentHashMap<Int, EnumMap<Qualities, Int>> = ConcurrentHashMap()
|
||||
fun getQualityPriority(profile: Int, quality: Qualities): Int {
|
||||
return qualityPriorityCache[profile]?.get(quality) ?: (getKey<Int>(
|
||||
return getKey<Int>(
|
||||
"$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile",
|
||||
quality.value.toString(),
|
||||
quality.defaultPriority
|
||||
)?.also {
|
||||
qualityPriorityCache.getOrPut(profile) { EnumMap(Qualities::class.java) }
|
||||
qualityPriorityCache[profile]?.set(quality, it)
|
||||
}) ?: quality.defaultPriority
|
||||
) ?: quality.defaultPriority
|
||||
}
|
||||
|
||||
fun setQualityPriority(profile: Int, quality: Qualities, priority: Int) {
|
||||
|
|
@ -126,24 +107,8 @@ object QualityDataHelper {
|
|||
quality.value.toString(),
|
||||
priority
|
||||
)
|
||||
qualityPriorityCache[profile]?.set(quality, priority)
|
||||
}
|
||||
|
||||
fun <T> setProfileSetting(profile: Int, setting: ProfileSettings<T>, value: T) {
|
||||
val folder = "$currentAccount/$VIDEO_PROFILE_SETTINGS/$profile"
|
||||
// Prevent unnecessary keys
|
||||
if (value == setting.defaultValue) {
|
||||
removeKey(folder, setting.key)
|
||||
} else {
|
||||
setKey(folder, setting.key, value)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> getProfileSetting(profile: Int, setting: ProfileSettings<T>): T {
|
||||
val folder = "$currentAccount/$VIDEO_PROFILE_SETTINGS/$profile"
|
||||
val value = getKey<T>(folder, setting.key)
|
||||
return value ?: setting.defaultValue
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun getQualityProfileTypes(profile: Int): Set<QualityProfileType> {
|
||||
|
|
@ -259,8 +224,3 @@ object QualityDataHelper {
|
|||
return Qualities.entries.minBy { abs(it.value - target) }
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ProfileSettings<T>(val key: String, val defaultValue: T) {
|
||||
object HideErrorSources : ProfileSettings<Boolean>("hide_error_sources", false)
|
||||
object HideNegativeSources : ProfileSettings<Boolean>("hide_negative_sources", false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding
|
|||
|
||||
class SourcePriorityDialog(
|
||||
val ctx: Context,
|
||||
@StyleRes val themeRes: Int,
|
||||
@StyleRes themeRes: Int,
|
||||
val links: List<LinkSource>,
|
||||
private val profile: QualityDataHelper.QualityProfile,
|
||||
/**
|
||||
|
|
@ -28,79 +28,76 @@ class SourcePriorityDialog(
|
|||
PlayerSelectSourcePriorityBinding.inflate(LayoutInflater.from(ctx), null, false)
|
||||
setContentView(binding.root)
|
||||
fixSystemBarsPadding(binding.root)
|
||||
val sourcesRecyclerView = binding.sortSources
|
||||
val qualitiesRecyclerView = binding.sortQualities
|
||||
val profileText = binding.profileTextEditable
|
||||
val saveBtt = binding.saveBtt
|
||||
val exitBtt = binding.closeBtt
|
||||
val helpBtt = binding.helpBtt
|
||||
|
||||
binding.apply {
|
||||
profileTextEditable.setText(
|
||||
QualityDataHelper.getProfileName(profile.id).asString(context)
|
||||
)
|
||||
profileTextEditable.hint = txt(R.string.profile_number, profile.id).asString(context)
|
||||
profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context))
|
||||
profileText.hint = txt(R.string.profile_number, profile.id).asString(context)
|
||||
|
||||
sortSources.adapter = PriorityAdapter<Nothing?>(
|
||||
).apply {
|
||||
val sortedLinks = links.map { link ->
|
||||
SourcePriority(
|
||||
null,
|
||||
link.source,
|
||||
QualityDataHelper.getSourcePriority(profile.id, link.source)
|
||||
)
|
||||
}.distinctBy { it.name }.sortedBy { -it.priority }
|
||||
|
||||
submitList(sortedLinks)
|
||||
}
|
||||
|
||||
sortQualities.adapter = PriorityAdapter<Qualities>(
|
||||
).apply {
|
||||
submitList(Qualities.entries.mapNotNull {
|
||||
SourcePriority(
|
||||
it,
|
||||
Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null },
|
||||
QualityDataHelper.getQualityPriority(profile.id, it)
|
||||
)
|
||||
}.sortedBy { -it.priority })
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST") // We know the types
|
||||
saveBtt.setOnClickListener {
|
||||
val qualityAdapter = sortQualities.adapter as? PriorityAdapter<Qualities>
|
||||
val sourcesAdapter = sortSources.adapter as? PriorityAdapter<Nothing?>
|
||||
|
||||
val qualities = qualityAdapter?.immutableCurrentList ?: emptyList()
|
||||
val sources = sourcesAdapter?.immutableCurrentList ?: emptyList()
|
||||
|
||||
qualities.forEach {
|
||||
QualityDataHelper.setQualityPriority(profile.id, it.data, it.priority)
|
||||
}
|
||||
|
||||
sources.forEach {
|
||||
QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority)
|
||||
}
|
||||
|
||||
qualityAdapter?.submitList(qualities.sortedBy { -it.priority })
|
||||
sourcesAdapter?.submitList(sources.sortedBy { -it.priority })
|
||||
|
||||
val savedProfileName = profileTextEditable.text.toString()
|
||||
if (savedProfileName.isBlank()) {
|
||||
QualityDataHelper.setProfileName(profile.id, null)
|
||||
} else {
|
||||
QualityDataHelper.setProfileName(profile.id, savedProfileName)
|
||||
}
|
||||
updatedCallback.invoke()
|
||||
}
|
||||
|
||||
closeBtt.setOnClickListener {
|
||||
dismissSafe()
|
||||
}
|
||||
|
||||
helpBtt.setOnClickListener {
|
||||
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
|
||||
setMessage(R.string.quality_profile_help)
|
||||
}.show()
|
||||
}
|
||||
|
||||
settingsBtt.setOnClickListener {
|
||||
SourceProfileSettingsDialog(ctx, themeRes, profile.id).show()
|
||||
}
|
||||
sourcesRecyclerView.adapter = PriorityAdapter<Nothing?>(
|
||||
).apply {
|
||||
submitList(links.map { link ->
|
||||
SourcePriority(
|
||||
null,
|
||||
link.source,
|
||||
QualityDataHelper.getSourcePriority(profile.id, link.source)
|
||||
)
|
||||
}.distinctBy { it.name }.sortedBy { -it.priority })
|
||||
}
|
||||
|
||||
qualitiesRecyclerView.adapter = PriorityAdapter<Qualities>(
|
||||
).apply {
|
||||
submitList(Qualities.entries.mapNotNull {
|
||||
SourcePriority(
|
||||
it,
|
||||
Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null },
|
||||
QualityDataHelper.getQualityPriority(profile.id, it)
|
||||
)
|
||||
}.sortedBy { -it.priority })
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST") // We know the types
|
||||
saveBtt.setOnClickListener {
|
||||
val qualityAdapter = qualitiesRecyclerView.adapter as? PriorityAdapter<Qualities>
|
||||
val sourcesAdapter = sourcesRecyclerView.adapter as? PriorityAdapter<Nothing?>
|
||||
|
||||
val qualities = qualityAdapter?.immutableCurrentList ?: emptyList()
|
||||
val sources = sourcesAdapter?.immutableCurrentList ?: emptyList()
|
||||
|
||||
qualities.forEach {
|
||||
QualityDataHelper.setQualityPriority(profile.id, it.data, it.priority)
|
||||
}
|
||||
|
||||
sources.forEach {
|
||||
QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority)
|
||||
}
|
||||
|
||||
qualityAdapter?.submitList(qualities.sortedBy { -it.priority })
|
||||
sourcesAdapter?.submitList(sources.sortedBy { -it.priority })
|
||||
|
||||
val savedProfileName = profileText.text.toString()
|
||||
if (savedProfileName.isBlank()) {
|
||||
QualityDataHelper.setProfileName(profile.id, null)
|
||||
} else {
|
||||
QualityDataHelper.setProfileName(profile.id, savedProfileName)
|
||||
}
|
||||
updatedCallback.invoke()
|
||||
}
|
||||
|
||||
exitBtt.setOnClickListener {
|
||||
this.dismissSafe()
|
||||
}
|
||||
|
||||
helpBtt.setOnClickListener {
|
||||
AlertDialog.Builder(context, R.style.AlertDialogCustom).apply {
|
||||
setMessage(R.string.quality_profile_help)
|
||||
}.show()
|
||||
}
|
||||
|
||||
super.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.StyleRes
|
||||
import com.lagradost.cloudstream3.databinding.SourceProfileSettingsDialogBinding
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding
|
||||
|
||||
class SourceProfileSettingsDialog(
|
||||
val ctx: Context,
|
||||
@StyleRes themeRes: Int,
|
||||
val profile: Int
|
||||
) : Dialog(ctx, themeRes) {
|
||||
override fun show() {
|
||||
val binding =
|
||||
SourceProfileSettingsDialogBinding.inflate(LayoutInflater.from(ctx), null, false)
|
||||
setContentView(binding.root)
|
||||
fixSystemBarsPadding(binding.root)
|
||||
|
||||
binding.apply {
|
||||
var hideErrorSources = QualityDataHelper.getProfileSetting(profile, ProfileSettings.HideErrorSources)
|
||||
var hideNegativeSources = QualityDataHelper.getProfileSetting(profile, ProfileSettings.HideNegativeSources)
|
||||
|
||||
profileHideErrorSources.isChecked = hideErrorSources
|
||||
profileHideErrorSources.setOnCheckedChangeListener { _, bool ->
|
||||
hideErrorSources = bool
|
||||
}
|
||||
|
||||
profileHideNegativeSources.isChecked = hideNegativeSources
|
||||
profileHideNegativeSources.setOnCheckedChangeListener { _, bool ->
|
||||
hideNegativeSources = bool
|
||||
}
|
||||
|
||||
applyBtt.setOnClickListener {
|
||||
QualityDataHelper.setProfileSetting(profile, ProfileSettings.HideErrorSources, hideErrorSources)
|
||||
QualityDataHelper.setProfileSetting(profile, ProfileSettings.HideNegativeSources, hideNegativeSources)
|
||||
dismissSafe()
|
||||
}
|
||||
|
||||
cancelBtt.setOnClickListener {
|
||||
dismissSafe()
|
||||
}
|
||||
}
|
||||
super.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -877,7 +877,7 @@ class ResultFragmentTv : BaseFragment<FragmentResultTvBinding>(
|
|||
resultCastText.setText(d.actorsText)
|
||||
resultNextAiring.setText(d.nextAiringEpisode)
|
||||
resultNextAiringTime.setText(d.nextAiringDate)
|
||||
resultPoster.loadImage(d.posterImage, headers = d.posterHeaders)
|
||||
resultPoster.loadImage(d.posterImage)
|
||||
|
||||
var isExpanded = false
|
||||
resultDescription.apply {
|
||||
|
|
@ -910,7 +910,7 @@ class ResultFragmentTv : BaseFragment<FragmentResultTvBinding>(
|
|||
R.drawable.profile_bg_teal
|
||||
).random()
|
||||
|
||||
backgroundPoster.loadImage(d.posterBackgroundImage, headers = d.posterHeaders) {
|
||||
backgroundPoster.loadImage(d.posterBackgroundImage) {
|
||||
error { getImageFromDrawable(context ?: return@error null, error) }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,6 @@ val appLanguages = arrayListOf(
|
|||
Pair("Azərbaycan dili", "az"),
|
||||
Pair("Bahasa Indonesia", "in"),
|
||||
Pair("Bahasa Melayu", "ms"),
|
||||
Pair("català", "ca"),
|
||||
Pair("Deutsch", "de"),
|
||||
Pair("English", "en"),
|
||||
Pair("Español", "es"),
|
||||
|
|
@ -98,7 +97,6 @@ val appLanguages = arrayListOf(
|
|||
Pair("Português", "pt"),
|
||||
Pair("Português (Brasil)", "pt-BR"),
|
||||
Pair("Română", "ro"),
|
||||
Pair("Shqip мова", "sq"),
|
||||
Pair("Slovenčina", "sk"),
|
||||
Pair("Soomaaliga", "so"),
|
||||
Pair("Svenska", "sv"),
|
||||
|
|
@ -108,7 +106,6 @@ val appLanguages = arrayListOf(
|
|||
Pair("Wikang Filipino", "fil"),
|
||||
Pair("Čeština", "cs"),
|
||||
Pair("Ελληνικά", "el"),
|
||||
Pair("беларуская мова", "be"),
|
||||
Pair("български", "bg"),
|
||||
Pair("македонски", "mk"),
|
||||
Pair("русский", "ru"),
|
||||
|
|
|
|||
|
|
@ -228,8 +228,6 @@ class SettingsUI : BasePreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
getPref(R.string.tv_layout_clock_key)?.hideOn(PHONE or EMULATOR)
|
||||
|
||||
getPref(R.string.confirm_exit_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.confirm_exit)
|
||||
val prefValues = resources.getIntArray(R.array.confirm_exit_values)
|
||||
|
|
|
|||
|
|
@ -115,9 +115,6 @@ class SubtitlesFragment : BaseDialogFragment<SubtitleSettingsBinding>(
|
|||
)
|
||||
}
|
||||
|
||||
view.clipToPadding = false
|
||||
view.clipChildren = false
|
||||
|
||||
// we default to 25sp, this is needed as RoundedBackgroundColorSpan breaks on override sizes
|
||||
val size = data.fixedTextSize ?: 25.0f
|
||||
view.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, size)
|
||||
|
|
|
|||
|
|
@ -90,9 +90,12 @@ import kotlinx.coroutines.sync.Mutex
|
|||
import kotlinx.coroutines.sync.withLock
|
||||
import okhttp3.Cache
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
object AppContextUtils {
|
||||
fun RecyclerView.isRecyclerScrollable(): Boolean {
|
||||
val layoutManager =
|
||||
|
|
@ -632,17 +635,16 @@ object AppContextUtils {
|
|||
}
|
||||
}
|
||||
|
||||
// Deprecate after next stable
|
||||
/* @Deprecated(
|
||||
message = "Use splitUrlParameters instead.",
|
||||
replaceWith = ReplaceWith(
|
||||
expression = "splitUrlParameters(url.toString())",
|
||||
imports = ["com.lagradost.cloudstream3.splitUrlParameters"],
|
||||
),
|
||||
level = DeprecationLevel.WARNING,
|
||||
) */
|
||||
fun splitQuery(url: java.net.URL): Map<String, String> {
|
||||
return com.lagradost.cloudstream3.splitUrlParameters(url.toString())
|
||||
fun splitQuery(url: URL): Map<String, String> {
|
||||
val queryPairs: MutableMap<String, String> = LinkedHashMap()
|
||||
val query: String = url.query
|
||||
val pairs = query.split("&").toTypedArray()
|
||||
for (pair in pairs) {
|
||||
val idx = pair.indexOf("=")
|
||||
queryPairs[URLDecoder.decode(pair.substring(0, idx), "UTF-8")] =
|
||||
URLDecoder.decode(pair.substring(idx + 1), "UTF-8")
|
||||
}
|
||||
return queryPairs
|
||||
}
|
||||
|
||||
/**| S1:E2 Hello World
|
||||
|
|
@ -900,4 +902,4 @@ object AppContextUtils {
|
|||
} else null
|
||||
return currentAudioFocusRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -181,11 +181,11 @@ object DataStore {
|
|||
}
|
||||
|
||||
fun <T : Any> Context.getKey(path: String, valueType: Class<T>): T? {
|
||||
return try {
|
||||
try {
|
||||
val json: String = getSharedPrefs().getString(path, null) ?: return null
|
||||
parseJson(json, valueType.kotlin)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
return parseJson(json, valueType.kotlin)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,37 +193,21 @@ object DataStore {
|
|||
setKey(getFolderName(folder, path), value)
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
message = "Use parseJson<T>(this) directly instead.",
|
||||
level = DeprecationLevel.WARNING,
|
||||
replaceWith = ReplaceWith(
|
||||
expression = "parseJson<T>(this)",
|
||||
imports = ["com.lagradost.cloudstream3.utils.AppUtils.parseJson"],
|
||||
),
|
||||
)
|
||||
inline fun <reified T : Any> String.toKotlinObject(): T {
|
||||
return parseJson(this)
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
message = "Use parseJson<T>(this) directly instead.",
|
||||
level = DeprecationLevel.WARNING,
|
||||
replaceWith = ReplaceWith(
|
||||
expression = "parseJson<T>(this)",
|
||||
imports = ["com.lagradost.cloudstream3.utils.AppUtils.parseJson"],
|
||||
),
|
||||
)
|
||||
fun <T : Any> String.toKotlinObject(valueType: Class<T>): T {
|
||||
return parseJson(this, valueType.kotlin)
|
||||
}
|
||||
|
||||
// GET KEY GIVEN PATH AND DEFAULT VALUE, NULL IF ERROR
|
||||
inline fun <reified T : Any> Context.getKey(path: String, defVal: T?): T? {
|
||||
return try {
|
||||
try {
|
||||
val json: String = getSharedPrefs().getString(path, null) ?: return defVal
|
||||
parseJson<T>(json)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
return json.toKotlinObject()
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -149,12 +149,10 @@ object TvChannelUtils {
|
|||
.setInputId(inputId)
|
||||
.build()
|
||||
|
||||
val channelUri = runCatching {
|
||||
context.contentResolver.insert(
|
||||
TvContractCompat.Channels.CONTENT_URI,
|
||||
channel.toContentValues()
|
||||
)
|
||||
}.getOrNull()
|
||||
val channelUri = context.contentResolver.insert(
|
||||
TvContractCompat.Channels.CONTENT_URI,
|
||||
channel.toContentValues()
|
||||
)
|
||||
|
||||
channelUri?.let {
|
||||
val channelId = ContentUris.parseId(it)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.lagradost.cloudstream3.utils.serializers
|
||||
|
||||
import android.net.Uri
|
||||
import com.lagradost.cloudstream3.InternalAPI
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
|
|
@ -27,7 +26,6 @@ import kotlinx.serialization.encoding.Encoder
|
|||
* val uri: Uri,
|
||||
* )
|
||||
*/
|
||||
@InternalAPI
|
||||
object UriSerializer : KSerializer<Uri> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="m612,668 l56,-56 -148,-148v-184h-80v216l172,172ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,480ZM480,800q133,0 226.5,-93.5T800,480q0,-133 -93.5,-226.5T480,160q-133,0 -226.5,93.5T160,480q0,133 93.5,226.5T480,800Z"
|
||||
android:fillColor="#e3e3e3"/>
|
||||
</vector>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -13,13 +14,13 @@
|
|||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/subtitles_click_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
|
||||
|
||||
|
|
@ -32,21 +33,14 @@
|
|||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/settings_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/title_settings" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/help_btt"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/help"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/baseline_help_outline_24" />
|
||||
android:src="@drawable/baseline_help_outline_24"
|
||||
android:contentDescription="@string/help" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
|
|
@ -68,7 +62,7 @@
|
|||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
@ -99,8 +93,8 @@
|
|||
android:id="@+id/apply_btt_holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingHorizontal="?android:attr/listPreferredItemPaddingStart">
|
||||
|
||||
<EditText
|
||||
|
|
@ -114,19 +108,19 @@
|
|||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
tools:ignore="LabelFor"
|
||||
tools:text="@string/profile_number" />
|
||||
tools:text="@string/profile_number"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/save_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/sort_save" />
|
||||
android:text="@string/sort_save"
|
||||
style="@style/WhiteButton" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/close_btt"
|
||||
style="@style/BlackButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:text="@string/sort_close" />
|
||||
android:text="@string/sort_close"
|
||||
style="@style/BlackButton" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
|
|
|||
|
|
@ -227,12 +227,6 @@
|
|||
tools:listitem="@layout/homepage_parent"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<TextClock
|
||||
android:id="@+id/home_clock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/home_api_fab"
|
||||
style="@style/ExtendedFloatingActionButton"
|
||||
|
|
|
|||
|
|
@ -93,7 +93,6 @@
|
|||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
|
@ -164,22 +163,6 @@
|
|||
tools:listitem="@layout/homepage_parent_tv"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<TextClock
|
||||
android:id="@+id/home_clock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxWidth="600dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:visibility="visible"
|
||||
android:format12Hour="hh:mm a"
|
||||
android:format24Hour="HH:mm"
|
||||
android:fontFamily="@font/google_sans"
|
||||
android:textStyle="bold"
|
||||
android:gravity="start"
|
||||
android:layout_gravity="start"
|
||||
android:layout_margin="10dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/home_api_holder"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
|
|
@ -264,15 +264,6 @@
|
|||
tools:visibility="visible"
|
||||
android:layout_gravity="center"/>
|
||||
</LinearLayout>
|
||||
|
||||
<TextClock
|
||||
android:id="@+id/player_video_clock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:gravity="end"
|
||||
android:visibility="gone"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Removed as it has no use anymore-->
|
||||
|
|
@ -1085,8 +1076,6 @@
|
|||
|
||||
<FrameLayout
|
||||
android:id="@+id/subtitle_holder"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
|
|
|||
|
|
@ -352,22 +352,6 @@
|
|||
android:layout_marginEnd="32dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextClock
|
||||
android:id="@+id/player_video_clock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:gravity="end"
|
||||
android:maxWidth="600dp"
|
||||
android:textAlignment="viewEnd"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:visibility="visible"
|
||||
android:format12Hour="hh:mm a"
|
||||
android:format24Hour="HH:mm"
|
||||
android:fontFamily="@font/google_sans"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/player_video_title_holder"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
@ -1132,8 +1116,6 @@
|
|||
|
||||
<FrameLayout
|
||||
android:id="@+id/subtitle_holder"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
|
|
|||
|
|
@ -160,13 +160,6 @@
|
|||
<!-- android:layout_gravity="center"-->
|
||||
<!-- android:src="@drawable/outline_edit_24" />-->
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/settings_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:text="@string/title_settings" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
|||
|
|
@ -10,31 +10,13 @@
|
|||
style="@style/CheckLabel"
|
||||
android:layout_gravity="center"
|
||||
tools:text="hello" />
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
<ImageView
|
||||
android:id="@+id/pinicon"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/action_settings"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/ic_baseline_tune_24"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:contentDescription="@string/title_settings" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pinicon"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/pin_ic"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
</LinearLayout>
|
||||
android:src="@drawable/pin_ic"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
tools:ignore="RtlHardcoded" />
|
||||
</FrameLayout>
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/profile_settings_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- <ScrollView-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="match_parent">-->
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_rowWeight="1"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/profile_settings"
|
||||
android:textColor="?attr/textColor"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/profile_hide_negative_sources"
|
||||
style="@style/SettingsItem"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/google_sans"
|
||||
android:nextFocusLeft="@id/apply_btt"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:nextFocusDown="@id/profile_hide_error_sources"
|
||||
android:text="@string/profile_hide_negative_sources"
|
||||
app:drawableEndCompat="@null"
|
||||
app:trackTint="@color/toggle_selector" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/profile_hide_error_sources"
|
||||
style="@style/SettingsItem"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/google_sans"
|
||||
android:nextFocusLeft="@id/apply_btt"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:nextFocusUp="@id/profile_hide_negative_sources"
|
||||
android:nextFocusDown="@id/apply_btt"
|
||||
android:text="@string/profile_hide_error_sources"
|
||||
app:drawableEndCompat="@null"
|
||||
app:trackTint="@color/toggle_selector" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:gravity="bottom|end"
|
||||
android:orientation="horizontal"
|
||||
tools:ignore="UselessParent">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/apply_btt"
|
||||
style="@style/WhiteButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:nextFocusRight="@id/cancel_btt"
|
||||
android:nextFocusUp="@id/subtitles_italic"
|
||||
android:text="@string/sort_apply"
|
||||
android:visibility="visible">
|
||||
|
||||
<requestFocus />
|
||||
</com.google.android.material.button.MaterialButton>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cancel_btt"
|
||||
style="@style/BlackButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:nextFocusLeft="@id/apply_btt"
|
||||
android:nextFocusUp="@id/subtitles_remove_captions"
|
||||
android:text="@string/sort_cancel" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
<!-- </ScrollView>-->
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
@ -102,8 +102,6 @@
|
|||
|
||||
<FrameLayout
|
||||
android:id="@+id/subtitle_holder"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
|
@ -171,12 +169,6 @@
|
|||
android:layout_marginEnd="32dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextClock
|
||||
android:id="@+id/player_video_clock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/player_video_title_holder"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -756,9 +756,4 @@
|
|||
<string name="video_singular">مقطع</string>
|
||||
<string name="skip_type_preview">استعراض</string>
|
||||
<string name="player_is_live">البث قائم</string>
|
||||
<string name="tv_layout_clock_settings">أظهر ساعة الوقت الحقيقي</string>
|
||||
<string name="tv_layout_clock_settings_des">أظهر شاشة الوقت الحقيقي في قمة الشاشة. ينطبق على الصفحة الرئيسية و المشغل</string>
|
||||
<string name="profile_settings">اعدادات الملف الشخصي</string>
|
||||
<string name="profile_hide_negative_sources">أخفي المصادر صاحبة الأولوية السلبية</string>
|
||||
<string name="profile_hide_error_sources">أخفي المصادر التي تحتوي أخطاء</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<string name="result_poster_img_des">পোস্টার</string>
|
||||
<string name="play_with_app_name">ক্লাউডস্ট্রিম দিয়ে চালান</string>
|
||||
<string name="title_home">হোম</string>
|
||||
<string name="cast_format" formatted="true">অভিনয়ে: %s</string>
|
||||
<string name="cast_format" formatted="true">অভিনয়েঃ %s</string>
|
||||
<string name="next_episode_time_day_format" formatted="true">%1$dদিন %2$dঘন্টা %3$dমিনিট</string>
|
||||
<string name="next_episode_time_hour_format" formatted="true">%1$dঘন্টা %2$dমিনিট</string>
|
||||
<string name="next_episode_time_min_format" formatted="true">%d মিনিট</string>
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
<string name="title_search">খুঁজুন</string>
|
||||
<string name="title_downloads">ডাউনলোডসমূহ</string>
|
||||
<string name="title_settings">সেটিংস</string>
|
||||
<string name="app_dub_sub_episode_text_format" formatted="true">%1$s পর্ব %2$d</string>
|
||||
<string name="app_dub_sub_episode_text_format" formatted="true">%1$s এপি %2$d</string>
|
||||
<string name="next_episode_format" formatted="true">পর্ব %d মুক্তির তারিখ</string>
|
||||
<string name="result_share">শেয়ার</string>
|
||||
<string name="result_open_in_browser">ব্রাউজারে খুলুন</string>
|
||||
|
|
@ -131,8 +131,8 @@
|
|||
<string name="backup_failed">স্টোরেজ এর অনুমতি অনুপস্থিত। দয়া করে আবার চেষ্টা করুন।</string>
|
||||
<string name="settings_info">তথ্য</string>
|
||||
<string name="show_trailers_settings">ট্রেইলার প্রদর্শন করুন</string>
|
||||
<string name="kitsu_settings">Kitsu থেকে পোস্টারসমূহ প্রদর্শন করুন</string>
|
||||
<string name="automatic_plugin_updates">স্বয়ংক্রিয় প্লাগইন আপডেট</string>
|
||||
<string name="kitsu_settings">কিটসু হতে পোস্টারসমূহ প্রদর্শন করুন</string>
|
||||
<string name="automatic_plugin_updates">স্বয়ংক্রিয়ভাবে প্লাগিন এর হালনাগাদ</string>
|
||||
<string name="automatic_plugin_download">স্বয়ংক্রিয়ভাবে প্লাগিনসমুহের ডাউনলোড</string>
|
||||
<string name="category_updates">হালনাগাদ ও ব্যাকআপ</string>
|
||||
<string name="updates_settings">অ্যাপ এর হালনাগাদ দেখান</string>
|
||||
|
|
@ -148,9 +148,9 @@
|
|||
<string name="double_tap_to_seek_settings_des">সামনে বা পিছনের দিকে যেতে ডান বা বাম দিকে দুবার আলতো চাপুন</string>
|
||||
<string name="delete_file">ফাইল ডিলিট</string>
|
||||
<string name="subs_default_reset_toast">মান ডিফল্ট এ রিসেট করুন</string>
|
||||
<string name="updates_settings_des">স্টার্টআপে নতুন আপডেটের জন্য স্বয়ংক্রিয়ভাবে অনুসন্ধান করুন যোগ।</string>
|
||||
<string name="updates_settings_des">স্টার্টআপে নতুন আপডেটের জন্য স্বয়ংক্রিয়ভাবে অনুসন্ধান করুন</string>
|
||||
<string name="movies_singular">সিনেমা</string>
|
||||
<string name="show_fillers_settings">অ্যানিমে ফিলার পর্ব প্রদর্শন</string>
|
||||
<string name="show_fillers_settings">এনিমে এর ফিলার পর্ব দেখায়</string>
|
||||
<string name="tv_series">টিভি সিরিজ</string>
|
||||
<string name="no_links_found_toast">লিংক পাওয়া যায়নি</string>
|
||||
<string name="benene_des">চা খাওয়ানো হয়েছে</string>
|
||||
|
|
@ -174,7 +174,7 @@
|
|||
<string name="delete">ডিলিট</string>
|
||||
<string name="start">শুরু</string>
|
||||
<string name="cartoons">কার্টুন</string>
|
||||
<string name="apk_installer_settings_des">Some devices do not support the new package installer. Try the legacy option if updates do not install।</string>
|
||||
<string name="apk_installer_settings_des">কিছু ফোন নতুন প্যাকেজ ইনস্টলার সাপোর্ট করে না। যদি আপডেটগুলি ইনস্টল না হয় তবে পুরোনো পদ্ধতি ব্যবহার করে দেখুন।</string>
|
||||
<string name="no_subtitles">সাবটাইটেল নেই</string>
|
||||
<string name="no_chromecast_support_toast">এই প্রোভাইডার ক্রোমকাস্ট সাপোর্ট করে না</string>
|
||||
<string name="advanced_search">উন্নত অনুসন্ধান</string>
|
||||
|
|
@ -204,7 +204,7 @@
|
|||
<string name="anim">আমাদের তৈরি Anime দেখার অ্যাপ্লিকেশন</string>
|
||||
<string name="nsfw">18+</string>
|
||||
<string name="anime">এনিমে</string>
|
||||
<string name="pref_filter_search_quality">অনুসন্ধানের ফলে নির্বাচিত ভিডিওর মান লুকান</string>
|
||||
<string name="pref_filter_search_quality">অনুসন্ধান ফলাফলে নির্বাচিত ভিডিও কুয়ালিটি লুকান</string>
|
||||
<string name="app_storage">অ্যাপ</string>
|
||||
<string name="livestreams">লাইভস্ট্রিম</string>
|
||||
<string name="apk_installer_settings">APK ইনস্টলার</string>
|
||||
|
|
@ -353,38 +353,4 @@
|
|||
<string name="play_from_beginning_img_des">শুরু থেকে চালু করুন</string>
|
||||
<string name="speech_recognition_unavailable">স্পিচ রিকগনিশন উপলব্ধ নেই</string>
|
||||
<string name="begin_speaking">কথা বলা শুরু করুন…</string>
|
||||
<string name="download_queue">ডাউনলোড তালিকা</string>
|
||||
<string name="play_full_series_button">সম্পূর্ণ সিরিজ প্লে করুন</string>
|
||||
<string name="torrent_info">এই ভিডিওটি একটি টরেন্ট, অর্থাৎ আপনার ভিডিও অ্যাক্টিভিটি ট্র্যাক করা সম্ভব।\nএগোনোর আগে টরেন্টিং সম্পর্কে ভালোভাবে বুঝে নিন।</string>
|
||||
<string name="downloads_delete_select">ডিলিট করার জন্য আইটেম সিলেক্ট করুন</string>
|
||||
<string name="downloads_empty">বর্তমানে কোনো ডাউনলোড নেই।</string>
|
||||
<string name="queue_empty_message">বর্তমানে কোনো ডাউনলোড কিউতে নেই।</string>
|
||||
<string name="offline_file">অফলাইনে দেখার জন্য উপলব্ধ</string>
|
||||
<string name="select_all">সব সিলেক্ট করুন</string>
|
||||
<string name="deselect_all">সব ডিসিলেক্ট করুন</string>
|
||||
<string name="open_local_video">লোকাল ভিডিও খুলুন</string>
|
||||
<string name="extra_brightness_settings">অতিরিক্ত ব্রাইটনেস</string>
|
||||
<string name="extra_brightness_settings_des">ডিসপ্লে ব্রাইটনেস ১০০% ছাড়িয়ে গেলে ব্রাইটনেস ফিল্টার চালু করুন</string>
|
||||
<string name="search_suggestions">সার্চ সাজেশন</string>
|
||||
<string name="search_suggestions_des">টাইপ করার সময় সার্চ সাজেশন দেখান</string>
|
||||
<string name="clear_suggestions">সাজেশন মুছে ফেলুন</string>
|
||||
<string name="show_player_metadata_overlay">প্লেয়ার মেটাডেটা ওভারলে প্রদর্শন</string>
|
||||
<string name="show_cast_in_details">কাস্ট প্যানেল প্রদর্শন</string>
|
||||
<string name="install_prerelease">প্রাক-মুক্তি সংস্করণ ইনস্টলেশন</string>
|
||||
<string name="prerelease_already_installed">প্রাক-মুক্তি সংস্করণ ইতোমধ্যে ইনস্টল রয়েছে।</string>
|
||||
<string name="prerelease_install_failed">প্রাক-মুক্তি সংস্করণ ইনস্টল করতে সমস্যা হয়েছে।</string>
|
||||
<string name="delete_format" formatted="true">(%1$d | %2$s) মুছুন</string>
|
||||
<string name="test_warning">সাবধান</string>
|
||||
<string name="delete_message_multiple" formatted="true">আপনি কি নিম্নলিখিত আইটেমগুলো স্থায়ীভাবে মুছতে চান?\n\n%s</string>
|
||||
<string name="delete_files">ফাইল মুছুন</string>
|
||||
<string name="delete_message_series_episodes" formatted="true">আপনি কি নিশ্চিত যে আপনি %1$s-এ নিচের এপিসোডগুলো স্থায়ীভাবে মুছে ফেলতে চান?\n\n%2$s</string>
|
||||
<string name="delete_message_series_section" formatted="true">আপনি নিচের সিরিজগুলোর সব এপিসোডও স্থায়ীভাবে মুছে ফেলবেন:\n\n%s</string>
|
||||
<string name="delete_message_series_only" formatted="true">আপনি কি নিশ্চিত যে আপনি নিচের সিরিজটির সব এপিসোড স্থায়ীভাবে মুছে ফেলতে চান?\n\n%s</string>
|
||||
<string name="music_singular">সঙ্গীত</string>
|
||||
<string name="audio_book_singular">অডিও বই</string>
|
||||
<string name="custom_media_singular">মিডিয়া</string>
|
||||
<string name="audio_singular">অডিও</string>
|
||||
<string name="podcast_singular">পডকাস্ট</string>
|
||||
<string name="video_singular">ভিডিও</string>
|
||||
<string name="encoding_error">এনকোডিং ত্রুটি</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -754,15 +754,4 @@
|
|||
<string name="video_singular">Video</string>
|
||||
<string name="skip_type_preview">Náhled</string>
|
||||
<string name="player_is_live">Živě</string>
|
||||
<string name="profile_settings">Nastavení profilu</string>
|
||||
<string name="profile_hide_negative_sources">Skrýt zdroje s negativní prioritou</string>
|
||||
<string name="profile_hide_error_sources">Skrýt zdroje s chybami</string>
|
||||
<plurals name="links_hidden">
|
||||
<item quantity="one">%d skrytý odkaz</item>
|
||||
<item quantity="few">%d skryté odkazy</item>
|
||||
<item quantity="many">%d skrytých odkazů</item>
|
||||
<item quantity="other">%d skrytých odkazů</item>
|
||||
</plurals>
|
||||
<string name="tv_layout_clock_settings">Zobrazit hodiny s reálným časem</string>
|
||||
<string name="tv_layout_clock_settings_des">Zobrazit hodiny s reálným časem v horní části obrazovky. Platí pro domovskou obrazovku a přehrávač</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -737,9 +737,4 @@
|
|||
<string name="video_singular">Vídeo</string>
|
||||
<string name="skip_type_preview">Vista previa</string>
|
||||
<string name="player_is_live">En Vivo</string>
|
||||
<string name="tv_layout_clock_settings">Mostrar reloj en tiempo real</string>
|
||||
<string name="tv_layout_clock_settings_des">Muestra un reloj en tiempo real en la parte superior de la pantalla. Se aplica a la página de inicio y al reproductor</string>
|
||||
<string name="profile_settings">Configuración del perfil</string>
|
||||
<string name="profile_hide_negative_sources">Ocultar fuentes con prioridad negativa</string>
|
||||
<string name="profile_hide_error_sources">Ocultar fuentes con errores</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@
|
|||
<string name="live_singular">Livestream</string>
|
||||
<string name="nsfw_singular">NSFW</string>
|
||||
<string name="other_singular">Altro</string>
|
||||
<string name="source_error">Errore fonte</string>
|
||||
<string name="source_error">Errore sorgente</string>
|
||||
<string name="remote_error">Errore remoto</string>
|
||||
<string name="render_error">Errore del renderer</string>
|
||||
<string name="unexpected_error">Errore imprevisto del lettore</string>
|
||||
|
|
@ -247,7 +247,7 @@
|
|||
<string name="check_for_update">Controlla aggiornamenti</string>
|
||||
<string name="video_lock">Blocca</string>
|
||||
<string name="video_aspect_ratio_resize">Ridimensiona</string>
|
||||
<string name="video_source">Fonte</string>
|
||||
<string name="video_source">Sorgente</string>
|
||||
<string name="video_skip_op">Salta OP</string>
|
||||
<string name="dont_show_again">Non mostrare di nuovo</string>
|
||||
<string name="skip_update">Salta questo aggiornamento</string>
|
||||
|
|
@ -346,7 +346,7 @@
|
|||
<string name="actor_main">Protagonista</string>
|
||||
<string name="actor_supporting">Supporto</string>
|
||||
<string name="actor_background">Secondario</string>
|
||||
<string name="home_source">Fonte</string>
|
||||
<string name="home_source">Sorgente</string>
|
||||
<string name="home_random">Casuale</string>
|
||||
<string name="coming_soon">Prossimamente…</string>
|
||||
<string name="quality_cam">Cam</string>
|
||||
|
|
@ -670,7 +670,7 @@
|
|||
<string name="sort_episodes_date_oldest">Data di messa in onda (più vecchia)</string>
|
||||
<string name="sort_button_rating">Valutazione %s</string>
|
||||
<string name="starting_plugin_update_manually">Avvio del processo di aggiornamento plugin!</string>
|
||||
<string name="plugins_updated_manually">%d plugin aggiornati correttamente</string>
|
||||
<string name="plugins_updated_manually">%d plugin aggiornati con successo!</string>
|
||||
<string name="no_plugins_updated_manually">Nessun plugin è stato aggiornato.</string>
|
||||
<string name="player_notification_channel_name">Notifiche lettore</string>
|
||||
<string name="player_notification_channel_description">La notifica del lettore per controllare la riproduzione in background</string>
|
||||
|
|
@ -733,7 +733,7 @@
|
|||
<string name="clear_suggestions">Cancella suggerimenti</string>
|
||||
<string name="show_cast_in_details">Mostra pannello cast</string>
|
||||
<string name="video_info">Info sui media</string>
|
||||
<string name="source_name">Nome fonte</string>
|
||||
<string name="source_name">Nome sorgente</string>
|
||||
<string name="extra_brightness_settings">Luminosità extra</string>
|
||||
<string name="extra_brightness_settings_des">Attiva il filtro di luminosità quando viene superato il 100% della luminosità dello schermo</string>
|
||||
<string name="extra_brightness_key">extra_brightness_enabled</string>
|
||||
|
|
@ -753,20 +753,10 @@
|
|||
<item quantity="many">%d download in coda</item>
|
||||
<item quantity="other">%d download in coda</item>
|
||||
</plurals>
|
||||
<string name="source_priority">Priorità fonti</string>
|
||||
<string name="source_priority_help">Decidi come le fonti video devono essere ordinate nel lettore</string>
|
||||
<string name="source_priority">Priorità sorgente</string>
|
||||
<string name="source_priority_help">Decidi come le sorgenti video devono essere ordinate nel lettore</string>
|
||||
<string name="show_player_metadata_overlay">Mostra sovrapposizione metadati lettore</string>
|
||||
<string name="video_singular">Video</string>
|
||||
<string name="skip_type_preview">Anteprima</string>
|
||||
<string name="player_is_live">Live</string>
|
||||
<string name="profile_settings">Impostazioni profilo</string>
|
||||
<string name="profile_hide_negative_sources">Nascondi fonti con priorità negativa</string>
|
||||
<string name="profile_hide_error_sources">Nascondi fonti con errori</string>
|
||||
<plurals name="links_hidden">
|
||||
<item quantity="one">%d collegamento nascosto</item>
|
||||
<item quantity="many">%d collegamenti nascosti</item>
|
||||
<item quantity="other">%d collegamenti nascosti</item>
|
||||
</plurals>
|
||||
<string name="tv_layout_clock_settings">Mostra orologio in tempo reale</string>
|
||||
<string name="tv_layout_clock_settings_des">Mostra un orologio in tempo reale nella parte superiore dello schermo. Si applica alla homepage e al lettore</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -730,7 +730,4 @@
|
|||
<string name="source_priority">ソースの優先順位</string>
|
||||
<string name="source_priority_help">プレイヤーでのビデオソースの並び順を設定します</string>
|
||||
<string name="show_player_metadata_overlay">プレイヤーメタデータオーバーレイを表示</string>
|
||||
<string name="video_singular">動画</string>
|
||||
<string name="skip_type_preview">プレビュー</string>
|
||||
<string name="player_is_live">ライブ</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,18 +25,18 @@
|
|||
<string name="sort_copy">Kopijuoti</string>
|
||||
<string name="benene_des">Duoti bananai</string>
|
||||
<string name="home_more_info">Daugiau informacijos</string>
|
||||
<string name="title_downloads">Atsisiuntimai</string>
|
||||
<string name="title_downloads">Atsiuntimai</string>
|
||||
<string name="subs_auto_select_language">Automatiškai pasirinkti kalbą</string>
|
||||
<string name="error_loading_links_toast">Klaida kraunant nuorodas</string>
|
||||
<string name="go_forward_30">+30</string>
|
||||
<string name="download_done">Atsisiuntimas baigtas</string>
|
||||
<string name="download_done">Atsiuntimas baigtas</string>
|
||||
<string name="continue_watching">Tęsti žiūrėjimą</string>
|
||||
<string name="new_update_format" formatted="true">Rastas atnaujinimas!\n%1$s -> %2$s</string>
|
||||
<string name="new_update_format" formatted="true">Rastas atnaujinimas! \n%1$s -> %2$s</string>
|
||||
<string name="subs_download_languages">Atsisiųsti kalbas</string>
|
||||
<string name="search_provider_text_providers">Ieškoti naudojant tiekėjus</string>
|
||||
<string name="go_back_img_des">Grįžti atgal</string>
|
||||
<string name="downloading">Siunčiama</string>
|
||||
<string name="episode_more_options_des">Daugiau parinkčių</string>
|
||||
<string name="episode_more_options_des">Daugiau pasirinkčiu</string>
|
||||
<string name="play_episode">Paleisti seriją</string>
|
||||
<string name="player_speed">Grotuvo greitis</string>
|
||||
<string name="benene_count_text">%d Bananai duoti kūrėjams</string>
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
<string name="result_tags">Žanrai</string>
|
||||
<string name="go_back_30">-30</string>
|
||||
<string name="episode_poster_img_des">Serijos plakatas</string>
|
||||
<string name="vpn_might_be_needed">Gali reikėti VPN šiam tiekėjui, kad veiktų teisingai</string>
|
||||
<string name="vpn_might_be_needed">Gali reikėti VPN šitam tiekėjui, kad veiktų teisingai</string>
|
||||
<string name="search_hint_site" formatted="true">Ieškoti %s…</string>
|
||||
<string name="github">Github</string>
|
||||
<string name="benene_count_text_none">Nėra duotu bananų</string>
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
<string name="cancel">Atšaukti</string>
|
||||
<string name="start">Pradėti</string>
|
||||
<string name="cartoons_singular">Filmukas</string>
|
||||
<string name="download_canceled">Atsisiuntimas atšauktas</string>
|
||||
<string name="download_canceled">Atsiuntimas atšauktas</string>
|
||||
<string name="advanced_search">Išplėstinė paieška</string>
|
||||
<string name="empty_library_logged_in_message">Tuščias sąrašas. Pabandykite pasirinkti kitą sąrašą.</string>
|
||||
<string name="chromecast_subtitles_settings">Chromecast subtitrai</string>
|
||||
|
|
@ -93,10 +93,10 @@
|
|||
<string name="type_completed">Užbaigta</string>
|
||||
<string name="use_system_brightness_settings_des">Naudoti sistemos ryškumą programos grotuve vietoj tamsumo</string>
|
||||
<string name="restore_failed_format" formatted="true">Nepavyko atstatyti duomenis iš failo %s</string>
|
||||
<string name="play_trailer_button">Paleisti anonsą</string>
|
||||
<string name="play_livestream_button">Paleisti gyvą transliaciją</string>
|
||||
<string name="play_trailer_button">Paleisti anonsa</string>
|
||||
<string name="play_livestream_button">Paleisti gyva transliacija</string>
|
||||
<string name="no_episodes_found">Nerasta serijų</string>
|
||||
<string name="vpn_torrent">Šis tiekėjas yra iš Torrent\'ų, rekomenduojamas VPN</string>
|
||||
<string name="vpn_torrent">Šis tiekėjas yra iš Torrentų, VPN rekomenduojama naudoti</string>
|
||||
<string name="test_failed">Nepavyko</string>
|
||||
<string name="result_poster_img_des">Plakatas</string>
|
||||
<string name="popup_play_file">Paleisti failą</string>
|
||||
|
|
@ -108,21 +108,21 @@
|
|||
<string name="redo_setup_process">Perdaryti nustatymo procesą</string>
|
||||
<string name="episodes_range">%1$d-%2$d</string>
|
||||
<string name="benene">Duoti bananą kūrėjams</string>
|
||||
<string name="go_back">Sugrįšti</string>
|
||||
<string name="go_back">Sugryšti</string>
|
||||
<string name="copy_link_toast">Nuoroda nukopijuota į iškarpinę</string>
|
||||
<string name="search">Paieška</string>
|
||||
<string name="settings_info">Informacija</string>
|
||||
<string name="skip_loading">Praleisti įkėlimą</string>
|
||||
<string name="skip_loading">Praleisti įkėlima</string>
|
||||
<string name="home_info">Informacija</string>
|
||||
<string name="next_episode_format" formatted="true">Serija %d bus išleista</string>
|
||||
<string name="sort_save">Išsaugoti</string>
|
||||
<string name="clipboard_too_large">Perdaug teksto. Nepavyko išsaugoti i iškarpynę.</string>
|
||||
<string name="download_failed">Atsisiuntimas nepavyko</string>
|
||||
<string name="download_failed">Atsiuntimas nepavyko</string>
|
||||
<string name="result_share">Pasidalinti</string>
|
||||
<string name="home_main_poster_img_des">Pagrindinis Plakatas</string>
|
||||
<string name="pick_source">Šaltiniai</string>
|
||||
<string name="title_settings">Nustatymai</string>
|
||||
<string name="title_search">Paieška</string>
|
||||
<string name="title_search">Ieškoti</string>
|
||||
<string name="loading">Kraunama…</string>
|
||||
<string name="action_remove_watching">Pašalinti</string>
|
||||
<string name="action_open_watching">Daugiau informacijos</string>
|
||||
|
|
@ -138,7 +138,7 @@
|
|||
<string name="sort_close">Uždaryti</string>
|
||||
<string name="action_add_to_bookmarks">Nustatyti žiūrėjimo statusą</string>
|
||||
<string name="play_with_app_name">Paleisti su CloudStream</string>
|
||||
<string name="subs_subtitle_elevation">Subtitrų lygis</string>
|
||||
<string name="subs_subtitle_elevation">Subtitrų iškėlimas</string>
|
||||
<string name="episodes">Serijos</string>
|
||||
<string name="cast_format" formatted="true">Skleisti: %s</string>
|
||||
<string name="season">Sezonas</string>
|
||||
|
|
@ -248,51 +248,4 @@
|
|||
<string name="confirm_exit_dialog">Ar tikrai norite išeiti?</string>
|
||||
<string name="action_remove_from_watched">Pašalinti iš žiūrimų</string>
|
||||
<string name="audio_tracks">Garso takelis</string>
|
||||
<string name="filler" formatted="true">Užpildymas</string>
|
||||
<string name="duration_format" formatted="true">%d min</string>
|
||||
<string name="title_home">Namai</string>
|
||||
<string name="download_queue">Atsisiuntimų eilė</string>
|
||||
<string name="speech_recognition_unavailable">Balso atpažinimas nepasiekiamas</string>
|
||||
<string name="begin_speaking">Pradėkite kalbėti…</string>
|
||||
<string name="type_dropped">Nežiūrimas</string>
|
||||
<string name="play_full_series_button">Paleisti visą seriją</string>
|
||||
<string name="torrent_info">Šis įrašas yra Torrent\'e, tai reiškia, kad tavo įrašo veikla gali būti sekama. Įsitikinkite, kad suprantate Torrenting prieš tęsiant.</string>
|
||||
<string name="reload_error">Atkurti ryšį…</string>
|
||||
<string name="downloads_delete_select">Pasirinkite elementus, kuriuos norite pašalinti</string>
|
||||
<string name="downloads_empty">Šiuo metu atsisiuntimų nėra.</string>
|
||||
<string name="queue_empty_message">Šiuo metu atsisiuntimų eilėje nėra.</string>
|
||||
<string name="offline_file">Pasiekiama žiūrėti neprisijungus</string>
|
||||
<string name="select_all">Pasirinkti viską</string>
|
||||
<string name="deselect_all">Panaikinti visus pasirinkimus</string>
|
||||
<string name="stream">Tinklo srautas</string>
|
||||
<string name="open_local_video">Atidaryti vietinį video</string>
|
||||
<string name="links_reloaded_toast">Nuorodos perkrautos</string>
|
||||
<string name="app_subbed_text">Subtitrai</string>
|
||||
<string name="repo_copy_label">Repozitorijos pavadinimas ir adresas</string>
|
||||
<string name="toast_copied">nukopijuota!</string>
|
||||
<string name="subscribe_tooltip">Naujos serijos pranešimas</string>
|
||||
<string name="result_search_tooltip">Ieškoti kituose plėtiniuose</string>
|
||||
<string name="recommendations_tooltip">Rodyti rekomendacijas</string>
|
||||
<string name="subs_outline_color">Apvado spalva</string>
|
||||
<string name="subs_edge_type">Rėmelio tipas</string>
|
||||
<string name="search_provider_text_types">Ieškoti naudojant tipus</string>
|
||||
<string name="subs_hold_to_reset_to_default">Laikyti ilgai, kad sugražinti pradinius nustatymus</string>
|
||||
<string name="subs_import_text" formatted="true">Įkelti šriftus sudedant juos į %s</string>
|
||||
<string name="provider_info_meta">Metaduomenys nesuteikiamos svetainės, vaizdo įkėlimas nepavyks, jeigu jo nebus svetainėje.</string>
|
||||
<string name="torrent_plot">Aprašymas</string>
|
||||
<string name="normal_no_plot">Nėra siužeto aprašymo</string>
|
||||
<string name="torrent_no_plot">Aprašymas nerastas</string>
|
||||
<string name="show_log_cat">Rodyti Logcat</string>
|
||||
<string name="picture_in_picture">Vaizdas vaizde</string>
|
||||
<string name="picture_in_picture_des">Tęsia atkūrimą mažame grotuve virš kitų programų</string>
|
||||
<string name="player_size_settings">Grotuvo dydžio pakeitimo mygtukas</string>
|
||||
<string name="player_size_settings_des">Pašalinti juodas paraštes</string>
|
||||
<string name="player_subtitles_settings">Subtitrai</string>
|
||||
<string name="chromecast_subtitles_settings_des">Chromecast subtitrų nustatymai</string>
|
||||
<string name="eigengraumode_settings">Atkūrimo sparta</string>
|
||||
<string name="speed_setting_summary">Prideda greičio pasirinkimą grotuve</string>
|
||||
<string name="swipe_to_seek_settings">Braukite, kas ieškotumėte</string>
|
||||
<string name="swipe_to_seek_settings_des">Braukite į šonus, kad kontroliuotumėte padėtį vaizdo įraše</string>
|
||||
<string name="swipe_to_change_settings">Braukite, kad pakeistumėte nustatymus</string>
|
||||
<string name="swipe_to_change_settings_des">Braukite aukštyn arba žemyn ekrano kairėje arba dešinėje, jei norite pakeisti ryškumą arba garsą</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -287,12 +287,12 @@
|
|||
<string name="pref_category_defaults">Parasts</string>
|
||||
<string name="pref_category_looks">Izskats</string>
|
||||
<string name="pref_category_ui_features">Funkcijas</string>
|
||||
<string name="category_general">Vispārīgie</string>
|
||||
<string name="category_general">Ģenerāls</string>
|
||||
<string name="random_button_settings">Randomā poga</string>
|
||||
<string name="random_button_settings_desc">Rādīt izlases pogu Sākums un Bibliotēka sadaļās</string>
|
||||
<string name="provider_lang_settings">Papildinājuma valodas</string>
|
||||
<string name="app_layout">Lietotnes izkārtojums</string>
|
||||
<string name="preferred_media_settings">Vēlamie multimediji</string>
|
||||
<string name="preferred_media_settings">Izvēlētā media</string>
|
||||
<string name="enable_nsfw_on_providers">Iespējot nepiedienīgu, izaicinošu saturu (NSFW) atbalstītajos papildinājumos</string>
|
||||
<string name="subtitles_encoding">Subtitru kodējums</string>
|
||||
<string name="category_providers">Devēji</string>
|
||||
|
|
@ -372,7 +372,7 @@
|
|||
<string name="error">Kļūda</string>
|
||||
<string name="subtitles_remove_captions">Noņemt slēgtos parakstus no subtitriem</string>
|
||||
<string name="subtitles_remove_bloat">Noņemt lieko no subtitriem (piemēram, reklāmu)</string>
|
||||
<string name="subtitles_filter_lang">Atlasīt pēc vēlamās multimediju valodas</string>
|
||||
<string name="subtitles_filter_lang">Filtrēt pēc vēlamās multivides valodas</string>
|
||||
<string name="extras">Ekstras</string>
|
||||
<string name="trailer">Treileris</string>
|
||||
<string name="network_adress_example">https://piemērs.com/piemērs.mp4</string>
|
||||
|
|
@ -382,7 +382,7 @@
|
|||
<string name="previous">Iepriekšējais</string>
|
||||
<string name="skip_setup">Izlaist uzstādīšanu</string>
|
||||
<string name="app_layout_subtext">Mainiet lietotnes izskatu, lai tā atbilstu savai ierīcei</string>
|
||||
<string name="preferred_media_subtext">Ko jūs vēlētos skatīt</string>
|
||||
<string name="preferred_media_subtext">Ko tu vēlies redzēt</string>
|
||||
<string name="setup_done">Pabeigts</string>
|
||||
<string name="extensions">Papildinājumi</string>
|
||||
<string name="add_repository">Pievienot repozitoriju</string>
|
||||
|
|
@ -607,11 +607,4 @@
|
|||
<string name="delete_message_series_only" formatted="true">Vai tiešām vēlaties neatgriezeniski dzēst visas šī seriāla, raidījuma epizodes?\n\n%s</string>
|
||||
<string name="queue_empty_message">Pašlaik nav nevienas rindā ievietotas lejupielādes.</string>
|
||||
<string name="open_local_video">Atvērt vietējo video</string>
|
||||
<string name="custom_media_singular">Multimedija</string>
|
||||
<string name="video_info">Multimediju informācija</string>
|
||||
<string name="torrent_preferred_media">Iespējojiet torrentu Iestatījumi/Pakalpojumu sniedzēji/Vēlamie multimediji sadaļā</string>
|
||||
<string name="subscribe_tooltip">Paziņojums par jaunu epizodi</string>
|
||||
<string name="extra_brightness_settings">Papildu spilgtums</string>
|
||||
<string name="extra_brightness_key">Papildu spilgtums iespējots</string>
|
||||
<string name="search_suggestions">Meklēšanas ieteikumi</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -677,22 +677,4 @@
|
|||
<string name="confirm_before_exiting_title">Bevestig voor afsluiten</string>
|
||||
<string name="confirm_before_exiting_desc">Toon dialoogvenster voordat de app wordt afgesloten</string>
|
||||
<string name="subs_edge_size">Randgrote</string>
|
||||
<string name="show_player_metadata_overlay">Laat Speler Metadata Overlay zien</string>
|
||||
<string name="device_pin_error_message">Kon de apparaat PIN niet vinden, probeer locale authenticatie</string>
|
||||
<string name="torrent_preferred_media">Zet torrent aan in Instellingen/Providers/Media voorkeur</string>
|
||||
<string name="torrent_not_accepted">Herstart de app en accepteer de Stream Torrent pop-up om verder te gaan.</string>
|
||||
<string name="update_plugins_manually">Handmatige Update Knop</string>
|
||||
<string name="biometric_setting_summary">Open de app met Vingerafdruk, Face ID, PIN, Pattern en Wachtwoord.</string>
|
||||
<string name="preview_seekbar">Seekbar preview</string>
|
||||
<string name="preview_seekbar_desc">Zet preview thumbnail aan op seekbar</string>
|
||||
<string name="software_decoding">Software decodering</string>
|
||||
<string name="software_decoding_desc">Software decodering staat de mogelijkheid toe om de speler videobestanden af te laten spelen op jouw apparaat, maar het kan een haperige of onstabiele kijkervaring zorgen op hoge resolutie.</string>
|
||||
<string name="volume_exceeded_100">Volume heeft de 100% overschreden</string>
|
||||
<string name="update_plugins">Update Plugins</string>
|
||||
<string name="no_plugins_updated_manually">Geen plugins zijn geupdate.</string>
|
||||
<string name="player_notification_channel_name">Speler meldingen</string>
|
||||
<string name="player_notification_channel_description">De speler melding om de kijkervaring van de achtergrond de besturen</string>
|
||||
<string name="subtitles_from_online">Online</string>
|
||||
<string name="download_parallel_settings_des">Hoeveel verschillende items kunnen in parallel gedownload worden</string>
|
||||
<string name="parallel_downloads">Parallel downloads</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -649,7 +649,7 @@
|
|||
<string name="sort_episodes_number_desc">Odcinek (malejąco)</string>
|
||||
<string name="sort_episodes_rating_high_low">Ocena (najwyższa)</string>
|
||||
<string name="update_plugins">Zaktualizuj wtyczki</string>
|
||||
<string name="plugins_updated_manually">Pomyślnie zaktualizowano wtyczki: %d</string>
|
||||
<string name="plugins_updated_manually">Pomyślnie zaktualizowano wtyczki: %d!</string>
|
||||
<string name="no_plugins_updated_manually">Nie zaktualizowano żadnych wtyczek.</string>
|
||||
<string name="update_plugins_manually">Zaktualizuj wtyczki ręcznie</string>
|
||||
<string name="starting_plugin_update_manually">Rozpoczęcie procesu aktualizacji wtyczek!</string>
|
||||
|
|
@ -742,15 +742,4 @@
|
|||
<string name="video_singular">Wideo</string>
|
||||
<string name="skip_type_preview">Zapowiedź</string>
|
||||
<string name="player_is_live">Na żywo</string>
|
||||
<string name="profile_settings">Ustawienia profilu</string>
|
||||
<string name="profile_hide_negative_sources">Ukryj źródła z negatywnym priorytetem</string>
|
||||
<string name="profile_hide_error_sources">Ukryj źródła z błędami</string>
|
||||
<plurals name="links_hidden">
|
||||
<item quantity="one">%d ukryty odnośnik</item>
|
||||
<item quantity="few">%d ukryte odnośniki</item>
|
||||
<item quantity="many">%d ukrytych odnośników</item>
|
||||
<item quantity="other">%d ukrytych odnośników</item>
|
||||
</plurals>
|
||||
<string name="tv_layout_clock_settings">Pokaż zegar czasu rzeczywistego</string>
|
||||
<string name="tv_layout_clock_settings_des">Wyświetl zegar czasu rzeczywistego u góry ekranu. Dotyczy strony głównej i odtwarzacza</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -735,13 +735,4 @@
|
|||
<string name="video_singular">Video</string>
|
||||
<string name="skip_type_preview">Förhandsvisning</string>
|
||||
<string name="player_is_live">Live</string>
|
||||
<string name="tv_layout_clock_settings">Visa realtidsklocka</string>
|
||||
<string name="tv_layout_clock_settings_des">Visa en realtidsklocka högst upp på skärmen. Gäller startsidan och spelaren</string>
|
||||
<string name="profile_settings">Profilinställningar</string>
|
||||
<string name="profile_hide_negative_sources">Dölj källor med negativ prioritet</string>
|
||||
<string name="profile_hide_error_sources">Dölj källor med fel</string>
|
||||
<plurals name="links_hidden">
|
||||
<item quantity="one">%d dold länk</item>
|
||||
<item quantity="other">%d dolda länkar</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -609,7 +609,7 @@
|
|||
<string name="confirm_before_exiting_desc">பயன்பாட்டிலிருந்து வெளியேறுவதற்கு முன் உரையாடலைக் காட்டு</string>
|
||||
<string name="auth_locally">உள்நாட்டில் அங்கீகரிக்கவும்</string>
|
||||
<string name="dismiss">தள்ளுபடி</string>
|
||||
<string name="device_pin_url_message">உங்கள் ச்மார்ட்போன் அல்லது கணினியில் <b>%s</b> ஐப் பார்வையிட்டு மேலே உள்ள குறியீட்டை உள்ளிடவும்</string>
|
||||
<string name="device_pin_url_message">உங்கள் ச்மார்ட்போன் அல்லது கணினியில் <b>%s </b> ஐப் பார்வையிட்டு மேலே உள்ள குறியீட்டை உள்ளிடவும்</string>
|
||||
<string name="device_pin_error_message">சாதன முள் குறியீட்டைப் பெற முடியாது, உள்ளக அங்கீகாரத்தை முயற்சிக்கவும்</string>
|
||||
<string name="device_pin_expired_message">முள் குறியீடு இப்போது காலாவதியானது!</string>
|
||||
<string name="subs_edge_size">விளிம்பு அளவு</string>
|
||||
|
|
@ -652,7 +652,7 @@
|
|||
<string name="update_plugins">செருகுநிரல்களைப் புதுப்பிக்கவும்</string>
|
||||
<string name="update_plugins_manually">செருகுநிரல்களை கைமுறையாக புதுப்பிக்கவும்</string>
|
||||
<string name="starting_plugin_update_manually">சொருகி புதுப்பிப்பு செயல்முறையைத் தொடங்குகிறது!</string>
|
||||
<string name="plugins_updated_manually">%d செருகுநிரல்(கள்) வெற்றிகரமாக புதுப்பிக்கப்பட்டது</string>
|
||||
<string name="plugins_updated_manually">வெற்றிகரமாக புதுப்பிக்கப்பட்டது %d சொருகி (கள்)!</string>
|
||||
<string name="no_plugins_updated_manually">செருகுநிரல்கள் எதுவும் புதுப்பிக்கப்படவில்லை.</string>
|
||||
<string name="player_notification_channel_name">பிளேயர் அறிவிப்புகள்</string>
|
||||
<string name="player_notification_channel_description">பின்னணியில் இருந்து பின்னணியைக் கட்டுப்படுத்துவதற்கான பிளேயர் அறிவிப்பு</string>
|
||||
|
|
@ -731,17 +731,4 @@
|
|||
<item quantity="one">%d பதிவிறக்கம் வரிசையில் உள்ளது</item>
|
||||
<item quantity="other">%d பதிவிறக்கங்கள் வரிசையில் உள்ளன</item>
|
||||
</plurals>
|
||||
<string name="show_player_metadata_overlay">பிளேயர் மேனிலை தரவு மேலடுக்கைக் காட்டு</string>
|
||||
<string name="video_singular">ஒளிதோற்றம்</string>
|
||||
<string name="tv_layout_clock_settings">உண்மையான நேர கடிகாரத்தைக் காட்டு</string>
|
||||
<string name="tv_layout_clock_settings_des">திரையின் மேற்புறத்தில் நிகழ் நேர கடிகாரத்தைக் காட்டு. முகப்புப்பக்கம் மற்றும் பிளேயருக்குப் பொருந்தும்</string>
|
||||
<string name="skip_type_preview">முன்னோட்டம்</string>
|
||||
<string name="profile_settings">சுயவிவர அமைப்புகள்</string>
|
||||
<string name="profile_hide_negative_sources">எதிர்மறையான முன்னுரிமையுடன் ஆதாரங்களை மறை</string>
|
||||
<string name="profile_hide_error_sources">பிழைகளுடன் ஆதாரங்களை மறைக்கவும்</string>
|
||||
<plurals name="links_hidden">
|
||||
<item quantity="one">%d மறைக்கப்பட்ட இணைப்பு</item>
|
||||
<item quantity="other">%d மறைக்கப்பட்ட இணைப்புகள்</item>
|
||||
</plurals>
|
||||
<string name="player_is_live">வாழ்க</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -661,7 +661,7 @@
|
|||
<string name="sort_button_date">Tarih %s</string>
|
||||
<string name="update_plugins">Eklentileri Güncelle</string>
|
||||
<string name="starting_plugin_update_manually">Eklenti güncellemesi başlıyor!</string>
|
||||
<string name="plugins_updated_manually">%d eklenti başarıyla güncellendi</string>
|
||||
<string name="plugins_updated_manually">%d eklenti başarıyla güncellendi!</string>
|
||||
<string name="no_plugins_updated_manually">Hiçbir eklenti güncellenmedi.</string>
|
||||
<string name="update_plugins_manually">Eklentileri el ile güncelle</string>
|
||||
<string name="player_notification_channel_description">Oynatıcı bildirimi arka planda oynatmasını kontrol etmek içindir</string>
|
||||
|
|
@ -749,13 +749,4 @@
|
|||
<string name="player_is_live">Canlı</string>
|
||||
<string name="skip_type_preview">Ön Gösterim</string>
|
||||
<string name="video_singular">Video</string>
|
||||
<string name="tv_layout_clock_settings">Gerçek zamanlı saati göster</string>
|
||||
<string name="tv_layout_clock_settings_des">Ekranın üst kısmında gerçek zamanlı bir saat göster. Ana sayfa ve oynatıcı için uygulanır</string>
|
||||
<string name="profile_settings">Profil ayarları</string>
|
||||
<string name="profile_hide_negative_sources">Eksi önceliğe sahip kaynakları gizle</string>
|
||||
<string name="profile_hide_error_sources">Hatalı kaynakları gizle</string>
|
||||
<plurals name="links_hidden">
|
||||
<item quantity="one">%d gizli bağlantı</item>
|
||||
<item quantity="other">%d gizli bağlantı</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -646,7 +646,7 @@
|
|||
<string name="sort_button_episode">Еп. %s</string>
|
||||
<string name="sort_button_date">Дата %s</string>
|
||||
<string name="update_plugins">Оновити розширення</string>
|
||||
<string name="plugins_updated_manually">Успішно оновлено %d розширення(-ь)</string>
|
||||
<string name="plugins_updated_manually">Успішно оновлено %d розширення(-ь)!</string>
|
||||
<string name="update_plugins_manually">Оновити розширення вручну</string>
|
||||
<string name="starting_plugin_update_manually">Починається оновлення розширень!</string>
|
||||
<string name="no_plugins_updated_manually">Не оновлено жодного розширення.</string>
|
||||
|
|
@ -739,15 +739,4 @@
|
|||
<string name="video_singular">Відео</string>
|
||||
<string name="skip_type_preview">Передперегляд</string>
|
||||
<string name="player_is_live">Наживо</string>
|
||||
<string name="tv_layout_clock_settings">Показувати поточний час</string>
|
||||
<string name="tv_layout_clock_settings_des">Показувати поточний час у верхній частині екрана (на головній та у програвачі)</string>
|
||||
<string name="profile_settings">Налаштування профілю</string>
|
||||
<string name="profile_hide_negative_sources">Приховати джерела з від’ємним пріоритетом</string>
|
||||
<string name="profile_hide_error_sources">Приховати джерела з помилками</string>
|
||||
<plurals name="links_hidden">
|
||||
<item quantity="one">%d приховане посилання</item>
|
||||
<item quantity="few">%d приховані посилання</item>
|
||||
<item quantity="many">%d прихованих посилань</item>
|
||||
<item quantity="other">%d прихованих посилань</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
<string name="next_episode">Tập tiếp theo</string>
|
||||
<string name="result_tags">Thể loại</string>
|
||||
<string name="result_share">Chia sẻ</string>
|
||||
<string name="result_open_in_browser">Phát bằng Trình duyệt</string>
|
||||
<string name="result_open_in_browser">Mở bằng trình duyệt</string>
|
||||
<string name="skip_loading">Bỏ tải</string>
|
||||
<string name="loading">Đang tải…</string>
|
||||
<string name="type_watching">Đang xem</string>
|
||||
|
|
@ -255,13 +255,13 @@
|
|||
<string name="watch_quality_pref">Chất lượng xem ưu tiên (WiFi)</string>
|
||||
<string name="limit_title">Số ký tự tối đa tiêu đề trình phát</string>
|
||||
<string name="limit_title_rez">Hiển thị thông tin trình phát</string>
|
||||
<string name="video_buffer_size_settings">Kích thước bộ đệm video</string>
|
||||
<string name="video_buffer_length_settings">Thời lượng bộ đệm video</string>
|
||||
<string name="video_buffer_size_settings">Kích thước bộ nhớ đệm video</string>
|
||||
<string name="video_buffer_length_settings">Thời lượng bộ nhớ đệm</string>
|
||||
<string name="video_buffer_disk_settings">Bộ nhớ đệm video trên thiết bị</string>
|
||||
<string name="video_buffer_clear_settings">Xoá bộ nhớ đệm hình ảnh và video</string>
|
||||
<string name="video_ram_description">Sẽ gây lỗi nếu đặt quá cao trên thiết bị có bộ nhớ thấp như Android TV.</string>
|
||||
<string name="video_disk_description">Sẽ gây lỗi nếu đặt quá cao trên thiết bị có dung lượng lưu trữ thấp như Android TV.</string>
|
||||
<string name="dns_pref">DNS qua HTTPS</string>
|
||||
<string name="video_disk_description">Sẽ gây lỗi nếu đặt quá cao trên máy có dung lượng lưu trữ thấp như Android TV.</string>
|
||||
<string name="dns_pref">DNS over HTTPS</string>
|
||||
<string name="dns_pref_summary">Rất hữu ích để bỏ chặn ISP</string>
|
||||
<string name="add_site_pref">Bản sao trang web</string>
|
||||
<string name="remove_site_pref">Xoá trang web</string>
|
||||
|
|
@ -748,12 +748,4 @@
|
|||
<string name="player_is_live">Trực tiếp</string>
|
||||
<string name="video_singular">Video</string>
|
||||
<string name="skip_type_preview">Xem trước</string>
|
||||
<string name="tv_layout_clock_settings">Hiển thị đồng hồ thời gian thực</string>
|
||||
<string name="tv_layout_clock_settings_des">Hiển thị đồng hồ thời gian thực ở phía trên màn hình. Áp dụng cho trang chủ và trình phát</string>
|
||||
<string name="profile_settings">Cài đặt hồ sơ</string>
|
||||
<string name="profile_hide_negative_sources">Ẩn nguồn có độ ưu tiên âm</string>
|
||||
<string name="profile_hide_error_sources">Ẩn nguồn có lỗi</string>
|
||||
<plurals name="links_hidden">
|
||||
<item quantity="other">%d liên kết bị ẩn</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -655,7 +655,7 @@
|
|||
<string name="sort_button_rating">得分 %s</string>
|
||||
<string name="sort_button_date">日期 %s</string>
|
||||
<string name="update_plugins">更新插件</string>
|
||||
<string name="plugins_updated_manually">成功更新了 %d 个插件</string>
|
||||
<string name="plugins_updated_manually">成功更新了 %d 个插件!</string>
|
||||
<string name="no_plugins_updated_manually">没有插件被更新。</string>
|
||||
<string name="update_plugins_manually">手动更新插件</string>
|
||||
<string name="sort_episodes_number_asc">集数(升序)</string>
|
||||
|
|
@ -747,12 +747,4 @@
|
|||
<string name="video_singular">视频</string>
|
||||
<string name="skip_type_preview">预览</string>
|
||||
<string name="player_is_live">播放中</string>
|
||||
<string name="tv_layout_clock_settings">显示实时时钟</string>
|
||||
<string name="tv_layout_clock_settings_des">在屏幕顶部显示实时时钟。应用于主页和播放器</string>
|
||||
<string name="profile_settings">配置文件设置</string>
|
||||
<string name="profile_hide_negative_sources">隐藏负优先级的来源</string>
|
||||
<string name="profile_hide_error_sources">隐藏有错误的来源</string>
|
||||
<plurals name="links_hidden">
|
||||
<item quantity="other">%d 条隐藏链接</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@
|
|||
<string name="continue_watching">Працягнуць прагляд</string>
|
||||
<string name="action_remove_watching">Выдаліць</string>
|
||||
<string name="action_open_watching">Больш інфармацыі</string>
|
||||
<string name="action_open_play">@string/home_play</string>
|
||||
<string name="action_open_play">\@string/home_play</string>
|
||||
<string name="vpn_might_be_needed">Для карэктнай працы гэтага пастаўшчыка можа спатрэбіцца VPN</string>
|
||||
<string name="vpn_torrent">Гэты пастаўшчык — Torrent, рэкамендуецца VPN</string>
|
||||
<string name="provider_info_meta">Вэб-сайт не пастаўляе метададзеных, загрузіць відэа не ўдасца, калі на сайце яго няма.</string>
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
<string name="continue_watching">Continua mirant</string>
|
||||
<string name="action_remove_watching">Eliminar</string>
|
||||
<string name="action_open_watching">Més info</string>
|
||||
<string name="action_open_play">@string/home_play</string>
|
||||
<string name="action_open_play">\@string/home_play</string>
|
||||
<string name="vpn_might_be_needed">Potser necessiteu una VPN per que aquest proveïdor funcioni correctament</string>
|
||||
<string name="vpn_torrent">Aquest proveïdor és un Torrent, es recomana fer servir una VPN</string>
|
||||
<string name="provider_info_meta">El lloc no proveeix metadades, la càrrega del video fallarà si no existeix al lloc.</string>
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
<string name="subs_font_size">Madhësia e shkrimit</string>
|
||||
<string name="search_provider_text_providers">Kërko duke përdorur ofruesit</string>
|
||||
<string name="search_provider_text_types">Kërko duke përdorur tipet</string>
|
||||
<string name="benene_count_text">%d Banane të dhëna për developer-at</string>
|
||||
<string name="benene_count_text">%d Banane të dhëna per developer-at</string>
|
||||
<string name="benene_count_text_none">Asnjë Banane e dhënë</string>
|
||||
<string name="subs_auto_select_language">Zgjedhje automatike e gjuhës</string>
|
||||
<string name="subs_download_languages">Shkarko gjuhët</string>
|
||||
|
|
@ -127,7 +127,7 @@
|
|||
<string name="continue_watching">Vazhdo shikimin</string>
|
||||
<string name="action_remove_watching">Hiq</string>
|
||||
<string name="action_open_watching">Më shumë informacion</string>
|
||||
<string name="action_open_play">@string/home_play</string>
|
||||
<string name="action_open_play">\@string/home_play</string>
|
||||
<string name="vpn_might_be_needed">Një VPN mund të nevojitet që ky ofrues të funksionojë në rregull</string>
|
||||
<string name="vpn_torrent">Ky ofrues është Torrent, rekomandohet një VPN</string>
|
||||
<string name="provider_info_meta">Metadata nuk ofrohet nga kjo faqe, ngarkimi i videos do të dështojë nëse nuk ekziston në këtë faqe.</string>
|
||||
|
|
@ -216,7 +216,7 @@
|
|||
<string name="season_format">%1$s %2$d%3$s</string>
|
||||
<string name="no_season">Asnje sezon</string>
|
||||
<string name="episode">Episodi</string>
|
||||
<string name="episodes">Episode</string>
|
||||
<string name="episodes">Episodet</string>
|
||||
<string name="episodes_range">%1$d-%2$d</string>
|
||||
<string name="episode_format" formatted="true">%1$d %2$s</string>
|
||||
<string name="episode_upcoming_format" formatted="true">Vazhdon në %s</string>
|
||||
|
|
@ -679,7 +679,7 @@
|
|||
<string name="update_plugins">Përditëso shtesat</string>
|
||||
<string name="update_plugins_manually">Përditëso shtesat manualisht</string>
|
||||
<string name="starting_plugin_update_manually">Filloi procesi i përditësimit të shtesave!</string>
|
||||
<string name="plugins_updated_manually">%d Shtes(a) u përditësuan me sukses</string>
|
||||
<string name="plugins_updated_manually">%d Shtes(at) u përditësuan me sukses!</string>
|
||||
<string name="no_plugins_updated_manually">Nuk u përditësua asnjë shtesë.</string>
|
||||
<string name="player_notification_channel_name">Njoftimet e luajtësit</string>
|
||||
<string name="player_notification_channel_description">Njoftimi për kontrollin e luajtjes nga sfondi</string>
|
||||
|
|
@ -735,13 +735,4 @@
|
|||
<item quantity="other">%d shkarkime në radhë</item>
|
||||
</plurals>
|
||||
<string name="player_is_live">Live</string>
|
||||
<string name="profile_settings">Cilësimet e profil-it</string>
|
||||
<string name="profile_hide_negative_sources">Fshih burimet me prioritet negativ</string>
|
||||
<string name="profile_hide_error_sources">Fshih burimet me error-e</string>
|
||||
<plurals name="links_hidden">
|
||||
<item quantity="one">%d link i fshehur</item>
|
||||
<item quantity="other">%d linqe të fshehura</item>
|
||||
</plurals>
|
||||
<string name="tv_layout_clock_settings">Shfaq orën në kohë reale</string>
|
||||
<string name="tv_layout_clock_settings_des">Shfaq një orë në krye të ekranit. Aplikohet në faqen kryesore dhe në luajtës</string>
|
||||
</resources>
|
||||
|
|
@ -101,7 +101,6 @@
|
|||
<string name="opensubtitles_key">opensubtitles_key</string>
|
||||
<string name="subdl_key">subdl_key</string>
|
||||
<string name="animeskip_key">animeskip_key</string>
|
||||
<string name="tv_layout_clock_key">tv_layout_clock_key</string>
|
||||
|
||||
<string name="pref_category_security_key">pref_category_security_key</string>
|
||||
<string name="pref_category_gestures_key">pref_category_gestures_key</string>
|
||||
|
|
|
|||
|
|
@ -374,8 +374,6 @@
|
|||
<string name="pref_category_looks">Looks</string>
|
||||
<string name="pref_category_ui_features">Features</string>
|
||||
<string name="category_general">General</string>
|
||||
<string name="tv_layout_clock_settings">Show real time clock</string>
|
||||
<string name="tv_layout_clock_settings_des">Show a real time clock at the top of the screen. Applies to the homepage and player</string>
|
||||
<string name="random_button_settings">Random Button</string>
|
||||
<string name="random_button_settings_desc">Show random button on Homepage and Library</string>
|
||||
<string name="provider_lang_settings">Extension languages</string>
|
||||
|
|
@ -624,13 +622,6 @@
|
|||
<string name="action_subscribe">Subscribe</string>
|
||||
<string name="action_unsubscribe">Unsubscribe</string>
|
||||
<string name="profile_number">Profile %d</string>
|
||||
<string name="profile_settings">Profile settings</string>
|
||||
<string name="profile_hide_negative_sources">Hide sources with a negative priority</string>
|
||||
<string name="profile_hide_error_sources">Hide sources with errors</string>
|
||||
<plurals name="links_hidden">
|
||||
<item quantity="one">%d hidden link</item>
|
||||
<item quantity="other">%d hidden links</item>
|
||||
</plurals>
|
||||
<string name="wifi">Wi-Fi</string>
|
||||
<string name="mobile_data">Mobile data</string>
|
||||
<string name="set_default">Set default</string>
|
||||
|
|
@ -733,7 +724,7 @@
|
|||
<!--Manual Update Button-->
|
||||
<string name="update_plugins_manually">Update plugins manually</string>
|
||||
<string name="starting_plugin_update_manually">Starting plugin update process!</string>
|
||||
<string name="plugins_updated_manually">Successfully updated %d plugin(s)</string>
|
||||
<string name="plugins_updated_manually">Successfully updated %d plugin(s)!</string>
|
||||
<string name="no_plugins_updated_manually">No plugins were updated.</string>
|
||||
<string name="player_notification_channel_name">Player notifications</string>
|
||||
<string name="player_notification_channel_description">The player notification for controlling the playback from the background</string>
|
||||
|
|
@ -792,4 +783,5 @@
|
|||
<item quantity="other">%d downloads queued</item>
|
||||
</plurals>
|
||||
<string name="player_is_live">Live</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -89,12 +89,6 @@
|
|||
android:icon="@drawable/metadata_overlay_icon"
|
||||
android:key="@string/show_player_metadata_key"
|
||||
android:title="@string/show_player_metadata_overlay" />
|
||||
<SwitchPreference
|
||||
android:icon="@drawable/ic_baseline_clock_24"
|
||||
android:summary="@string/tv_layout_clock_settings_des"
|
||||
android:title="@string/tv_layout_clock_settings"
|
||||
android:defaultValue="false"
|
||||
android:key="@string/tv_layout_clock_key" />
|
||||
<SwitchPreference
|
||||
android:icon="@drawable/ic_baseline_play_arrow_24"
|
||||
android:summary="@string/random_button_settings_desc"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
CloudStream
|
||||
كلاودستريم
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
- চেঞ্জলগ যোগ করা হয়েছে!
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
CloudStream-3 দিয়ে সিনেমা, টিভি-সিরিজ ও অ্যানিমে স্ট্রিম ও ডাউনলোড করা যায়।
|
||||
|
||||
অ্যাপে কোনো বিজ্ঞাপন ও অ্যানালিটিক্স নেই এবং
|
||||
এটি একাধিক ট্রেলার ও মুভি সাইট সাপোর্ট করে, আরও আছে যেমন—
|
||||
বুকমার্ক
|
||||
সাবটাইটেল ডাউনলোড
|
||||
ক্রোমকাস্ট সাপোর্ট
|
||||
|
|
@ -1 +0,0 @@
|
|||
সিনেমা, টিভি সিরিজ ও অ্যানিমে স্ট্রিম ও ডাউনলোড করুন।
|
||||
|
|
@ -1 +0,0 @@
|
|||
CloudStream
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
CloudStream-3 ju lejon të shikoni dhe shkarkoni filma, seriale televizive dhe anime.
|
||||
|
||||
Aplikacioni vjen pa asnjë reklamë dhe analitikë
|
||||
dhe suporton faqe të shumta trailer-ash dhe filmash, dhe më shumë, p.sh.
|
||||
dhe suporton faqe të shumta trailerash dhe filmash dhe më shumë, p.sh.
|
||||
|
||||
Të ruajturat
|
||||
|
||||
|
|
|
|||
|
|
@ -26,12 +26,11 @@ junit = "4.13.2"
|
|||
junitKtx = "1.3.0"
|
||||
junitVersion = "1.3.0"
|
||||
juniversalchardet = "2.5.0"
|
||||
kotlinGradlePlugin = "2.4.0"
|
||||
kotlinGradlePlugin = "2.3.20"
|
||||
kotlinxAtomicfu = "0.33.0"
|
||||
kotlinxCollectionsImmutable = "0.4.0"
|
||||
kotlinxCoroutines = "1.11.0"
|
||||
kotlinxDatetime = "0.8.0"
|
||||
kotlinxIOCore = "0.9.1"
|
||||
kotlinxSerializationJson = "1.11.0"
|
||||
ksoup = "0.2.6"
|
||||
ktor = "3.5.0"
|
||||
|
|
@ -50,6 +49,7 @@ qrcodeKotlin = "4.5.0"
|
|||
rhino = { strictly = "1.8.1" } # Requires minSdk 26 or later beginning at version 1.9.0
|
||||
safefile = "0.0.8"
|
||||
shimmer = "0.5.0"
|
||||
tmdbJava = "2.13.0"
|
||||
torrentserver = "7861970"
|
||||
tvprovider = "1.1.0"
|
||||
video = "1.0.0"
|
||||
|
|
@ -59,7 +59,7 @@ zipline = "1.27.0"
|
|||
jvmTarget = "1.8"
|
||||
jdkToolchain = "17"
|
||||
minSdk = "23"
|
||||
compileSdk = "37"
|
||||
compileSdk = "36"
|
||||
targetSdk = "36"
|
||||
|
||||
versionCode = "68"
|
||||
|
|
@ -99,7 +99,6 @@ kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collec
|
|||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
|
||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
|
||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
|
||||
kotlinx-io-core = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinxIOCore" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
|
||||
ksoup = { module = "com.fleeksoft.ksoup:ksoup", version.ref = "ksoup" }
|
||||
ktor-http = { module = "io.ktor:ktor-http", version.ref = "ktor" }
|
||||
|
|
@ -130,6 +129,7 @@ qrcode-kotlin = { module = "io.github.g0dkar:qrcode-kotlin", version.ref = "qrco
|
|||
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
||||
safefile = { module = "com.github.LagradOst:SafeFile", version.ref = "safefile" }
|
||||
shimmer = { module = "com.facebook.shimmer:shimmer", version.ref = "shimmer" }
|
||||
tmdb-java = { module = "com.uwetrottmann.tmdb2:tmdb-java", version.ref = "tmdbJava" }
|
||||
torrentserver = { module = "com.github.recloudstream:torrentserver", version.ref = "torrentserver" }
|
||||
tvprovider = { module = "androidx.tvprovider:tvprovider", version.ref = "tvprovider" }
|
||||
video = { module = "com.google.android.mediahome:video", version.ref = "video" }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -41,7 +41,10 @@ kotlin {
|
|||
jvm()
|
||||
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xexpect-actual-classes")
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xexpect-actual-classes",
|
||||
"-Xannotation-default-target=param-property"
|
||||
)
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
|
@ -59,12 +62,12 @@ kotlin {
|
|||
implementation(libs.kotlinx.atomicfu)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.kotlinx.io.core)
|
||||
implementation(libs.kotlinx.serialization.json) // JSON Parser
|
||||
implementation(libs.ksoup) // HTML Parser
|
||||
implementation(libs.ktor.http)
|
||||
implementation(libs.nicehttp) // HTTP Library
|
||||
implementation(libs.rhino) // Run JavaScript
|
||||
implementation(libs.tmdb.java) // TMDB API v3 Wrapper Made with RetroFit
|
||||
implementation(libs.bundles.cryptography) // Cryptography
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +91,8 @@ kotlin {
|
|||
@OptIn(org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation::class)
|
||||
// https://kotlinlang.org/docs/gradle-binary-compatibility-validation.html
|
||||
abiValidation {
|
||||
filters {
|
||||
enabled.set(true)
|
||||
this.filters {
|
||||
exclude {
|
||||
annotatedWith.add("com.lagradost.cloudstream3.Prerelease")
|
||||
annotatedWith.add("com.lagradost.cloudstream3.InternalAPI")
|
||||
|
|
|
|||
|
|
@ -3,14 +3,19 @@ package com.lagradost.api
|
|||
import android.content.Context
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
private var contextRef: WeakReference<Context>? = null
|
||||
var ctx: WeakReference<Context>? = null
|
||||
|
||||
/**
|
||||
* Helper function for Android specific context. Not usable in JVM.
|
||||
* Do not use this unless absolutely necessary.
|
||||
*/
|
||||
actual fun getContext(): Any? = contextRef?.get()
|
||||
|
||||
actual fun setContext(context: Any?) {
|
||||
contextRef = (context as? Context)?.let { WeakReference(it) }
|
||||
actual fun getContext(): Any? {
|
||||
return ctx?.get()
|
||||
}
|
||||
|
||||
actual fun setContext(context: WeakReference<Any>) {
|
||||
val actualContext = context.get() as? Context
|
||||
if (actualContext != null) {
|
||||
ctx = WeakReference(actualContext)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
// TODO: add androidMain actual using LocaleListCompat.getAdjustedDefault() to respect
|
||||
// per-app language preferences set via AppCompatDelegate.setApplicationLocales()
|
||||
|
||||
actual fun getCurrentLocale(): String =
|
||||
Locale.getDefault().toLanguageTag()
|
||||
|
||||
actual fun localizedLanguageName(ietfTag: String, localizedTo: String): String? {
|
||||
val localeOfLangCode = Locale.forLanguageTag(ietfTag)
|
||||
val localeOfLocalizeTo = Locale.forLanguageTag(localizedTo)
|
||||
val displayName = localeOfLangCode.getDisplayName(localeOfLocalizeTo)
|
||||
|
||||
// Locale.getDisplayName() falls back to the raw tag or "language (country)" form
|
||||
// when it doesn't know how to render the name.
|
||||
val langCodeWithCountry = "${localeOfLangCode.language} ("
|
||||
val failed =
|
||||
displayName.equals(ietfTag, ignoreCase = true) ||
|
||||
displayName.contains(langCodeWithCountry, ignoreCase = true)
|
||||
|
||||
return if (failed) null else displayName
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
package com.lagradost.api
|
||||
|
||||
/**
|
||||
* Set context for Android-specific code such as webview.
|
||||
* Does nothing on non-Android platforms.
|
||||
*/
|
||||
expect fun setContext(context: Any?)
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Set context for android specific code such as webview.
|
||||
* Does nothing on JVM.
|
||||
*/
|
||||
expect fun setContext(context: WeakReference<Any>)
|
||||
/**
|
||||
* Helper function for Android specific context.
|
||||
* Do not use this unless absolutely necessary.
|
||||
* setContext() must be called before this is called.
|
||||
* @return Context if on Android, null if not.
|
||||
* @return Context if on android, null if not.
|
||||
*/
|
||||
expect fun getContext(): Any?
|
||||
|
|
|
|||
|
|
@ -761,64 +761,18 @@ fun MainAPI.fixUrl(url: String): String {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the urls based on quality
|
||||
*
|
||||
/** Sort the urls based on quality
|
||||
* @param urls Set of [ExtractorLink]
|
||||
*/
|
||||
* */
|
||||
fun sortUrls(urls: Set<ExtractorLink>): List<ExtractorLink> {
|
||||
return urls.sortedBy { t -> -t.quality }
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the parameters of a [Url] into a map of key-value pairs.
|
||||
*
|
||||
* Unlike a manual `split("&")` / `split("=")` implementation, this relies on Ktor's
|
||||
* built-in parameters parser ([Url.parameters]), which already handles URL-decoding,
|
||||
* malformed pairs, and parameters without a value.
|
||||
*
|
||||
* Note: if a parameter key appears multiple times (e.g. `?a=1&a=2`), only the **first**
|
||||
* value is kept, since the return type is `Map<String, String>`. Use [Url.parameters]
|
||||
* directly if you need all values for repeated keys.
|
||||
*
|
||||
* @param url the [Url] whose parameters should be extracted.
|
||||
* @return a map of decoded parameter names to their first decoded value.
|
||||
*
|
||||
* @sample
|
||||
* splitUrlParameters(Url("https://example.com/path?foo=bar&baz=qux"))
|
||||
* // returns {"foo": "bar", "baz": "qux"}
|
||||
*/
|
||||
@Prerelease
|
||||
fun splitUrlParameters(url: Url): Map<String, String> {
|
||||
return url.parameters.entries().associate { (key, values) -> key to values.firstOrNull().orEmpty() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the parameters of a raw URL [String] into a map of key-value pairs.
|
||||
*
|
||||
* Convenience overload for callers that have a URL as plain text rather than a parsed
|
||||
* [Url] instance. Internally parses [url] with Ktor's [Url] constructor and delegates
|
||||
* to [splitUrlParameters].
|
||||
*
|
||||
* @param url the URL string whose parameters should be extracted.
|
||||
* @return a map of decoded parameter names to their first decoded value.
|
||||
*
|
||||
* @sample
|
||||
* splitUrlParameters("https://example.com/path?foo=bar&baz=qux")
|
||||
* // returns {"foo": "bar", "baz": "qux"}
|
||||
*/
|
||||
@Prerelease
|
||||
fun splitUrlParameters(url: String): Map<String, String> {
|
||||
return splitUrlParameters(Url(url))
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize the first letter of string.
|
||||
*
|
||||
/** Capitalize the first letter of string.
|
||||
* @param str String to be capitalized
|
||||
* @return non-nullable String
|
||||
* @see capitalizeStringNullable
|
||||
*/
|
||||
* */
|
||||
fun capitalizeString(str: String): String {
|
||||
return capitalizeStringNullable(str) ?: str
|
||||
}
|
||||
|
|
@ -2576,21 +2530,13 @@ constructor(
|
|||
|
||||
@OptIn(FormatStringsInDatetimeFormats::class)
|
||||
fun Episode.addDate(date: String?, format: String = "yyyy-MM-dd") {
|
||||
if (date.isNullOrBlank()) return
|
||||
if (date == null) return
|
||||
this.date = runCatching {
|
||||
// First try standard ISO 8601 (e.g. "2026-01-01T12:30:00.000Z", "2026-05-17T14:35+02:00")
|
||||
runCatching { Instant.parse(date).toEpochMilliseconds() }
|
||||
.getOrElse {
|
||||
val fmt = DateTimeComponents.Format { byUnicodePattern(format) }
|
||||
|
||||
// Try parsing the full date first then only parse the beginning of the string
|
||||
// May lose time, but better than failing.
|
||||
val components = runCatching {
|
||||
DateTimeComponents.parse(date, fmt)
|
||||
}.recoverCatching {
|
||||
DateTimeComponents.parse(date.trimStart().take(format.length), fmt)
|
||||
}.getOrThrow()
|
||||
|
||||
val components = DateTimeComponents.parse(date, fmt)
|
||||
/**
|
||||
* Try multiple conversions in order of precision for non-ISO-8601 formats,
|
||||
* since the date string may or may not include time and/or timezone offset:
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class Acefile : ExtractorApi() {
|
||||
override val name = "Acefile"
|
||||
|
|
@ -16,7 +13,7 @@ open class Acefile : ExtractorApi() {
|
|||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = "/(?:d|download|player|f|file)/(\\w+)".toRegex().find(url)?.groupValues?.get(1)
|
||||
val script = getAndUnpack(app.get("$mainUrl/player/${id ?: return}").text)
|
||||
|
|
@ -30,14 +27,14 @@ open class Acefile : ExtractorApi() {
|
|||
newExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
video ?: return,
|
||||
video ?: return
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Source(
|
||||
@JsonProperty("data") @SerialName("data") val data: String? = null,
|
||||
val data: String? = null,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
|||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class Blogger : ExtractorApi() {
|
||||
override val name = "Blogger"
|
||||
|
|
@ -24,10 +22,10 @@ open class Blogger : ExtractorApi() {
|
|||
newExtractorLink(
|
||||
name,
|
||||
name,
|
||||
it.playUrl,
|
||||
it.play_url,
|
||||
) {
|
||||
this.referer = "https://www.youtube.com/"
|
||||
this.quality = when (it.formatId) {
|
||||
this.quality = when (it.format_id) {
|
||||
18 -> 360
|
||||
22 -> 720
|
||||
else -> Qualities.Unknown.value
|
||||
|
|
@ -38,13 +36,11 @@ open class Blogger : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class ResponseSource(
|
||||
@JsonProperty("play_url") @SerialName("play_url") val playUrl: String,
|
||||
@JsonProperty("format_id") @SerialName("format_id") val formatId: Int,
|
||||
@JsonProperty("play_url") val play_url: String,
|
||||
@JsonProperty("format_id") val format_id: Int
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,32 +13,30 @@ import dev.whyoleg.cryptography.DelicateCryptographyApi
|
|||
import dev.whyoleg.cryptography.algorithms.AES
|
||||
import io.ktor.http.Url
|
||||
import io.ktor.http.decodeURLPart
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
class Bysezejataos : ByseSX() {
|
||||
override val name = "Bysezejataos"
|
||||
override val mainUrl = "https://bysezejataos.com"
|
||||
override var name = "Bysezejataos"
|
||||
override var mainUrl = "https://bysezejataos.com"
|
||||
}
|
||||
|
||||
class ByseBuho : ByseSX() {
|
||||
override val name = "ByseBuho"
|
||||
override val mainUrl = "https://bysebuho.com"
|
||||
override var name = "ByseBuho"
|
||||
override var mainUrl = "https://bysebuho.com"
|
||||
}
|
||||
|
||||
class ByseVepoin : ByseSX() {
|
||||
override val name = "ByseVepoin"
|
||||
override val mainUrl = "https://bysevepoin.com"
|
||||
override var name = "ByseVepoin"
|
||||
override var mainUrl = "https://bysevepoin.com"
|
||||
}
|
||||
|
||||
class ByseQekaho : ByseSX() {
|
||||
override val name = "ByseQekaho"
|
||||
override val mainUrl = "https://byseqekaho.com"
|
||||
override var name = "ByseQekaho"
|
||||
override var mainUrl = "https://byseqekaho.com"
|
||||
}
|
||||
|
||||
open class ByseSX : ExtractorApi() {
|
||||
override val name = "Byse"
|
||||
override val mainUrl = "https://byse.sx"
|
||||
override var name = "Byse"
|
||||
override var mainUrl = "https://byse.sx"
|
||||
override val requiresReferer = true
|
||||
|
||||
private val aesGcm = CryptographyProvider.Default.get(AES.GCM)
|
||||
|
|
@ -114,7 +112,7 @@ open class ByseSX : ExtractorApi() {
|
|||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val refererUrl = getBaseUrl(url)
|
||||
val playbackRoot = getPlayback(url) ?: return
|
||||
|
|
@ -125,58 +123,64 @@ open class ByseSX : ExtractorApi() {
|
|||
name,
|
||||
streamUrl,
|
||||
mainUrl,
|
||||
headers = headers,
|
||||
headers = headers
|
||||
).forEach(callback)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class DetailsRoot(
|
||||
@JsonProperty("id") @SerialName("id") val id: Long,
|
||||
@JsonProperty("code") @SerialName("code") val code: String,
|
||||
@JsonProperty("title") @SerialName("title") val title: String,
|
||||
@JsonProperty("poster_url") @SerialName("poster_url") val posterUrl: String,
|
||||
@JsonProperty("description") @SerialName("description") val description: String,
|
||||
@JsonProperty("created_at") @SerialName("created_at") val createdAt: String,
|
||||
@JsonProperty("owner_private") @SerialName("owner_private") val ownerPrivate: Boolean,
|
||||
@JsonProperty("embed_frame_url") @SerialName("embed_frame_url") val embedFrameUrl: String,
|
||||
val id: Long,
|
||||
val code: String,
|
||||
val title: String,
|
||||
@JsonProperty("poster_url")
|
||||
val posterUrl: String,
|
||||
val description: String,
|
||||
@JsonProperty("created_at")
|
||||
val createdAt: String,
|
||||
@JsonProperty("owner_private")
|
||||
val ownerPrivate: Boolean,
|
||||
@JsonProperty("embed_frame_url")
|
||||
val embedFrameUrl: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PlaybackRoot(
|
||||
@JsonProperty("playback") @SerialName("playback") val playback: Playback,
|
||||
val playback: Playback,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Playback(
|
||||
@JsonProperty("algorithm") @SerialName("algorithm") val algorithm: String,
|
||||
@JsonProperty("iv") @SerialName("iv") val iv: String,
|
||||
@JsonProperty("payload") @SerialName("payload") val payload: String,
|
||||
@JsonProperty("key_parts") @SerialName("key_parts") val keyParts: List<String>,
|
||||
@JsonProperty("expires_at") @SerialName("expires_at") val expiresAt: String,
|
||||
@JsonProperty("decrypt_keys") @SerialName("decrypt_keys") val decryptKeys: DecryptKeys,
|
||||
@JsonProperty("iv2") @SerialName("iv2") val iv2: String,
|
||||
@JsonProperty("payload2") @SerialName("payload2") val payload2: String,
|
||||
val algorithm: String,
|
||||
val iv: String,
|
||||
val payload: String,
|
||||
@JsonProperty("key_parts")
|
||||
val keyParts: List<String>,
|
||||
@JsonProperty("expires_at")
|
||||
val expiresAt: String,
|
||||
@JsonProperty("decrypt_keys")
|
||||
val decryptKeys: DecryptKeys,
|
||||
val iv2: String,
|
||||
val payload2: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DecryptKeys(
|
||||
@JsonProperty("edge_1") @SerialName("edge_1") val edge1: String,
|
||||
@JsonProperty("edge_2") @SerialName("edge_2") val edge2: String,
|
||||
@JsonProperty("legacy_fallback") @SerialName("legacy_fallback") val legacyFallback: String,
|
||||
@JsonProperty("edge_1")
|
||||
val edge1: String,
|
||||
@JsonProperty("edge_2")
|
||||
val edge2: String,
|
||||
@JsonProperty("legacy_fallback")
|
||||
val legacyFallback: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PlaybackDecrypt(
|
||||
@JsonProperty("sources") @SerialName("sources") val sources: List<PlaybackDecryptSource>,
|
||||
val sources: List<PlaybackDecryptSource>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PlaybackDecryptSource(
|
||||
@JsonProperty("quality") @SerialName("quality") val quality: String,
|
||||
@JsonProperty("label") @SerialName("label") val label: String,
|
||||
@JsonProperty("mime_type") @SerialName("mime_type") val mimeType: String,
|
||||
@JsonProperty("url") @SerialName("url") val url: String,
|
||||
@JsonProperty("bitrate_kbps") @SerialName("bitrate_kbps") val bitrateKbps: Long,
|
||||
@JsonProperty("height") @SerialName("height") val height: Int?,
|
||||
val quality: String,
|
||||
val label: String,
|
||||
@JsonProperty("mime_type")
|
||||
val mimeType: String,
|
||||
val url: String,
|
||||
@JsonProperty("bitrate_kbps")
|
||||
val bitrateKbps: Long,
|
||||
val height: Any?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.USER_AGENT
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
|
|
@ -9,8 +8,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
|||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class Cda : ExtractorApi() {
|
||||
override var mainUrl = "https://ebd.cda.pl"
|
||||
|
|
@ -25,7 +22,7 @@ open class Cda : ExtractorApi() {
|
|||
"https://ebd.cda.pl/647x500/$mediaId", headers = mapOf(
|
||||
"Referer" to "https://ebd.cda.pl/647x500/$mediaId",
|
||||
"User-Agent" to USER_AGENT,
|
||||
"Cookie" to "cda.player=html5",
|
||||
"Cookie" to "cda.player=html5"
|
||||
)
|
||||
).document
|
||||
val dataRaw = doc.selectFirst("[player_data]")?.attr("player_data") ?: return null
|
||||
|
|
@ -89,17 +86,15 @@ open class Cda : ExtractorApi() {
|
|||
else -> a
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class VideoPlayerData(
|
||||
@JsonProperty("file") @SerialName("file") val file: String,
|
||||
@JsonProperty("qualities") @SerialName("qualities") val qualities: Map<String, String> = mapOf(),
|
||||
@JsonProperty("quality") @SerialName("quality") val quality: String?,
|
||||
@JsonProperty("ts") @SerialName("ts") val ts: Int?,
|
||||
@JsonProperty("hash2") @SerialName("hash2") val hash2: String?,
|
||||
val file: String,
|
||||
val qualities: Map<String, String> = mapOf(),
|
||||
val quality: String?,
|
||||
val ts: Int?,
|
||||
val hash2: String?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PlayerData(
|
||||
@JsonProperty("video") @SerialName("video") val video: VideoPlayerData,
|
||||
val video: VideoPlayerData
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,15 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.google.gson.Gson
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.newSubtitleFile
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||
import io.ktor.http.Url
|
||||
import io.ktor.http.decodeURLPart
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
class Geodailymotion : Dailymotion() {
|
||||
override val name = "GeoDailymotion"
|
||||
|
|
@ -31,25 +28,25 @@ open class Dailymotion : ExtractorApi() {
|
|||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val embedUrl = getEmbedUrl(url) ?: return
|
||||
val id = getVideoId(embedUrl) ?: return
|
||||
val metadataUrl = "$baseUrl/player/metadata/video/$id"
|
||||
val metaDataUrl = "$baseUrl/player/metadata/video/$id"
|
||||
|
||||
val response = app.get(metaDataUrl, referer = embedUrl).text
|
||||
val gson = Gson()
|
||||
val meta = gson.fromJson(response, MetaData::class.java)
|
||||
|
||||
val response = app.get(metadataUrl, referer = embedUrl).text
|
||||
val meta = parseJson<Metadata>(response)
|
||||
meta.qualities?.get("auto")?.forEach { quality ->
|
||||
val videoUrl = quality.url
|
||||
if (!videoUrl.isNullOrEmpty() && videoUrl.contains(".m3u8")) {
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
name,
|
||||
name,
|
||||
videoUrl,
|
||||
ExtractorLinkType.M3U8,
|
||||
)
|
||||
)
|
||||
callback.invoke(newExtractorLink(
|
||||
name,
|
||||
name,
|
||||
videoUrl,
|
||||
ExtractorLinkType.M3U8
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +55,7 @@ open class Dailymotion : ExtractorApi() {
|
|||
subtitleCallback(
|
||||
newSubtitleFile(
|
||||
subData.label,
|
||||
subUrl,
|
||||
subUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -71,7 +68,6 @@ open class Dailymotion : ExtractorApi() {
|
|||
val videoId = url.substringAfter("video=")
|
||||
return "$baseUrl/embed/video/$videoId"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
@ -81,27 +77,23 @@ open class Dailymotion : ExtractorApi() {
|
|||
return if (id.matches(videoIdRegex)) id else null
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
@JsonProperty("qualities") @SerialName("qualities") val qualities: Map<String, List<Quality>>?,
|
||||
@JsonProperty("subtitles") @SerialName("subtitles") val subtitles: SubtitlesWrapper?,
|
||||
data class MetaData(
|
||||
val qualities: Map<String, List<Quality>>?,
|
||||
val subtitles: SubtitlesWrapper?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Quality(
|
||||
@JsonProperty("type") @SerialName("type") val type: String?,
|
||||
@JsonProperty("url") @SerialName("url") val url: String?,
|
||||
val type: String?,
|
||||
val url: String?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SubtitlesWrapper(
|
||||
@JsonProperty("enable") @SerialName("enable") val enable: Boolean,
|
||||
@JsonProperty("data") @SerialName("data") val data: Map<String, SubtitleData>?,
|
||||
val enable: Boolean,
|
||||
val data: Map<String, SubtitleData>?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SubtitleData(
|
||||
@JsonProperty("label") @SerialName("label") val label: String,
|
||||
@JsonProperty("urls") @SerialName("urls") val urls: List<String>,
|
||||
val label: String,
|
||||
val urls: List<String>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.Prerelease
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
|
|
@ -11,29 +10,31 @@ import kotlinx.serialization.SerialName
|
|||
import kotlinx.serialization.Serializable
|
||||
|
||||
class Firestream : ExtractorApi() {
|
||||
override val name = "Firestream"
|
||||
override val mainUrl = "https://firestream.to"
|
||||
override val requiresReferer = false
|
||||
override val name: String = "Firestream"
|
||||
override val mainUrl: String = "https://firestream.to"
|
||||
override val requiresReferer: Boolean = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = url.removeSuffix("/").substringAfterLast("/")
|
||||
val url = getExtractorUrl(id)
|
||||
|
||||
val doc = app.get(url).document
|
||||
val token = doc.selectFirst("script[id=token-blob]")!!.data()
|
||||
val videoResponse = app.post("$mainUrl/api/videos/$id/resolve", json = mapOf("blob" to token))
|
||||
.parsed<VideoResponse>()
|
||||
|
||||
val videoResponse =
|
||||
app.post("$mainUrl/api/videos/$id/resolve", json = mapOf("blob" to token))
|
||||
.parsed<VideoResponse>()
|
||||
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
source = name,
|
||||
name = name,
|
||||
url = videoResponse.signedVideoUrl,
|
||||
url = videoResponse.signedVideoUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -44,6 +45,7 @@ class Firestream : ExtractorApi() {
|
|||
|
||||
@Serializable
|
||||
private data class VideoResponse(
|
||||
@JsonProperty("signedVideoUrl") @SerialName("signedVideoUrl") val signedVideoUrl: String,
|
||||
@SerialName("signedVideoUrl")
|
||||
val signedVideoUrl: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,16 +11,16 @@ import kotlinx.serialization.SerialName
|
|||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class Flyfile : ExtractorApi() {
|
||||
override val name = "FlyFile"
|
||||
override val mainUrl = "https://flyfile.app"
|
||||
override val requiresReferer = false
|
||||
override val name: String = "FlyFile"
|
||||
override val mainUrl: String = "https://flyfile.app"
|
||||
open val apiUrl: String = "https://api.flyfile.app"
|
||||
override val requiresReferer: Boolean = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val videoId = url.substringAfterLast("/")
|
||||
val videoInfo = app.get("$apiUrl/api/streaming/assign/$videoId")
|
||||
|
|
@ -32,14 +32,16 @@ open class Flyfile : ExtractorApi() {
|
|||
source = name,
|
||||
name = name,
|
||||
url = streamUrl,
|
||||
type = ExtractorLinkType.M3U8,
|
||||
type = ExtractorLinkType.M3U8
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class StreamInfo(
|
||||
@SerialName("url") val url: String,
|
||||
@SerialName("token") val token: String,
|
||||
@SerialName("url")
|
||||
val url: String,
|
||||
@SerialName("token")
|
||||
val token: String
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +1,31 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.google.gson.JsonParser
|
||||
import com.lagradost.api.Log
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.base64Decode
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||
import io.ktor.http.Url
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
class Techinmind : GDMirrorbot() {
|
||||
override val name = "Techinmind Cloud AIO"
|
||||
override val mainUrl = "https://stream.techinmind.space"
|
||||
override val requiresReferer = true
|
||||
class Techinmind: GDMirrorbot() {
|
||||
override var name = "Techinmind Cloud AIO"
|
||||
override var mainUrl = "https://stream.techinmind.space"
|
||||
override var requiresReferer = true
|
||||
}
|
||||
|
||||
open class GDMirrorbot : ExtractorApi() {
|
||||
override val name = "GDMirrorbot"
|
||||
override val mainUrl = "https://gdmirrorbot.nl"
|
||||
override var name = "GDMirrorbot"
|
||||
override var mainUrl = "https://gdmirrorbot.nl"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val (sid, host) = if (!url.contains("key=")) {
|
||||
Pair(url.substringAfterLast("embed/"), getBaseUrl(app.get(url).url))
|
||||
|
|
@ -39,58 +36,63 @@ open class GDMirrorbot : ExtractorApi() {
|
|||
val idType = Regex("""idType\s*=\s*"([^"]+)"""").find(pageText)?.groupValues?.get(1) ?: "imdbid"
|
||||
val baseUrl = Regex("""let\s+baseUrl\s*=\s*"([^"]+)"""").find(pageText)?.groupValues?.get(1)
|
||||
val hostUrl = baseUrl?.let { getBaseUrl(it) }
|
||||
|
||||
if (finalId != null && myKey != null) {
|
||||
val apiUrl = if (url.contains("/tv/")) {
|
||||
val season = Regex("""/tv/\d+/(\d+)/""").find(url)?.groupValues?.get(1) ?: "1"
|
||||
val episode = Regex("""/tv/\d+/\d+/(\d+)""").find(url)?.groupValues?.get(1) ?: "1"
|
||||
"$mainUrl/myseriesapi?tmdbid=$finalId&season=$season&epname=$episode&key=$myKey"
|
||||
} else "$mainUrl/mymovieapi?$idType=$finalId&key=$myKey"
|
||||
} else {
|
||||
"$mainUrl/mymovieapi?$idType=$finalId&key=$myKey"
|
||||
}
|
||||
pageText = app.get(apiUrl).text
|
||||
}
|
||||
|
||||
val embedData = tryParseJson<EmbedData>(pageText)
|
||||
val jsonElement = JsonParser.parseString(pageText)
|
||||
if (!jsonElement.isJsonObject) return
|
||||
val jsonObject = jsonElement.asJsonObject
|
||||
|
||||
val embedId = url.substringAfterLast("/")
|
||||
val sidValue = embedData?.data?.firstOrNull()?.fileSlug
|
||||
val sidValue = jsonObject["data"]?.asJsonArray
|
||||
?.takeIf { it.size() > 0 }
|
||||
?.get(0)?.asJsonObject
|
||||
?.get("fileslug")?.asString
|
||||
?.takeIf { it.isNotBlank() } ?: embedId
|
||||
|
||||
Pair(sidValue, hostUrl)
|
||||
}
|
||||
|
||||
val postData = mapOf("sid" to sid)
|
||||
val responseText = app.post("$host/embedhelper.php", data = postData).text
|
||||
|
||||
val root = tryParseJson<EmbedHelper>(responseText) ?: return
|
||||
val siteUrls = root.siteUrls ?: return
|
||||
val siteFriendlyNames = root.siteFriendlyNames
|
||||
val rootElement = JsonParser.parseString(responseText)
|
||||
if (!rootElement.isJsonObject) return
|
||||
val root = rootElement.asJsonObject
|
||||
|
||||
// mresult can arrive as a JSON object or a base64-encoded string
|
||||
val mresult: Map<String, String>? = run {
|
||||
val raw = responseText
|
||||
.substringAfter("\"mresult\":")
|
||||
.trimStart()
|
||||
when {
|
||||
raw.startsWith("\"") -> {
|
||||
// base64-encoded string
|
||||
tryParseJson<Map<String, String>>(
|
||||
try { base64Decode(raw.trim('"')) } catch (_: Exception) { return }
|
||||
)
|
||||
}
|
||||
raw.startsWith("{") -> tryParseJson<Map<String, String>>(
|
||||
raw.substringBefore("\n}").substringBefore(",\n\"").let { "$it" }
|
||||
.let { responseText.substringAfter("\"mresult\":").trimStart() }
|
||||
)
|
||||
else -> null
|
||||
val siteUrls = root["siteUrls"]?.asJsonObject ?: return
|
||||
val siteFriendlyNames = root["siteFriendlyNames"]?.asJsonObject
|
||||
|
||||
val decodedMresult = when {
|
||||
root["mresult"]?.isJsonObject == true -> root["mresult"]!!.asJsonObject
|
||||
root["mresult"]?.isJsonPrimitive == true -> try {
|
||||
base64Decode(root["mresult"]!!.asString)
|
||||
.let { JsonParser.parseString(it).asJsonObject }
|
||||
} catch (e: Exception) {
|
||||
Log.e("GDMirrorbot", "Failed to decode mresult: $e")
|
||||
return
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
|
||||
if (mresult == null) return
|
||||
siteUrls.keys.intersect(mresult.keys).forEach { key ->
|
||||
val base = siteUrls[key]?.trimEnd('/') ?: return@forEach
|
||||
val path = mresult[key]?.trimStart('/') ?: return@forEach
|
||||
siteUrls.keySet().intersect(decodedMresult.keySet()).forEach { key ->
|
||||
val base = siteUrls[key]?.asString?.trimEnd('/') ?: return@forEach
|
||||
val path = decodedMresult[key]?.asString?.trimStart('/') ?: return@forEach
|
||||
val fullUrl = "$base/$path"
|
||||
val friendlyName = siteFriendlyNames?.get(key) ?: key
|
||||
val friendlyName = siteFriendlyNames?.get(key)?.asString ?: key
|
||||
|
||||
try {
|
||||
when (friendlyName) {
|
||||
"StreamHG", "EarnVids" -> VidHidePro().getUrl(fullUrl, referer, subtitleCallback, callback)
|
||||
"StreamHG","EarnVids" -> VidHidePro().getUrl(fullUrl, referer, subtitleCallback, callback)
|
||||
"RpmShare", "UpnShare", "StreamP2p" -> VidStack().getUrl(fullUrl, referer, subtitleCallback, callback)
|
||||
else -> loadExtractor(fullUrl, referer ?: mainUrl, subtitleCallback, callback)
|
||||
}
|
||||
|
|
@ -103,20 +105,5 @@ open class GDMirrorbot : ExtractorApi() {
|
|||
private fun getBaseUrl(url: String): String {
|
||||
return Url(url).let { "${it.protocol.name}://${it.host}" }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class EmbedData(
|
||||
@JsonProperty("data") @SerialName("data") val data: List<FileSlug>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class FileSlug(
|
||||
@JsonProperty("fileslug") @SerialName("fileslug") val fileSlug: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class EmbedHelper(
|
||||
@JsonProperty("siteUrls") @SerialName("siteUrls") val siteUrls: Map<String, String>? = null,
|
||||
@JsonProperty("siteFriendlyNames") @SerialName("siteFriendlyNames") val siteFriendlyNames: Map<String, String>? = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,11 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
|||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.base64Decode
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
open class GUpload: ExtractorApi() {
|
||||
override val name: String = "GUpload"
|
||||
|
|
@ -22,12 +19,12 @@ open class GUpload: ExtractorApi() {
|
|||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val response = app.get(url, referer = referer).text
|
||||
|
||||
val playerConfigString = response.substringAfter("const config = ").substringBefore(";")
|
||||
val playerConfig = parseJson<VideoInfo>(playerConfigString)
|
||||
val playerConfig = AppUtils.parseJson<VideoInfo>(playerConfigString)
|
||||
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
|
|
@ -42,15 +39,14 @@ open class GUpload: ExtractorApi() {
|
|||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class VideoInfo(
|
||||
@JsonProperty("videoUrl") @SerialName("videoUrl") val videoUrl: String,
|
||||
@JsonProperty("posterUrl") @SerialName("posterUrl") val posterUrl: String? = null,
|
||||
@JsonProperty("videoId") @SerialName("videoId") val videoId: String? = null,
|
||||
@JsonProperty("primaryColor") @SerialName("primaryColor") val primaryColor: String? = null,
|
||||
@JsonProperty("audioTracks") @SerialName("audioTracks") val audioTracks: List<JsonElement> = emptyList(),
|
||||
@JsonProperty("subtitleTracks") @SerialName("subtitleTracks") val subtitleTracks: List<JsonElement> = emptyList(),
|
||||
@JsonProperty("vastFallbackList") @SerialName("vastFallbackList") val vastFallbackList: List<String> = emptyList(),
|
||||
@JsonProperty("videoOwnerId") @SerialName("videoOwnerId") val videoOwnerId: Long = 0,
|
||||
@JsonProperty("videoUrl") val videoUrl: String,
|
||||
@JsonProperty("posterUrl") val posterUrl: String? = null,
|
||||
@JsonProperty("videoId") val videoId: String? = null,
|
||||
@JsonProperty("primaryColor") val primaryColor: String? = null,
|
||||
@JsonProperty("audioTracks") val audioTracks: List<Any?> = emptyList(),
|
||||
@JsonProperty("subtitleTracks") val subtitleTracks: List<Any?> = emptyList(),
|
||||
@JsonProperty("vastFallbackList") val vastFallbackList: List<String> = emptyList(),
|
||||
@JsonProperty("videoOwnerId") val videoOwnerId: Long = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,16 +5,14 @@ import com.lagradost.cloudstream3.*
|
|||
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class DatabaseGdrive2 : Gdriveplayer() {
|
||||
override val mainUrl = "https://databasegdriveplayer.co"
|
||||
override var mainUrl = "https://databasegdriveplayer.co"
|
||||
}
|
||||
|
||||
class DatabaseGdrive : Gdriveplayer() {
|
||||
override val mainUrl = "https://series.databasegdriveplayer.co"
|
||||
override var mainUrl = "https://series.databasegdriveplayer.co"
|
||||
}
|
||||
|
||||
class Gdriveplayerapi : Gdriveplayer() {
|
||||
|
|
@ -75,9 +73,10 @@ open class Gdriveplayer : ExtractorApi() {
|
|||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val document = app.get(url).document
|
||||
|
||||
val eval = unpackJs(document)?.replace("\\", "") ?: return
|
||||
val data = Regex("data='(\\S+?)'").first(eval) ?: return
|
||||
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
|
||||
|
|
@ -85,9 +84,9 @@ open class Gdriveplayer : ExtractorApi() {
|
|||
?.joinToString("") {
|
||||
it.toInt().toChar().toString()
|
||||
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.encodeToByteArray() }
|
||||
?: throw ErrorLoadingException("can't find password")
|
||||
|
||||
?: throw ErrorLoadingException("can't find password")
|
||||
val decryptedData = cryptoAESHandler(data, password, false, false)?.let { getAndUnpack(it) }?.replace("\\", "")
|
||||
|
||||
val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
|
||||
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
|
||||
|
||||
|
|
@ -112,17 +111,16 @@ open class Gdriveplayer : ExtractorApi() {
|
|||
subtitleCallback.invoke(
|
||||
newSubtitleFile(
|
||||
sub.label,
|
||||
httpsify(sub.file),
|
||||
httpsify(sub.file)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Tracks(
|
||||
@JsonProperty("file") @SerialName("file") val file: String,
|
||||
@JsonProperty("kind") @SerialName("kind") val kind: String,
|
||||
@JsonProperty("label") @SerialName("label") val label: String,
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("kind") val kind: String,
|
||||
@JsonProperty("label") val label: String,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
|||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.math.round
|
||||
|
||||
open class Gofile : ExtractorApi() {
|
||||
|
|
@ -22,18 +20,20 @@ open class Gofile : ExtractorApi() {
|
|||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1) ?: return
|
||||
|
||||
val token = app.post(
|
||||
"$mainApi/accounts",
|
||||
).parsedSafe<AccountResponse>()?.data?.token ?: return
|
||||
|
||||
val globalRes = app.get("$mainUrl/dist/js/config.js").text
|
||||
val wt = Regex("""appdata\.wt\s*=\s*[\"']([^\"']+)[\"']""").find(globalRes)?.groupValues?.get(1) ?: return
|
||||
|
||||
val headers = mapOf(
|
||||
"Authorization" to "Bearer $token",
|
||||
"X-Website-Token" to wt,
|
||||
"X-Website-Token" to wt
|
||||
)
|
||||
|
||||
val parsedResponse = app.get(
|
||||
|
|
@ -42,6 +42,7 @@ open class Gofile : ExtractorApi() {
|
|||
).parsedSafe<GofileResponse>()
|
||||
|
||||
val childrenMap = parsedResponse?.data?.children ?: return
|
||||
|
||||
for ((_, file) in childrenMap) {
|
||||
if (file.link.isNullOrEmpty() || file.type != "file") continue
|
||||
val fileName = file.name ?: ""
|
||||
|
|
@ -53,7 +54,7 @@ open class Gofile : ExtractorApi() {
|
|||
"Gofile",
|
||||
"[Gofile] $fileName [$formattedSize]",
|
||||
file.link,
|
||||
ExtractorLinkType.VIDEO,
|
||||
ExtractorLinkType.VIDEO
|
||||
) {
|
||||
this.quality = getQuality(fileName)
|
||||
this.headers = mapOf("Cookie" to "accountToken=$token")
|
||||
|
|
@ -83,31 +84,26 @@ open class Gofile : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AccountResponse(
|
||||
@JsonProperty("data") @SerialName("data") val data: AccountData? = null,
|
||||
@JsonProperty("data") val data: AccountData? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AccountData(
|
||||
@JsonProperty("token") @SerialName("token") val token: String? = null,
|
||||
@JsonProperty("token") val token: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GofileResponse(
|
||||
@JsonProperty("data") @SerialName("data") val data: GofileData? = null,
|
||||
@JsonProperty("data") val data: GofileData? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GofileData(
|
||||
@JsonProperty("children") @SerialName("children") val children: Map<String, GofileFile>? = null,
|
||||
@JsonProperty("children") val children: Map<String, GofileFile>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GofileFile(
|
||||
@JsonProperty("type") @SerialName("type") val type: String? = null,
|
||||
@JsonProperty("name") @SerialName("name") val name: String? = null,
|
||||
@JsonProperty("link") @SerialName("link") val link: String? = null,
|
||||
@JsonProperty("size") @SerialName("size") val size: Long? = 0L,
|
||||
@JsonProperty("type") val type: String? = null,
|
||||
@JsonProperty("name") val name: String? = null,
|
||||
@JsonProperty("link") val link: String? = null,
|
||||
@JsonProperty("size") val size: Long? = 0L
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,41 +8,39 @@ import com.lagradost.cloudstream3.*
|
|||
import com.lagradost.cloudstream3.extractors.helper.AesHelper
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class HDMomPlayer : ExtractorApi() {
|
||||
override val name = "HDMomPlayer"
|
||||
override val mainUrl = "https://hdmomplayer.com"
|
||||
override val name = "HDMomPlayer"
|
||||
override val mainUrl = "https://hdmomplayer.com"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
) {
|
||||
val m3uLink: String?
|
||||
val extRef = referer ?: ""
|
||||
val iSource = app.get(url, referer = extRef).text
|
||||
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||
val m3uLink:String?
|
||||
val extRef = referer ?: ""
|
||||
val iSource = app.get(url, referer=extRef).text
|
||||
|
||||
val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(iSource)?.groupValues
|
||||
if (bePlayer != null) {
|
||||
val bePlayerPass = bePlayer.get(1)
|
||||
val bePlayerData = bePlayer.get(2)
|
||||
val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.encodeToByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||
val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.encodeToByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||
|
||||
m3uLink = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1)
|
||||
} else {
|
||||
m3uLink = Regex("""file:\"([^\"]+)""").find(iSource)?.groupValues?.get(1)
|
||||
|
||||
val trackStr = Regex("""tracks:\[([^\]]+)""").find(iSource)?.groupValues?.get(1)
|
||||
if (trackStr != null) {
|
||||
val tracks = parseJson<List<Track>>("[${trackStr}]")
|
||||
val tracks:List<Track> = parseJson<List<Track>>("[${trackStr}]")
|
||||
|
||||
for (track in tracks) {
|
||||
if (track.file == null || track.label == null) continue
|
||||
if (track.label.contains("Forced")) continue
|
||||
|
||||
subtitleCallback.invoke(
|
||||
newSubtitleFile(
|
||||
lang = track.label,
|
||||
url = fixUrl(mainUrl + track.file),
|
||||
url = fixUrl(mainUrl + track.file)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -51,10 +49,10 @@ open class HDMomPlayer : ExtractorApi() {
|
|||
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = m3uLink ?: throw ErrorLoadingException("m3u link not found"),
|
||||
type = ExtractorLinkType.M3U8,
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = m3uLink ?: throw ErrorLoadingException("m3u link not found"),
|
||||
type = ExtractorLinkType.M3U8
|
||||
) {
|
||||
this.referer = url
|
||||
this.quality = Qualities.Unknown.value
|
||||
|
|
@ -62,12 +60,11 @@ open class HDMomPlayer : ExtractorApi() {
|
|||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Track(
|
||||
@JsonProperty("file") @SerialName("file") val file: String?,
|
||||
@JsonProperty("label") @SerialName("label") val label: String?,
|
||||
@JsonProperty("kind") @SerialName("kind") val kind: String?,
|
||||
@JsonProperty("language") @SerialName("language") val language: String?,
|
||||
@JsonProperty("default") @SerialName("default") val default: String?,
|
||||
@JsonProperty("file") val file: String?,
|
||||
@JsonProperty("label") val label: String?,
|
||||
@JsonProperty("kind") val kind: String?,
|
||||
@JsonProperty("language") val language: String?,
|
||||
@JsonProperty("default") val default: String?
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,60 +2,56 @@
|
|||
|
||||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.api.Log
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
open class HDPlayerSystem : ExtractorApi() {
|
||||
override val name = "HDPlayerSystem"
|
||||
override val mainUrl = "https://hdplayersystem.live"
|
||||
override val name = "HDPlayerSystem"
|
||||
override val mainUrl = "https://hdplayersystem.live"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
) {
|
||||
val extRef = referer ?: ""
|
||||
val vidId = if (url.contains("video/")) {
|
||||
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||
val extRef = referer ?: ""
|
||||
val vidId = if (url.contains("video/")) {
|
||||
url.substringAfter("video/")
|
||||
} else {
|
||||
url.substringAfter("?data=")
|
||||
}
|
||||
val postUrl = "$mainUrl/player/index.php?data=$vidId&do=getVideo"
|
||||
val postUrl = "${mainUrl}/player/index.php?data=${vidId}&do=getVideo"
|
||||
|
||||
val response = app.post(
|
||||
postUrl,
|
||||
data = mapOf(
|
||||
"hash" to vidId,
|
||||
"r" to extRef,
|
||||
"r" to extRef
|
||||
),
|
||||
referer = extRef,
|
||||
headers = mapOf(
|
||||
"Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"X-Requested-With" to "XMLHttpRequest",
|
||||
"Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"X-Requested-With" to "XMLHttpRequest"
|
||||
)
|
||||
)
|
||||
|
||||
val videoResponse = response.parsedSafe<SystemResponse>() ?: throw ErrorLoadingException("failed to parse response")
|
||||
val m3uLink = videoResponse.securedLink
|
||||
val m3uLink = videoResponse.securedLink
|
||||
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = m3uLink,
|
||||
) { this.referer = extRef }
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = m3uLink
|
||||
) {
|
||||
this.referer = extRef
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SystemResponse(
|
||||
@JsonProperty("hls") @SerialName("hls") val hls: String,
|
||||
@JsonProperty("videoImage") @SerialName("videoImage") val videoImage: String? = null,
|
||||
@JsonProperty("videoSource") @SerialName("videoSource") val videoSource: String,
|
||||
@JsonProperty("securedLink") @SerialName("securedLink") val securedLink: String,
|
||||
@JsonProperty("hls") val hls: String,
|
||||
@JsonProperty("videoImage") val videoImage: String? = null,
|
||||
@JsonProperty("videoSource") val videoSource: String,
|
||||
@JsonProperty("securedLink") val securedLink: String
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,8 +9,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
|||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import com.lagradost.cloudstream3.utils.getAndUnpack
|
||||
import com.lagradost.cloudstream3.utils.getPacked
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class Jeniusplay : ExtractorApi() {
|
||||
override val name = "Jeniusplay"
|
||||
|
|
@ -21,15 +19,16 @@ open class Jeniusplay : ExtractorApi() {
|
|||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val document = app.get(url, referer = "$mainUrl/").document
|
||||
val hash = url.split("/").last().substringAfter("data=")
|
||||
|
||||
val m3uLink = app.post(
|
||||
url = "$mainUrl/player/index.php?data=$hash&do=getVideo",
|
||||
data = mapOf("hash" to hash, "r" to "$referer"),
|
||||
referer = url,
|
||||
headers = mapOf("X-Requested-With" to "XMLHttpRequest"),
|
||||
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
|
||||
).parsed<ResponseSource>().videoSource
|
||||
|
||||
M3u8Helper.generateM3u8(
|
||||
|
|
@ -37,6 +36,7 @@ open class Jeniusplay : ExtractorApi() {
|
|||
m3uLink,
|
||||
url,
|
||||
).forEach(callback)
|
||||
|
||||
document.select("script").map { script ->
|
||||
if (getPacked(script.data()) != null) {
|
||||
val unpacked = getAndUnpack(script.data())
|
||||
|
|
@ -45,10 +45,9 @@ open class Jeniusplay : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ResponseSource(
|
||||
@JsonProperty("hls") @SerialName("hls") val hls: Boolean,
|
||||
@JsonProperty("videoSource") @SerialName("videoSource") val videoSource: String,
|
||||
@JsonProperty("securedLink") @SerialName("securedLink") val securedLink: String?,
|
||||
@JsonProperty("hls") val hls: Boolean,
|
||||
@JsonProperty("videoSource") val videoSource: String,
|
||||
@JsonProperty("securedLink") val securedLink: String?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,6 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
|||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class Linkbox : ExtractorApi() {
|
||||
override val name = "Linkbox"
|
||||
|
|
@ -19,7 +17,7 @@ open class Linkbox : ExtractorApi() {
|
|||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val token = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1)
|
||||
val id = app.get("$mainUrl/api/file/share_out_list/?sortField=utime&sortAsc=0&pageNo=1&pageSize=50&shareToken=$token").parsedSafe<Responses>()?.data?.itemId
|
||||
|
|
@ -38,25 +36,22 @@ open class Linkbox : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Resolutions(
|
||||
@JsonProperty("url") @SerialName("url") val url: String? = null,
|
||||
@JsonProperty("resolution") @SerialName("resolution") val resolution: String? = null,
|
||||
@JsonProperty("url") val url: String? = null,
|
||||
@JsonProperty("resolution") val resolution: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ItemInfo(
|
||||
@JsonProperty("resolutionList") @SerialName("resolutionList") val resolutionList: ArrayList<Resolutions>? = arrayListOf(),
|
||||
@JsonProperty("resolutionList") val resolutionList: ArrayList<Resolutions>? = arrayListOf(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Data(
|
||||
@JsonProperty("itemInfo") @SerialName("itemInfo") val itemInfo: ItemInfo? = null,
|
||||
@JsonProperty("itemId") @SerialName("itemId") val itemId: String? = null,
|
||||
@JsonProperty("itemInfo") val itemInfo: ItemInfo? = null,
|
||||
@JsonProperty("itemId") val itemId: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Responses(
|
||||
@JsonProperty("data") @SerialName("data") val data: Data? = null,
|
||||
@JsonProperty("data") val data: Data? = null,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,57 +2,51 @@
|
|||
|
||||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.api.Log
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
open class MailRu : ExtractorApi() {
|
||||
override val name = "MailRu"
|
||||
override val mainUrl = "https://my.mail.ru"
|
||||
override val name = "MailRu"
|
||||
override val mainUrl = "https://my.mail.ru"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
) {
|
||||
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||
val extRef = referer ?: ""
|
||||
val vidId = url.substringAfter("video/embed/").trim()
|
||||
val videoReq = app.get("$mainUrl/+/video/meta/$vidId", referer = url)
|
||||
val videoKey = videoReq.cookies["video_key"].toString()
|
||||
val videoData = AppUtils.tryParseJson<MailRuData>(videoReq.text) ?:
|
||||
throw ErrorLoadingException("Video not found")
|
||||
|
||||
val vidId = url.substringAfter("video/embed/").trim()
|
||||
val videoReq = app.get("${mainUrl}/+/video/meta/${vidId}", referer=url)
|
||||
val videoKey = videoReq.cookies["video_key"].toString()
|
||||
|
||||
val videoData = AppUtils.tryParseJson<MailRuData>(videoReq.text) ?: throw ErrorLoadingException("Video not found")
|
||||
|
||||
for (video in videoData.videos) {
|
||||
|
||||
val videoUrl = if (video.url.startsWith("//")) "https:${video.url}" else video.url
|
||||
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = videoUrl,
|
||||
type = ExtractorLinkType.M3U8,
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = videoUrl,
|
||||
type = ExtractorLinkType.M3U8
|
||||
) {
|
||||
this.referer = url
|
||||
this.headers = mapOf("Cookie" to "video_key=$videoKey")
|
||||
this.headers = mapOf("Cookie" to "video_key=${videoKey}")
|
||||
this.quality = getQualityFromName(video.key)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MailRuData(
|
||||
@JsonProperty("provider") @SerialName("provider") val provider: String,
|
||||
@JsonProperty("videos") @SerialName("videos") val videos: List<MailRuVideoData>,
|
||||
@JsonProperty("provider") val provider: String,
|
||||
@JsonProperty("videos") val videos: List<MailRuVideoData>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MailRuVideoData(
|
||||
@JsonProperty("url") @SerialName("url") val url: String,
|
||||
@JsonProperty("key") @SerialName("key") val key: String,
|
||||
@JsonProperty("url") val url: String,
|
||||
@JsonProperty("key") val key: String
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,26 +7,19 @@ import com.lagradost.cloudstream3.ErrorLoadingException
|
|||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.USER_AGENT
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class Odnoklassniki : ExtractorApi() {
|
||||
override val name = "Odnoklassniki"
|
||||
override val mainUrl = "https://odnoklassniki.ru"
|
||||
override val name = "Odnoklassniki"
|
||||
override val mainUrl = "https://odnoklassniki.ru"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
) {
|
||||
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||
val headers = mapOf(
|
||||
"Accept" to "*/*",
|
||||
"Connection" to "keep-alive",
|
||||
|
|
@ -36,33 +29,34 @@ open class Odnoklassniki : ExtractorApi() {
|
|||
"Origin" to mainUrl,
|
||||
"User-Agent" to USER_AGENT,
|
||||
)
|
||||
|
||||
val embedUrl = url.replace("/video/", "/videoembed/")
|
||||
val videoReq = app.get(embedUrl, headers = headers).text.replace("\\"", "\"").replace("\\\\", "\\")
|
||||
val embedUrl = url.replace("/video/","/videoembed/")
|
||||
val videoReq = app.get(embedUrl, headers=headers).text.replace("\\"", "\"").replace("\\\\", "\\")
|
||||
.replace(Regex("\\\\u([0-9A-Fa-f]{4})")) { matchResult ->
|
||||
matchResult.groupValues[1].toInt(16).toChar().toString()
|
||||
}
|
||||
|
||||
val videosStr = Regex(""""videos":(\[[^]]*])""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found")
|
||||
val videos = tryParseJson<List<OkRuVideo>>(videosStr) ?: throw ErrorLoadingException("Video not found")
|
||||
val videos = AppUtils.tryParseJson<List<OkRuVideo>>(videosStr) ?: throw ErrorLoadingException("Video not found")
|
||||
|
||||
for (video in videos) {
|
||||
val videoUrl = if (video.url.startsWith("//")) "https:${video.url}" else video.url
|
||||
val quality = video.name.uppercase()
|
||||
|
||||
val videoUrl = if (video.url.startsWith("//")) "https:${video.url}" else video.url
|
||||
|
||||
val quality = video.name.uppercase()
|
||||
.replace("MOBILE", "144p")
|
||||
.replace("LOWEST", "240p")
|
||||
.replace("LOW", "360p")
|
||||
.replace("SD", "480p")
|
||||
.replace("HD", "720p")
|
||||
.replace("FULL", "1080p")
|
||||
.replace("QUAD", "1440p")
|
||||
.replace("ULTRA", "4k")
|
||||
.replace("LOW", "360p")
|
||||
.replace("SD", "480p")
|
||||
.replace("HD", "720p")
|
||||
.replace("FULL", "1080p")
|
||||
.replace("QUAD", "1440p")
|
||||
.replace("ULTRA", "4k")
|
||||
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = videoUrl,
|
||||
type = INFER_TYPE,
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = videoUrl,
|
||||
type = INFER_TYPE
|
||||
) {
|
||||
this.referer = "$mainUrl/"
|
||||
this.quality = getQualityFromName(quality)
|
||||
|
|
@ -72,9 +66,8 @@ open class Odnoklassniki : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class OkRuVideo(
|
||||
@JsonProperty("name") @SerialName("name") val name: String,
|
||||
@JsonProperty("url") @SerialName("url") val url: String,
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("url") val url: String,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,63 +2,57 @@
|
|||
|
||||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.api.Log
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
open class PeaceMakerst : ExtractorApi() {
|
||||
override val name = "PeaceMakerst"
|
||||
override val mainUrl = "https://peacemakerst.com"
|
||||
override val name = "PeaceMakerst"
|
||||
override val mainUrl = "https://peacemakerst.com"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
) {
|
||||
val m3uLink: String?
|
||||
val extRef = referer ?: ""
|
||||
val postUrl = "$url?do=getVideo"
|
||||
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||
val m3uLink:String?
|
||||
val extRef = referer ?: ""
|
||||
val postUrl = "${url}?do=getVideo"
|
||||
|
||||
val response = app.post(
|
||||
postUrl,
|
||||
data = mapOf(
|
||||
"hash" to url.substringAfter("video/"),
|
||||
"r" to extRef,
|
||||
"s" to "",
|
||||
"r" to extRef,
|
||||
"s" to ""
|
||||
),
|
||||
referer = extRef,
|
||||
headers = mapOf(
|
||||
"Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"X-Requested-With" to "XMLHttpRequest",
|
||||
),
|
||||
"Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"X-Requested-With" to "XMLHttpRequest"
|
||||
)
|
||||
)
|
||||
|
||||
if (response.text.contains("teve2.com.tr\\/embed\\/")) {
|
||||
val teve2Id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"")
|
||||
val teve2Id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"")
|
||||
val teve2Response = app.get(
|
||||
"https://www.teve2.com.tr/action/media/$teve2Id",
|
||||
referer = "https://www.teve2.com.tr/embed/$teve2Id",
|
||||
"https://www.teve2.com.tr/action/media/${teve2Id}",
|
||||
referer = "https://www.teve2.com.tr/embed/${teve2Id}"
|
||||
).parsedSafe<Teve2ApiResponse>() ?: throw ErrorLoadingException("teve2 response is null")
|
||||
m3uLink = teve2Response.media.link.serviceUrl + "//" + teve2Response.media.link.securePath
|
||||
|
||||
m3uLink = teve2Response.media.link.serviceUrl + "//" + teve2Response.media.link.securePath
|
||||
} else {
|
||||
val videoResponse = response.parsedSafe<PeaceResponse>() ?: throw ErrorLoadingException("peace response is null")
|
||||
val videoSources = videoResponse.videoSources
|
||||
m3uLink = if (videoSources.isNotEmpty()) {
|
||||
videoSources.lastOrNull()?.file
|
||||
val videoSources = videoResponse.videoSources
|
||||
if (videoSources.isNotEmpty()) {
|
||||
m3uLink = videoSources.lastOrNull()?.file
|
||||
} else {
|
||||
null
|
||||
m3uLink = null
|
||||
}
|
||||
}
|
||||
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = m3uLink ?: throw ErrorLoadingException("m3u link not found"),
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = m3uLink ?: throw ErrorLoadingException("m3u link not found"),
|
||||
) {
|
||||
this.referer = extRef
|
||||
this.quality = Qualities.Unknown.value
|
||||
|
|
@ -66,34 +60,29 @@ open class PeaceMakerst : ExtractorApi() {
|
|||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PeaceResponse(
|
||||
@JsonProperty("videoImage") @SerialName("videoImage") val videoImage: String?,
|
||||
@JsonProperty("videoSources") @SerialName("videoSources") val videoSources: List<VideoSource>,
|
||||
@JsonProperty("sIndex") @SerialName("sIndex") val sourceIndex: String,
|
||||
@JsonProperty("sourceList") @SerialName("sourceList") val sourceList: Map<String, String>,
|
||||
@JsonProperty("videoImage") val videoImage: String?,
|
||||
@JsonProperty("videoSources") val videoSources: List<VideoSource>,
|
||||
@JsonProperty("sIndex") val sIndex: String,
|
||||
@JsonProperty("sourceList") val sourceList: Map<String, String>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VideoSource(
|
||||
@JsonProperty("file") @SerialName("file") val file: String,
|
||||
@JsonProperty("label") @SerialName("label") val label: String,
|
||||
@JsonProperty("type") @SerialName("type") val type: String,
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("label") val label: String,
|
||||
@JsonProperty("type") val type: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Teve2ApiResponse(
|
||||
@JsonProperty("Media") @SerialName("Media") val media: Teve2Media,
|
||||
@JsonProperty("Media") val media: Teve2Media
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Teve2Media(
|
||||
@JsonProperty("Link") @SerialName("Link") val link: Teve2Link,
|
||||
@JsonProperty("Link") val link: Teve2Link
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Teve2Link(
|
||||
@JsonProperty("ServiceUrl") @SerialName("ServiceUrl") val serviceUrl: String,
|
||||
@JsonProperty("SecurePath") @SerialName("SecurePath") val securePath: String,
|
||||
@JsonProperty("ServiceUrl") val serviceUrl: String,
|
||||
@JsonProperty("SecurePath") val securePath: String
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,33 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.api.Log
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class PlayLtXyz : ExtractorApi() {
|
||||
override val name = "PlayLt"
|
||||
override val mainUrl = "https://play.playlt.xyz"
|
||||
open class PlayLtXyz: ExtractorApi() {
|
||||
override val name: String = "PlayLt"
|
||||
override val mainUrl: String = "https://play.playlt.xyz"
|
||||
override val requiresReferer = true
|
||||
|
||||
private data class ResponseData(
|
||||
@JsonProperty("data") val data: String? = null
|
||||
)
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val extractedLinksList = mutableListOf<ExtractorLink>()
|
||||
//Log.i(this.name, "Result => (url) $url")
|
||||
var idUser = ""
|
||||
var idFile = ""
|
||||
var bodyText = ""
|
||||
val doc = app.get(url, referer = referer).document
|
||||
//Log.i(this.name, "Result => (url, script) $url / ${doc.select("script")}")
|
||||
bodyText = doc.select("script").firstOrNull {
|
||||
val text = it.toString()
|
||||
text.contains("var idUser")
|
||||
}?.toString() ?: ""
|
||||
//Log.i(this.name, "Result => (bodyText) $bodyText")
|
||||
if (bodyText.isNotBlank()) {
|
||||
idUser = "(?<=var idUser = \")(.*)(?=\";)".toRegex().find(bodyText)
|
||||
?.groupValues?.get(0) ?: ""
|
||||
|
|
@ -30,27 +35,29 @@ open class PlayLtXyz : ExtractorApi() {
|
|||
idFile = "(?<=var idfile = \")(.*)(?=\";)".toRegex().find(bodyText)
|
||||
?.groupValues?.get(0) ?: ""
|
||||
}
|
||||
|
||||
//Log.i(this.name, "Result => (idUser, idFile) $idUser / $idFile")
|
||||
if (idUser.isNotBlank() && idFile.isNotBlank()) {
|
||||
//val sess = HttpSession()
|
||||
val ajaxHead = mapOf(
|
||||
Pair("Origin", mainUrl),
|
||||
Pair("Referer", mainUrl),
|
||||
Pair("Sec-Fetch-Site", "same-site"),
|
||||
Pair("Sec-Fetch-Mode", "cors"),
|
||||
Pair("Sec-Fetch-Dest", "empty"),
|
||||
Pair("Sec-Fetch-Dest", "empty")
|
||||
)
|
||||
|
||||
val ajaxData = mapOf(
|
||||
Pair("referrer", referer ?: mainUrl),
|
||||
Pair("typeend", "html"),
|
||||
Pair("typeend", "html")
|
||||
)
|
||||
|
||||
val postUrl = "https://api-plhq.playlt.xyz/apiv5/$idUser/$idFile"
|
||||
val data = app.post(postUrl, headers = ajaxHead, data = ajaxData)
|
||||
//idUser = 608f7c85cf0743547f1f1b4e
|
||||
val posturl = "https://api-plhq.playlt.xyz/apiv5/$idUser/$idFile"
|
||||
val data = app.post(posturl, headers = ajaxHead, data = ajaxData)
|
||||
//Log.i(this.name, "Result => (posturl) $posturl")
|
||||
if (data.isSuccessful) {
|
||||
val itemStr = data.text
|
||||
Log.i(this.name, "Result => (data) $itemStr")
|
||||
tryParseJson<ResponseData>(itemStr)?.let { item ->
|
||||
val itemstr = data.text
|
||||
Log.i(this.name, "Result => (data) $itemstr")
|
||||
tryParseJson<ResponseData?>(itemstr)?.let { item ->
|
||||
val linkUrl = item.data ?: ""
|
||||
if (linkUrl.isNotBlank()) {
|
||||
extractedLinksList.add(
|
||||
|
|
@ -58,7 +65,7 @@ open class PlayLtXyz : ExtractorApi() {
|
|||
source = name,
|
||||
name = name,
|
||||
url = linkUrl,
|
||||
type = ExtractorLinkType.M3U8,
|
||||
type = ExtractorLinkType.M3U8
|
||||
) {
|
||||
this.referer = url
|
||||
this.quality = Qualities.Unknown.value
|
||||
|
|
@ -68,12 +75,6 @@ open class PlayLtXyz : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extractedLinksList
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class ResponseData(
|
||||
@JsonProperty("data") @SerialName("data") val data: String? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.Prerelease
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Prerelease
|
||||
class Playmate : ExtractorApi() {
|
||||
override val name: String = "Playmate"
|
||||
override val mainUrl: String = "https://playmate.to"
|
||||
override val requiresReferer: Boolean = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = url.substringAfterLast("/")
|
||||
|
||||
val resp = app.post(
|
||||
"$mainUrl/api/s",
|
||||
json = mapOf("c" to id, "d" to "web"),
|
||||
headers = mapOf("User-Agent" to "Mozilla/5.0 (X11; Linux x86_64; rv:153.0) Gecko/20100101 Firefox/153.0")
|
||||
).parsed<StreamInfo>()
|
||||
|
||||
callback.invoke(newExtractorLink(source = name, name = name, url = resp.sx, type = ExtractorLinkType.M3U8))
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class StreamInfo(
|
||||
@SerialName("sx")
|
||||
@JsonProperty("sx")
|
||||
val sx: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ import com.lagradost.cloudstream3.app
|
|||
import com.lagradost.cloudstream3.base64DecodeArray
|
||||
import com.lagradost.cloudstream3.base64Encode
|
||||
import com.lagradost.cloudstream3.newSubtitleFile
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
|
|
@ -16,8 +16,6 @@ import dev.whyoleg.cryptography.CryptographyProvider
|
|||
import dev.whyoleg.cryptography.DelicateCryptographyApi
|
||||
import dev.whyoleg.cryptography.algorithms.AES
|
||||
import dev.whyoleg.cryptography.algorithms.MD5
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
class Megacloud : Rabbitstream() {
|
||||
override val name = "Megacloud"
|
||||
|
|
@ -38,7 +36,6 @@ class Megacloud : Rabbitstream() {
|
|||
extractedKey += sourcesArray[i].toString()
|
||||
sourcesArray[i] = ' '
|
||||
}
|
||||
|
||||
currentIndex += index[1]
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +55,7 @@ class Megacloud : Rabbitstream() {
|
|||
val matchKey2 = matchingKey(match.groupValues[2])
|
||||
try {
|
||||
listOf(matchKey1.toInt(16), matchKey2.toInt(16))
|
||||
} catch (_: NumberFormatException) {
|
||||
} catch (e: NumberFormatException) {
|
||||
emptyList()
|
||||
}
|
||||
}.filter { it.isNotEmpty() }
|
||||
|
|
@ -89,25 +86,26 @@ open class Rabbitstream : ExtractorApi() {
|
|||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = url.substringAfterLast("/").substringBefore("?")
|
||||
|
||||
val response = app.get(
|
||||
"$mainUrl/$embed/getSources?id=$id",
|
||||
referer = mainUrl,
|
||||
headers = mapOf("X-Requested-With" to "XMLHttpRequest"),
|
||||
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
|
||||
)
|
||||
|
||||
val encryptedMap = response.parsedSafe<SourcesEncrypted>()
|
||||
val sources = encryptedMap?.sources
|
||||
val decryptedSources = if (sources == null || encryptedMap.encrypted == false) {
|
||||
response.parsedSafe<SourcesResponses>()
|
||||
response.parsedSafe()
|
||||
} else {
|
||||
val (key, encData) = extractRealKey(sources)
|
||||
val decrypted = tryParseJson<List<Sources>>(decrypt(encData, key))
|
||||
val decrypted = decryptMapped<List<Sources>>(encData, key)
|
||||
SourcesResponses(
|
||||
sources = decrypted,
|
||||
tracks = encryptedMap.tracks,
|
||||
tracks = encryptedMap.tracks
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +121,7 @@ open class Rabbitstream : ExtractorApi() {
|
|||
subtitleCallback.invoke(
|
||||
newSubtitleFile(
|
||||
track?.label ?: return@map,
|
||||
track.file ?: return@map,
|
||||
track.file ?: return@map
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -135,11 +133,16 @@ open class Rabbitstream : ExtractorApi() {
|
|||
return extractedKey to sources
|
||||
}
|
||||
|
||||
private inline suspend fun <reified T> decryptMapped(input: String, key: String): T? {
|
||||
val decrypt = decrypt(input, key)
|
||||
return AppUtils.tryParseJson(decrypt)
|
||||
}
|
||||
|
||||
private suspend fun decrypt(input: String, key: String): String {
|
||||
return decryptSourceUrl(
|
||||
generateKey(
|
||||
salt = base64DecodeArray(input).copyOfRange(8, 16),
|
||||
secret = key.encodeToByteArray(),
|
||||
secret = key.encodeToByteArray()
|
||||
), input
|
||||
)
|
||||
}
|
||||
|
|
@ -151,7 +154,6 @@ open class Rabbitstream : ExtractorApi() {
|
|||
key = md5(key + secret + salt)
|
||||
currentKey += key
|
||||
}
|
||||
|
||||
return currentKey
|
||||
}
|
||||
|
||||
|
|
@ -170,30 +172,26 @@ open class Rabbitstream : ExtractorApi() {
|
|||
return decryptedData.decodeToString()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Tracks(
|
||||
@JsonProperty("file") @SerialName("file") val file: String? = null,
|
||||
@JsonProperty("label") @SerialName("label") val label: String? = null,
|
||||
@JsonProperty("kind") @SerialName("kind") val kind: String? = null,
|
||||
@JsonProperty("file") val file: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
@JsonProperty("kind") val kind: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Sources(
|
||||
@JsonProperty("file") @SerialName("file") val file: String? = null,
|
||||
@JsonProperty("type") @SerialName("type") val type: String? = null,
|
||||
@JsonProperty("label") @SerialName("label") val label: String? = null,
|
||||
@JsonProperty("file") val file: String? = null,
|
||||
@JsonProperty("type") val type: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SourcesResponses(
|
||||
@JsonProperty("sources") @SerialName("sources") val sources: List<Sources?>? = emptyList(),
|
||||
@JsonProperty("tracks") @SerialName("tracks") val tracks: List<Tracks?>? = emptyList(),
|
||||
@JsonProperty("sources") val sources: List<Sources?>? = emptyList(),
|
||||
@JsonProperty("tracks") val tracks: List<Tracks?>? = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SourcesEncrypted(
|
||||
@JsonProperty("sources") @SerialName("sources") val sources: String? = null,
|
||||
@JsonProperty("encrypted") @SerialName("encrypted") val encrypted: Boolean? = null,
|
||||
@JsonProperty("tracks") @SerialName("tracks") val tracks: List<Tracks?>? = emptyList(),
|
||||
@JsonProperty("sources") val sources: String? = null,
|
||||
@JsonProperty("encrypted") val encrypted: Boolean? = null,
|
||||
@JsonProperty("tracks") val tracks: List<Tracks?>? = emptyList(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,53 +2,51 @@
|
|||
|
||||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
open class Sobreatsesuyp : ExtractorApi() {
|
||||
override val name = "Sobreatsesuyp"
|
||||
override val mainUrl = "https://sobreatsesuyp.com"
|
||||
override val name = "Sobreatsesuyp"
|
||||
override val mainUrl = "https://sobreatsesuyp.com"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
) {
|
||||
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||
val extRef = referer ?: ""
|
||||
|
||||
val videoReq = app.get(url, referer = extRef).text
|
||||
val file = Regex("""file\":\"([^\"]+)""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found")
|
||||
val postLink = "$mainUrl/" + file.replace("\\", "")
|
||||
val rawList = app.post(postLink, referer = extRef).parsedSafe<List<JsonElement>>() ?: throw ErrorLoadingException("Post link not found")
|
||||
|
||||
val file = Regex("""file\":\"([^\"]+)""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found")
|
||||
val postLink = "${mainUrl}/" + file.replace("\\", "")
|
||||
val rawList = app.post(postLink, referer = extRef).parsedSafe<List<Any>>() ?: throw ErrorLoadingException("Post link not found")
|
||||
|
||||
val postJson: List<SobreatsesuypVideoData> = rawList.drop(1).map { item ->
|
||||
val mapItem = item as Map<*, *>
|
||||
SobreatsesuypVideoData(
|
||||
title = mapItem["title"] as? String,
|
||||
file = mapItem["file"] as? String,
|
||||
file = mapItem["file"] as? String
|
||||
)
|
||||
}
|
||||
|
||||
for (item in postJson) {
|
||||
if (item.file == null || item.title == null) continue
|
||||
val videoData = app.post("$mainUrl/playlist/${item.file.substring(1)}.txt", referer = extRef).text
|
||||
|
||||
val videoData = app.post("${mainUrl}/playlist/${item.file.substring(1)}.txt", referer = extRef).text
|
||||
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
source = this.name,
|
||||
name = "${this.name} - ${item.title}",
|
||||
url = videoData,
|
||||
) { this.referer = extRef }
|
||||
source = this.name,
|
||||
name = "${this.name} - ${item.title}",
|
||||
url = videoData,
|
||||
) {
|
||||
this.referer = extRef
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SobreatsesuypVideoData(
|
||||
@JsonProperty("title") @SerialName("title") val title: String? = null,
|
||||
@JsonProperty("file") @SerialName("file") val file: String? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("file") val file: String? = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,23 +7,22 @@ import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
|||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class StreamEmbed : ExtractorApi() {
|
||||
override val name = "StreamEmbed"
|
||||
override val mainUrl = "https://watch.gxplayer.xyz"
|
||||
override var name = "StreamEmbed"
|
||||
override var mainUrl = "https://watch.gxplayer.xyz"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val jsonString = app.get(url, referer = mainUrl).text
|
||||
.substringAfter("var video = ").substringBefore(";")
|
||||
val video = parseJson<Details>(jsonString)
|
||||
|
||||
M3u8Helper.generateM3u8(
|
||||
this.name,
|
||||
"$mainUrl/m3u8/${video.uid}/${video.md5}/master.txt?s=1&id=${video.id}&cache=${video.status}",
|
||||
|
|
@ -31,15 +30,14 @@ open class StreamEmbed : ExtractorApi() {
|
|||
).forEach(callback)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class Details(
|
||||
@JsonProperty("id") @SerialName("id") val id: String,
|
||||
@JsonProperty("uid") @SerialName("uid") val uid: String,
|
||||
@JsonProperty("slug") @SerialName("slug") val slug: String,
|
||||
@JsonProperty("title") @SerialName("title") val title: String,
|
||||
@JsonProperty("quality") @SerialName("quality") val quality: String,
|
||||
@JsonProperty("type") @SerialName("type") val type: String,
|
||||
@JsonProperty("status") @SerialName("status") val status: String,
|
||||
@JsonProperty("md5") @SerialName("md5") val md5: String,
|
||||
@JsonProperty("id") val id: String,
|
||||
@JsonProperty("uid") val uid: String,
|
||||
@JsonProperty("slug") val slug: String,
|
||||
@JsonProperty("title") val title: String,
|
||||
@JsonProperty("quality") val quality: String,
|
||||
@JsonProperty("type") val type: String,
|
||||
@JsonProperty("status") val status: String,
|
||||
@JsonProperty("md5") val md5: String,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import com.lagradost.cloudstream3.newSubtitleFile
|
|||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.random.Random
|
||||
|
||||
class Sblona : StreamSB() {
|
||||
|
|
@ -143,15 +141,17 @@ open class StreamSB : ExtractorApi() {
|
|||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val regexId =
|
||||
val regexID =
|
||||
Regex("(embed-[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+|/e/[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+)")
|
||||
val id = regexId.findAll(url).map {
|
||||
val id = regexID.findAll(url).map {
|
||||
it.value.replace(Regex("(embed-|/e/)"), "")
|
||||
}.first()
|
||||
val master = "$mainUrl/375664356a494546326c4b797c7c6e756577776778623171737/${encodeId(id)}"
|
||||
val headers = mapOf("watchsb" to "sbstream")
|
||||
val headers = mapOf(
|
||||
"watchsb" to "sbstream",
|
||||
)
|
||||
val mapped = app.get(
|
||||
master.lowercase(),
|
||||
headers = headers,
|
||||
|
|
@ -161,8 +161,9 @@ open class StreamSB : ExtractorApi() {
|
|||
name,
|
||||
mapped?.streamData?.file ?: return,
|
||||
url,
|
||||
headers = headers,
|
||||
headers = headers
|
||||
).forEach(callback)
|
||||
|
||||
mapped.streamData.subs?.map {sub ->
|
||||
subtitleCallback.invoke(
|
||||
newSubtitleFile(
|
||||
|
|
@ -188,27 +189,25 @@ open class StreamSB : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Subs(
|
||||
@JsonProperty("file") @SerialName("file") val file: String? = null,
|
||||
@JsonProperty("label") @SerialName("label") val label: String? = null,
|
||||
data class Subs (
|
||||
@JsonProperty("file") val file: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StreamData(
|
||||
@JsonProperty("file") @SerialName("file") val file: String,
|
||||
@JsonProperty("cdn_img") @SerialName("cdn_img") val cdnImg: String,
|
||||
@JsonProperty("hash") @SerialName("hash") val hash: String,
|
||||
@JsonProperty("subs") @SerialName("subs") val subs: ArrayList<Subs>? = arrayListOf(),
|
||||
@JsonProperty("length") @SerialName("length") val length: String,
|
||||
@JsonProperty("id") @SerialName("id") val id: String,
|
||||
@JsonProperty("title") @SerialName("title") val title: String,
|
||||
@JsonProperty("backup") @SerialName("backup") val backup: String,
|
||||
data class StreamData (
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("cdn_img") val cdnImg: String,
|
||||
@JsonProperty("hash") val hash: String,
|
||||
@JsonProperty("subs") val subs: ArrayList<Subs>? = arrayListOf(),
|
||||
@JsonProperty("length") val length: String,
|
||||
@JsonProperty("id") val id: String,
|
||||
@JsonProperty("title") val title: String,
|
||||
@JsonProperty("backup") val backup: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Main(
|
||||
@JsonProperty("stream_data") @SerialName("stream_data") val streamData: StreamData,
|
||||
@JsonProperty("status_code") @SerialName("status_code") val statusCode: Int,
|
||||
data class Main (
|
||||
@JsonProperty("stream_data") val streamData: StreamData,
|
||||
@JsonProperty("status_code") val statusCode: Int,
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,11 +9,10 @@ import com.lagradost.cloudstream3.utils.INFER_TYPE
|
|||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.nicehttp.RequestBodyTypes
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
|
||||
|
||||
class Streamlare : Slmaxed() {
|
||||
override val mainUrl = "https://streamlare.com/"
|
||||
}
|
||||
|
|
@ -25,11 +24,27 @@ open class Slmaxed : ExtractorApi() {
|
|||
|
||||
// https://slmaxed.com/e/oLvgezw3LjPzbp8E -> oLvgezw3LjPzbp8E
|
||||
val embedRegex = Regex("""/e/([^/]*)""")
|
||||
|
||||
|
||||
data class JsonResponse(
|
||||
@JsonProperty val status: String? = null,
|
||||
@JsonProperty val message: String? = null,
|
||||
@JsonProperty val type: String? = null,
|
||||
@JsonProperty val token: String? = null,
|
||||
@JsonProperty val result: Map<String, Result>? = null
|
||||
)
|
||||
|
||||
data class Result(
|
||||
@JsonProperty val label: String? = null,
|
||||
@JsonProperty val file: String? = null,
|
||||
@JsonProperty val type: String? = null
|
||||
)
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val id = embedRegex.find(url)!!.groupValues[1]
|
||||
val json = app.post(
|
||||
"${mainUrl}api/video/stream/get",
|
||||
requestBody = """{"id":"$id"}""".toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull()),
|
||||
requestBody = """{"id":"$id"}""".toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
|
||||
).parsed<JsonResponse>()
|
||||
return json.result?.mapNotNull {
|
||||
it.value.let { result ->
|
||||
|
|
@ -37,29 +52,18 @@ open class Slmaxed : ExtractorApi() {
|
|||
this.name,
|
||||
this.name,
|
||||
result.file ?: return@mapNotNull null,
|
||||
type = if (result.type?.contains("hls", ignoreCase = true) == true) ExtractorLinkType.M3U8 else INFER_TYPE,
|
||||
type = if (result.type?.contains(
|
||||
"hls",
|
||||
ignoreCase = true
|
||||
) == true
|
||||
) ExtractorLinkType.M3U8 else INFER_TYPE
|
||||
) {
|
||||
this.referer = url
|
||||
this.quality = result.label?.replace("p", "", ignoreCase = true)?.trim()?.toIntOrNull()
|
||||
?: Qualities.Unknown.value
|
||||
this.quality =
|
||||
result.label?.replace("p", "", ignoreCase = true)?.trim()?.toIntOrNull()
|
||||
?: Qualities.Unknown.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class JsonResponse(
|
||||
@JsonProperty("status") @SerialName("status") val status: String? = null,
|
||||
@JsonProperty("message") @SerialName("message") val message: String? = null,
|
||||
@JsonProperty("type") @SerialName("type") val type: String? = null,
|
||||
@JsonProperty("token") @SerialName("token") val token: String? = null,
|
||||
@JsonProperty("result") @SerialName("result") val result: Map<String, Result>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Result(
|
||||
@JsonProperty("label") @SerialName("label") val label: String? = null,
|
||||
@JsonProperty("file") @SerialName("file") val file: String? = null,
|
||||
@JsonProperty("type") @SerialName("type") val type: String? = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,6 @@ import com.lagradost.cloudstream3.app
|
|||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import io.ktor.http.Url
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class Streamplay : ExtractorApi() {
|
||||
override val name = "Streamplay"
|
||||
|
|
@ -20,36 +18,37 @@ open class Streamplay : ExtractorApi() {
|
|||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val request = app.get(url, referer = referer)
|
||||
val redirectUrl = request.url
|
||||
val mainServer = Url(redirectUrl).let { "${it.protocol.name}://${it.host}" }
|
||||
val key = redirectUrl.substringAfter("embed-").substringBefore(".html")
|
||||
val token = request.document.select("script").find { it.data().contains("sitekey:") }?.data()
|
||||
?.substringAfterLast("sitekey: '")?.substringBefore("',")?.let { captchaKey ->
|
||||
getCaptchaToken(
|
||||
redirectUrl,
|
||||
captchaKey,
|
||||
referer = "$mainServer/",
|
||||
)
|
||||
} ?: throw ErrorLoadingException("can't bypass captcha")
|
||||
|
||||
val token =
|
||||
request.document.select("script").find { it.data().contains("sitekey:") }?.data()
|
||||
?.substringAfterLast("sitekey: '")?.substringBefore("',")?.let { captchaKey ->
|
||||
getCaptchaToken(
|
||||
redirectUrl,
|
||||
captchaKey,
|
||||
referer = "$mainServer/"
|
||||
)
|
||||
} ?: throw ErrorLoadingException("can't bypass captcha")
|
||||
app.post(
|
||||
"$mainServer/player-$key-488x286.html", data = mapOf(
|
||||
"op" to "embed",
|
||||
"token" to token,
|
||||
"token" to token
|
||||
),
|
||||
referer = redirectUrl,
|
||||
headers = mapOf(
|
||||
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||
"Content-Type" to "application/x-www-form-urlencoded",
|
||||
),
|
||||
"Content-Type" to "application/x-www-form-urlencoded"
|
||||
)
|
||||
).document.select("script").find { script ->
|
||||
script.data().contains("eval(function(p,a,c,k,e,d)")
|
||||
}?.let {
|
||||
val data = getAndUnpack(it.data()).substringAfter("sources=[").substringBefore(",desc")
|
||||
.replace("file", "\"file\"").replace("label", "\"label\"")
|
||||
.replace("file", "\"file\"")
|
||||
.replace("label", "\"label\"")
|
||||
tryParseJson<List<Source>>("[$data}]")?.map { res ->
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
|
|
@ -58,21 +57,24 @@ open class Streamplay : ExtractorApi() {
|
|||
res.file ?: return@map null,
|
||||
) {
|
||||
this.referer = "$mainServer/"
|
||||
this.headers = mapOf("Range" to "bytes=0-")
|
||||
this.quality = when (res.label) {
|
||||
"HD" -> Qualities.P720.value
|
||||
"SD" -> Qualities.P480.value
|
||||
else -> Qualities.Unknown.value
|
||||
}
|
||||
this.headers = mapOf(
|
||||
"Range" to "bytes=0-"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Source(
|
||||
@JsonProperty("file") @SerialName("file") val file: String? = null,
|
||||
@JsonProperty("label") @SerialName("label") val label: String? = null,
|
||||
@JsonProperty("file") val file: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,68 +2,68 @@
|
|||
|
||||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.api.Log
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
open class TRsTX : ExtractorApi() {
|
||||
override val name = "TRsTX"
|
||||
override val mainUrl = "https://trstx.org"
|
||||
override val name = "TRsTX"
|
||||
override val mainUrl = "https://trstx.org"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
) {
|
||||
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||
val extRef = referer ?: ""
|
||||
val videoReq = app.get(url, referer = extRef).text
|
||||
val file = Regex("""file\":\"([^\"]+)""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found")
|
||||
val postLink = "$mainUrl/" + file.replace("\\", "")
|
||||
val rawList = app.post(postLink, referer = extRef).parsedSafe<List<JsonElement>>() ?: throw ErrorLoadingException("Post link not found")
|
||||
|
||||
val videoReq = app.get(url, referer=extRef).text
|
||||
|
||||
val file = Regex("""file\":\"([^\"]+)""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found")
|
||||
val postLink = "${mainUrl}/" + file.replace("\\", "")
|
||||
val rawList = app.post(postLink, referer=extRef).parsedSafe<List<Any>>() ?: throw ErrorLoadingException("Post link not found")
|
||||
|
||||
val postJson: List<TrstxVideoData> = rawList.drop(1).map { item ->
|
||||
val mapItem = item as Map<*, *>
|
||||
TrstxVideoData(
|
||||
title = mapItem["title"] as? String,
|
||||
file = mapItem["file"] as? String,
|
||||
file = mapItem["file"] as? String
|
||||
)
|
||||
}
|
||||
|
||||
val vidLinks = mutableSetOf<String>()
|
||||
val vidMap = mutableListOf<Map<String, String>>()
|
||||
val vidMap = mutableListOf<Map<String, String>>()
|
||||
for (item in postJson) {
|
||||
if (item.file == null || item.title == null) continue
|
||||
val fileUrl = "$mainUrl/playlist/" + item.file.substring(1) + ".txt"
|
||||
val videoData = app.post(fileUrl, referer = extRef).text
|
||||
if (videoData in vidLinks) continue
|
||||
|
||||
val fileUrl = "${mainUrl}/playlist/" + item.file.substring(1) + ".txt"
|
||||
val videoData = app.post(fileUrl, referer=extRef).text
|
||||
|
||||
if (videoData in vidLinks) { continue }
|
||||
vidLinks.add(videoData)
|
||||
|
||||
vidMap.add(mapOf(
|
||||
"title" to item.title,
|
||||
"videoData" to videoData,
|
||||
"title" to item.title,
|
||||
"videoData" to videoData
|
||||
))
|
||||
}
|
||||
|
||||
for (mapEntry in vidMap) {
|
||||
val title = mapEntry["title"] ?: continue
|
||||
val title = mapEntry["title"] ?: continue
|
||||
val m3uLink = mapEntry["videoData"] ?: continue
|
||||
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
source = this.name,
|
||||
name = "${this.name} - $title",
|
||||
url = m3uLink,
|
||||
) { this.referer = extRef }
|
||||
source = this.name,
|
||||
name = "${this.name} - ${title}",
|
||||
url = m3uLink,
|
||||
) {
|
||||
this.referer = extRef
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class TrstxVideoData(
|
||||
@JsonProperty("title") @SerialName("title") val title: String? = null,
|
||||
@JsonProperty("file") @SerialName("file") val file: String? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("file") val file: String? = null
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,42 +6,38 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
|||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class Tantifilm : ExtractorApi() {
|
||||
override val name = "Tantifilm"
|
||||
override val mainUrl = "https://cercafilm.net"
|
||||
override var name = "Tantifilm"
|
||||
override var mainUrl = "https://cercafilm.net"
|
||||
override val requiresReferer = false
|
||||
|
||||
data class TantifilmJsonData (
|
||||
@JsonProperty("success") val success : Boolean,
|
||||
@JsonProperty("data") val data : List<TantifilmData>,
|
||||
@JsonProperty("captions")val captions : List<String>,
|
||||
@JsonProperty("is_vr") val is_vr : Boolean
|
||||
)
|
||||
|
||||
data class TantifilmData (
|
||||
@JsonProperty("file") val file : String,
|
||||
@JsonProperty("label") val label : String,
|
||||
@JsonProperty("type") val type : String
|
||||
)
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val link = "$mainUrl/api/source/${url.substringAfterLast("/")}"
|
||||
val response = app.post(link).text.replace("""\""","")
|
||||
val jsonVideoData = parseJson<TantifilmJsonData>(response)
|
||||
return jsonVideoData.data.map {
|
||||
val jsonvideodata = parseJson<TantifilmJsonData>(response)
|
||||
return jsonvideodata.data.map {
|
||||
newExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
it.file + ".${it.type}",
|
||||
it.file+".${it.type}"
|
||||
) {
|
||||
this.referer = mainUrl
|
||||
this.quality = it.label.filter{ it.isDigit() }.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class TantifilmJsonData(
|
||||
@JsonProperty("success") @SerialName("success") val success: Boolean,
|
||||
@JsonProperty("data") @SerialName("data") val data: List<TantifilmData>,
|
||||
@JsonProperty("captions") @SerialName("captions") val captions: List<String>,
|
||||
@JsonProperty("is_vr") @SerialName("is_vr") val isVr: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TantifilmData(
|
||||
@JsonProperty("file") @SerialName("file") val file: String,
|
||||
@JsonProperty("label") @SerialName("label") val label: String,
|
||||
@JsonProperty("type") @SerialName("type") val type: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,35 +2,29 @@
|
|||
|
||||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.api.Log
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
open class TauVideo : ExtractorApi() {
|
||||
override val name = "TauVideo"
|
||||
override val mainUrl = "https://tau-video.xyz"
|
||||
override val name = "TauVideo"
|
||||
override val mainUrl = "https://tau-video.xyz"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
) {
|
||||
val extRef = referer ?: ""
|
||||
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||
val extRef = referer ?: ""
|
||||
val videoKey = url.split("/").last()
|
||||
val videoUrl = "$mainUrl/api/video/$videoKey"
|
||||
val videoUrl = "${mainUrl}/api/video/${videoKey}"
|
||||
|
||||
val api = app.get(videoUrl).parsedSafe<TauVideoUrls>() ?: throw ErrorLoadingException("TauVideo")
|
||||
|
||||
for (video in api.urls) {
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = video.url,
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = video.url,
|
||||
) {
|
||||
this.referer = extRef
|
||||
this.quality = getQualityFromName(video.label)
|
||||
|
|
@ -39,14 +33,12 @@ open class TauVideo : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class TauVideoUrls(
|
||||
@JsonProperty("urls") @SerialName("urls") val urls: List<TauVideoData>,
|
||||
@JsonProperty("urls") val urls: List<TauVideoData>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TauVideoData(
|
||||
@JsonProperty("url") @SerialName("url") val url: String,
|
||||
@JsonProperty("label") @SerialName("label") val label: String,
|
||||
@JsonProperty("url") val url: String,
|
||||
@JsonProperty("label") val label: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,51 +3,51 @@ package com.lagradost.cloudstream3.extractors
|
|||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class Uservideo : ExtractorApi() {
|
||||
override val name = "Uservideo"
|
||||
override val mainUrl = "https://uservideo.xyz"
|
||||
override val name: String = "Uservideo"
|
||||
override val mainUrl: String = "https://uservideo.xyz"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val script = app.get(url).document.selectFirst("script:containsData(hosts =)")?.data()
|
||||
val host = script?.substringAfter("hosts = [\"")?.substringBefore("\"];")
|
||||
val servers = script?.substringAfter("servers = \"")?.substringBefore("\";")
|
||||
val quality = Regex("(\\d{3,4})[Pp]").find(url)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||
|
||||
val sources = app.get("$host/s/$servers").text.substringAfter("\"sources\":[").substringBefore("],").let {
|
||||
tryParseJson<List<Sources>>("[$it]")
|
||||
AppUtils.tryParseJson<List<Sources>>("[$it]")
|
||||
}
|
||||
val quality = Regex("(\\d{3,4})[Pp]").find(url)?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||
|
||||
sources?.map { source ->
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
name,
|
||||
name,
|
||||
source.src ?: return@map null,
|
||||
source.src ?: return@map null
|
||||
) {
|
||||
this.referer = url
|
||||
this.quality = quality ?: Qualities.Unknown.value
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Sources(
|
||||
@JsonProperty("src") @SerialName("src") val src: String? = null,
|
||||
@JsonProperty("type") @SerialName("type") val type: String? = null,
|
||||
@JsonProperty("label") @SerialName("label") val label: String? = null,
|
||||
@JsonProperty("src") val src: String? = null,
|
||||
@JsonProperty("type") val type: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue