Compare commits

..

1 commit

Author SHA1 Message Date
firelight
8ac7e98b83
Feat: MpvRx 2026-06-09 18:30:21 +02:00
35 changed files with 277 additions and 422 deletions

View file

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

View file

@ -101,7 +101,7 @@ class SerializationClassTester {
} }
// DEX files are the best solution to read all our classes dynamically. // 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") @Suppress("DEPRECATION")
private fun findSerializableClasses(packageName: String): List<KClass<*>> { private fun findSerializableClasses(packageName: String): List<KClass<*>> {
val context = InstrumentationRegistry val context = InstrumentationRegistry
@ -109,6 +109,7 @@ class SerializationClassTester {
.targetContext .targetContext
val dexFile = DexFile(context.packageCodePath) val dexFile = DexFile(context.packageCodePath)
return dexFile.entries() return dexFile.entries()
.toList() .toList()
.filter { it.startsWith(packageName) } .filter { it.startsWith(packageName) }

View file

@ -23,7 +23,6 @@ import com.lagradost.cloudstream3.actions.temp.MpvPackage
import com.lagradost.cloudstream3.actions.temp.MpvRxPackage import com.lagradost.cloudstream3.actions.temp.MpvRxPackage
import com.lagradost.cloudstream3.actions.temp.MpvYTDLPackage import com.lagradost.cloudstream3.actions.temp.MpvYTDLPackage
import com.lagradost.cloudstream3.actions.temp.NextPlayerPackage 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.PlayInBrowserAction
import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction
import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action
@ -66,7 +65,6 @@ object VideoClickActionHolder {
MpvYTDLPackage(), MpvYTDLPackage(),
MpvKtPackage(), MpvKtPackage(),
MpvKtPreviewPackage(), MpvKtPreviewPackage(),
OnlyPlayer(),
MpvRxPackage(), MpvRxPackage(),
// Always Ask option // Always Ask option
AlwaysAskAction(), AlwaysAskAction(),

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? { override suspend fun login(redirectUrl: String, payload: String?): AuthToken? {
val sanitizer = splitRedirectUrl(redirectUrl) val sanitizer = splitRedirectUrl(redirectUrl)
val token = AuthToken( val token = AuthToken(
accessToken = sanitizer["access_token"] accessToken = sanitizer["access_token"] ?: throw ErrorLoadingException("No access token"),
?: throw ErrorLoadingException("No access token"),
//refreshToken = sanitizer["refresh_token"], //refreshToken = sanitizer["refresh_token"],
accessTokenLifetime = unixTime + sanitizer["expires_in"]!!.toLong(), accessTokenLifetime = unixTime + sanitizer["expires_in"]!!.toLong(),
) )
@ -84,8 +83,8 @@ class AniListApi : SyncAPI() {
return "$mainUrl/anime/$id" return "$mainUrl/anime/$id"
} }
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? { override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
val data = searchShows(query) ?: return null val data = searchShows(name) ?: return null
return data.data?.page?.media?.map { return data.data?.page?.media?.map {
SyncAPI.SyncSearchResult( SyncAPI.SyncSearchResult(
it.title.romaji ?: return null, 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) val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId") ?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
val season = getSeason(internalId).data.media 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 internalId = id.toIntOrNull() ?: return null
val data = getDataAboutId(auth ?: return null, internalId) ?: 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 = val q =
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id) """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) 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( return app.post(
"https://graphql.anilist.co/", "https://graphql.anilist.co/",
headers = mapOf( 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 { val list = getAniListAnimeListSmart(auth ?: return null)?.groupBy {
convertAniListStringToStatus(it.status ?: "").stringRes convertAniListStringToStatus(it.status ?: "").stringRes
}?.mapValues { group -> }?.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 userID = auth.user.id
val mediaType = "ANIME" val mediaType = "ANIME"
@ -715,7 +714,7 @@ class AniListApi : SyncAPI() {
return text?.toKotlinObject() 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) { val q = """mutation (${'$'}animeId: Int = $id) {
ToggleFavourite (animeId: ${'$'}animeId) { ToggleFavourite (animeId: ${'$'}animeId) {
anime { anime {
@ -738,7 +737,7 @@ class AniListApi : SyncAPI() {
data class MediaListId(@JsonProperty("id") val id: Long? = null) data class MediaListId(@JsonProperty("id") val id: Long? = null)
private suspend fun postDataAboutId( private suspend fun postDataAboutId(
auth: AuthData, auth : AuthData,
id: Int, id: Int,
type: AniListStatusType, type: AniListStatusType,
score: Score?, score: Score?,
@ -787,7 +786,7 @@ class AniListApi : SyncAPI() {
return data != "" return data != ""
} }
private suspend fun getUser(token: AuthToken): AniListUser? { private suspend fun getUser(token : AuthToken): AniListUser? {
val q = """ val q = """
{ {
Viewer { 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 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( val res = app.get(
url, headers = mapOf( url, headers = mapOf(
"Authorization" to "Bearer $auth", "Authorization" to "Bearer $auth",
@ -122,7 +122,7 @@ class MALApi : SyncAPI() {
Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first() Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first()
override suspend fun updateStatus( override suspend fun updateStatus(
auth: AuthData?, auth : AuthData?,
id: String, id: String,
newStatus: SyncAPI.AbstractSyncStatus newStatus: SyncAPI.AbstractSyncStatus
): Boolean { ): 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 auth = auth?.token?.accessToken ?: return null
val internalId = id.toIntOrNull() ?: return null val internalId = id.toIntOrNull() ?: return null
val url = 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 val auth = auth?.token?.accessToken ?: return null
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get // 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? @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 { val list = getMalAnimeListSmart(auth ?: return null)?.groupBy {
convertToStatus(it.listStatus?.status ?: "").stringRes convertToStatus(it.listStatus?.status ?: "").stringRes
}?.mapValues { group -> }?.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) { return if (requireLibraryRefresh) {
val list = getMalAnimeList(auth.token) val list = getMalAnimeList(auth.token)
setKey(MAL_CACHED_LIST, auth.user.id.toString(), list) 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>? { override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
return app.get( 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() } ).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() }
} }

View file

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

View file

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

View file

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

View file

@ -252,7 +252,7 @@
<string name="update">Update</string> <string name="update">Update</string>
<string name="watch_quality_pref">Bevorzugte Videoqualität (WLAN)</string> <string name="watch_quality_pref">Bevorzugte Videoqualität (WLAN)</string>
<string name="limit_title">Videoplayertitel max. Zeichen</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_size_settings">Videopuffergröße</string>
<string name="video_buffer_length_settings">Videopufferlänge</string> <string name="video_buffer_length_settings">Videopufferlänge</string>
<string name="video_buffer_disk_settings">Video-Cache in Speicher</string> <string name="video_buffer_disk_settings">Video-Cache in Speicher</string>
@ -587,7 +587,7 @@
<string name="pref_category_security">Sicherheit</string> <string name="pref_category_security">Sicherheit</string>
<string name="pref_category_accounts">Konten</string> <string name="pref_category_accounts">Konten</string>
<string name="open_downloaded_repo">Repository öffnen</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="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="downloads_empty">Zur Zeit sind keine Downloads verfügbar.</string>
<string name="open_local_video">Lokales Video öffnen</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">Zusätzliche Helligkeit</string>
<string name="extra_brightness_settings_des">Aktiviere Helligkeitsfilter, wenn 100% Bildschirmhelligkeit überschritten ist</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="extra_brightness_key">Erhöhte Helligkeit aktiviert</string>
<string name="show_cast_in_details">Zeige Cast-Panel</string> <string name="show_cast_in_details">Cast-Panel zeigen</string>
<string name="video_info">Mediainfo</string> <string name="video_info">Medieninfo</string>
<string name="source_name">Quellname</string> <string name="source_name">Quellname</string>
<string name="download_all">Alle herunterladen</string> <string name="download_all">Alle herunterladen</string>
<string name="download_episode_range">Möchtest du Episode %s herunter laden?</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="queue_empty_message">Es befinden sich keine Downloads in der Warteschlange.</string>
<string name="source_priority">Quellpriorität</string> <string name="source_priority">Quellpriorität</string>
<string name="source_priority_help">Entscheide, wie Videoquellen im Player sortiert werden sollen</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> </resources>

View file

@ -244,7 +244,7 @@
<string name="quality_tc">TC</string> <string name="quality_tc">TC</string>
<string name="subscription_new">Претплатен на %s</string> <string name="subscription_new">Претплатен на %s</string>
<string name="pref_category_subtitles">Преводи</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="backup_failed">Недостасуваат дозволи за складирање. Обиди се повторно.</string>
<string name="sort_save">Зачувај</string> <string name="sort_save">Зачувај</string>
<string name="player_load_subtitles">Вчитај од датотека</string> <string name="player_load_subtitles">Вчитај од датотека</string>
@ -445,7 +445,7 @@
<string name="backup_failed_error_format">Грешка при правење резервна копија на %s</string> <string name="backup_failed_error_format">Грешка при правење резервна копија на %s</string>
<string name="pref_filter_search_quality">Сокриј го избраниот квалитет на видеото во резултатите од пребарувањето</string> <string name="pref_filter_search_quality">Сокриј го избраниот квалитет на видеото во резултатите од пребарувањето</string>
<string name="apk_installer_settings_des">Некои уреди не го поддржуваат новиот инсталатор на пакети. Испробај ја легаси(старата) опција, ако ажурирањата не се инсталираат.</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="video_buffer_size_settings">Големина на видео баферот</string>
<string name="pref_category_player_layout">Распоред</string> <string name="pref_category_player_layout">Распоред</string>
<string name="pref_category_defaults">Стандардно</string> <string name="pref_category_defaults">Стандардно</string>
@ -705,37 +705,4 @@
<string name="top_center">Горе во центар</string> <string name="top_center">Горе во центар</string>
<string name="top_right">Горе на десно</string> <string name="top_right">Горе на десно</string>
<string name="play_full_series_button">Пушти ја целата серија</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> </resources>

View file

@ -8,6 +8,7 @@ annotation = "1.10.0"
appcompat = "1.7.1" appcompat = "1.7.1"
biometric = "1.4.0-alpha07" biometric = "1.4.0-alpha07"
buildkonfigGradlePlugin = "0.21.2" buildkonfigGradlePlugin = "0.21.2"
classgraph = "4.8.184"
coil = { strictly = "3.3.0" } # Later versions require jvmTarget 11 or later coil = { strictly = "3.3.0" } # Later versions require jvmTarget 11 or later
colorpicker = "6b46b49" colorpicker = "6b46b49"
conscryptAndroid = { strictly = "2.5.2" } # 2.5.3 crashes everything conscryptAndroid = { strictly = "2.5.2" } # 2.5.3 crashes everything
@ -31,12 +32,11 @@ kotlinxCollectionsImmutable = "0.4.0"
kotlinxCoroutinesCore = "1.11.0" kotlinxCoroutinesCore = "1.11.0"
kotlinxDatetime = "0.8.0" kotlinxDatetime = "0.8.0"
kotlinxSerializationJson = "1.11.0" kotlinxSerializationJson = "1.11.0"
ktor = "3.5.0"
lifecycleKtx = "2.10.0" lifecycleKtx = "2.10.0"
material = "1.14.0" material = "1.14.0"
media3 = "1.9.3" media3 = "1.9.3"
navigationKtx = "2.9.8" navigationKtx = "2.9.8"
newpipeextractor = "v0.26.3" newpipeextractor = "v0.26.2"
nextlibMedia3 = "1.9.3-0.12.0" nextlibMedia3 = "1.9.3-0.12.0"
nicehttp = "0.4.18" nicehttp = "0.4.18"
overlappingpanels = "0.1.5" 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" } annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" } 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 = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", 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" } 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-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } 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-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleKtx" }
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-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" } 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.coroutines.core)
implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json) // JSON Parser implementation(libs.kotlinx.serialization.json) // JSON Parser
implementation(libs.ktor.http)
implementation(libs.jsoup) // HTML Parser implementation(libs.jsoup) // HTML Parser
implementation(libs.rhino) // Run JavaScript implementation(libs.rhino) // Run JavaScript
implementation(libs.tmdb.java) // TMDB API v3 Wrapper Made with RetroFit 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.mainWork
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.nicehttp.requestCreator import com.lagradost.nicehttp.requestCreator
import io.ktor.http.Url
import io.ktor.http.decodeURLPart
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import java.net.URI
/** /**
* When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...) * When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...)
@ -212,7 +211,7 @@ actual class WebViewResolver actual constructor(
* */ * */
return@runBlocking try { return@runBlocking try {
when { when {
blacklistedFiles.any { Url(webViewUrl).encodedPath.decodeURLPart().contains(it) } || webViewUrl.endsWith( blacklistedFiles.any { URI(webViewUrl).path.contains(it) } || webViewUrl.endsWith(
"/favicon.ico" "/favicon.ico"
) -> WebResourceResponse( ) -> WebResourceResponse(
"image/png", "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.fromCodeToLangTagIETF
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF
import com.lagradost.nicehttp.RequestBodyTypes 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.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@ -39,8 +35,11 @@ import kotlinx.datetime.format.byUnicodePattern
import kotlinx.datetime.format.char import kotlinx.datetime.format.char
import kotlinx.datetime.format.parse import kotlinx.datetime.format.parse
import kotlinx.datetime.toInstant import kotlinx.datetime.toInstant
import java.net.URI
import java.util.EnumSet
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.time.Clock import kotlin.time.Clock
@ -91,7 +90,6 @@ class ErrorLoadingException(message: String? = null) : Exception(message)
@Prerelease @Prerelease
val json = Json { val json = Json {
encodeDefaults = true encodeDefaults = true
explicitNulls = false
ignoreUnknownKeys = true ignoreUnknownKeys = true
} }
@ -178,9 +176,9 @@ object APIHolder {
// To get the key // To get the key
suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? { suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? {
try { try {
val _url = Url(url) val uri = URI.create(url)
val domain = base64Encode( val domain = base64Encode(
(_url.protocol.name + "://" + _url.host + ":443").encodeToByteArray(), (uri.scheme + "://" + uri.host + ":443").encodeToByteArray(),
).replace("\n", "").replace("=", ".") ).replace("\n", "").replace("=", ".")
val vToken = val vToken =
@ -715,10 +713,12 @@ fun base64Decode(string: String): String {
} }
} }
@OptIn(ExperimentalEncodingApi::class)
fun base64DecodeArray(string: String): ByteArray { fun base64DecodeArray(string: String): ByteArray {
return Base64.decode(string) return Base64.decode(string)
} }
@OptIn(ExperimentalEncodingApi::class)
fun base64Encode(array: ByteArray): String { fun base64Encode(array: ByteArray): String {
return Base64.encode(array) return Base64.encode(array)
} }
@ -1330,23 +1330,23 @@ fun getQualityFromString(string: String?): SearchQuality? {
* ``` * ```
*/ */
fun MainAPI.updateUrl(url: String): String { fun MainAPI.updateUrl(url: String): String {
return try { try {
val original = Url(url) val original = URI(url)
val updated = Url(mainUrl) val updated = URI(mainUrl)
URLBuilder().apply { // URI(String scheme, String userInfo, String host, int port, String path, String query, String fragment)
takeFrom(updated) return URI(
user = original.user updated.scheme,
password = original.password original.userInfo,
encodedPath = original.encodedPath updated.host,
fragment = original.fragment updated.port,
original.path,
parameters.clear() original.query,
parameters.appendAll(original.parameters) original.fragment
}.buildString() ).toString()
} catch (t: Throwable) { } catch (t: Throwable) {
logError(t) logError(t)
url return url
} }
} }
@ -1510,7 +1510,7 @@ constructor(
override var posterUrl: String? = null, override var posterUrl: String? = null,
var year: Int? = null, var year: Int? = null,
var dubStatus: MutableSet<DubStatus>? = null, var dubStatus: EnumSet<DubStatus>? = null,
var otherName: String? = null, var otherName: String? = null,
var episodes: MutableMap<DubStatus, Int> = mutableMapOf(), var episodes: MutableMap<DubStatus, Int> = mutableMapOf(),
@ -1522,7 +1522,7 @@ constructor(
) : SearchResponse ) : SearchResponse
fun AnimeSearchResponse.addDubStatus(status: DubStatus, episodes: Int? = null) { 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 (this.type?.isMovieType() != true)
if (episodes != null && episodes > 0) if (episodes != null && episodes > 0)
this.episodes[status] = episodes 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
import io.ktor.http.Url import java.net.URI
import io.ktor.http.decodeURLPart
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ -46,11 +45,11 @@ open class ByseSX : ExtractorApi() {
} }
private fun getBaseUrl(url: String): String { 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 { private fun getCodeFromUrl(url: String): String {
val path = Url(url).encodedPath.decodeURLPart() val path = URI(url).path ?: ""
return path.trimEnd('/').substringAfterLast('/') 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities 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 com.lagradost.cloudstream3.utils.newExtractorLink
open class Cda : ExtractorApi() { open class Cda : ExtractorApi() {
@ -64,7 +64,7 @@ open class Cda : ExtractorApi() {
.replace("_QWE", "") .replace("_QWE", "")
.replace("_Q5", "") .replace("_Q5", "")
.replace("_IKSDE", "") .replace("_IKSDE", "")
a = a.decodeUrl() a = a.decodeUri()
a = a.map { char -> a = a.map { char ->
if (char.code in 33..126) { if (char.code in 33..126) {
return@map (33 + (char.code + 14) % 94).toChar().toString() 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor 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/ // deobfuscated from https://hglink.to/main.js?v=1.1.3 using https://deobfuscate.io/
private val mirrors = arrayOf( private val mirrors = arrayOf(
@ -90,7 +90,7 @@ abstract class CineMMRedirect : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit, subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit callback: (ExtractorLink) -> Unit
) { ) {
val videoId = Url(url).encodedPath val videoId = url.toHttpUrl().encodedPath
val mirror = mirrors.random() val mirror = mirrors.random()
// re-use existing extractors by calling the ExtractorApi // re-use existing extractors by calling the ExtractorApi
@ -98,4 +98,4 @@ abstract class CineMMRedirect : ExtractorApi() {
val mirrorUrlWithVideoId = "https://$mirror$videoId" val mirrorUrlWithVideoId = "https://$mirror$videoId"
loadExtractor(mirrorUrlWithVideoId, referer, subtitleCallback, callback) 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import io.ktor.http.Url import java.net.URI
import io.ktor.http.decodeURLPart
class Geodailymotion : Dailymotion() { class Geodailymotion : Dailymotion() {
override val name = "GeoDailymotion" override val name = "GeoDailymotion"
@ -56,6 +57,7 @@ open class Dailymotion : ExtractorApi() {
} }
} }
private fun getEmbedUrl(url: String): String? { private fun getEmbedUrl(url: String): String? {
if (url.contains("/embed/") || url.contains("/video/")) return url if (url.contains("/embed/") || url.contains("/video/")) return url
if (url.contains("geo.dailymotion.com")) { if (url.contains("geo.dailymotion.com")) {
@ -65,8 +67,9 @@ open class Dailymotion : ExtractorApi() {
return null return null
} }
private fun getVideoId(url: String): String? { private fun getVideoId(url: String): String? {
val path = Url(url).encodedPath.decodeURLPart() val path = URI(url).path
val id = path.substringAfter("/video/") val id = path.substringAfter("/video/")
return if (id.matches(videoIdRegex)) id else null return if (id.matches(videoIdRegex)) id else null
} }
@ -79,6 +82,7 @@ open class Dailymotion : ExtractorApi() {
return generateM3u8(name, streamLink, "").forEach(callback) return generateM3u8(name, streamLink, "").forEach(callback)
} }
data class MetaData( data class MetaData(
val qualities: Map<String, List<Quality>>?, val qualities: Map<String, List<Quality>>?,
val subtitles: SubtitlesWrapper? val subtitles: SubtitlesWrapper?
@ -98,4 +102,5 @@ open class Dailymotion : ExtractorApi() {
val label: String, val label: String,
val urls: List<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.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.newExtractorLink
import io.ktor.http.Url import java.net.URI
class Doodspro : DoodLaExtractor() { class Doodspro : DoodLaExtractor() {
override var mainUrl = "https://doods.pro" override var mainUrl = "https://doods.pro"
@ -138,6 +138,8 @@ open class DoodLaExtractor : ExtractorApi() {
} }
private fun getBaseUrl(url: String): String { 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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor
import io.ktor.http.Url import java.net.URI
class Techinmind: GDMirrorbot() { class Techinmind: GDMirrorbot() {
override var name = "Techinmind Cloud AIO" override var name = "Techinmind Cloud AIO"
@ -103,7 +103,7 @@ open class GDMirrorbot : ExtractorApi() {
} }
private fun getBaseUrl(url: String): String { 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.Qualities
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor
import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.newExtractorLink
import io.ktor.http.Url import java.net.URI
class HubCloud : ExtractorApi() { class HubCloud : ExtractorApi() {
override val name = "Hub-Cloud" override val name = "Hub-Cloud"
@ -24,7 +24,7 @@ class HubCloud : ExtractorApi() {
) { ) {
val tag = "HubCloud" val tag = "HubCloud"
val realUrl = url.takeIf { 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 } ?: return
val baseUrl=getBaseUrl(realUrl) val baseUrl=getBaseUrl(realUrl)
@ -161,7 +161,7 @@ class HubCloud : ExtractorApi() {
private fun getBaseUrl(url: String): String { private fun getBaseUrl(url: String): String {
return try { return try {
Url(url).let { "${it.protocol.name}://${it.host}" } URI(url).let { "${it.scheme}://${it.host}" }
} catch (_: Exception) { } catch (_: Exception) {
"" ""
} }

View file

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

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import io.ktor.http.Url import java.net.URI
open class Streamplay : ExtractorApi() { open class Streamplay : ExtractorApi() {
override val name = "Streamplay" override val name = "Streamplay"
@ -22,7 +22,9 @@ open class Streamplay : ExtractorApi() {
) { ) {
val request = app.get(url, referer = referer) val request = app.get(url, referer = referer)
val redirectUrl = request.url 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 key = redirectUrl.substringAfter("embed-").substringBefore(".html")
val token = val token =
request.document.select("script").find { it.data().contains("sitekey:") }?.data() request.document.select("script").find { it.data().contains("sitekey:") }?.data()
@ -77,4 +79,4 @@ open class Streamplay : ExtractorApi() {
@JsonProperty("label") val label: String? = null, @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.Qualities
import com.lagradost.cloudstream3.utils.fixUrl import com.lagradost.cloudstream3.utils.fixUrl
import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.newExtractorLink
import io.ktor.http.Url import java.net.URI
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ -84,7 +84,7 @@ open class VidStack : ExtractorApi() {
private fun getBaseUrl(url: String): String { private fun getBaseUrl(url: String): String {
return try { return try {
Url(url).let { "${it.protocol.name}://${it.host}" } URI(url).let { "${it.scheme}://${it.host}" }
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Vidstack", "getBaseUrl fallback: ${e.message}") Log.e("Vidstack", "getBaseUrl fallback: ${e.message}")
mainUrl mainUrl

View file

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

View file

@ -1,7 +1,7 @@
package com.lagradost.cloudstream3.extractors.helper package com.lagradost.cloudstream3.extractors.helper
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl import com.lagradost.cloudstream3.utils.StringUtils.decodeUri
import com.lagradost.cloudstream3.utils.StringUtils.encodeUrl 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 // 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 // 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() fun encode(input: String): String =
private fun decode(input: String): String = input.decodeUrl() input.encodeUri().replace("+", "%20")
private fun decode(input: String): String = input.decodeUri()
} }

View file

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

View file

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

View file

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

View file

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

View file

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