Compare commits

..

1 commit

Author SHA1 Message Date
firelight
cdd2856b42
Update SerializationClassTester.kt 2026-06-06 17:13:03 +00:00
37 changed files with 280 additions and 503 deletions

View file

@ -207,6 +207,7 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.json)
androidTestImplementation(libs.core)
androidTestImplementation(libs.classgraph)
androidTestImplementation(libs.espresso.core)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.instancio.core)

View file

@ -101,7 +101,7 @@ class SerializationClassTester {
}
// DEX files are the best solution to read all our classes dynamically.
// classgraph could be used instead, but it only gives results on the JVM, not Android.
// ClassGraph() can be used instead, but it only gives results on the JVM, not Android.
@Suppress("DEPRECATION")
private fun findSerializableClasses(packageName: String): List<KClass<*>> {
val context = InstrumentationRegistry
@ -109,6 +109,7 @@ class SerializationClassTester {
.targetContext
val dexFile = DexFile(context.packageCodePath)
return dexFile.entries()
.toList()
.filter { it.startsWith(packageName) }

View file

@ -20,10 +20,8 @@ import com.lagradost.cloudstream3.actions.temp.MpvExPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage
import com.lagradost.cloudstream3.actions.temp.MpvPackage
import com.lagradost.cloudstream3.actions.temp.MpvRxPackage
import com.lagradost.cloudstream3.actions.temp.MpvYTDLPackage
import com.lagradost.cloudstream3.actions.temp.NextPlayerPackage
import com.lagradost.cloudstream3.actions.temp.OnlyPlayer
import com.lagradost.cloudstream3.actions.temp.PlayInBrowserAction
import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction
import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action
@ -66,8 +64,6 @@ object VideoClickActionHolder {
MpvYTDLPackage(),
MpvKtPackage(),
MpvKtPreviewPackage(),
OnlyPlayer(),
MpvRxPackage(),
// Always Ask option
AlwaysAskAction(),
// added by plugins

View file

@ -1,75 +0,0 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
import com.lagradost.api.Log
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.updateDurationAndPosition
import com.lagradost.cloudstream3.isEpisodeBased
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.txt
/** https://github.com/Riteshp2001/mpvRx
*
* https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L132
* https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L56
* */
class MpvRxPackage : OpenInAppAction(
appName = txt("mpvRx"),
packageName = "app.gyrolet.mpvrx",
intentClass = "app.gyrolet.mpvrx.ui.player.PlayerActivity"
) {
override val oneSource = true
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
intent.apply {
putExtra("title", video.name)
val link = result.links[index!!]
val headers = link.headers
setData(link.url.toUri())
if (headers.isNotEmpty()) {
// PlayerActivity expects a flat array: [key1, value1, key2, value2, ...]
val flat = headers.entries.flatMap { listOf(it.key, it.value) }.toTypedArray()
intent.putExtra("headers", flat)
}
/*val subs = result.subs // disabled due to https://github.com/Riteshp2001/mpvRx/issues/146
intent.putExtra("subs", subs.map { it.url.toUri() }.toTypedArray())
intent.putExtra(
"subs.titles",
subs.map { it.name }.toTypedArray(),
)
intent.putExtra(
"subs.langs",
subs.map { it.languageCode }.toTypedArray(),
)
val selected = subs.firstOrNull { it.matchesLanguageCode("en") }?.url?.toUri()
intent.putExtra("subs.enable", selected?.let { arrayOf(it) } ?: arrayOf<Uri>() )*/
if (video.tvType.isEpisodeBased()) {
video.season?.let { intent.putExtra("introdb_season", it) }
video.episode.let { intent.putExtra("introdb_episode", it) }
}
val position = getViewPos(video.id)?.position
if (position != null)
putExtra("position", position.toInt())
}
}
override fun onResult(activity: Activity, intent: Intent?) {
val position = intent?.getIntExtra("position", -1) ?: -1
val duration = intent?.getIntExtra("duration", -1) ?: -1
Log.d("MPV", "Position: $position, Duration: $duration")
updateDurationAndPosition(position.toLong(), duration.toLong())
}
}

View file

@ -1,44 +0,0 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.net.toUri
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.txt
/** https://github.com/Kindness-Kismet/only_player/tree/main
* https://github.com/Kindness-Kismet/only_player/blob/main/feature/player/src/main/java/one/only/player/feature/player/PlayerActivity.kt */
class OnlyPlayer : OpenInAppAction(
txt("Only Player"),
"one.only.player",
intentClass = "one.only.player.feature.player.PlayerActivity"
) {
override val oneSource = true
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
/** https://github.com/Kindness-Kismet/only_player/blob/d3f55049a2913fa762d31b311146073cc2da46cb/app/src/main/java/one/only/player/navigation/CloudNavGraph.kt#L39 */
intent.apply {
val link = result.links[index!!]
setData(link.url.toUri())
putExtra("headers", Bundle().apply {
for ((key, value) in link.headers) {
putExtra(key, value)
}
})
}
}
override fun onResult(activity: Activity, intent: Intent?) {
/* onResult does not get called */
}
}

View file

@ -50,8 +50,7 @@ class AniListApi : SyncAPI() {
override suspend fun login(redirectUrl: String, payload: String?): AuthToken? {
val sanitizer = splitRedirectUrl(redirectUrl)
val token = AuthToken(
accessToken = sanitizer["access_token"]
?: throw ErrorLoadingException("No access token"),
accessToken = sanitizer["access_token"] ?: throw ErrorLoadingException("No access token"),
//refreshToken = sanitizer["refresh_token"],
accessTokenLifetime = unixTime + sanitizer["expires_in"]!!.toLong(),
)
@ -84,8 +83,8 @@ class AniListApi : SyncAPI() {
return "$mainUrl/anime/$id"
}
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
val data = searchShows(query) ?: return null
override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
val data = searchShows(name) ?: return null
return data.data?.page?.media?.map {
SyncAPI.SyncSearchResult(
it.title.romaji ?: return null,
@ -97,7 +96,7 @@ class AniListApi : SyncAPI() {
}
}
override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? {
override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? {
val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
val season = getSeason(internalId).data.media
@ -159,7 +158,7 @@ class AniListApi : SyncAPI() {
)
}
override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
val internalId = id.toIntOrNull() ?: return null
val data = getDataAboutId(auth ?: return null, internalId) ?: return null
@ -460,7 +459,7 @@ class AniListApi : SyncAPI() {
}
}
private suspend fun getDataAboutId(auth: AuthData, id: Int): AniListTitleHolder? {
private suspend fun getDataAboutId(auth : AuthData, id: Int): AniListTitleHolder? {
val q =
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id)
Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
@ -507,7 +506,7 @@ class AniListApi : SyncAPI() {
}
private suspend fun postApi(token: AuthToken, q: String, cache: Boolean = false): String? {
private suspend fun postApi(token : AuthToken, q: String, cache: Boolean = false): String? {
return app.post(
"https://graphql.anilist.co/",
headers = mapOf(
@ -639,7 +638,7 @@ class AniListApi : SyncAPI() {
}
}
override suspend fun library(auth: AuthData?): SyncAPI.LibraryMetadata? {
override suspend fun library(auth : AuthData?): SyncAPI.LibraryMetadata? {
val list = getAniListAnimeListSmart(auth ?: return null)?.groupBy {
convertAniListStringToStatus(it.status ?: "").stringRes
}?.mapValues { group ->
@ -667,7 +666,7 @@ class AniListApi : SyncAPI() {
)
}
private suspend fun getFullAniListList(auth: AuthData): FullAnilistList? {
private suspend fun getFullAniListList(auth : AuthData): FullAnilistList? {
val userID = auth.user.id
val mediaType = "ANIME"
@ -715,7 +714,7 @@ class AniListApi : SyncAPI() {
return text?.toKotlinObject()
}
suspend fun toggleLike(auth: AuthData, id: Int): Boolean {
suspend fun toggleLike(auth : AuthData, id: Int): Boolean {
val q = """mutation (${'$'}animeId: Int = $id) {
ToggleFavourite (animeId: ${'$'}animeId) {
anime {
@ -738,7 +737,7 @@ class AniListApi : SyncAPI() {
data class MediaListId(@JsonProperty("id") val id: Long? = null)
private suspend fun postDataAboutId(
auth: AuthData,
auth : AuthData,
id: Int,
type: AniListStatusType,
score: Score?,
@ -787,7 +786,7 @@ class AniListApi : SyncAPI() {
return data != ""
}
private suspend fun getUser(token: AuthToken): AniListUser? {
private suspend fun getUser(token : AuthToken): AniListUser? {
val q = """
{
Viewer {

View file

@ -98,9 +98,9 @@ class MALApi : SyncAPI() {
)
}
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
val auth = auth?.token?.accessToken ?: return null
val url = "$apiUrl/v2/anime?q=$query&limit=$MAL_MAX_SEARCH_LIMIT"
val url = "$apiUrl/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
val res = app.get(
url, headers = mapOf(
"Authorization" to "Bearer $auth",
@ -122,7 +122,7 @@ class MALApi : SyncAPI() {
Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first()
override suspend fun updateStatus(
auth: AuthData?,
auth : AuthData?,
id: String,
newStatus: SyncAPI.AbstractSyncStatus
): Boolean {
@ -225,7 +225,7 @@ class MALApi : SyncAPI() {
)
}
override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? {
override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? {
val auth = auth?.token?.accessToken ?: return null
val internalId = id.toIntOrNull() ?: return null
val url =
@ -271,7 +271,7 @@ class MALApi : SyncAPI() {
}
}
override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
val auth = auth?.token?.accessToken ?: return null
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
@ -477,7 +477,7 @@ class MALApi : SyncAPI() {
@JsonProperty("start_time") val startTime: String?
)
override suspend fun library(auth: AuthData?): LibraryMetadata? {
override suspend fun library(auth : AuthData?): LibraryMetadata? {
val list = getMalAnimeListSmart(auth ?: return null)?.groupBy {
convertToStatus(it.listStatus?.status ?: "").stringRes
}?.mapValues { group ->
@ -505,7 +505,7 @@ class MALApi : SyncAPI() {
)
}
private suspend fun getMalAnimeListSmart(auth: AuthData): Array<Data>? {
private suspend fun getMalAnimeListSmart(auth : AuthData): Array<Data>? {
return if (requireLibraryRefresh) {
val list = getMalAnimeList(auth.token)
setKey(MAL_CACHED_LIST, auth.user.id.toString(), list)

View file

@ -911,7 +911,7 @@ class SimklApi : SyncAPI() {
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
return app.get(
"$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to query)
"$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name)
).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() }
}

View file

@ -11,7 +11,6 @@
android:id="@+id/player_metadata_scrim"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_marginTop="-10dp"
android:background="@drawable/bg_player_metadata_scrim_netflix"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

View file

@ -12,7 +12,6 @@
android:id="@+id/player_metadata_scrim"
android:layout_width="680dp"
android:layout_height="match_parent"
android:layout_marginTop="-10dp"
android:background="@drawable/bg_player_metadata_scrim_netflix"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -11,7 +11,6 @@
android:id="@+id/player_metadata_scrim"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_marginTop="-10dp"
android:background="@drawable/bg_player_metadata_scrim_netflix"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"

View file

@ -252,7 +252,7 @@
<string name="update">Update</string>
<string name="watch_quality_pref">Bevorzugte Videoqualität (WLAN)</string>
<string name="limit_title">Videoplayertitel max. Zeichen</string>
<string name="limit_title_rez">Zeige Playerinformationen</string>
<string name="limit_title_rez">Playerinformationen anzeigen</string>
<string name="video_buffer_size_settings">Videopuffergröße</string>
<string name="video_buffer_length_settings">Videopufferlänge</string>
<string name="video_buffer_disk_settings">Video-Cache in Speicher</string>
@ -587,7 +587,7 @@
<string name="pref_category_security">Sicherheit</string>
<string name="pref_category_accounts">Konten</string>
<string name="open_downloaded_repo">Repository öffnen</string>
<string name="device_pin_url_message">Besuche <b>%s</b> auf dem Smartphone oder Computer und gebe den obenstehenden Code ein</string>
<string name="device_pin_url_message">Besuche<b>%s</b> auf dem Smartphone oder Computer und gebe den obenstehenden Code ein</string>
<string name="device_pin_error_message">PIN-Code vom Gerät nicht abrufbar, versuche lokale Authentifizierung</string>
<string name="downloads_empty">Zur Zeit sind keine Downloads verfügbar.</string>
<string name="open_local_video">Lokales Video öffnen</string>
@ -712,8 +712,8 @@
<string name="extra_brightness_settings">Zusätzliche Helligkeit</string>
<string name="extra_brightness_settings_des">Aktiviere Helligkeitsfilter, wenn 100% Bildschirmhelligkeit überschritten ist</string>
<string name="extra_brightness_key">Erhöhte Helligkeit aktiviert</string>
<string name="show_cast_in_details">Zeige Cast-Panel</string>
<string name="video_info">Mediainfo</string>
<string name="show_cast_in_details">Cast-Panel zeigen</string>
<string name="video_info">Medieninfo</string>
<string name="source_name">Quellname</string>
<string name="download_all">Alle herunterladen</string>
<string name="download_episode_range">Möchtest du Episode %s herunter laden?</string>
@ -731,8 +731,4 @@
<string name="queue_empty_message">Es befinden sich keine Downloads in der Warteschlange.</string>
<string name="source_priority">Quellpriorität</string>
<string name="source_priority_help">Entscheide, wie Videoquellen im Player sortiert werden sollen</string>
<string name="show_player_metadata_overlay">Zeige Player-Metadaten</string>
<string name="video_singular">Video</string>
<string name="skip_type_preview">Vorschau</string>
<string name="player_is_live">Live</string>
</resources>

View file

@ -244,7 +244,7 @@
<string name="quality_tc">TC</string>
<string name="subscription_new">Претплатен на %s</string>
<string name="pref_category_subtitles">Преводи</string>
<string name="download_all_plugins_from_repo">Предупредување: CloudStream не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив!</string>
<string name="download_all_plugins_from_repo">Предупредување: CloudStream 3 не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив!</string>
<string name="backup_failed">Недостасуваат дозволи за складирање. Обиди се повторно.</string>
<string name="sort_save">Зачувај</string>
<string name="player_load_subtitles">Вчитај од датотека</string>
@ -445,7 +445,7 @@
<string name="backup_failed_error_format">Грешка при правење резервна копија на %s</string>
<string name="pref_filter_search_quality">Сокриј го избраниот квалитет на видеото во резултатите од пребарувањето</string>
<string name="apk_installer_settings_des">Некои уреди не го поддржуваат новиот инсталатор на пакети. Испробај ја легаси(старата) опција, ако ажурирањата не се инсталираат.</string>
<string name="limit_title_rez">Прикажи информации за плеерот</string>
<string name="limit_title_rez">Резолуција на видео плеер</string>
<string name="video_buffer_size_settings">Големина на видео баферот</string>
<string name="pref_category_player_layout">Распоред</string>
<string name="pref_category_defaults">Стандардно</string>
@ -705,37 +705,4 @@
<string name="top_center">Горе во центар</string>
<string name="top_right">Горе на десно</string>
<string name="play_full_series_button">Пушти ја целата серија</string>
<string name="download_queue">Редица за преземање</string>
<string name="queue_empty_message">Моментално нема преземања во редицата.</string>
<string name="extra_brightness_settings">Дополнителна осветленост</string>
<string name="extra_brightness_settings_des">Овозможи филтер за осветленост кога ќе се надмине 100% осветленост на екранот</string>
<string name="extra_brightness_key">овозможенаополнителна_осветленост</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="video_singular">Видео</string>
<string name="show_episode_text">Текст на епизода</string>
<string name="video_info">Информации за медиумот</string>
<string name="skip_type_preview">Преглед</string>
<string name="source_priority">Приоритет на извор</string>
<string name="source_priority_help">Одреди како ќе се подредуваат видео изворите во плеерот</string>
<string name="source_name">Име на изворот</string>
<string name="download_all">Преземи сѐ</string>
<string name="cancel_all">Откажи сѐ</string>
<string name="download_episode_range">Дали сакате да ја преземете епизодата %s?</string>
<string name="cancel_queue_message">Дали сакате да ги откажете сите преземања во редицата?</string>
<plurals name="downloads_active">
<item quantity="one">%d активно преземање</item>
<item quantity="other">%d активни преземања</item>
</plurals>
<plurals name="downloads_queued">
<item quantity="one">%d преземање во редицата</item>
<item quantity="other">%d преземања во редицата</item>
</plurals>
<string name="player_is_live">Во живо</string>
</resources>

View file

@ -8,6 +8,7 @@ annotation = "1.10.0"
appcompat = "1.7.1"
biometric = "1.4.0-alpha07"
buildkonfigGradlePlugin = "0.21.2"
classgraph = "4.8.184"
coil = { strictly = "3.3.0" } # Later versions require jvmTarget 11 or later
colorpicker = "6b46b49"
conscryptAndroid = { strictly = "2.5.2" } # 2.5.3 crashes everything
@ -31,12 +32,11 @@ kotlinxCollectionsImmutable = "0.4.0"
kotlinxCoroutinesCore = "1.11.0"
kotlinxDatetime = "0.8.0"
kotlinxSerializationJson = "1.11.0"
ktor = "3.5.0"
lifecycleKtx = "2.10.0"
material = "1.14.0"
media3 = "1.9.3"
navigationKtx = "2.9.8"
newpipeextractor = "v0.26.3"
newpipeextractor = "v0.26.2"
nextlibMedia3 = "1.9.3-0.12.0"
nicehttp = "0.4.18"
overlappingpanels = "0.1.5"
@ -69,6 +69,7 @@ anime-db = { module = "com.github.recloudstream:anime-db", version.ref = "animeD
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
classgraph = { group = "io.github.classgraph", name = "classgraph", version.ref = "classgraph" }
coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
colorpicker = { module = "com.github.recloudstream:color-picker-android", version.ref = "colorpicker" }
@ -94,7 +95,6 @@ kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collec
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
ktor-http = { module = "io.ktor:ktor-http", version.ref = "ktor" }
lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleKtx" }
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleKtx" }
material = { module = "com.google.android.material:material", version.ref = "material" }

View file

@ -61,7 +61,6 @@ kotlin {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json) // JSON Parser
implementation(libs.ktor.http)
implementation(libs.jsoup) // HTML Parser
implementation(libs.rhino) // Run JavaScript
implementation(libs.tmdb.java) // TMDB API v3 Wrapper Made with RetroFit

View file

@ -15,13 +15,12 @@ import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.nicehttp.requestCreator
import io.ktor.http.Url
import io.ktor.http.decodeURLPart
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import java.net.URI
/**
* When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...)
@ -212,7 +211,7 @@ actual class WebViewResolver actual constructor(
* */
return@runBlocking try {
when {
blacklistedFiles.any { Url(webViewUrl).encodedPath.decodeURLPart().contains(it) } || webViewUrl.endsWith(
blacklistedFiles.any { URI(webViewUrl).path.contains(it) } || webViewUrl.endsWith(
"/favicon.ico"
) -> WebResourceResponse(
"image/png",

View file

@ -22,10 +22,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.mainWork
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF
import com.lagradost.nicehttp.RequestBodyTypes
import io.ktor.http.Url
import io.ktor.http.URLBuilder
import io.ktor.http.encodedPath
import io.ktor.http.takeFrom
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
@ -39,8 +35,11 @@ import kotlinx.datetime.format.byUnicodePattern
import kotlinx.datetime.format.char
import kotlinx.datetime.format.parse
import kotlinx.datetime.toInstant
import java.net.URI
import java.util.EnumSet
import kotlinx.serialization.json.Json
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
import kotlin.time.Clock
@ -91,7 +90,6 @@ class ErrorLoadingException(message: String? = null) : Exception(message)
@Prerelease
val json = Json {
encodeDefaults = true
explicitNulls = false
ignoreUnknownKeys = true
}
@ -178,9 +176,9 @@ object APIHolder {
// To get the key
suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? {
try {
val _url = Url(url)
val uri = URI.create(url)
val domain = base64Encode(
(_url.protocol.name + "://" + _url.host + ":443").encodeToByteArray(),
(uri.scheme + "://" + uri.host + ":443").encodeToByteArray(),
).replace("\n", "").replace("=", ".")
val vToken =
@ -715,10 +713,12 @@ fun base64Decode(string: String): String {
}
}
@OptIn(ExperimentalEncodingApi::class)
fun base64DecodeArray(string: String): ByteArray {
return Base64.decode(string)
}
@OptIn(ExperimentalEncodingApi::class)
fun base64Encode(array: ByteArray): String {
return Base64.encode(array)
}
@ -1330,23 +1330,23 @@ fun getQualityFromString(string: String?): SearchQuality? {
* ```
*/
fun MainAPI.updateUrl(url: String): String {
return try {
val original = Url(url)
val updated = Url(mainUrl)
try {
val original = URI(url)
val updated = URI(mainUrl)
URLBuilder().apply {
takeFrom(updated)
user = original.user
password = original.password
encodedPath = original.encodedPath
fragment = original.fragment
parameters.clear()
parameters.appendAll(original.parameters)
}.buildString()
// URI(String scheme, String userInfo, String host, int port, String path, String query, String fragment)
return URI(
updated.scheme,
original.userInfo,
updated.host,
updated.port,
original.path,
original.query,
original.fragment
).toString()
} catch (t: Throwable) {
logError(t)
url
return url
}
}
@ -1510,7 +1510,7 @@ constructor(
override var posterUrl: String? = null,
var year: Int? = null,
var dubStatus: MutableSet<DubStatus>? = null,
var dubStatus: EnumSet<DubStatus>? = null,
var otherName: String? = null,
var episodes: MutableMap<DubStatus, Int> = mutableMapOf(),
@ -1522,7 +1522,7 @@ constructor(
) : SearchResponse
fun AnimeSearchResponse.addDubStatus(status: DubStatus, episodes: Int? = null) {
this.dubStatus = dubStatus?.also { it.add(status) } ?: mutableSetOf(status)
this.dubStatus = dubStatus?.also { it.add(status) } ?: EnumSet.of(status)
if (this.type?.isMovieType() != true)
if (episodes != null && episodes > 0)
this.episodes[status] = episodes

View file

@ -8,8 +8,7 @@ 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
import io.ktor.http.Url
import io.ktor.http.decodeURLPart
import java.net.URI
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
@ -46,11 +45,11 @@ open class ByseSX : ExtractorApi() {
}
private fun getBaseUrl(url: String): String {
return Url(url).let { "${it.protocol.name}://${it.host}" }
return URI(url).let { "${it.scheme}://${it.host}" }
}
private fun getCodeFromUrl(url: String): String {
val path = Url(url).encodedPath.decodeURLPart()
val path = URI(url).path ?: ""
return path.trimEnd('/').substringAfterLast('/')
}

View file

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
import com.lagradost.cloudstream3.utils.StringUtils.decodeUri
import com.lagradost.cloudstream3.utils.newExtractorLink
open class Cda : ExtractorApi() {
@ -64,7 +64,7 @@ open class Cda : ExtractorApi() {
.replace("_QWE", "")
.replace("_Q5", "")
.replace("_IKSDE", "")
a = a.decodeUrl()
a = a.decodeUri()
a = a.map { char ->
if (char.code in 33..126) {
return@map (33 + (char.code + 14) % 94).toChar().toString()

View file

@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor
import io.ktor.http.Url
import okhttp3.HttpUrl.Companion.toHttpUrl
// deobfuscated from https://hglink.to/main.js?v=1.1.3 using https://deobfuscate.io/
private val mirrors = arrayOf(
@ -90,7 +90,7 @@ abstract class CineMMRedirect : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val videoId = Url(url).encodedPath
val videoId = url.toHttpUrl().encodedPath
val mirror = mirrors.random()
// re-use existing extractors by calling the ExtractorApi
@ -98,4 +98,4 @@ abstract class CineMMRedirect : ExtractorApi() {
val mirrorUrlWithVideoId = "https://$mirror$videoId"
loadExtractor(mirrorUrlWithVideoId, referer, subtitleCallback, callback)
}
}
}

View file

@ -7,8 +7,9 @@ import com.lagradost.cloudstream3.newSubtitleFile
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import io.ktor.http.Url
import io.ktor.http.decodeURLPart
import java.net.URI
class Geodailymotion : Dailymotion() {
override val name = "GeoDailymotion"
@ -56,6 +57,7 @@ open class Dailymotion : ExtractorApi() {
}
}
private fun getEmbedUrl(url: String): String? {
if (url.contains("/embed/") || url.contains("/video/")) return url
if (url.contains("geo.dailymotion.com")) {
@ -65,8 +67,9 @@ open class Dailymotion : ExtractorApi() {
return null
}
private fun getVideoId(url: String): String? {
val path = Url(url).encodedPath.decodeURLPart()
val path = URI(url).path
val id = path.substringAfter("/video/")
return if (id.matches(videoIdRegex)) id else null
}
@ -79,6 +82,7 @@ open class Dailymotion : ExtractorApi() {
return generateM3u8(name, streamLink, "").forEach(callback)
}
data class MetaData(
val qualities: Map<String, List<Quality>>?,
val subtitles: SubtitlesWrapper?
@ -98,4 +102,5 @@ open class Dailymotion : ExtractorApi() {
val label: String,
val urls: List<String>
)
}

View file

@ -7,7 +7,7 @@ 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 io.ktor.http.Url
import java.net.URI
class Doodspro : DoodLaExtractor() {
override var mainUrl = "https://doods.pro"
@ -138,6 +138,8 @@ open class DoodLaExtractor : ExtractorApi() {
}
private fun getBaseUrl(url: String): String {
return Url(url).let { "${it.protocol.name}://${it.host}" }
return URI(url).let {
"${it.scheme}://${it.host}"
}
}
}

View file

@ -1,48 +0,0 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.Prerelease
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.newExtractorLink
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Prerelease
open class Flyfile : ExtractorApi() {
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
) {
val videoId = url.substringAfterLast("/")
val videoInfo = app.get("$apiUrl/api/streaming/assign/$videoId")
.parsed<StreamInfo>()
val streamUrl = "${videoInfo.url}/hls/${videoInfo.token}/master.m3u8"
callback.invoke(
newExtractorLink(
source = name,
name = name,
url = streamUrl,
type = ExtractorLinkType.M3U8
)
)
}
@Serializable
private data class StreamInfo(
@SerialName("url")
val url: String,
@SerialName("token")
val token: String
)
}

View file

@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor
import io.ktor.http.Url
import java.net.URI
class Techinmind: GDMirrorbot() {
override var name = "Techinmind Cloud AIO"
@ -103,7 +103,7 @@ open class GDMirrorbot : ExtractorApi() {
}
private fun getBaseUrl(url: String): String {
return Url(url).let { "${it.protocol.name}://${it.host}" }
return URI(url).let { "${it.scheme}://${it.host}" }
}
}

View file

@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.loadExtractor
import com.lagradost.cloudstream3.utils.newExtractorLink
import io.ktor.http.Url
import java.net.URI
class HubCloud : ExtractorApi() {
override val name = "Hub-Cloud"
@ -24,7 +24,7 @@ class HubCloud : ExtractorApi() {
) {
val tag = "HubCloud"
val realUrl = url.takeIf {
try { Url(it); true } catch (e: Exception) { Log.e(tag, "Invalid URL: ${e.message}"); false }
try { URI(it).toURL(); true } catch (e: Exception) { Log.e(tag, "Invalid URL: ${e.message}"); false }
} ?: return
val baseUrl=getBaseUrl(realUrl)
@ -161,7 +161,7 @@ class HubCloud : ExtractorApi() {
private fun getBaseUrl(url: String): String {
return try {
Url(url).let { "${it.protocol.name}://${it.host}" }
URI(url).let { "${it.scheme}://${it.host}" }
} catch (_: Exception) {
""
}

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.newSubtitleFile
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
import com.lagradost.cloudstream3.utils.StringUtils.decodeUri
import com.lagradost.cloudstream3.utils.newExtractorLink
import org.jsoup.nodes.Document
@ -96,7 +96,7 @@ open class InternetArchive : ExtractorApi() {
if (mediaUrl.isNotEmpty()) {
val name = if (mediaUrl.count() > 1) {
val fileExtension = mediaUrl.substringAfterLast(".")
val fileNameCleaned = fileName.decodeUrl().substringBeforeLast('.')
val fileNameCleaned = fileName.decodeUri().substringBeforeLast('.')
"$fileNameCleaned ($fileExtension)"
} else this.name
callback(

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import io.ktor.http.Url
import java.net.URI
open class Streamplay : ExtractorApi() {
override val name = "Streamplay"
@ -22,7 +22,9 @@ open class Streamplay : ExtractorApi() {
) {
val request = app.get(url, referer = referer)
val redirectUrl = request.url
val mainServer = Url(redirectUrl).let { "${it.protocol.name}://${it.host}" }
val mainServer = URI(redirectUrl).let {
"${it.scheme}://${it.host}"
}
val key = redirectUrl.substringAfter("embed-").substringBefore(".html")
val token =
request.document.select("script").find { it.data().contains("sitekey:") }?.data()
@ -77,4 +79,4 @@ open class Streamplay : ExtractorApi() {
@JsonProperty("label") val label: String? = null,
)
}
}

View file

@ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.fixUrl
import com.lagradost.cloudstream3.utils.newExtractorLink
import io.ktor.http.Url
import java.net.URI
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@ -84,7 +84,7 @@ open class VidStack : ExtractorApi() {
private fun getBaseUrl(url: String): String {
return try {
Url(url).let { "${it.protocol.name}://${it.host}" }
URI(url).let { "${it.scheme}://${it.host}" }
} catch (e: Exception) {
Log.e("Vidstack", "getBaseUrl fallback: ${e.message}")
mainUrl

View file

@ -12,8 +12,8 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.newExtractorLink
import io.ktor.http.Url
import org.jsoup.nodes.Document
import java.net.URI
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@ -88,8 +88,8 @@ object GogoHelper {
val foundKey = secretKey ?: getKey(base64Decode(id) + foundIv) ?: return@safeApiCall
val foundDecryptKey = secretDecryptKey ?: foundKey
val url = Url(iframeUrl)
val mainUrl = "https://${url.host}"
val uri = URI(iframeUrl)
val mainUrl = "https://" + uri.host
val encryptedId = cryptoHandler(id, foundIv, foundKey)
val encryptRequestData = if (isUsingAdaptiveData) {

View file

@ -1,7 +1,7 @@
package com.lagradost.cloudstream3.extractors.helper
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
import com.lagradost.cloudstream3.utils.StringUtils.encodeUrl
import com.lagradost.cloudstream3.utils.StringUtils.decodeUri
import com.lagradost.cloudstream3.utils.StringUtils.encodeUri
// Taken from https://github.com/saikou-app/saikou/blob/b35364c8c2a00364178a472fccf1ab72f09815b4/app/src/main/java/ani/saikou/parsers/anime/NineAnime.kt
// GNU General Public License v3.0 https://github.com/saikou-app/saikou/blob/main/LICENSE.md
@ -108,6 +108,8 @@ object NineAnimeHelper {
}
}
fun encode(input: String): String = input.encodeUrl()
private fun decode(input: String): String = input.decodeUrl()
fun encode(input: String): String =
input.encodeUri().replace("+", "%20")
private fun decode(input: String): String = input.decodeUri()
}

View file

@ -25,6 +25,7 @@ import com.lagradost.cloudstream3.addDate
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.isUpcoming
import com.lagradost.cloudstream3.mainPageOf
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.newEpisode
import com.lagradost.cloudstream3.newHomePageResponse
import com.lagradost.cloudstream3.newMovieLoadResponse
@ -113,7 +114,6 @@ open class TraktProvider : MainAPI() {
val posterUrl = fixPath(mediaDetails?.images?.poster?.firstOrNull())
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")
@ -183,7 +183,6 @@ open class TraktProvider : MainAPI() {
this.comingSoon = isUpcoming(mediaDetails.released)
//posterHeaders
this.backgroundPosterUrl = backDropUrl
this.logoUrl = logoUrl
this.contentRating = mediaDetails.certification
addTrailer(mediaDetails.trailer)
addImdbId(mediaDetails.ids?.imdb)
@ -274,7 +273,6 @@ open class TraktProvider : MainAPI() {
//posterHeaders
this.nextAiring = nextAir
this.backgroundPosterUrl = backDropUrl
this.logoUrl = logoUrl
this.contentRating = mediaDetails.certification
addTrailer(mediaDetails.trailer)
addImdbId(mediaDetails.ids?.imdb)

View file

@ -60,9 +60,9 @@ object AppUtils {
inline fun <reified T : Any> parseJson(value: String): T {
// @Serializable generates a serializer at compile time; contextual serializers are
// registered manually in serializersModule, we need both to support all cases
val serializer = runCatching { serializer<T>() }
.recoverCatching { json.serializersModule.getContextual(T::class) }
.getOrNull()
val serializer = runCatching { serializer<T>() }.runCatching {
json.serializersModule.getContextual(T::class)
}.getOrNull()
// Prefer Kotlin Serialization over Jackson
if (serializer != null) {
@ -70,8 +70,6 @@ object AppUtils {
return json.decodeFromString(serializer, value)
} catch (e: SerializationException) {
logError(e)
} catch (_: Throwable) {
// Pass, the above code will trigger a NoSuchMethodError on stable due to our previously undefined json variable
}
}
@ -96,4 +94,4 @@ object AppUtils {
null
}
}
}
}

View file

@ -81,7 +81,6 @@ import com.lagradost.cloudstream3.extractors.FilemoonV2
import com.lagradost.cloudstream3.extractors.Filesim
import com.lagradost.cloudstream3.extractors.Multimoviesshg
import com.lagradost.cloudstream3.extractors.FlaswishCom
import com.lagradost.cloudstream3.extractors.Flyfile
import com.lagradost.cloudstream3.extractors.FourCX
import com.lagradost.cloudstream3.extractors.FourPichive
import com.lagradost.cloudstream3.extractors.FourPlayRu
@ -313,12 +312,11 @@ import com.lagradost.cloudstream3.extractors.ZplayerV2
import com.lagradost.cloudstream3.extractors.Ztreamhub
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
import io.ktor.http.Url
import io.ktor.http.decodeURLPart
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import org.jsoup.Jsoup
import java.net.URI
import kotlin.coroutines.cancellation.CancellationException
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@ -422,7 +420,7 @@ enum class ExtractorLinkType {
private fun inferTypeFromUrl(url: String): ExtractorLinkType {
val path = try {
Url(url).encodedPath.decodeURLPart()
URI(url).path
} catch (_: Throwable) {
// don't log magnet links as errors
null
@ -821,7 +819,7 @@ constructor(
/**
* Removes https:// and www.
* To match urls regardless of schema, perhaps Url() can be used?
* To match urls regardless of schema, perhaps Uri() can be used?
*/
val schemaStripRegex = Regex("""^(https:|)//(www\.|)""")
@ -1299,7 +1297,6 @@ val extractorApis: AtomicMutableList<ExtractorApi> = atomicListOf(
GUpload(),
HlsWish(),
ByseQekaho(),
Flyfile()
)

View file

@ -19,11 +19,12 @@
*/
package com.lagradost.cloudstream3.utils
import com.lagradost.cloudstream3.base64DecodeArray
import io.ktor.http.Url
import java.io.IOException
import java.net.URI
import java.nio.ByteBuffer
import java.util.UUID
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@Suppress("unused")
object HlsPlaylistParser {
@ -275,29 +276,29 @@ object HlsPlaylistParser {
}
}
object UrlUtil {
fun resolveToUrl(baseUrl: String?, referenceUrl: String?): Url {
return Url(resolve(baseUrl, referenceUrl))
object UriUtil {
fun resolveToUri(baseUri: String?, referenceUri: String?): URI {
return URI.create(resolve(baseUri, referenceUri))
}
/** The length of arrays returned by [.getUrlIndices]. */
/** The length of arrays returned by [.getUriIndices]. */
private
const val INDEX_COUNT: Int = 4
/**
* An index into an array returned by [.getUrlIndices].
* An index into an array returned by [.getUriIndices].
*
*
* The value at this position in the array is the index of the ':' after the scheme. Equals -1
* if the URL is a relative reference (no scheme). The hier-part starts at (schemeColon + 1),
* including when the URL has no scheme.
* if the URI is a relative reference (no scheme). The hier-part starts at (schemeColon + 1),
* including when the URI has no scheme.
*/
private
const val SCHEME_COLON: Int = 0
/**
* An index into an array returned by [.getUrlIndices].
* An index into an array returned by [.getUriIndices].
*
*
* The value at this position in the array is the index of the path part. Equals (schemeColon +
@ -309,7 +310,7 @@ object HlsPlaylistParser {
const val PATH: Int = 1
/**
* An index into an array returned by [.getUrlIndices].
* An index into an array returned by [.getUriIndices].
*
*
* The value at this position in the array is the index of the query part, including the '?'
@ -320,87 +321,87 @@ object HlsPlaylistParser {
const val QUERY: Int = 2
/**
* An index into an array returned by [.getUrlIndices].
* An index into an array returned by [.getUriIndices].
*
*
* The value at this position in the array is the index of the fragment part, including the '#'
* before the fragment. Equal to the length of the URL if no fragment part, and (length - 1) if
* before the fragment. Equal to the length of the URI if no fragment part, and (length - 1) if
* the fragment part is a single '#' with no data.
*/
private
const val FRAGMENT: Int = 3
/**
* Performs relative resolution of a `referenceUrl` with respect to a `baseUrl`.
* Performs relative resolution of a `referenceUri` with respect to a `baseUri`.
*
*
* The resolution is performed as specified by RFC-3986.
*
* @param baseUrl The base URL.
* @param referenceUrl The reference URL to resolve.
* @param baseUri The base URI.
* @param referenceUri The reference URI to resolve.
*/
private fun resolve(baseUrl: String?, referenceUrl: String?): String {
var baseUrl = baseUrl
var referenceUrl = referenceUrl
val url = StringBuilder()
private fun resolve(baseUri: String?, referenceUri: String?): String {
var baseUri = baseUri
var referenceUri = referenceUri
val uri = StringBuilder()
// Map null onto empty string, to make the following logic simpler.
baseUrl = baseUrl ?: ""
referenceUrl = referenceUrl ?: ""
baseUri = baseUri ?: ""
referenceUri = referenceUri ?: ""
val refIndices: IntArray = getUrlIndices(referenceUrl)
val refIndices: IntArray = getUriIndices(referenceUri)
if (refIndices[SCHEME_COLON] != -1) {
// The reference is absolute. The target Url is the reference.
url.append(referenceUrl)
removeDotSegments(url, refIndices[PATH], refIndices[QUERY])
return url.toString()
// The reference is absolute. The target Uri is the reference.
uri.append(referenceUri)
removeDotSegments(uri, refIndices[PATH], refIndices[QUERY])
return uri.toString()
}
val baseIndices: IntArray = getUrlIndices(baseUrl)
val baseIndices: IntArray = getUriIndices(baseUri)
if (refIndices[FRAGMENT] == 0) {
// The reference is empty or contains just the fragment part, then the target Url is the
// concatenation of the base Url without its fragment, and the reference.
return url.append(baseUrl, 0, baseIndices[FRAGMENT]).append(referenceUrl).toString()
// The reference is empty or contains just the fragment part, then the target Uri is the
// concatenation of the base Uri without its fragment, and the reference.
return uri.append(baseUri, 0, baseIndices[FRAGMENT]).append(referenceUri).toString()
}
if (refIndices[QUERY] == 0) {
// The reference starts with the query part. The target is the base up to (but excluding) the
// query, plus the reference.
return url.append(baseUrl, 0, baseIndices[QUERY]).append(referenceUrl).toString()
return uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString()
}
if (refIndices[PATH] != 0) {
// The reference has authority. The target is the base scheme plus the reference.
val baseLimit = baseIndices[SCHEME_COLON] + 1
url.append(baseUrl, 0, baseLimit).append(referenceUrl)
uri.append(baseUri, 0, baseLimit).append(referenceUri)
return removeDotSegments(
url,
uri,
baseLimit + refIndices[PATH],
baseLimit + refIndices[QUERY]
)
}
if (referenceUrl[refIndices[PATH]] == '/') {
if (referenceUri[refIndices[PATH]] == '/') {
// The reference path is rooted. The target is the base scheme and authority (if any), plus
// the reference.
url.append(baseUrl, 0, baseIndices[PATH]).append(referenceUrl)
uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri)
return removeDotSegments(
url,
uri,
baseIndices[PATH],
baseIndices[PATH] + refIndices[QUERY]
)
}
// The target Url is the concatenation of the base Url up to (but excluding) the last segment,
// The target Uri is the concatenation of the base Uri up to (but excluding) the last segment,
// and the reference. This can be split into 2 cases:
if (baseIndices[SCHEME_COLON] + 2 < baseIndices[PATH]
&& baseIndices[PATH] == baseIndices[QUERY]
) {
// Case 1: The base hier-part is just the authority, with an empty path. An additional '/' is
// needed after the authority, before appending the reference.
url.append(baseUrl, 0, baseIndices[PATH]).append('/').append(referenceUrl)
uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri)
return removeDotSegments(
url,
uri,
baseIndices[PATH],
baseIndices[PATH] + refIndices[QUERY] + 1
)
@ -409,22 +410,22 @@ object HlsPlaylistParser {
// it. If base hier-part has no '/', it could only mean that it is completely empty or
// contains only one segment, in which case the whole hier-part is excluded and the reference
// is appended right after the base scheme colon without an added '/'.
val lastSlashIndex = baseUrl.lastIndexOf('/', baseIndices[QUERY] - 1)
val lastSlashIndex = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1)
val baseLimit = if (lastSlashIndex == -1) baseIndices[PATH] else lastSlashIndex + 1
url.append(baseUrl, 0, baseLimit).append(referenceUrl)
return removeDotSegments(url, baseIndices[PATH], baseLimit + refIndices[QUERY])
uri.append(baseUri, 0, baseLimit).append(referenceUri)
return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY])
}
}
/**
* Removes dot segments from the path of a URL.
* Removes dot segments from the path of a URI.
*
* @param url A [StringBuilder] containing the URL.
* @param offset The index of the start of the path in `url`.
* @param limit The limit (exclusive) of the path in `url`.
* @param uri A [StringBuilder] containing the URI.
* @param offset The index of the start of the path in `uri`.
* @param limit The limit (exclusive) of the path in `uri`.
*/
private fun removeDotSegments(
url: StringBuilder,
uri: StringBuilder,
offset: Int,
limit: Int
): String {
@ -432,9 +433,9 @@ object HlsPlaylistParser {
var limit = limit
if (offset >= limit) {
// Nothing to do.
return url.toString()
return uri.toString()
}
if (url[offset] == '/') {
if (uri[offset] == '/') {
// If the path starts with a /, always retain it.
offset++
}
@ -444,7 +445,7 @@ object HlsPlaylistParser {
while (i <= limit) {
val nextSegmentStart = if (i == limit) {
i
} else if (url[i] == '/') {
} else if (uri[i] == '/') {
i + 1
} else {
i++
@ -452,16 +453,16 @@ object HlsPlaylistParser {
}
// We've encountered the end of a segment or the end of the path. If the final segment was
// "." or "..", remove the appropriate segments of the path.
if (i == segmentStart + 1 && url[segmentStart] == '.') {
if (i == segmentStart + 1 && uri[segmentStart] == '.') {
// Given "abc/def/./ghi", remove "./" to get "abc/def/ghi".
url.delete(segmentStart, nextSegmentStart)
uri.delete(segmentStart, nextSegmentStart)
limit -= nextSegmentStart - segmentStart
i = segmentStart
} else if (i == segmentStart + 2 && url[segmentStart] == '.' && url[segmentStart + 1] == '.') {
} else if (i == segmentStart + 2 && uri[segmentStart] == '.' && uri[segmentStart + 1] == '.') {
// Given "abc/def/../ghi", remove "def/../" to get "abc/ghi".
val prevSegmentStart = url.lastIndexOf("/", segmentStart - 2) + 1
val prevSegmentStart = uri.lastIndexOf("/", segmentStart - 2) + 1
val removeFrom = if (prevSegmentStart > offset) prevSegmentStart else offset
url.delete(removeFrom, nextSegmentStart)
uri.delete(removeFrom, nextSegmentStart)
limit -= nextSegmentStart - removeFrom
segmentStart = prevSegmentStart
i = prevSegmentStart
@ -470,41 +471,41 @@ object HlsPlaylistParser {
segmentStart = i
}
}
return url.toString()
return uri.toString()
}
/**
* Calculates indices of the constituent components of a URL.
* Calculates indices of the constituent components of a URI.
*
* @param urlString The URL as a string.
* @param uriString The URI as a string.
* @return The corresponding indices.
*/
private fun getUrlIndices(urlString: String?): IntArray {
private fun getUriIndices(uriString: String?): IntArray {
val indices = IntArray(INDEX_COUNT)
if (urlString.isNullOrEmpty()) {
if (uriString.isNullOrEmpty()) {
indices[SCHEME_COLON] = -1
return indices
}
// Determine outer structure from right to left.
// Url = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
val length = urlString.length
var fragmentIndex = urlString.indexOf('#')
// Uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
val length = uriString.length
var fragmentIndex = uriString.indexOf('#')
if (fragmentIndex == -1) {
fragmentIndex = length
}
var queryIndex = urlString.indexOf('?')
var queryIndex = uriString.indexOf('?')
if (queryIndex == -1 || queryIndex > fragmentIndex) {
// '#' before '?': '?' is within the fragment.
queryIndex = fragmentIndex
}
// Slashes are allowed only in hier-part so any colon after the first slash is part of the
// hier-part, not the scheme colon separator.
var schemeIndexLimit = urlString.indexOf('/')
var schemeIndexLimit = uriString.indexOf('/')
if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) {
schemeIndexLimit = queryIndex
}
var schemeIndex = urlString.indexOf(':')
var schemeIndex = uriString.indexOf(':')
if (schemeIndex > schemeIndexLimit) {
// '/' before ':'
schemeIndex = -1
@ -513,10 +514,10 @@ object HlsPlaylistParser {
// Determine hier-part structure: hier-part = "//" authority path / path
// This block can also cope with schemeIndex == -1.
val hasAuthority =
schemeIndex + 2 < queryIndex && urlString[schemeIndex + 1] == '/' && urlString[schemeIndex + 2] == '/'
schemeIndex + 2 < queryIndex && uriString[schemeIndex + 1] == '/' && uriString[schemeIndex + 2] == '/'
var pathIndex: Int
if (hasAuthority) {
pathIndex = urlString.indexOf('/', schemeIndex + 3) // find first '/' after "://"
pathIndex = uriString.indexOf('/', schemeIndex + 3) // find first '/' after "://"
if (pathIndex == -1 || pathIndex > queryIndex) {
pathIndex = queryIndex
}
@ -805,7 +806,7 @@ object HlsPlaylistParser {
const val APPLICATION_MEDIA3_CUES: String = "$BASE_TYPE_APPLICATION/x-media3-cues"
/** MIME type for an image URL loaded from an external image management framework. */
/** MIME type for an image URI loaded from an external image management framework. */
const val APPLICATION_EXTERNALLY_LOADED_IMAGE: String = "$BASE_TYPE_APPLICATION/x-image-uri"
@ -1168,6 +1169,7 @@ object HlsPlaylistParser {
return parseOptionalStringAttr(line, pattern, null, variableDefinitions)
}
@OptIn(ExperimentalEncodingApi::class)
@Throws(ParserException::class)
private fun parseDrmSchemeData(
line: String, keyFormat: String, variableDefinitions: Map<String, String>
@ -1175,11 +1177,11 @@ object HlsPlaylistParser {
val keyFormatVersions =
parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions)
if (KEYFORMAT_WIDEVINE_PSSH_BINARY == keyFormat) {
val urlString = parseStringAttr(line, REGEX_URI, variableDefinitions)
val uriString = parseStringAttr(line, REGEX_URI, variableDefinitions)
return SchemeData(
uuid = C.WIDEVINE_UUID,
mimeType = MimeTypes.VIDEO_MP4,
data = base64DecodeArray(urlString.substring(urlString.indexOf(',')))
data = Base64.Default.decode(uriString.substring(uriString.indexOf(',')))
)
} else if (KEYFORMAT_WIDEVINE_PSSH_JSON == keyFormat) {
return SchemeData(
@ -1188,9 +1190,9 @@ object HlsPlaylistParser {
data = line.encodeToByteArray()
)
} else if (KEYFORMAT_PLAYREADY == keyFormat && "1" == keyFormatVersions) {
val urlString = parseStringAttr(line, REGEX_URI, variableDefinitions)
val uriString = parseStringAttr(line, REGEX_URI, variableDefinitions)
val data: ByteArray =
base64DecodeArray(urlString.substring(urlString.indexOf(',')))
Base64.Default.decode(uriString.substring(uriString.indexOf(',')))
val psshData: ByteArray =
PsshAtomUtil.buildPsshAtom(
systemId = C.PLAYREADY_UUID,
@ -1268,7 +1270,7 @@ object HlsPlaylistParser {
}
data class Variant(
val url: Url,
val url: URI,
val format: Format,
val videoGroupId: String?,
val audioGroupId: String?,
@ -1321,7 +1323,7 @@ object HlsPlaylistParser {
data class Rendition(
/** The rendition's url, or null if the tag does not have a URI attribute. */
val url: Url?,
val url: URI?,
/** Format information associated with this rendition. */
val format: Format,
@ -1334,14 +1336,14 @@ object HlsPlaylistParser {
)
data class HlsMultivariantPlaylist(
/** The base url. Used to resolve relative paths. */
/** The base uri. Used to resolve relative paths. */
val baseUri: String,
/** The list of tags in the playlist. */
val tags: List<String>,
/** All of the media playlist URLs referenced by the playlist. */
//val mediaPlaylistUrls: List<Url>,
//val mediaPlaylistUrls: List<URI>,
/** The variants declared by the playlist. */
val variants: List<Variant>,
@ -1727,8 +1729,8 @@ object HlsPlaylistParser {
private fun parseMultivariantPlaylist(
iterator: Iterator<String>, baseUri: String
): HlsMultivariantPlaylist {
val urlToVariantInfos: HashMap<Url, ArrayList<VariantInfo>?> =
HashMap<Url, ArrayList<VariantInfo>?>()
val urlToVariantInfos: HashMap<URI, ArrayList<VariantInfo>?> =
HashMap<URI, ArrayList<VariantInfo>?>()
val variableDefinitions = HashMap<String, String>()
val variants: ArrayList<Variant> = ArrayList<Variant>()
val videos: ArrayList<Rendition> = ArrayList<Rendition>()
@ -1851,10 +1853,10 @@ object HlsPlaylistParser {
parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions)
val closedCaptionsGroupId: String? =
parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions)
val url: Url
val uri: URI
if (isIFrameOnlyVariant) {
url =
UrlUtil.resolveToUrl(
uri =
UriUtil.resolveToUri(
baseUri,
parseStringAttr(line, REGEX_URI, variableDefinitions)
)
@ -1863,14 +1865,14 @@ object HlsPlaylistParser {
"#EXT-X-STREAM-INF must be followed by another line", /* cause= */null
)
} else {
// The following line contains #EXT-X-STREAM-INF's URL.
// The following line contains #EXT-X-STREAM-INF's URI.
line = replaceVariableReferences(iterator.next(), variableDefinitions)
url = UrlUtil.resolveToUrl(baseUri, line)
uri = UriUtil.resolveToUri(baseUri, line)
}
val variant =
Variant(
url = url,
url = uri,
format = Format(
id = variants.size.toString(),
containerMimeType = MimeTypes.APPLICATION_M3U8,
@ -1888,10 +1890,10 @@ object HlsPlaylistParser {
captionGroupId = closedCaptionsGroupId
)
variants.add(variant)
var variantInfosForUrl: ArrayList<VariantInfo>? = urlToVariantInfos[url]
var variantInfosForUrl: ArrayList<VariantInfo>? = urlToVariantInfos[uri]
if (variantInfosForUrl == null) {
variantInfosForUrl = ArrayList()
urlToVariantInfos[url] = variantInfosForUrl
urlToVariantInfos[uri] = variantInfosForUrl
}
variantInfosForUrl.add(
VariantInfo(
@ -1909,7 +1911,7 @@ object HlsPlaylistParser {
// TODO: Don't deduplicate variants by URL.
val deduplicatedVariants = variants.distinctBy { it.url }
/*val deduplicatedVariants: ArrayList<Variant> = ArrayList<Variant>()
val urlsInDeduplicatedVariants = HashSet<Url>()
val urlsInDeduplicatedVariants = HashSet<URI>()
for (i in variants.indices) {
val variant: Variant = variants[i]
if (urlsInDeduplicatedVariants.add(variant.url)) {
@ -1943,10 +1945,10 @@ object HlsPlaylistParser {
containerMimeType = MimeTypes.APPLICATION_M3U8,
)
val referenceUrl: String? =
val referenceUri: String? =
parseOptionalStringAttr(line, REGEX_URI, variableDefinitions)
val url: Url? =
if (referenceUrl == null) null else UrlUtil.resolveToUrl(baseUri, referenceUrl)
val uri: URI? =
if (referenceUri == null) null else UriUtil.resolveToUri(baseUri, referenceUri)
//val metadata =
// Metadata(HlsTrackMetadataEntry(groupId, name, emptyList<T>()))
when (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) {
@ -1961,11 +1963,11 @@ object HlsPlaylistParser {
codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO)
)
}
if (url == null) {
// TODO: Remove this case and add a Rendition with a null url to videos.
if (uri == null) {
// TODO: Remove this case and add a Rendition with a null uri to videos.
} else {
//formatBuilder.setMetadata(metadata)
videos.add(Rendition(url = url, format = formatBuilder, groupId, name))
videos.add(Rendition(url = uri, format = formatBuilder, groupId, name))
}
}
@ -1993,11 +1995,11 @@ object HlsPlaylistParser {
}
}
val format = formatBuilder.copy(sampleMimeType = sampleMimeType)
if (url != null) {
if (uri != null) {
//formatBuilder.setMetadata(metadata)
audios.add(Rendition(url, format, groupId, name))
audios.add(Rendition(uri, format, groupId, name))
} else if (variant != null) {
// TODO: Remove muxedAudioFormat and add a Rendition with a null url to audios.
// TODO: Remove muxedAudioFormat and add a Rendition with a null uri to audios.
muxedAudioFormat = format
}
}
@ -2016,10 +2018,10 @@ object HlsPlaylistParser {
if (sampleMimeType == null) {
sampleMimeType = MimeTypes.TEXT_VTT
}
if (url != null) {
if (uri != null) {
subtitles.add(
Rendition(
url,
uri,
formatBuilder.copy(sampleMimeType = sampleMimeType),
groupId,
name

View file

@ -112,8 +112,8 @@ object M3u8Helper2 {
return c.doFinal(data)
}
private fun getParentLink(url: String): String {
val split = url.split("/").toMutableList()
private fun getParentLink(uri: String): String {
val split = uri.split("/").toMutableList()
split.removeAt(split.lastIndex)
return split.joinToString("/")
}
@ -322,15 +322,15 @@ object M3u8Helper2 {
if (!match.isNullOrEmpty()) {
encryptionState = true
var encryptionUrl = match[2]
var encryptionUri = match[2]
if (isNotCompleteUrl(encryptionUrl)) {
encryptionUrl = "${getParentLink(playlistStream.streamUrl)}/$encryptionUrl"
if (isNotCompleteUrl(encryptionUri)) {
encryptionUri = "${getParentLink(playlistStream.streamUrl)}/$encryptionUri"
}
encryptionIv = match[3].encodeToByteArray()
val encryptionKeyResponse =
app.get(encryptionUrl, headers = playlistStream.headers, verify = false)
app.get(encryptionUri, headers = playlistStream.headers, verify = false)
val body = encryptionKeyResponse.body
encryptionData = body.bytes()
body.close()

View file

@ -1,32 +1,14 @@
package com.lagradost.cloudstream3.utils
import com.lagradost.cloudstream3.Prerelease
import io.ktor.http.decodeURLQueryComponent
import io.ktor.http.encodeURLParameter
import java.net.URLDecoder
import java.net.URLEncoder
object StringUtils {
@Prerelease
fun String.decodeUrl(): String {
return this.decodeURLQueryComponent()
fun String.encodeUri(): String {
return URLEncoder.encode(this, "UTF-8")
}
@Prerelease
fun String.encodeUrl(): String {
return this.encodeURLParameter()
fun String.decodeUri(): String {
return URLDecoder.decode(this, "UTF-8")
}
// Deprecate after next stable
/* @Deprecated(
message = "Use Ktor 'Url' naming convention instead.",
replaceWith = ReplaceWith("this.encodeUrl()")
) */
fun String.encodeUri(): String = encodeUrl()
/* @Deprecated(
message = "Use Ktor 'Url' naming convention instead.",
replaceWith = ReplaceWith("this.decodeUrl()")
) */
fun String.decodeUri(): String = decodeUrl()
}
}

View file

@ -2,9 +2,9 @@ package com.lagradost.cloudstream3.utils
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
import com.lagradost.cloudstream3.utils.StringUtils.decodeUri
import com.lagradost.nicehttp.NiceResponse
import io.ktor.http.Url
import java.net.URI
// Code heavily based on unshortenit.py form kodiondemand /addon
@ -48,8 +48,8 @@ object ShortLink {
}
}
suspend fun unshorten(url: String, type: String? = null): String {
var currentUrl = url
suspend fun unshorten(uri: String, type: String? = null): String {
var currentUrl = uri
val visitedUrls = mutableSetOf<String>()
var count = 10
@ -57,7 +57,9 @@ object ShortLink {
visitedUrls += currentUrl
count -= 1
val domain = Url(currentUrl.trim()).host
val domain =
URI(currentUrl.trim()).host
?: throw IllegalArgumentException("No domain found in URI!")
currentUrl = shortList.firstOrNull {
it.regex.find(domain) != null || type == it.type
}?.function?.let { it(currentUrl) } ?: break
@ -65,8 +67,8 @@ object ShortLink {
return currentUrl.trim()
}
suspend fun unshortenAdfly(url: String): String {
val html = app.get(url).text
suspend fun unshortenAdfly(uri: String): String {
val html = app.get(uri).text
val ysmm = Regex("""var ysmm =.*;?""").find(html)!!.value
if (ysmm.isNotEmpty()) {
@ -79,46 +81,46 @@ object ShortLink {
left += c[0]
right = c[1] + right
}
val encodedUrl = (left + right).toMutableList()
val encodedUri = (left + right).toMutableList()
val numbers =
encodedUrl.mapIndexed { i, n -> Pair(i, n) }.filter { it.second.isDigit() }
encodedUri.mapIndexed { i, n -> Pair(i, n) }.filter { it.second.isDigit() }
for (el in numbers.chunked(2).dropLastWhile { it.size == 1 }) {
val xor = (el[0].second).code.xor(el[1].second.code)
if (xor < 10) {
encodedUrl[el[0].first] = xor.digitToChar()
encodedUri[el[0].first] = xor.digitToChar()
}
}
val encodedbytearray = encodedUrl.map { it.code.toByte() }.toByteArray()
var decodedUrl =
val encodedbytearray = encodedUri.map { it.code.toByte() }.toByteArray()
var decodedUri =
base64Decode(encodedbytearray.toString()).dropLast(16)
.drop(16)
if (Regex("""go\.php\?u=""").find(decodedUrl) != null) {
decodedUrl =
base64Decode(decodedUrl.replace(Regex("""(.*?)u="""), ""))
if (Regex("""go\.php\?u=""").find(decodedUri) != null) {
decodedUri =
base64Decode(decodedUri.replace(Regex("""(.*?)u="""), ""))
}
return decodedUrl
return decodedUri
} else {
return url
return uri
}
}
suspend fun unshortenLinkup(url: String): String {
suspend fun unshortenLinkup(uri: String): String {
var r: NiceResponse? = null
var url = url
var uri = uri
when {
url.contains("/tv/") -> url = url.replace("/tv/", "/tva/")
url.contains("delta") -> url = url.replace("/delta/", "/adelta/")
(url.contains("/ga/") || url.contains("/ga2/")) -> url =
base64Decode(url.split('/').last()).trim()
uri.contains("/tv/") -> uri = uri.replace("/tv/", "/tva/")
uri.contains("delta") -> uri = uri.replace("/delta/", "/adelta/")
(uri.contains("/ga/") || uri.contains("/ga2/")) -> uri =
base64Decode(uri.split('/').last()).trim()
url.contains("/speedx/") -> url =
url.replace("http://linkup.pro/speedx", "http://speedvideo.net")
uri.contains("/speedx/") -> uri =
uri.replace("http://linkup.pro/speedx", "http://speedvideo.net")
else -> {
r = app.get(url, allowRedirects = true)
url = r.url
r = app.get(uri, allowRedirects = true)
uri = r.url
val link =
Regex("<iframe[^<>]*src=\\'([^'>]*)\\'[^<>]*>").find(r.text)?.value
?: Regex("""action="(?:[^/]+.*?/[^/]+/([a-zA-Z0-9_]+))">""").find(r.text)?.value
@ -126,40 +128,40 @@ object ShortLink {
.elementAtOrNull(1)?.groupValues?.get(1)
if (link != null) {
url = link
uri = link
}
}
}
val short = Regex("""^https?://.*?(https?://.*)""").find(url)?.value
val short = Regex("""^https?://.*?(https?://.*)""").find(uri)?.value
if (short != null) {
url = short
uri = short
}
if (r == null) {
r = app.get(
url,
uri,
allowRedirects = false
)
if (r.headers["location"] != null) {
url = r.headers["location"].toString()
uri = r.headers["location"].toString()
}
}
if (url.contains("snip.")) {
if (url.contains("out_generator")) {
url = Regex("url=(.*)\$").find(url)!!.value
} else if (url.contains("/decode/")) {
url = app.get(url, allowRedirects = true).url
if (uri.contains("snip.")) {
if (uri.contains("out_generator")) {
uri = Regex("url=(.*)\$").find(uri)!!.value
} else if (uri.contains("/decode/")) {
uri = app.get(uri, allowRedirects = true).url
}
}
return url
return uri
}
fun unshortenLinksafe(url: String): String {
return base64Decode(url.split("?url=").last())
fun unshortenLinksafe(uri: String): String {
return base64Decode(uri.split("?url=").last())
}
suspend fun unshortenNuovoIndirizzo(url: String): String {
val soup = app.get(url, allowRedirects = true)
suspend fun unshortenNuovoIndirizzo(uri: String): String {
val soup = app.get(uri, allowRedirects = true)
val header = soup.headers["refresh"]
val link: String = if (header != null) {
soup.headers["refresh"]!!.substringAfter("=")
@ -169,29 +171,29 @@ object ShortLink {
return link
}
suspend fun unshortenNuovoLink(url: String): String {
return app.get(url, allowRedirects = true).document.selectFirst("a")!!.attr("href")
suspend fun unshortenNuovoLink(uri: String): String {
return app.get(uri, allowRedirects = true).document.selectFirst("a")!!.attr("href")
}
suspend fun unshortenUprot(url: String): String {
val page = app.get(url).text
suspend fun unshortenUprot(uri: String): String {
val page = app.get(uri).text
Regex("""<a[^>]+href="([^"]+)".*Continue""").findAll(page)
.map { it.value.replace("""<a href="""", "") }
.toList().forEach { link ->
if (link.contains("https://maxstream.video") || link.contains("https://uprot.net") && link != url) {
if (link.contains("https://maxstream.video") || link.contains("https://uprot.net") && link != uri) {
return link
}
}
return url
return uri
}
fun unshortenDavisonbarker(url: String): String {
return url.substringAfter("dest=").decodeUrl()
fun unshortenDavisonbarker(uri: String): String {
return uri.substringAfter("dest=").decodeUri()
}
suspend fun unshortenIsecure(url: String): String {
val doc = app.get(url).document
return doc.selectFirst("iframe")?.attr("src")?.trim() ?: url
suspend fun unshortenIsecure(uri: String): String {
val doc = app.get(uri).document
return doc.selectFirst("iframe")?.attr("src")?.trim() ?: uri
}
}
}