mirror of
https://github.com/recloudstream/cloudstream.git
synced 2026-08-02 04:43:17 +00:00
Compare commits
2 commits
master
...
migration-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
137d6173b4 |
||
|
|
abd73b4f12 |
129 changed files with 2118 additions and 5676 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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -742,13 +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>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -735,11 +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>
|
||||
</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,14 +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>
|
||||
|
|
@ -793,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
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -8,25 +8,25 @@ 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 Vicloud : ExtractorApi() {
|
||||
override val name = "Vicloud"
|
||||
override val mainUrl = "https://vicloud.sbs"
|
||||
override val name: String = "Vicloud"
|
||||
override val mainUrl: String = "https://vicloud.sbs"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = Regex("\"apiQuery\":\"(.*?)\"").find(app.get(url).text)?.groupValues?.getOrNull(1)
|
||||
app.get(
|
||||
"$mainUrl/api/?$id=&_=$unixTimeMS",
|
||||
headers = mapOf("X-Requested-With" to "XMLHttpRequest"),
|
||||
referer = url,
|
||||
headers = mapOf(
|
||||
"X-Requested-With" to "XMLHttpRequest"
|
||||
),
|
||||
referer = url
|
||||
).parsedSafe<Responses>()?.sources?.map { source ->
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
|
|
@ -39,16 +39,16 @@ open class Vicloud : ExtractorApi() {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class Sources(
|
||||
@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,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class Responses(
|
||||
@JsonProperty("sources") @SerialName("sources") val sources: List<Sources>? = arrayListOf(),
|
||||
@JsonProperty("sources") val sources: List<Sources>? = arrayListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -44,29 +44,27 @@ class VidaraSo : Vidara() {
|
|||
}
|
||||
|
||||
open class Vidara : ExtractorApi() {
|
||||
override val name = "Vidara"
|
||||
override val name: String = "Vidara"
|
||||
override val mainUrl = "https://vidara.to"
|
||||
override val requiresReferer = false
|
||||
override val requiresReferer: Boolean = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val fileCode = url.substringAfterLast("/")
|
||||
val fileInfo = app.post(
|
||||
"$mainUrl/api/stream", json = mapOf(
|
||||
"filecode" to fileCode,
|
||||
"device" to "web"
|
||||
)
|
||||
).parsed<StreamUpFileInfo>()
|
||||
val fileInfo =
|
||||
app.post("$mainUrl/api/stream", json = mapOf("filecode" to fileCode, "device" to "web"))
|
||||
.parsed<StreamUpFileInfo>()
|
||||
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
source = name,
|
||||
name = name,
|
||||
url = fileInfo.streamingUrl,
|
||||
type = ExtractorLinkType.M3U8,
|
||||
type = ExtractorLinkType.M3U8
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -79,15 +77,19 @@ open class Vidara : ExtractorApi() {
|
|||
|
||||
@Serializable
|
||||
private data class StreamUpFileInfo(
|
||||
@JsonProperty("title") @SerialName("title") val title: String,
|
||||
@JsonProperty("thumbnail") @SerialName("thumbnail") val thumbnail: String,
|
||||
@JsonProperty("streaming_url") @SerialName("streaming_url") val streamingUrl: String,
|
||||
@JsonProperty("subtitles") @SerialName("subtitles") val subtitles: List<StreamUpSubtitle>?,
|
||||
val title: String,
|
||||
val thumbnail: String,
|
||||
@SerialName("streaming_url")
|
||||
@JsonProperty("streaming_url")
|
||||
val streamingUrl: String,
|
||||
val subtitles: List<StreamUpSubtitle>?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class StreamUpSubtitle(
|
||||
@JsonProperty("file_path") @SerialName("file_path") val filePath: String,
|
||||
@JsonProperty("language") @SerialName("language") val language: String,
|
||||
@SerialName("file_path")
|
||||
@JsonProperty("file_path")
|
||||
val filePath: String,
|
||||
val language: String,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,32 +7,27 @@ import com.lagradost.api.Log
|
|||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
open class VideoSeyred : ExtractorApi() {
|
||||
override val name = "VideoSeyred"
|
||||
override val mainUrl = "https://videoseyred.in"
|
||||
override val name = "VideoSeyred"
|
||||
override val mainUrl = "https://videoseyred.in"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
) {
|
||||
val extRef = referer ?: ""
|
||||
val videoId = url.substringAfter("embed/").substringBefore("?")
|
||||
val videoUrl = "$mainUrl/playlist/$videoId.json"
|
||||
val responseRaw = app.get(videoUrl)
|
||||
val responseList = tryParseJson<List<VideoSeyredSource>>(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred")
|
||||
val response = responseList[0]
|
||||
override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) {
|
||||
val extRef = referer ?: ""
|
||||
val videoId = url.substringAfter("embed/").substringBefore("?")
|
||||
val videoUrl = "${mainUrl}/playlist/${videoId}.json"
|
||||
|
||||
val responseRaw = app.get(videoUrl)
|
||||
val responseList: List<VideoSeyredSource> = tryParseJson<List<VideoSeyredSource>>(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred")
|
||||
val response = responseList[0]
|
||||
|
||||
for (track in response.tracks) {
|
||||
if (track.label != null && track.kind == "captions") {
|
||||
subtitleCallback.invoke(
|
||||
newSubtitleFile(
|
||||
lang = track.label,
|
||||
url = fixUrl(track.file),
|
||||
url = fixUrl(track.file)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -41,38 +36,35 @@ open class VideoSeyred : ExtractorApi() {
|
|||
for (source in response.sources) {
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = source.file,
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = source.file,
|
||||
) {
|
||||
this.referer = "$mainUrl/"
|
||||
this.referer = "${mainUrl}/"
|
||||
this.quality = Qualities.Unknown.value
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class VideoSeyredSource(
|
||||
@JsonProperty("image") @SerialName("image") val image: String,
|
||||
@JsonProperty("title") @SerialName("title") val title: String,
|
||||
@JsonProperty("sources") @SerialName("sources") val sources: List<VSSource>,
|
||||
@JsonProperty("tracks") @SerialName("tracks") val tracks: List<VSTrack>,
|
||||
@JsonProperty("image") val image: String,
|
||||
@JsonProperty("title") val title: String,
|
||||
@JsonProperty("sources") val sources: List<VSSource>,
|
||||
@JsonProperty("tracks") val tracks: List<VSTrack>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VSSource(
|
||||
@JsonProperty("file") @SerialName("file") val file: String,
|
||||
@JsonProperty("type") @SerialName("type") val type: String,
|
||||
@JsonProperty("default") @SerialName("default") val default: String,
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("type") val type: String,
|
||||
@JsonProperty("default") val default: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VSTrack(
|
||||
@JsonProperty("file") @SerialName("file") val file: String,
|
||||
@JsonProperty("kind") @SerialName("kind") val kind: String,
|
||||
@JsonProperty("language") @SerialName("language") val language: String? = null,
|
||||
@JsonProperty("label") @SerialName("label") val label: String? = null,
|
||||
@JsonProperty("default") @SerialName("default") val default: String? = null,
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("kind") val kind: String,
|
||||
@JsonProperty("language") val language: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
@JsonProperty("default") val default: String? = null
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,28 +4,26 @@ import com.fasterxml.jackson.annotation.JsonProperty
|
|||
import com.lagradost.api.Log
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
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
|
||||
|
||||
class Videzz : Vidoza() {
|
||||
override val mainUrl = "https://videzz.net"
|
||||
class Videzz: Vidoza() {
|
||||
override val mainUrl: String = "https://videzz.net"
|
||||
}
|
||||
|
||||
open class Vidoza : ExtractorApi() {
|
||||
override val name = "Vidoza"
|
||||
override val mainUrl = "https://vidoza.net"
|
||||
override val requiresReferer = false
|
||||
open class Vidoza: ExtractorApi() {
|
||||
override val name: String = "Vidoza"
|
||||
override val mainUrl: String = "https://vidoza.net"
|
||||
override val requiresReferer: Boolean = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val response = app.get(url).document
|
||||
val script = response.selectFirst("script:containsData(sourcesCode)")?.data()
|
||||
|
|
@ -34,27 +32,31 @@ open class Vidoza : ExtractorApi() {
|
|||
// e.g. sourcesCode: [{ src: "https://str38.vidoza.net/vod/v2/.../v.mp4", type: "video/mp4", label:"SD", res:"720"}],
|
||||
var sourcesArray = script.substringAfter("sourcesCode:").substringBefore("\n")
|
||||
arrayOf("src", "type", "label", "res").forEach {
|
||||
// Add missing quotation marks, e.g. src: "https..." -> "src": "https..."
|
||||
sourcesArray = sourcesArray.replace(Regex(""""?$it"?:"""), """"$it":""")
|
||||
// add missing quotation marks, e.g. src: "https..." -> "src": "https..."
|
||||
sourcesArray = sourcesArray
|
||||
.replace(Regex(""""?$it"?:"""), """"$it":""")
|
||||
}
|
||||
val videoData = AppUtils.parseJson<VinovoDataList>(sourcesArray)
|
||||
|
||||
val videoData = parseJson<ArrayList<VinovoVideoData>>(sourcesArray)
|
||||
for (stream in videoData) {
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
source = name,
|
||||
name = name,
|
||||
url = stream.source,
|
||||
) { quality = getQualityFromName(stream.resolution) }
|
||||
url = stream.source
|
||||
) {
|
||||
quality = getQualityFromName(stream.resolution)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private class VinovoDataList: ArrayList<VinovoVideoData>()
|
||||
|
||||
private data class VinovoVideoData(
|
||||
@JsonProperty("src") @SerialName("src") val source: String,
|
||||
@JsonProperty("type") @SerialName("type") val type: String?,
|
||||
@JsonProperty("label") @SerialName("label") val label: String?,
|
||||
@JsonProperty("res") @SerialName("res") val resolution: String?,
|
||||
@JsonProperty("src") val source: String,
|
||||
@JsonProperty("type") val type: String?,
|
||||
@JsonProperty("label") val label: String?,
|
||||
@JsonProperty("res") val resolution: String?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +1,32 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
|
||||
import com.lagradost.cloudstream3.APIHolder
|
||||
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.Qualities
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
class VinovoSi : VinovoTo() {
|
||||
override val name = "VinovoSi"
|
||||
override val mainUrl = "https://vinovo.si"
|
||||
override var name = "VinovoSi"
|
||||
override var mainUrl = "https://vinovo.si"
|
||||
}
|
||||
|
||||
open class VinovoTo : ExtractorApi() {
|
||||
override val mainUrl = "https://vinovo.to"
|
||||
override val name = "VinovoTo"
|
||||
override val requiresReferer = false
|
||||
override val mainUrl: String = "https://vinovo.to"
|
||||
override val name: String = "VinovoTo"
|
||||
override val requiresReferer: Boolean = false
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val fixedUrl = url.replace("/d/", "/e/")
|
||||
|
||||
val resp = app.get(fixedUrl, referer = referer)
|
||||
val doc = resp.document
|
||||
|
||||
|
|
@ -36,20 +35,21 @@ open class VinovoTo : ExtractorApi() {
|
|||
val fileCode = doc.selectFirst("meta[name=\"file_code\"]")?.attr("content") ?: return
|
||||
|
||||
val captchaToken = doc.selectFirst("meta[name=recaptcha]")?.attr("content") ?: return
|
||||
val captchaSolution = getCaptchaToken(fixedUrl, captchaToken, "$mainUrl/") ?: return
|
||||
val captchaSolution =
|
||||
APIHolder.getCaptchaToken(fixedUrl, captchaToken, "$mainUrl/") ?: return
|
||||
|
||||
val streamInfo = app.post(
|
||||
url = "$mainUrl/api/file/url/$fileCode",
|
||||
data = mapOf("token" to videoToken, "recaptcha" to captchaSolution),
|
||||
headers = mapOf(
|
||||
"Origin" to mainUrl,
|
||||
"X-Requested-With" to "XMLHttpRequest",
|
||||
"X-Requested-With" to "XMLHttpRequest"
|
||||
),
|
||||
cookies = resp.cookies,
|
||||
referer = fixedUrl,
|
||||
referer = fixedUrl
|
||||
).parsed<VinovoFileResp>()
|
||||
|
||||
val fileUrl = "$videoBaseUrl/stream/${streamInfo.token}"
|
||||
|
||||
callback.invoke(
|
||||
newExtractorLink(source = name, name = name, url = fileUrl) {
|
||||
val dataTitle = doc.selectFirst("video")?.attr("data-title").orEmpty()
|
||||
|
|
@ -59,9 +59,8 @@ open class VinovoTo : ExtractorApi() {
|
|||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class VinovoFileResp(
|
||||
@JsonProperty("status") @SerialName("status") val status: String,
|
||||
@JsonProperty("token") @SerialName("token") val token: String,
|
||||
@JsonProperty("status") val status: String,
|
||||
@JsonProperty("token") val token: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,16 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
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.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
class Tubeless : Voe() {
|
||||
override val name = "Tubeless"
|
||||
|
|
@ -21,12 +19,12 @@ class Tubeless : Voe() {
|
|||
|
||||
class Simpulumlamerop : Voe() {
|
||||
override val name = "Simplum"
|
||||
override val mainUrl = "https://simpulumlamerop.com"
|
||||
override var mainUrl = "https://simpulumlamerop.com"
|
||||
}
|
||||
|
||||
class Urochsunloath : Voe() {
|
||||
override val name = "Uroch"
|
||||
override val mainUrl = "https://urochsunloath.com"
|
||||
override var mainUrl = "https://urochsunloath.com"
|
||||
}
|
||||
|
||||
class NathanFromSubject : Voe() {
|
||||
|
|
@ -35,7 +33,7 @@ class NathanFromSubject : Voe() {
|
|||
|
||||
class Yipsu : Voe() {
|
||||
override val name = "Yipsu"
|
||||
override val mainUrl = "https://yip.su"
|
||||
override var mainUrl = "https://yip.su"
|
||||
}
|
||||
|
||||
class MetaGnathTuggers : Voe() {
|
||||
|
|
@ -61,43 +59,38 @@ open class Voe : ExtractorApi() {
|
|||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
var res = app.get(url, referer = referer)
|
||||
val redirectUrl = redirectRegex.find(res.document.data())?.groupValues?.get(1)
|
||||
if (redirectUrl != null) {
|
||||
res = app.get(redirectUrl, referer = referer)
|
||||
}
|
||||
|
||||
val encodedString = res.document.selectFirst("script[type=application/json]")
|
||||
?.data()?.trim()
|
||||
?.substringAfter("[\"")
|
||||
?.substringBeforeLast("\"]")
|
||||
|
||||
val encodedString = res.document.selectFirst("script[type=application/json]")?.data()?.trim()?.substringAfter("[\"")?.substringBeforeLast("\"]")
|
||||
if (encodedString == null) {
|
||||
println("encoded string not found.")
|
||||
return
|
||||
}
|
||||
|
||||
val decryptedJson = decryptF7(encodedString)
|
||||
val m3u8 = decryptedJson?.source
|
||||
val mp4 = decryptedJson?.directAccessUrl
|
||||
val m3u8 = decryptedJson.get("source")?.asString
|
||||
val mp4 = decryptedJson.get("direct_access_url")?.asString
|
||||
|
||||
if (m3u8 != null) {
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
m3u8,
|
||||
"$mainUrl/",
|
||||
headers = mapOf("Origin" to "$mainUrl/"),
|
||||
headers = mapOf("Origin" to "$mainUrl/")
|
||||
).forEach(callback)
|
||||
}
|
||||
|
||||
if (mp4 != null) {
|
||||
if (mp4!=null)
|
||||
{
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
source = "$name MP4",
|
||||
name = "$name MP4",
|
||||
url = mp4,
|
||||
INFER_TYPE,
|
||||
INFER_TYPE
|
||||
) {
|
||||
this.referer = url
|
||||
this.quality = Qualities.Unknown.value
|
||||
|
|
@ -106,19 +99,20 @@ open class Voe : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun decryptF7(p8: String): VoeDecrypted? {
|
||||
private fun decryptF7(p8: String): JsonObject {
|
||||
return try {
|
||||
val vF = rot13(p8)
|
||||
val vF2 = replacePatterns(vF)
|
||||
val vF3 = removeUnderscores(vF2)
|
||||
val vF4 = base64Decode(vF3)
|
||||
val vF5 = charShift(vF4, 3)
|
||||
val vF6 = vF5.reversed()
|
||||
val vF6 = reverse(vF5)
|
||||
val vAtob = base64Decode(vF6)
|
||||
parseJson<VoeDecrypted>(vAtob)
|
||||
|
||||
JsonParser.parseString(vAtob).asJsonObject
|
||||
} catch (e: Exception) {
|
||||
println("Decryption error: ${e.message}")
|
||||
null
|
||||
JsonObject()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,9 +139,6 @@ open class Voe : ExtractorApi() {
|
|||
return input.map { (it.code - shift).toChar() }.joinToString("")
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class VoeDecrypted(
|
||||
@JsonProperty("source") @SerialName("source") val source: String? = null,
|
||||
@JsonProperty("direct_access_url") @SerialName("direct_access_url") val directAccessUrl: String? = null,
|
||||
)
|
||||
private fun reverse(input: String): String = input.reversed()
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,74 +9,96 @@ 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
|
||||
|
||||
class StreamM4u : XStreamCdn() {
|
||||
override val name = "StreamM4u"
|
||||
override val mainUrl = "https://streamm4u.club"
|
||||
override val name: String = "StreamM4u"
|
||||
override val mainUrl: String = "https://streamm4u.club"
|
||||
}
|
||||
|
||||
class Fembed9hd : XStreamCdn() {
|
||||
override val name = "Fembed9hd"
|
||||
override val mainUrl = "https://fembed9hd.com"
|
||||
override var mainUrl = "https://fembed9hd.com"
|
||||
override var name = "Fembed9hd"
|
||||
}
|
||||
|
||||
class Cdnplayer : XStreamCdn() {
|
||||
override val name = "Cdnplayer"
|
||||
override val mainUrl = "https://cdnplayer.online"
|
||||
class Cdnplayer: XStreamCdn() {
|
||||
override val name: String = "Cdnplayer"
|
||||
override val mainUrl: String = "https://cdnplayer.online"
|
||||
}
|
||||
|
||||
class Kotakajair : XStreamCdn() {
|
||||
override val name = "Kotakajair"
|
||||
override val mainUrl = "https://kotakajair.xyz"
|
||||
class Kotakajair: XStreamCdn() {
|
||||
override val name: String = "Kotakajair"
|
||||
override val mainUrl: String = "https://kotakajair.xyz"
|
||||
}
|
||||
|
||||
class FEnet : XStreamCdn() {
|
||||
override val name = "FEnet"
|
||||
override val mainUrl = "https://fembed.net"
|
||||
class FEnet: XStreamCdn() {
|
||||
override val name: String = "FEnet"
|
||||
override val mainUrl: String = "https://fembed.net"
|
||||
}
|
||||
|
||||
class Rasacintaku : XStreamCdn() {
|
||||
override val mainUrl = "https://rasa-cintaku-semakin-berantai.xyz"
|
||||
class Rasacintaku: XStreamCdn() {
|
||||
override val mainUrl: String = "https://rasa-cintaku-semakin-berantai.xyz"
|
||||
}
|
||||
|
||||
class LayarKaca : XStreamCdn() {
|
||||
override val name = "LayarKaca-xxi"
|
||||
override val mainUrl = "https://layarkacaxxi.icu"
|
||||
class LayarKaca: XStreamCdn() {
|
||||
override val name: String = "LayarKaca-xxi"
|
||||
override val mainUrl: String = "https://layarkacaxxi.icu"
|
||||
}
|
||||
|
||||
class DBfilm : XStreamCdn() {
|
||||
override val name = "DBfilm"
|
||||
override val mainUrl = "https://dbfilm.bar"
|
||||
class DBfilm: XStreamCdn() {
|
||||
override val name: String = "DBfilm"
|
||||
override val mainUrl: String = "https://dbfilm.bar"
|
||||
}
|
||||
|
||||
class Luxubu : XStreamCdn() {
|
||||
override val name = "FE"
|
||||
override val mainUrl = "https://www.luxubu.review"
|
||||
class Luxubu : XStreamCdn(){
|
||||
override val name: String = "FE"
|
||||
override val mainUrl: String = "https://www.luxubu.review"
|
||||
}
|
||||
|
||||
class FEmbed : XStreamCdn() {
|
||||
override val name = "FEmbed"
|
||||
override val mainUrl = "https://www.fembed.com"
|
||||
class FEmbed: XStreamCdn() {
|
||||
override val name: String = "FEmbed"
|
||||
override val mainUrl: String = "https://www.fembed.com"
|
||||
}
|
||||
|
||||
class Fplayer : XStreamCdn() {
|
||||
override val name = "Fplayer"
|
||||
override val mainUrl = "https://fplayer.info"
|
||||
class Fplayer: XStreamCdn() {
|
||||
override val name: String = "Fplayer"
|
||||
override val mainUrl: String = "https://fplayer.info"
|
||||
}
|
||||
|
||||
class FeHD : XStreamCdn() {
|
||||
override val name = "FeHD"
|
||||
override val mainUrl = "https://fembed-hd.com"
|
||||
override val domainUrl = "fembed-hd.com"
|
||||
class FeHD: XStreamCdn() {
|
||||
override val name: String = "FeHD"
|
||||
override val mainUrl: String = "https://fembed-hd.com"
|
||||
override var domainUrl: String = "fembed-hd.com"
|
||||
}
|
||||
|
||||
open class XStreamCdn : ExtractorApi() {
|
||||
override val name = "XStreamCdn"
|
||||
override val mainUrl = "https://embedsito.com"
|
||||
override val name: String = "XStreamCdn"
|
||||
override val mainUrl: String = "https://embedsito.com"
|
||||
override val requiresReferer = false
|
||||
open val domainUrl = "embedsito.com"
|
||||
open var domainUrl: String = "embedsito.com"
|
||||
|
||||
private data class ResponseData(
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("label") val label: String,
|
||||
//val type: String // Mp4
|
||||
)
|
||||
|
||||
private data class Player(
|
||||
@JsonProperty("poster_file") val poster_file: String? = null,
|
||||
)
|
||||
|
||||
private data class ResponseJson(
|
||||
@JsonProperty("success") val success: Boolean,
|
||||
@JsonProperty("player") val player: Player? = null,
|
||||
@JsonProperty("data") val data: List<ResponseData>?,
|
||||
@JsonProperty("captions") val captions: List<Captions?>?,
|
||||
)
|
||||
|
||||
private data class Captions(
|
||||
@JsonProperty("id") val id: String,
|
||||
@JsonProperty("hash") val hash: String,
|
||||
@JsonProperty("language") val language: String,
|
||||
@JsonProperty("extension") val extension: String
|
||||
)
|
||||
|
||||
override fun getExtractorUrl(id: String): String {
|
||||
return "$domainUrl/api/source/$id"
|
||||
|
|
@ -86,17 +108,16 @@ open class XStreamCdn : ExtractorApi() {
|
|||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val headers = mapOf(
|
||||
"Referer" to url,
|
||||
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0",
|
||||
)
|
||||
|
||||
val id = url.trimEnd('/').split("/").last()
|
||||
val newUrl = "https://$domainUrl/api/source/$id"
|
||||
val newUrl = "https://${domainUrl}/api/source/${id}"
|
||||
app.post(newUrl, headers = headers).let { res ->
|
||||
val sources = tryParseJson<ResponseJson>(res.text)
|
||||
val sources = tryParseJson<ResponseJson?>(res.text)
|
||||
sources?.let {
|
||||
if (it.success && it.data != null) {
|
||||
it.data.map { source ->
|
||||
|
|
@ -114,7 +135,7 @@ open class XStreamCdn : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
|
||||
val userData = sources?.player?.posterFile?.split("/")?.get(2)
|
||||
val userData = sources?.player?.poster_file?.split("/")?.get(2)
|
||||
sources?.captions?.map {
|
||||
subtitleCallback.invoke(
|
||||
newSubtitleFile(
|
||||
|
|
@ -125,31 +146,4 @@ open class XStreamCdn : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class ResponseData(
|
||||
@JsonProperty("file") @SerialName("file") val file: String,
|
||||
@JsonProperty("label") @SerialName("label") val label: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class Player(
|
||||
@JsonProperty("poster_file") @SerialName("poster_file") val posterFile: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class ResponseJson(
|
||||
@JsonProperty("success") @SerialName("success") val success: Boolean,
|
||||
@JsonProperty("player") @SerialName("player") val player: Player? = null,
|
||||
@JsonProperty("data") @SerialName("data") val data: List<ResponseData>?,
|
||||
@JsonProperty("captions") @SerialName("captions") val captions: List<Captions?>?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class Captions(
|
||||
@JsonProperty("id") @SerialName("id") val id: String,
|
||||
@JsonProperty("hash") @SerialName("hash") val hash: String,
|
||||
@JsonProperty("language") @SerialName("language") val language: String,
|
||||
@JsonProperty("extension") @SerialName("extension") val extension: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,10 +7,8 @@ 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 YourUpload : ExtractorApi() {
|
||||
open class YourUpload: ExtractorApi() {
|
||||
override val name = "Yourupload"
|
||||
override val mainUrl = "https://www.yourupload.com"
|
||||
override val requiresReferer = false
|
||||
|
|
@ -21,11 +19,13 @@ open class YourUpload : ExtractorApi() {
|
|||
val quality = Regex("\\d{3,4}p").find(this.select("title").text())?.groupValues?.get(0)
|
||||
this.select("script").map { script ->
|
||||
if (script.data().contains("var jwplayerOptions = {")) {
|
||||
val data = script.data().substringAfter("var jwplayerOptions = {").substringBefore(",\n")
|
||||
val data =
|
||||
script.data().substringAfter("var jwplayerOptions = {").substringBefore(",\n")
|
||||
val link = tryParseJson<ResponseSource>(
|
||||
"{${data.replace("file", "\"file\"").replace("'", "\"")}}"
|
||||
"{${
|
||||
data.replace("file", "\"file\"").replace("'", "\"")
|
||||
}}"
|
||||
)
|
||||
|
||||
sources.add(
|
||||
newExtractorLink(
|
||||
source = name,
|
||||
|
|
@ -39,12 +39,11 @@ open class YourUpload : ExtractorApi() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class ResponseSource(
|
||||
@JsonProperty("file") @SerialName("file") val file: String,
|
||||
@JsonProperty("file") val file: String,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -9,8 +9,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
|||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.collections.orEmpty
|
||||
|
|
@ -54,12 +52,11 @@ object JwPlayerHelper {
|
|||
}.toList()
|
||||
|
||||
var extractedLinks = sourceMatches.flatMap { link ->
|
||||
val cleanUrl = link.file.replace("\\/", "/")
|
||||
if (cleanUrl.contains(".m3u8") || cleanUrl.contains(".txt")) {
|
||||
if (link.file.contains(".m3u8")) {
|
||||
try {
|
||||
M3u8Helper.generateM3u8(
|
||||
source = sourceName,
|
||||
streamUrl = cleanUrl,
|
||||
streamUrl = link.file,
|
||||
referer = mainUrl,
|
||||
headers = headers,
|
||||
)
|
||||
|
|
@ -72,7 +69,7 @@ object JwPlayerHelper {
|
|||
newExtractorLink(
|
||||
source = sourceName,
|
||||
name = sourceName,
|
||||
url = fixUrl(cleanUrl, mainUrl),
|
||||
url = fixUrl(link.file, mainUrl),
|
||||
) {
|
||||
this.referer = url
|
||||
this.headers = headers
|
||||
|
|
@ -101,13 +98,11 @@ object JwPlayerHelper {
|
|||
*/
|
||||
if (extractedLinks.isEmpty()) {
|
||||
extractedLinks = m3u8Regex.findAll(script).toList().map { match ->
|
||||
val cleanUrl = match.groupValues[1].replace("\\/", "/")
|
||||
val isM3u8 = cleanUrl.contains(".m3u8") || cleanUrl.contains(".txt")
|
||||
val link = match.groupValues[1]
|
||||
newExtractorLink(
|
||||
source = sourceName,
|
||||
name = sourceName,
|
||||
url = fixUrl(cleanUrl, mainUrl),
|
||||
type = if (isM3u8) ExtractorLinkType.M3U8 else INFER_TYPE,
|
||||
url = fixUrl(link, mainUrl),
|
||||
) {
|
||||
this.referer = url
|
||||
this.headers = headers
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
package com.lagradost.cloudstream3.metaproviders
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.Actor
|
||||
import com.lagradost.cloudstream3.ActorData
|
||||
import com.lagradost.cloudstream3.Episode
|
||||
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||
import com.lagradost.cloudstream3.HomePageList
|
||||
|
|
@ -22,8 +20,6 @@ import com.lagradost.cloudstream3.SearchResponseList
|
|||
import com.lagradost.cloudstream3.TvSeriesLoadResponse
|
||||
import com.lagradost.cloudstream3.TvSeriesSearchResponse
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.addDate
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.newEpisode
|
||||
import com.lagradost.cloudstream3.newHomePageResponse
|
||||
import com.lagradost.cloudstream3.newMovieLoadResponse
|
||||
|
|
@ -32,22 +28,35 @@ import com.lagradost.cloudstream3.newTvSeriesLoadResponse
|
|||
import com.lagradost.cloudstream3.newTvSeriesSearchResponse
|
||||
import com.lagradost.cloudstream3.runAllAsync
|
||||
import com.lagradost.cloudstream3.toNewSearchResponseList
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import com.uwetrottmann.tmdb2.Tmdb
|
||||
import com.uwetrottmann.tmdb2.entities.AppendToResponse
|
||||
import com.uwetrottmann.tmdb2.entities.BaseMovie
|
||||
import com.uwetrottmann.tmdb2.entities.BaseTvShow
|
||||
import com.uwetrottmann.tmdb2.entities.CastMember
|
||||
import com.uwetrottmann.tmdb2.entities.ContentRating
|
||||
import com.uwetrottmann.tmdb2.entities.Movie
|
||||
import com.uwetrottmann.tmdb2.entities.ReleaseDate
|
||||
import com.uwetrottmann.tmdb2.entities.ReleaseDatesResult
|
||||
import com.uwetrottmann.tmdb2.entities.TvSeason
|
||||
import com.uwetrottmann.tmdb2.entities.TvShow
|
||||
import com.uwetrottmann.tmdb2.entities.Videos
|
||||
import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem
|
||||
import com.uwetrottmann.tmdb2.enumerations.VideoType
|
||||
import retrofit2.awaitResponse
|
||||
import retrofit2.Response
|
||||
import java.util.Calendar
|
||||
|
||||
/**
|
||||
* episode and season starting from 1
|
||||
* they are null if movie
|
||||
*/
|
||||
@Serializable
|
||||
* */
|
||||
data class TmdbLink(
|
||||
@JsonProperty("imdbID") @SerialName("imdbID") val imdbID: String?,
|
||||
@JsonProperty("tmdbID") @SerialName("tmdbID") val tmdbID: Int?,
|
||||
@JsonProperty("episode") @SerialName("episode") val episode: Int?,
|
||||
@JsonProperty("season") @SerialName("season") val season: Int?,
|
||||
@JsonProperty("movieName") @SerialName("movieName") val movieName: String? = null,
|
||||
@JsonProperty("imdbID") val imdbID: String?,
|
||||
@JsonProperty("tmdbID") val tmdbID: Int?,
|
||||
@JsonProperty("episode") val episode: Int?,
|
||||
@JsonProperty("season") val season: Int?,
|
||||
@JsonProperty("movieName") val movieName: String? = null,
|
||||
)
|
||||
|
||||
open class TmdbProvider : MainAPI() {
|
||||
|
|
@ -58,184 +67,19 @@ open class TmdbProvider : MainAPI() {
|
|||
open val useMetaLoadResponse = false
|
||||
open val apiName = "TMDB"
|
||||
|
||||
// As some sites don't support s0
|
||||
// As some sites doesn't support s0
|
||||
open val disableSeasonZero = true
|
||||
|
||||
override val hasMainPage = true
|
||||
override val providerType = ProviderType.MetaProvider
|
||||
|
||||
private val tmdbApiKey = "e6333b32409e02a4a6eba6fb7ff866bb"
|
||||
private val tmdbApiUrl = "https://api.themoviedb.org/3"
|
||||
|
||||
@Serializable
|
||||
data class TmdbIds(
|
||||
@JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String? = null,
|
||||
@JsonProperty("tvdb_id") @SerialName("tvdb_id") val tvdbId: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbGenre(
|
||||
@JsonProperty("id") @SerialName("id") val id: Int? = null,
|
||||
@JsonProperty("name") @SerialName("name") val name: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbCastMember(
|
||||
@JsonProperty("name") @SerialName("name") val name: String? = null,
|
||||
@JsonProperty("character") @SerialName("character") val character: String? = null,
|
||||
@JsonProperty("profile_path") @SerialName("profile_path") val profilePath: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbCredits(
|
||||
@JsonProperty("cast") @SerialName("cast") val cast: List<TmdbCastMember>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbVideo(
|
||||
@JsonProperty("key") @SerialName("key") val key: String? = null,
|
||||
@JsonProperty("site") @SerialName("site") val site: String? = null,
|
||||
@JsonProperty("type") @SerialName("type") val type: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbVideos(
|
||||
@JsonProperty("results") @SerialName("results") val results: List<TmdbVideo>? = null,
|
||||
)
|
||||
|
||||
// Shared between movie and tv search results
|
||||
@Serializable
|
||||
data class TmdbSearchResult(
|
||||
@JsonProperty("id") @SerialName("id") val id: Int? = null,
|
||||
@JsonProperty("title") @SerialName("title") val title: String? = null, // movies
|
||||
@JsonProperty("original_title") @SerialName("original_title") val originalTitle: String? = null,
|
||||
@JsonProperty("name") @SerialName("name") val name: String? = null, // tv
|
||||
@JsonProperty("original_name") @SerialName("original_name") val originalName: String? = null,
|
||||
@JsonProperty("poster_path") @SerialName("poster_path") val posterPath: String? = null,
|
||||
@JsonProperty("vote_average") @SerialName("vote_average") val voteAverage: Double? = null,
|
||||
@JsonProperty("release_date") @SerialName("release_date") val releaseDate: String? = null,
|
||||
@JsonProperty("first_air_date") @SerialName("first_air_date") val firstAirDate: String? = null,
|
||||
@JsonProperty("media_type") @SerialName("media_type") val mediaType: String? = null, // for multi-search
|
||||
) {
|
||||
@get:JsonIgnore val isTv get() = name != null || mediaType == "tv"
|
||||
@get:JsonIgnore val displayTitle get() = title ?: originalTitle ?: name ?: originalName ?: ""
|
||||
@get:JsonIgnore val year get() = (releaseDate ?: firstAirDate)?.take(4)?.toIntOrNull()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class TmdbPageResult(
|
||||
@JsonProperty("results") @SerialName("results") val results: List<TmdbSearchResult>? = null,
|
||||
@JsonProperty("total_pages") @SerialName("total_pages") val totalPages: Int? = null,
|
||||
@JsonProperty("total_results") @SerialName("total_results") val totalResults: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbMultiResult(
|
||||
@JsonProperty("results") @SerialName("results") val results: List<TmdbSearchResult>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbEpisode(
|
||||
@JsonProperty("id") @SerialName("id") val id: Int? = null,
|
||||
@JsonProperty("name") @SerialName("name") val name: String? = null,
|
||||
@JsonProperty("overview") @SerialName("overview") val overview: String? = null,
|
||||
@JsonProperty("episode_number") @SerialName("episode_number") val episodeNumber: Int? = null,
|
||||
@JsonProperty("season_number") @SerialName("season_number") val seasonNumber: Int? = null,
|
||||
@JsonProperty("still_path") @SerialName("still_path") val stillPath: String? = null,
|
||||
@JsonProperty("air_date") @SerialName("air_date") val airDate: String? = null,
|
||||
@JsonProperty("vote_average") @SerialName("vote_average") val voteAverage: Double? = null,
|
||||
@JsonProperty("external_ids") @SerialName("external_ids") val externalIds: TmdbIds? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbSeasonDetail(
|
||||
@JsonProperty("season_number") @SerialName("season_number") val seasonNumber: Int? = null,
|
||||
@JsonProperty("episodes") @SerialName("episodes") val episodes: List<TmdbEpisode>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbSeasonSummary(
|
||||
@JsonProperty("season_number") @SerialName("season_number") val seasonNumber: Int? = null,
|
||||
@JsonProperty("episode_count") @SerialName("episode_count") val episodeCount: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbContentRating(
|
||||
@JsonProperty("iso_3166_1") @SerialName("iso_3166_1") val country: String? = null,
|
||||
@JsonProperty("rating") @SerialName("rating") val rating: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbContentRatings(
|
||||
@JsonProperty("results") @SerialName("results") val results: List<TmdbContentRating>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbReleaseDateEntry(
|
||||
@JsonProperty("certification") @SerialName("certification") val certification: String? = null,
|
||||
@JsonProperty("type") @SerialName("type") val type: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbReleaseDateResult(
|
||||
@JsonProperty("iso_3166_1") @SerialName("iso_3166_1") val country: String? = null,
|
||||
@JsonProperty("release_dates") @SerialName("release_dates") val releaseDates: List<TmdbReleaseDateEntry>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbReleaseDates(
|
||||
@JsonProperty("results") @SerialName("results") val results: List<TmdbReleaseDateResult>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TmdbTvDetail(
|
||||
@JsonProperty("id") @SerialName("id") val id: Int? = null,
|
||||
@JsonProperty("name") @SerialName("name") val name: String? = null,
|
||||
@JsonProperty("original_name") @SerialName("original_name") val originalName: String? = null,
|
||||
@JsonProperty("overview") @SerialName("overview") val overview: String? = null,
|
||||
@JsonProperty("poster_path") @SerialName("poster_path") val posterPath: String? = null,
|
||||
@JsonProperty("first_air_date") @SerialName("first_air_date") val firstAirDate: String? = null,
|
||||
@JsonProperty("vote_average") @SerialName("vote_average") val voteAverage: Double? = null,
|
||||
@JsonProperty("genres") @SerialName("genres") val genres: List<TmdbGenre>? = null,
|
||||
@JsonProperty("episode_run_time") @SerialName("episode_run_time") val episodeRunTime: List<Int>? = null,
|
||||
@JsonProperty("seasons") @SerialName("seasons") val seasons: List<TmdbSeasonSummary>? = null,
|
||||
@JsonProperty("external_ids") @SerialName("external_ids") val externalIds: TmdbIds? = null,
|
||||
@JsonProperty("videos") @SerialName("videos") val videos: TmdbVideos? = null,
|
||||
@JsonProperty("credits") @SerialName("credits") val credits: TmdbCredits? = null,
|
||||
@JsonProperty("recommendations") @SerialName("recommendations") val recommendations: TmdbPageResult? = null,
|
||||
@JsonProperty("similar") @SerialName("similar") val similar: TmdbPageResult? = null,
|
||||
@JsonProperty("content_ratings") @SerialName("content_ratings") val contentRatings: TmdbContentRatings? = null,
|
||||
) {
|
||||
@get:JsonIgnore val displayTitle get() = name ?: originalName ?: ""
|
||||
@get:JsonIgnore val year get() = firstAirDate?.take(4)?.toIntOrNull()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class TmdbMovieDetail(
|
||||
@JsonProperty("id") @SerialName("id") val id: Int? = null,
|
||||
@JsonProperty("title") @SerialName("title") val title: String? = null,
|
||||
@JsonProperty("original_title") @SerialName("original_title") val originalTitle: String? = null,
|
||||
@JsonProperty("overview") @SerialName("overview") val overview: String? = null,
|
||||
@JsonProperty("poster_path") @SerialName("poster_path") val posterPath: String? = null,
|
||||
@JsonProperty("release_date") @SerialName("release_date") val releaseDate: String? = null,
|
||||
@JsonProperty("vote_average") @SerialName("vote_average") val voteAverage: Double? = null,
|
||||
@JsonProperty("genres") @SerialName("genres") val genres: List<TmdbGenre>? = null,
|
||||
@JsonProperty("runtime") @SerialName("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String? = null,
|
||||
@JsonProperty("external_ids") @SerialName("external_ids") val externalIds: TmdbIds? = null,
|
||||
@JsonProperty("videos") @SerialName("videos") val videos: TmdbVideos? = null,
|
||||
@JsonProperty("credits") @SerialName("credits") val credits: TmdbCredits? = null,
|
||||
@JsonProperty("recommendations") @SerialName("recommendations") val recommendations: TmdbPageResult? = null,
|
||||
@JsonProperty("similar") @SerialName("similar") val similar: TmdbPageResult? = null,
|
||||
@JsonProperty("release_dates") @SerialName("release_dates") val releaseDates: TmdbReleaseDates? = null,
|
||||
) {
|
||||
@get:JsonIgnore val displayTitle get() = title ?: originalTitle ?: ""
|
||||
@get:JsonIgnore val year get() = releaseDate?.take(4)?.toIntOrNull()
|
||||
}
|
||||
// Fuck it, public private api key because github actions won't co-operate.
|
||||
// Please no stealy.
|
||||
private val tmdb = Tmdb("e6333b32409e02a4a6eba6fb7ff866bb")
|
||||
|
||||
private fun getImageUrl(link: String?): String? {
|
||||
link ?: return null
|
||||
return if (link.startsWith("/")) "https://image.tmdb.org/t/p/w500$link" else link
|
||||
if (link == null) return null
|
||||
return if (link.startsWith("/")) "https://image.tmdb.org/t/p/w500/$link" else link
|
||||
}
|
||||
|
||||
private fun getUrl(id: Int?, tvShow: Boolean): String {
|
||||
|
|
@ -243,197 +87,199 @@ open class TmdbProvider : MainAPI() {
|
|||
else "https://www.themoviedb.org/movie/${id ?: -1}"
|
||||
}
|
||||
|
||||
private suspend fun getApi(path: String, extraParams: Map<String, String> = emptyMap()): String {
|
||||
val params = buildMap {
|
||||
put("api_key", tmdbApiKey)
|
||||
putAll(extraParams)
|
||||
}
|
||||
return app.get(
|
||||
url = "$tmdbApiUrl$path",
|
||||
params = params,
|
||||
).text
|
||||
}
|
||||
|
||||
private fun TmdbSearchResult.toSearchResponse() = if (isTv) {
|
||||
newTvSeriesSearchResponse(
|
||||
name = displayTitle,
|
||||
private fun BaseTvShow.toSearchResponse(): TvSeriesSearchResponse {
|
||||
return newTvSeriesSearchResponse(
|
||||
name = this.name ?: this.original_name,
|
||||
url = getUrl(id, true),
|
||||
type = TvType.TvSeries,
|
||||
fix = false,
|
||||
fix = false
|
||||
) {
|
||||
this.id = this@toSearchResponse.id
|
||||
this.posterUrl = getImageUrl(posterPath)
|
||||
this.score = Score.from10(voteAverage)
|
||||
this.year = this@toSearchResponse.year
|
||||
this.posterUrl = getImageUrl(poster_path)
|
||||
this.score = Score.from10(vote_average)
|
||||
this.year = first_air_date?.let {
|
||||
Calendar.getInstance().apply {
|
||||
time = it
|
||||
}.get(Calendar.YEAR)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newMovieSearchResponse(
|
||||
name = displayTitle,
|
||||
}
|
||||
|
||||
private fun BaseMovie.toSearchResponse(): MovieSearchResponse {
|
||||
return newMovieSearchResponse(
|
||||
name = this.title ?: this.original_title,
|
||||
url = getUrl(id, false),
|
||||
type = TvType.Movie,
|
||||
fix = false,
|
||||
fix = false
|
||||
) {
|
||||
this.id = this@toSearchResponse.id
|
||||
this.posterUrl = getImageUrl(posterPath)
|
||||
this.score = Score.from10(voteAverage)
|
||||
this.year = this@toSearchResponse.year
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<TmdbCastMember?>?.toActors(): List<Pair<Actor, String?>>? {
|
||||
return this?.mapNotNull {
|
||||
it ?: return@mapNotNull null
|
||||
Pair(
|
||||
Actor(it.name ?: return@mapNotNull null, getImageUrl(it.profilePath)),
|
||||
it.character,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TmdbVideos?.toTrailers(): List<String>? {
|
||||
val skipTypes = setOf("Opening Credits", "Featurette")
|
||||
return this?.results
|
||||
?.filter { it.type !in skipTypes }
|
||||
?.sortedBy { it.type }
|
||||
?.mapNotNull {
|
||||
when (it.site?.trim()?.lowercase()) {
|
||||
"youtube" -> "https://www.youtube.com/watch?v=${it.key}"
|
||||
else -> null
|
||||
}
|
||||
this.posterUrl = getImageUrl(poster_path)
|
||||
this.score = Score.from10(vote_average)
|
||||
this.year = release_date?.let {
|
||||
Calendar.getInstance().apply {
|
||||
time = it
|
||||
}.get(Calendar.YEAR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun fetchContentRating(id: Int?, country: String): String? {
|
||||
id ?: return null
|
||||
// Try TV content ratings first
|
||||
val tvRating = parseJson<TmdbContentRatings>(
|
||||
getApi("/tv/$id/content_ratings")
|
||||
).results?.firstOrNull { it.country == country }?.rating
|
||||
if (tvRating != null) return tvRating
|
||||
|
||||
// Fall back to movie release dates
|
||||
return parseJson<TmdbReleaseDates>(
|
||||
getApi("/movie/$id/release_dates")
|
||||
).results?.firstOrNull { it.country == country }
|
||||
?.releaseDates?.firstOrNull { !it.certification.isNullOrBlank() }
|
||||
?.certification
|
||||
}
|
||||
|
||||
private suspend fun TmdbTvDetail.toLoadResponse(): TvSeriesLoadResponse {
|
||||
val episodes = mutableListOf<Episode>()
|
||||
val validSeasons = seasons?.filter { !disableSeasonZero || (it.seasonNumber ?: 0) != 0 }
|
||||
?: emptyList()
|
||||
|
||||
for (season in validSeasons) {
|
||||
val seasonNum = season.seasonNumber ?: continue
|
||||
val fullSeason = parseJson<TmdbSeasonDetail>(
|
||||
getApi("/tv/$id/season/$seasonNum", mapOf("append_to_response" to "external_ids"))
|
||||
private fun List<CastMember?>?.toActors(): List<Pair<Actor, String?>>? {
|
||||
return this?.mapNotNull {
|
||||
Pair(
|
||||
Actor(it?.name ?: return@mapNotNull null, getImageUrl(it.profile_path)),
|
||||
it.character
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun TvShow.toLoadResponse(): TvSeriesLoadResponse {
|
||||
val tvSeasonsService = tmdb.tvSeasonsService()
|
||||
val episodes = mutableListOf<Episode>()
|
||||
|
||||
val validSeasons = this.seasons?.filter { !disableSeasonZero || (it.season_number ?: 0) != 0 } ?: emptyList()
|
||||
for (season in validSeasons) {
|
||||
val seasonNumber = season.season_number ?: continue
|
||||
|
||||
val response: Response<TvSeason> = tmdb.tvSeasonsService()
|
||||
.season(this.id, seasonNumber, "external_ids,images,episodes")
|
||||
.awaitResponse()
|
||||
|
||||
val fullSeason = response.body() ?: continue
|
||||
|
||||
fullSeason.episodes?.forEach { episode ->
|
||||
episodes += newEpisode(
|
||||
TmdbLink(
|
||||
episode.externalIds?.imdbId ?: externalIds?.imdbId,
|
||||
id,
|
||||
episode.episodeNumber,
|
||||
episode.seasonNumber,
|
||||
displayTitle,
|
||||
episode.external_ids?.imdb_id ?: this.external_ids?.imdb_id,
|
||||
this.id,
|
||||
episode.episode_number,
|
||||
episode.season_number,
|
||||
this.name ?: this.original_name
|
||||
).toJson()
|
||||
) {
|
||||
this.name = episode.name
|
||||
this.season = episode.seasonNumber
|
||||
this.episode = episode.episodeNumber
|
||||
this.score = Score.from10(episode.voteAverage)
|
||||
this.season = episode.season_number
|
||||
this.episode = episode.episode_number
|
||||
this.score = Score.from10(episode.vote_average)
|
||||
this.description = episode.overview
|
||||
this.posterUrl = getImageUrl(episode.stillPath)
|
||||
this.addDate(episode.airDate)
|
||||
this.date = episode.air_date?.time
|
||||
this.posterUrl = getImageUrl(episode.still_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newTvSeriesLoadResponse(
|
||||
displayTitle,
|
||||
this.name ?: this.original_name,
|
||||
getUrl(id, true),
|
||||
TvType.TvSeries,
|
||||
episodes,
|
||||
episodes
|
||||
) {
|
||||
posterUrl = getImageUrl(posterPath)
|
||||
this.year = this@toLoadResponse.year
|
||||
posterUrl = getImageUrl(poster_path)
|
||||
year = first_air_date?.let {
|
||||
Calendar.getInstance().apply {
|
||||
time = it
|
||||
}.get(Calendar.YEAR)
|
||||
}
|
||||
plot = overview
|
||||
addImdbId(externalIds?.imdbId)
|
||||
addImdbId(external_ids?.imdb_id)
|
||||
tags = genres?.mapNotNull { it.name }
|
||||
duration = episodeRunTime?.average()?.toInt()
|
||||
score = Score.from10(voteAverage)
|
||||
duration = episode_run_time?.average()?.toInt()
|
||||
score = Score.from10(vote_average)
|
||||
addTrailer(videos.toTrailers())
|
||||
recommendations = (this@toLoadResponse.recommendations
|
||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||
addActors(credits?.cast?.toList().toActors())
|
||||
contentRating = contentRatings?.results?.firstOrNull { it.country == "US" }?.rating
|
||||
?: fetchContentRating(id, "US")
|
||||
contentRating = fetchContentRating(id, "US")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun TmdbMovieDetail.toLoadResponse(): MovieLoadResponse {
|
||||
private fun Videos?.toTrailers(): List<String>? {
|
||||
return this?.results?.filter { it.type != VideoType.OPENING_CREDITS && it.type != VideoType.FEATURETTE }
|
||||
?.sortedBy { it.type?.ordinal ?: 10000 }
|
||||
?.mapNotNull {
|
||||
when (it.site?.trim()?.lowercase()) {
|
||||
"youtube" -> { // TODO FILL SITES
|
||||
"https://www.youtube.com/watch?v=${it.key}"
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Movie.toLoadResponse(): MovieLoadResponse {
|
||||
return newMovieLoadResponse(
|
||||
displayTitle,
|
||||
getUrl(id, false),
|
||||
TvType.Movie,
|
||||
TmdbLink(
|
||||
imdbId ?: externalIds?.imdbId,
|
||||
id,
|
||||
this.title ?: this.original_title, getUrl(id, false), TvType.Movie, TmdbLink(
|
||||
this.imdb_id,
|
||||
this.id,
|
||||
null,
|
||||
null,
|
||||
displayTitle,
|
||||
this.title ?: this.original_title,
|
||||
).toJson()
|
||||
) {
|
||||
posterUrl = getImageUrl(posterPath)
|
||||
this.year = this@toLoadResponse.year
|
||||
posterUrl = getImageUrl(poster_path)
|
||||
year = release_date?.let {
|
||||
Calendar.getInstance().apply {
|
||||
time = it
|
||||
}.get(Calendar.YEAR)
|
||||
}
|
||||
plot = overview
|
||||
addImdbId(imdbId ?: externalIds?.imdbId)
|
||||
addImdbId(external_ids?.imdb_id)
|
||||
tags = genres?.mapNotNull { it.name }
|
||||
duration = runtime
|
||||
score = Score.from10(voteAverage)
|
||||
score = Score.from10(vote_average)
|
||||
addTrailer(videos.toTrailers())
|
||||
|
||||
recommendations = (this@toLoadResponse.recommendations
|
||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||
addActors(credits?.cast?.toList().toActors())
|
||||
contentRating = releaseDates?.results
|
||||
?.firstOrNull { it.country == "US" }
|
||||
?.releaseDates?.firstOrNull { !it.certification.isNullOrBlank() }
|
||||
?.certification
|
||||
|
||||
contentRating = fetchContentRating(id, "US")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||
|
||||
// SAME AS DISCOVER IT SEEMS
|
||||
// val popularSeries = tmdb.tvService().popular(page, "en-US").execute().body()?.results?.map {
|
||||
// it.toSearchResponse()
|
||||
// } ?: listOf()
|
||||
//
|
||||
// val popularMovies =
|
||||
// tmdb.moviesService().popular(page, "en-US", "840").execute().body()?.results?.map {
|
||||
// it.toSearchResponse()
|
||||
// } ?: listOf()
|
||||
|
||||
var discoverMovies: List<MovieSearchResponse> = listOf()
|
||||
var discoverSeries: List<TvSeriesSearchResponse> = listOf()
|
||||
var topMovies: List<MovieSearchResponse> = listOf()
|
||||
var topSeries: List<TvSeriesSearchResponse> = listOf()
|
||||
runAllAsync(
|
||||
{
|
||||
discoverMovies = parseJson<TmdbPageResult>(
|
||||
getApi("/discover/movie", mapOf("page" to "$page"))
|
||||
).results?.map { it.toSearchResponse() as MovieSearchResponse } ?: listOf()
|
||||
},
|
||||
{
|
||||
discoverSeries = parseJson<TmdbPageResult>(
|
||||
getApi("/discover/tv", mapOf("page" to "$page"))
|
||||
).results?.map { it.toSearchResponse() as TvSeriesSearchResponse } ?: listOf()
|
||||
},
|
||||
{
|
||||
topMovies = parseJson<TmdbPageResult>(
|
||||
getApi("/movie/top_rated", mapOf("page" to "$page", "language" to "en-US", "region" to "US"))
|
||||
).results?.map { it.toSearchResponse() as MovieSearchResponse } ?: listOf()
|
||||
},
|
||||
{
|
||||
topSeries = parseJson<TmdbPageResult>(
|
||||
getApi("/tv/top_rated", mapOf("page" to "$page", "language" to "en-US"))
|
||||
).results?.map { it.toSearchResponse() as TvSeriesSearchResponse } ?: listOf()
|
||||
},
|
||||
discoverMovies = tmdb.discoverMovie().page(page).build().awaitResponse().body()?.results?.map {
|
||||
it.toSearchResponse()
|
||||
} ?: listOf()
|
||||
}, {
|
||||
discoverSeries = tmdb.discoverTv().page(page).build().awaitResponse().body()?.results?.map {
|
||||
it.toSearchResponse()
|
||||
} ?: listOf()
|
||||
}, {
|
||||
// https://en.wikipedia.org/wiki/ISO_3166-1
|
||||
topMovies =
|
||||
tmdb.moviesService().topRated(page, "en-US", "US").awaitResponse()
|
||||
.body()?.results?.map {
|
||||
it.toSearchResponse()
|
||||
} ?: listOf()
|
||||
}, {
|
||||
topSeries =
|
||||
tmdb.tvService().topRated(page, "en-US").awaitResponse().body()?.results?.map {
|
||||
it.toSearchResponse()
|
||||
} ?: listOf()
|
||||
}
|
||||
)
|
||||
|
||||
return newHomePageResponse(
|
||||
listOf(
|
||||
// HomePageList("Popular Series", popularSeries),
|
||||
// HomePageList("Popular Movies", popularMovies),
|
||||
HomePageList("Popular Movies", discoverMovies),
|
||||
HomePageList("Popular Series", discoverSeries),
|
||||
HomePageList("Top Movies", topMovies),
|
||||
|
|
@ -442,14 +288,47 @@ open class TmdbProvider : MainAPI() {
|
|||
)
|
||||
}
|
||||
|
||||
open fun loadFromImdb(imdb: String, seasons: List<TmdbSeasonSummary>): LoadResponse? = null
|
||||
open fun loadFromTmdb(tmdbId: Int, seasons: List<TmdbSeasonSummary>): LoadResponse? = null
|
||||
open fun loadFromImdb(imdb: String): LoadResponse? = null
|
||||
open fun loadFromTmdb(tmdbId: Int): LoadResponse? = null
|
||||
open fun loadFromImdb(imdb: String, seasons: List<TvSeason>): LoadResponse? {
|
||||
return null
|
||||
}
|
||||
|
||||
open fun loadFromTmdb(tmdb: Int, seasons: List<TvSeason>): LoadResponse? {
|
||||
return null
|
||||
}
|
||||
|
||||
open fun loadFromImdb(imdb: String): LoadResponse? {
|
||||
return null
|
||||
}
|
||||
|
||||
open fun loadFromTmdb(tmdb: Int): LoadResponse? {
|
||||
return null
|
||||
}
|
||||
|
||||
open suspend fun fetchContentRating(id: Int?, country: String): String? {
|
||||
id ?: return null
|
||||
|
||||
val contentRatings = tmdb.tvService().content_ratings(id).awaitResponse().body()?.results
|
||||
return if (!contentRatings.isNullOrEmpty()) {
|
||||
contentRatings.firstOrNull { it: ContentRating ->
|
||||
it.iso_3166_1 == country
|
||||
}?.rating
|
||||
} else {
|
||||
val releaseDates = tmdb.moviesService().releaseDates(id).awaitResponse().body()?.results
|
||||
val certification = releaseDates?.firstOrNull { it: ReleaseDatesResult ->
|
||||
it.iso_3166_1 == country
|
||||
}?.release_dates?.firstOrNull { it: ReleaseDate ->
|
||||
!it.certification.isNullOrBlank()
|
||||
}?.certification
|
||||
|
||||
certification
|
||||
}
|
||||
}
|
||||
|
||||
// Possible to add recommendations and such here.
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
// https://www.themoviedb.org/movie/7445-brothers
|
||||
// https://www.themoviedb.org/tv/71914-the-wheel-of-time
|
||||
|
||||
val idRegex = Regex("""themoviedb\.org/(.*)/(\d+)""")
|
||||
val found = idRegex.find(url)
|
||||
|
||||
|
|
@ -458,64 +337,86 @@ open class TmdbProvider : MainAPI() {
|
|||
?: throw ErrorLoadingException("No id found")
|
||||
|
||||
return if (useMetaLoadResponse) {
|
||||
if (isTvSeries) {
|
||||
val detail = parseJson<TmdbTvDetail>(
|
||||
getApi(
|
||||
"/tv/$id",
|
||||
mapOf(
|
||||
"language" to "en-US",
|
||||
"append_to_response" to "external_ids,videos,credits,recommendations,similar,content_ratings",
|
||||
return if (isTvSeries) {
|
||||
val body = tmdb.tvService()
|
||||
.tv(
|
||||
id,
|
||||
"en-US",
|
||||
AppendToResponse(
|
||||
AppendToResponseItem.EXTERNAL_IDS,
|
||||
AppendToResponseItem.VIDEOS
|
||||
)
|
||||
)
|
||||
)
|
||||
detail.toLoadResponse()
|
||||
.awaitResponse().body()
|
||||
val response = body?.toLoadResponse()
|
||||
if (response != null) {
|
||||
if (response.recommendations.isNullOrEmpty())
|
||||
tmdb.tvService().recommendations(id, 1, "en-US").awaitResponse().body()
|
||||
?.let {
|
||||
it.results?.map { res -> res.toSearchResponse() }
|
||||
}?.let { list ->
|
||||
response.recommendations = list
|
||||
}
|
||||
|
||||
if (response.actors.isNullOrEmpty())
|
||||
tmdb.tvService().credits(id, "en-US").awaitResponse().body()?.let {
|
||||
response.addActors(it.cast?.toActors())
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
} else {
|
||||
val detail = parseJson<TmdbMovieDetail>(
|
||||
getApi(
|
||||
"/movie/$id",
|
||||
mapOf(
|
||||
"language" to "en-US",
|
||||
"append_to_response" to "external_ids,videos,credits,recommendations,similar,release_dates",
|
||||
val body = tmdb.moviesService()
|
||||
.summary(
|
||||
id,
|
||||
"en-US",
|
||||
AppendToResponse(
|
||||
AppendToResponseItem.EXTERNAL_IDS,
|
||||
AppendToResponseItem.VIDEOS
|
||||
)
|
||||
)
|
||||
)
|
||||
detail.toLoadResponse()
|
||||
.awaitResponse().body()
|
||||
val response = body?.toLoadResponse()
|
||||
if (response != null) {
|
||||
if (response.recommendations.isNullOrEmpty())
|
||||
tmdb.moviesService().recommendations(id, 1, "en-US").awaitResponse().body()
|
||||
?.let {
|
||||
it.results?.map { res -> res.toSearchResponse() }
|
||||
}?.let { list ->
|
||||
response.recommendations = list
|
||||
}
|
||||
|
||||
if (response.actors.isNullOrEmpty())
|
||||
tmdb.moviesService().credits(id).awaitResponse().body()?.let {
|
||||
response.addActors(it.cast?.toActors())
|
||||
}
|
||||
}
|
||||
response
|
||||
}
|
||||
} else {
|
||||
loadFromTmdb(id)?.let { return it }
|
||||
if (isTvSeries) {
|
||||
val externalIds = parseJson<TmdbIds>(getApi("/tv/$id/external_ids"))
|
||||
val imdbId = externalIds.imdbId
|
||||
if (imdbId != null) {
|
||||
val fromImdb = loadFromImdb(imdbId)
|
||||
if (fromImdb != null) return fromImdb
|
||||
}
|
||||
val seasons = parseJson<TmdbTvDetail>(getApi("/tv/$id")).seasons ?: listOf()
|
||||
if (imdbId != null) {
|
||||
loadFromImdb(imdbId, seasons) ?: loadFromTmdb(id, seasons)
|
||||
} else {
|
||||
loadFromTmdb(id, seasons)
|
||||
tmdb.tvService().externalIds(id).awaitResponse().body()?.imdb_id?.let {
|
||||
val fromImdb = loadFromImdb(it)
|
||||
val result = if (fromImdb == null) {
|
||||
val details = tmdb.tvService().tv(id, "en-US").awaitResponse().body()
|
||||
loadFromImdb(it, details?.seasons ?: listOf())
|
||||
?: loadFromTmdb(id, details?.seasons ?: listOf())
|
||||
} else fromImdb
|
||||
|
||||
result
|
||||
}
|
||||
} else {
|
||||
val imdbId = parseJson<TmdbMovieDetail>(getApi("/movie/$id")).imdbId
|
||||
if (imdbId != null) loadFromImdb(imdbId) else null
|
||||
tmdb.moviesService().externalIds(id).awaitResponse()
|
||||
.body()?.imdb_id?.let { loadFromImdb(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String, page: Int): SearchResponseList? {
|
||||
return parseJson<TmdbMultiResult>(
|
||||
getApi(
|
||||
"/search/multi",
|
||||
mapOf(
|
||||
"query" to query,
|
||||
"page" to "$page",
|
||||
"language" to "en-US",
|
||||
"include_adult" to "$includeAdult",
|
||||
)
|
||||
)
|
||||
).results?.mapNotNull {
|
||||
if (it.mediaType == "person") null else it.toSearchResponse()
|
||||
}?.toNewSearchResponseList()
|
||||
return tmdb.searchService().multi(query, page, "en-US", "US", includeAdult).awaitResponse()
|
||||
.body()?.results?.mapNotNull {
|
||||
it.movie?.toSearchResponse() ?: it.tvShow?.toSearchResponse()
|
||||
}?.toNewSearchResponseList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,10 +34,6 @@ import com.lagradost.cloudstream3.newTvSeriesLoadResponse
|
|||
import com.lagradost.cloudstream3.newTvSeriesSearchResponse
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonNames
|
||||
|
||||
open class TraktProvider : MainAPI() {
|
||||
override var name = "Trakt"
|
||||
|
|
@ -50,6 +46,7 @@ open class TraktProvider : MainAPI() {
|
|||
)
|
||||
|
||||
private val traktApiUrl = "https://api.trakt.tv"
|
||||
|
||||
private val traktClientId: String = BuildConfig.TRAKT_CLIENT_ID
|
||||
|
||||
override val mainPage = mainPageOf(
|
||||
|
|
@ -61,21 +58,16 @@ open class TraktProvider : MainAPI() {
|
|||
|
||||
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
||||
val apiResponse = getApi("${request.data}?extended=full,images&page=$page")
|
||||
|
||||
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
||||
element.toSearchResponse()
|
||||
}
|
||||
|
||||
return newHomePageResponse(request.name, results)
|
||||
}
|
||||
|
||||
private fun MediaDetails.toSearchResponse(): SearchResponse {
|
||||
val media = this.media ?: MediaSummary(
|
||||
title = this.title,
|
||||
year = this.year,
|
||||
ids = this.ids,
|
||||
rating = this.rating,
|
||||
images = this.images,
|
||||
)
|
||||
|
||||
val media = this.media ?: this
|
||||
val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries
|
||||
val poster = media.images?.poster?.firstOrNull()
|
||||
return if (mediaType == TvType.Movie) {
|
||||
|
|
@ -83,7 +75,7 @@ open class TraktProvider : MainAPI() {
|
|||
name = media.title ?: "",
|
||||
url = Data(
|
||||
type = mediaType,
|
||||
mediaDetails = this,
|
||||
mediaDetails = media,
|
||||
).toJson(),
|
||||
type = TvType.Movie,
|
||||
) {
|
||||
|
|
@ -95,7 +87,7 @@ open class TraktProvider : MainAPI() {
|
|||
name = media.title ?: "",
|
||||
url = Data(
|
||||
type = mediaType,
|
||||
mediaDetails = this,
|
||||
mediaDetails = media,
|
||||
).toJson(),
|
||||
type = TvType.TvSeries,
|
||||
) {
|
||||
|
|
@ -106,7 +98,9 @@ open class TraktProvider : MainAPI() {
|
|||
}
|
||||
|
||||
override suspend fun search(query: String, page: Int): SearchResponseList? {
|
||||
val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=full,images&limit=20&page=$page&query=$query")
|
||||
val apiResponse =
|
||||
getApi("$traktApiUrl/search/movie,show?extended=full,images&limit=20&page=$page&query=$query")
|
||||
|
||||
return newSearchResponseList(parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
||||
element.toSearchResponse()
|
||||
})
|
||||
|
|
@ -121,7 +115,9 @@ open class TraktProvider : MainAPI() {
|
|||
val backDropUrl = fixPath(mediaDetails?.images?.fanart?.firstOrNull())
|
||||
val logoUrl = fixPath(mediaDetails?.images?.logo?.firstOrNull())
|
||||
|
||||
val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=full,images")
|
||||
val resActor =
|
||||
getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=full,images")
|
||||
|
||||
val actors = parseJson<People>(resActor).cast?.map {
|
||||
ActorData(
|
||||
Actor(
|
||||
|
|
@ -132,7 +128,9 @@ open class TraktProvider : MainAPI() {
|
|||
)
|
||||
}
|
||||
|
||||
val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=full,images&limit=20")
|
||||
val resRelated =
|
||||
getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=full,images&limit=20")
|
||||
|
||||
val relatedMedia = parseJson<List<MediaDetails>>(resRelated).map { it.toSearchResponse() }
|
||||
|
||||
val isCartoon =
|
||||
|
|
@ -144,6 +142,7 @@ open class TraktProvider : MainAPI() {
|
|||
val uniqueUrl = data.mediaDetails?.ids?.trakt?.toJson() ?: data.toJson()
|
||||
|
||||
if (data.type == TvType.Movie) {
|
||||
|
||||
val linkData = LinkData(
|
||||
id = mediaDetails?.ids?.tmdb,
|
||||
traktId = mediaDetails?.ids?.trakt,
|
||||
|
|
@ -157,7 +156,7 @@ open class TraktProvider : MainAPI() {
|
|||
year = mediaDetails?.year,
|
||||
orgTitle = mediaDetails?.title,
|
||||
isAnime = isAnime,
|
||||
// jpTitle = later if needed as it requires another network request,
|
||||
//jpTitle = later if needed as it requires another network request,
|
||||
airedDate = mediaDetails?.released
|
||||
?: mediaDetails?.firstAired,
|
||||
isAsian = isAsian,
|
||||
|
|
@ -191,6 +190,7 @@ open class TraktProvider : MainAPI() {
|
|||
addTMDbId(mediaDetails.ids?.tmdb.toString())
|
||||
}
|
||||
} else {
|
||||
|
||||
val resSeasons =
|
||||
getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=full,images,episodes")
|
||||
val episodes = mutableListOf<Episode>()
|
||||
|
|
@ -198,7 +198,9 @@ open class TraktProvider : MainAPI() {
|
|||
var nextAir: NextAiring? = null
|
||||
|
||||
seasons.forEach { season ->
|
||||
|
||||
season.episodes?.map { episode ->
|
||||
|
||||
val linkData = LinkData(
|
||||
id = mediaDetails?.ids?.tmdb,
|
||||
traktId = mediaDetails?.ids?.trakt,
|
||||
|
|
@ -232,7 +234,8 @@ open class TraktProvider : MainAPI() {
|
|||
this.episode = episode.number
|
||||
this.description = episode.overview
|
||||
this.runTime = episode.runtime
|
||||
this.posterUrl = fixPath(episode.images?.screenshot?.firstOrNull())
|
||||
this.posterUrl = fixPath( episode.images?.screenshot?.firstOrNull())
|
||||
//this.rating = episode.rating?.times(10)?.roundToInt()
|
||||
this.score = Score.from10(episode.rating)
|
||||
|
||||
this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
|
||||
|
|
@ -304,164 +307,143 @@ open class TraktProvider : MainAPI() {
|
|||
return "https://$url"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Data(
|
||||
@JsonProperty("type") @SerialName("type") val type: TvType? = null,
|
||||
@JsonProperty("mediaDetails") @SerialName("mediaDetails") val mediaDetails: MediaDetails? = null,
|
||||
val type: TvType? = null,
|
||||
val mediaDetails: MediaDetails? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaSummary(
|
||||
@JsonProperty("title") @SerialName("title") val title: String? = null,
|
||||
@JsonProperty("year") @SerialName("year") val year: Int? = null,
|
||||
@JsonProperty("ids") @SerialName("ids") val ids: Ids? = null,
|
||||
@JsonProperty("rating") @SerialName("rating") val rating: Double? = null,
|
||||
@JsonProperty("images") @SerialName("images") val images: Images? = null,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class) // JsonNames is an experimental annotation for now
|
||||
@Serializable
|
||||
data class MediaDetails(
|
||||
@JsonProperty("title") @SerialName("title") val title: String? = null,
|
||||
@JsonProperty("year") @SerialName("year") val year: Int? = null,
|
||||
@JsonProperty("ids") @SerialName("ids") val ids: Ids? = null,
|
||||
@JsonProperty("tagline") @SerialName("tagline") val tagline: String? = null,
|
||||
@JsonProperty("overview") @SerialName("overview") val overview: String? = null,
|
||||
@JsonProperty("released") @SerialName("released") val released: String? = null,
|
||||
@JsonProperty("runtime") @SerialName("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("country") @SerialName("country") val country: String? = null,
|
||||
@JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: String? = null,
|
||||
@JsonProperty("trailer") @SerialName("trailer") val trailer: String? = null,
|
||||
@JsonProperty("homepage") @SerialName("homepage") val homepage: String? = null,
|
||||
@JsonProperty("status") @SerialName("status") val status: String? = null,
|
||||
@JsonProperty("rating") @SerialName("rating") val rating: Double? = null,
|
||||
@JsonProperty("votes") @SerialName("votes") val votes: Long? = null,
|
||||
@JsonProperty("comment_count") @SerialName("comment_count") val commentCount: Long? = null,
|
||||
@JsonProperty("language") @SerialName("language") val language: String? = null,
|
||||
@JsonProperty("languages") @SerialName("languages") val languages: List<String>? = null,
|
||||
@JsonProperty("available_translations") @SerialName("available_translations") val availableTranslations: List<String>? = null,
|
||||
@JsonProperty("genres") @SerialName("genres") val genres: List<String>? = null,
|
||||
@JsonProperty("certification") @SerialName("certification") val certification: String? = null,
|
||||
@JsonProperty("aired_episodes") @SerialName("aired_episodes") val airedEpisodes: Int? = null,
|
||||
@JsonProperty("first_aired") @SerialName("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("airs") @SerialName("airs") val airs: Airs? = null,
|
||||
@JsonProperty("network") @SerialName("network") val network: String? = null,
|
||||
@JsonProperty("images") @SerialName("images") val images: Images? = null,
|
||||
@JsonProperty("media") @JsonAlias("movie", "show") @SerialName("media") @JsonNames("movie", "show") val media: MediaSummary? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("year") val year: Int? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("tagline") val tagline: String? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("released") val released: String? = null,
|
||||
@JsonProperty("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("country") val country: String? = null,
|
||||
@JsonProperty("updatedAt") val updatedAt: String? = null,
|
||||
@JsonProperty("trailer") val trailer: String? = null,
|
||||
@JsonProperty("homepage") val homepage: String? = null,
|
||||
@JsonProperty("status") val status: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("votes") val votes: Long? = null,
|
||||
@JsonProperty("comment_count") val commentCount: Long? = null,
|
||||
@JsonProperty("language") val language: String? = null,
|
||||
@JsonProperty("languages") val languages: List<String>? = null,
|
||||
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
||||
@JsonProperty("genres") val genres: List<String>? = null,
|
||||
@JsonProperty("certification") val certification: String? = null,
|
||||
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("airs") val airs: Airs? = null,
|
||||
@JsonProperty("network") val network: String? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Airs(
|
||||
@JsonProperty("day") @SerialName("day") val day: String? = null,
|
||||
@JsonProperty("time") @SerialName("time") val time: String? = null,
|
||||
@JsonProperty("timezone") @SerialName("timezone") val timezone: String? = null,
|
||||
@JsonProperty("day") val day: String? = null,
|
||||
@JsonProperty("time") val time: String? = null,
|
||||
@JsonProperty("timezone") val timezone: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Ids(
|
||||
@JsonProperty("trakt") @SerialName("trakt") val trakt: Int? = null,
|
||||
@JsonProperty("slug") @SerialName("slug") val slug: String? = null,
|
||||
@JsonProperty("tvdb") @SerialName("tvdb") val tvdb: Int? = null,
|
||||
@JsonProperty("imdb") @SerialName("imdb") val imdb: String? = null,
|
||||
@JsonProperty("tmdb") @SerialName("tmdb") val tmdb: Int? = null,
|
||||
@JsonProperty("tvrage") @SerialName("tvrage") val tvrage: String? = null,
|
||||
@JsonProperty("trakt") val trakt: Int? = null,
|
||||
@JsonProperty("slug") val slug: String? = null,
|
||||
@JsonProperty("tvdb") val tvdb: Int? = null,
|
||||
@JsonProperty("imdb") val imdb: String? = null,
|
||||
@JsonProperty("tmdb") val tmdb: Int? = null,
|
||||
@JsonProperty("tvrage") val tvrage: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Images(
|
||||
@JsonProperty("poster") @SerialName("poster") val poster: List<String>? = null,
|
||||
@JsonProperty("fanart") @SerialName("fanart") val fanart: List<String>? = null,
|
||||
@JsonProperty("logo") @SerialName("logo") val logo: List<String>? = null,
|
||||
@JsonProperty("clearart") @SerialName("clearart") val clearArt: List<String>? = null,
|
||||
@JsonProperty("banner") @SerialName("banner") val banner: List<String>? = null,
|
||||
@JsonProperty("thumb") @SerialName("thumb") val thumb: List<String>? = null,
|
||||
@JsonProperty("screenshot") @SerialName("screenshot") val screenshot: List<String>? = null,
|
||||
@JsonProperty("headshot") @SerialName("headshot") val headshot: List<String>? = null,
|
||||
@JsonProperty("poster") val poster: List<String>? = null,
|
||||
@JsonProperty("fanart") val fanart: List<String>? = null,
|
||||
@JsonProperty("logo") val logo: List<String>? = null,
|
||||
@JsonProperty("clearart") val clearArt: List<String>? = null,
|
||||
@JsonProperty("banner") val banner: List<String>? = null,
|
||||
@JsonProperty("thumb") val thumb: List<String>? = null,
|
||||
@JsonProperty("screenshot") val screenshot: List<String>? = null,
|
||||
@JsonProperty("headshot") val headshot: List<String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class People(
|
||||
@JsonProperty("cast") @SerialName("cast") val cast: List<Cast>? = null,
|
||||
@JsonProperty("cast") val cast: List<Cast>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Cast(
|
||||
@JsonProperty("character") @SerialName("character") val character: String? = null,
|
||||
@JsonProperty("characters") @SerialName("characters") val characters: List<String>? = null,
|
||||
@JsonProperty("episode_count") @SerialName("episode_count") val episodeCount: Long? = null,
|
||||
@JsonProperty("person") @SerialName("person") val person: Person? = null,
|
||||
@JsonProperty("images") @SerialName("images") val images: Images? = null,
|
||||
@JsonProperty("character") val character: String? = null,
|
||||
@JsonProperty("characters") val characters: List<String>? = null,
|
||||
@JsonProperty("episode_count") val episodeCount: Long? = null,
|
||||
@JsonProperty("person") val person: Person? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Person(
|
||||
@JsonProperty("name") @SerialName("name") val name: String? = null,
|
||||
@JsonProperty("ids") @SerialName("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") @SerialName("images") val images: Images? = null,
|
||||
@JsonProperty("name") val name: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Seasons(
|
||||
@JsonProperty("aired_episodes") @SerialName("aired_episodes") val airedEpisodes: Int? = null,
|
||||
@JsonProperty("episode_count") @SerialName("episode_count") val episodeCount: Int? = null,
|
||||
@JsonProperty("episodes") @SerialName("episodes") val episodes: List<TraktEpisode>? = null,
|
||||
@JsonProperty("first_aired") @SerialName("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("ids") @SerialName("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") @SerialName("images") val images: Images? = null,
|
||||
@JsonProperty("network") @SerialName("network") val network: String? = null,
|
||||
@JsonProperty("number") @SerialName("number") val number: Int? = null,
|
||||
@JsonProperty("overview") @SerialName("overview") val overview: String? = null,
|
||||
@JsonProperty("rating") @SerialName("rating") val rating: Double? = null,
|
||||
@JsonProperty("title") @SerialName("title") val title: String? = null,
|
||||
@JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String? = null,
|
||||
@JsonProperty("votes") @SerialName("votes") val votes: Int? = null,
|
||||
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
||||
@JsonProperty("episode_count") val episodeCount: Int? = null,
|
||||
@JsonProperty("episodes") val episodes: List<TraktEpisode>? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("network") val network: String? = null,
|
||||
@JsonProperty("number") val number: Int? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("updated_at") val updatedAt: String? = null,
|
||||
@JsonProperty("votes") val votes: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TraktEpisode(
|
||||
@JsonProperty("available_translations") @SerialName("available_translations") val availableTranslations: List<String>? = null,
|
||||
@JsonProperty("comment_count") @SerialName("comment_count") val commentCount: Int? = null,
|
||||
@JsonProperty("episode_type") @SerialName("episode_type") val episodeType: String? = null,
|
||||
@JsonProperty("first_aired") @SerialName("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("ids") @SerialName("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") @SerialName("images") val images: Images? = null,
|
||||
@JsonProperty("number") @SerialName("number") val number: Int? = null,
|
||||
@JsonProperty("number_abs") @SerialName("number_abs") val numberAbs: Int? = null,
|
||||
@JsonProperty("overview") @SerialName("overview") val overview: String? = null,
|
||||
@JsonProperty("rating") @SerialName("rating") val rating: Double? = null,
|
||||
@JsonProperty("runtime") @SerialName("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("season") @SerialName("season") val season: Int? = null,
|
||||
@JsonProperty("title") @SerialName("title") val title: String? = null,
|
||||
@JsonProperty("updated_at") @SerialName("updated_at") val updatedAt: String? = null,
|
||||
@JsonProperty("votes") @SerialName("votes") val votes: Int? = null,
|
||||
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
||||
@JsonProperty("comment_count") val commentCount: Int? = null,
|
||||
@JsonProperty("episode_type") val episodeType: String? = null,
|
||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
||||
@JsonProperty("ids") val ids: Ids? = null,
|
||||
@JsonProperty("images") val images: Images? = null,
|
||||
@JsonProperty("number") val number: Int? = null,
|
||||
@JsonProperty("number_abs") val numberAbs: Int? = null,
|
||||
@JsonProperty("overview") val overview: String? = null,
|
||||
@JsonProperty("rating") val rating: Double? = null,
|
||||
@JsonProperty("runtime") val runtime: Int? = null,
|
||||
@JsonProperty("season") val season: Int? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("updated_at") val updatedAt: String? = null,
|
||||
@JsonProperty("votes") val votes: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LinkData(
|
||||
@JsonProperty("id") @SerialName("id") val id: Int? = null,
|
||||
@JsonProperty("trakt_id") @SerialName("trakt_id") val traktId: Int? = null,
|
||||
@JsonProperty("trakt_slug") @SerialName("trakt_slug") val traktSlug: String? = null,
|
||||
@JsonProperty("tmdb_id") @SerialName("tmdb_id") val tmdbId: Int? = null,
|
||||
@JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String? = null,
|
||||
@JsonProperty("tvdb_id") @SerialName("tvdb_id") val tvdbId: Int? = null,
|
||||
@JsonProperty("tvrage_id") @SerialName("tvrage_id") val tvrageId: String? = null,
|
||||
@JsonProperty("type") @SerialName("type") val type: String? = null,
|
||||
@JsonProperty("season") @SerialName("season") val season: Int? = null,
|
||||
@JsonProperty("episode") @SerialName("episode") val episode: Int? = null,
|
||||
@JsonProperty("ani_id") @SerialName("ani_id") val aniId: String? = null,
|
||||
@JsonProperty("anime_id") @SerialName("anime_id") val animeId: String? = null,
|
||||
@JsonProperty("title") @SerialName("title") val title: String? = null,
|
||||
@JsonProperty("year") @SerialName("year") val year: Int? = null,
|
||||
@JsonProperty("org_title") @SerialName("org_title") val orgTitle: String? = null,
|
||||
@JsonProperty("is_anime") @SerialName("is_anime") val isAnime: Boolean = false,
|
||||
@JsonProperty("aired_year") @SerialName("aired_year") val airedYear: Int? = null,
|
||||
@JsonProperty("last_season") @SerialName("last_season") val lastSeason: Int? = null,
|
||||
@JsonProperty("eps_title") @SerialName("eps_title") val epsTitle: String? = null,
|
||||
@JsonProperty("jp_title") @SerialName("jp_title") val jpTitle: String? = null,
|
||||
@JsonProperty("date") @SerialName("date") val date: String? = null,
|
||||
@JsonProperty("aired_date") @SerialName("aired_date") val airedDate: String? = null,
|
||||
@JsonProperty("is_asian") @SerialName("is_asian") val isAsian: Boolean = false,
|
||||
@JsonProperty("is_bollywood") @SerialName("is_bollywood") val isBollywood: Boolean = false,
|
||||
@JsonProperty("is_cartoon") @SerialName("is_cartoon") val isCartoon: Boolean = false,
|
||||
@JsonProperty("id") val id: Int? = null,
|
||||
@JsonProperty("trakt_id") val traktId: Int? = null,
|
||||
@JsonProperty("trakt_slug") val traktSlug: String? = null,
|
||||
@JsonProperty("tmdb_id") val tmdbId: Int? = null,
|
||||
@JsonProperty("imdb_id") val imdbId: String? = null,
|
||||
@JsonProperty("tvdb_id") val tvdbId: Int? = null,
|
||||
@JsonProperty("tvrage_id") val tvrageId: String? = null,
|
||||
@JsonProperty("type") val type: String? = null,
|
||||
@JsonProperty("season") val season: Int? = null,
|
||||
@JsonProperty("episode") val episode: Int? = null,
|
||||
@JsonProperty("ani_id") val aniId: String? = null,
|
||||
@JsonProperty("anime_id") val animeId: String? = null,
|
||||
@JsonProperty("title") val title: String? = null,
|
||||
@JsonProperty("year") val year: Int? = null,
|
||||
@JsonProperty("org_title") val orgTitle: String? = null,
|
||||
@JsonProperty("is_anime") val isAnime: Boolean = false,
|
||||
@JsonProperty("aired_year") val airedYear: Int? = null,
|
||||
@JsonProperty("last_season") val lastSeason: Int? = null,
|
||||
@JsonProperty("eps_title") val epsTitle: String? = null,
|
||||
@JsonProperty("jp_title") val jpTitle: String? = null,
|
||||
@JsonProperty("date") val date: String? = null,
|
||||
@JsonProperty("aired_date") val airedDate: String? = null,
|
||||
@JsonProperty("is_asian") val isAsian: Boolean = false,
|
||||
@JsonProperty("is_bollywood") val isBollywood: Boolean = false,
|
||||
@JsonProperty("is_cartoon") val isCartoon: Boolean = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
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