mirror of
https://github.com/recloudstream/cloudstream.git
synced 2026-06-19 20:05:41 +00:00
Compare commits
20 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c6bf2984e |
||
|
|
6f458fc9b5 |
||
|
|
b4100dbfca |
||
|
|
943bc551e9 |
||
|
|
c045bfdc0d |
||
|
|
2c03a3d976 |
||
|
|
3417fe0160 |
||
|
|
55450a02fa |
||
|
|
6f9646e52f |
||
|
|
b222911e29 |
||
|
|
5667f52648 |
||
|
|
b2a02a174f |
||
|
|
18a857723b |
||
|
|
292d3f1442 |
||
|
|
8012c58069 |
||
|
|
4f8a79669c |
||
|
|
2181243dd1 |
||
|
|
eae18bb50d |
||
|
|
f7cbf25b30 |
||
|
|
fd579fcc18 |
35 changed files with 422 additions and 277 deletions
|
|
@ -207,7 +207,6 @@ 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)
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class SerializationClassTester {
|
|||
}
|
||||
|
||||
// DEX files are the best solution to read all our classes dynamically.
|
||||
// ClassGraph() can be used instead, but it only gives results on the JVM, not Android.
|
||||
// classgraph could 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,7 +109,6 @@ class SerializationClassTester {
|
|||
.targetContext
|
||||
|
||||
val dexFile = DexFile(context.packageCodePath)
|
||||
|
||||
return dexFile.entries()
|
||||
.toList()
|
||||
.filter { it.startsWith(packageName) }
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ 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
|
||||
|
|
@ -65,6 +66,7 @@ object VideoClickActionHolder {
|
|||
MpvYTDLPackage(),
|
||||
MpvKtPackage(),
|
||||
MpvKtPreviewPackage(),
|
||||
OnlyPlayer(),
|
||||
MpvRxPackage(),
|
||||
// Always Ask option
|
||||
AlwaysAskAction(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
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 */
|
||||
}
|
||||
}
|
||||
|
|
@ -50,7 +50,8 @@ 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(),
|
||||
)
|
||||
|
|
@ -83,8 +84,8 @@ class AniListApi : SyncAPI() {
|
|||
return "$mainUrl/anime/$id"
|
||||
}
|
||||
|
||||
override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
|
||||
val data = searchShows(name) ?: return null
|
||||
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
|
||||
val data = searchShows(query) ?: return null
|
||||
return data.data?.page?.media?.map {
|
||||
SyncAPI.SyncSearchResult(
|
||||
it.title.romaji ?: return null,
|
||||
|
|
@ -96,7 +97,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
|
||||
|
|
@ -158,7 +159,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
|
||||
|
||||
|
|
@ -459,7 +460,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)
|
||||
|
|
@ -506,7 +507,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(
|
||||
|
|
@ -638,7 +639,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 ->
|
||||
|
|
@ -666,7 +667,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"
|
||||
|
||||
|
|
@ -714,7 +715,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 {
|
||||
|
|
@ -737,7 +738,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?,
|
||||
|
|
@ -786,7 +787,7 @@ class AniListApi : SyncAPI() {
|
|||
return data != ""
|
||||
}
|
||||
|
||||
private suspend fun getUser(token : AuthToken): AniListUser? {
|
||||
private suspend fun getUser(token: AuthToken): AniListUser? {
|
||||
val q = """
|
||||
{
|
||||
Viewer {
|
||||
|
|
|
|||
|
|
@ -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=$name&limit=$MAL_MAX_SEARCH_LIMIT"
|
||||
val url = "$apiUrl/v2/anime?q=$query&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)
|
||||
|
|
|
|||
|
|
@ -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 name)
|
||||
"$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to query)
|
||||
).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
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"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
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"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
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"
|
||||
|
|
|
|||
|
|
@ -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">Playerinformationen anzeigen</string>
|
||||
<string name="limit_title_rez">Zeige Playerinformationen</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">Cast-Panel zeigen</string>
|
||||
<string name="video_info">Medieninfo</string>
|
||||
<string name="show_cast_in_details">Zeige Cast-Panel</string>
|
||||
<string name="video_info">Mediainfo</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,4 +731,8 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -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 3 не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив!</string>
|
||||
<string name="download_all_plugins_from_repo">Предупредување: CloudStream не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив!</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,4 +705,37 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ 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
|
||||
|
|
@ -32,11 +31,12 @@ 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.2"
|
||||
newpipeextractor = "v0.26.3"
|
||||
nextlibMedia3 = "1.9.3-0.12.0"
|
||||
nicehttp = "0.4.18"
|
||||
overlappingpanels = "0.1.5"
|
||||
|
|
@ -69,7 +69,6 @@ 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" }
|
||||
|
|
@ -95,6 +94,7 @@ 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" }
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -15,12 +15,13 @@ 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(...)
|
||||
|
|
@ -211,7 +212,7 @@ actual class WebViewResolver actual constructor(
|
|||
* */
|
||||
return@runBlocking try {
|
||||
when {
|
||||
blacklistedFiles.any { URI(webViewUrl).path.contains(it) } || webViewUrl.endsWith(
|
||||
blacklistedFiles.any { Url(webViewUrl).encodedPath.decodeURLPart().contains(it) } || webViewUrl.endsWith(
|
||||
"/favicon.ico"
|
||||
) -> WebResourceResponse(
|
||||
"image/png",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ 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
|
||||
|
|
@ -35,11 +39,8 @@ 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
|
||||
|
|
@ -90,6 +91,7 @@ class ErrorLoadingException(message: String? = null) : Exception(message)
|
|||
@Prerelease
|
||||
val json = Json {
|
||||
encodeDefaults = true
|
||||
explicitNulls = false
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
|
|
@ -176,9 +178,9 @@ object APIHolder {
|
|||
// To get the key
|
||||
suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? {
|
||||
try {
|
||||
val uri = URI.create(url)
|
||||
val _url = Url(url)
|
||||
val domain = base64Encode(
|
||||
(uri.scheme + "://" + uri.host + ":443").encodeToByteArray(),
|
||||
(_url.protocol.name + "://" + _url.host + ":443").encodeToByteArray(),
|
||||
).replace("\n", "").replace("=", ".")
|
||||
|
||||
val vToken =
|
||||
|
|
@ -713,12 +715,10 @@ 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 {
|
||||
try {
|
||||
val original = URI(url)
|
||||
val updated = URI(mainUrl)
|
||||
return try {
|
||||
val original = Url(url)
|
||||
val updated = Url(mainUrl)
|
||||
|
||||
// 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()
|
||||
URLBuilder().apply {
|
||||
takeFrom(updated)
|
||||
user = original.user
|
||||
password = original.password
|
||||
encodedPath = original.encodedPath
|
||||
fragment = original.fragment
|
||||
|
||||
parameters.clear()
|
||||
parameters.appendAll(original.parameters)
|
||||
}.buildString()
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
return url
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1510,7 +1510,7 @@ constructor(
|
|||
|
||||
override var posterUrl: String? = null,
|
||||
var year: Int? = null,
|
||||
var dubStatus: EnumSet<DubStatus>? = null,
|
||||
var dubStatus: MutableSet<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) } ?: EnumSet.of(status)
|
||||
this.dubStatus = dubStatus?.also { it.add(status) } ?: mutableSetOf(status)
|
||||
if (this.type?.isMovieType() != true)
|
||||
if (episodes != null && episodes > 0)
|
||||
this.episodes[status] = episodes
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ 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 java.net.URI
|
||||
import io.ktor.http.Url
|
||||
import io.ktor.http.decodeURLPart
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
|
@ -45,11 +46,11 @@ open class ByseSX : ExtractorApi() {
|
|||
}
|
||||
|
||||
private fun getBaseUrl(url: String): String {
|
||||
return URI(url).let { "${it.scheme}://${it.host}" }
|
||||
return Url(url).let { "${it.protocol.name}://${it.host}" }
|
||||
}
|
||||
|
||||
private fun getCodeFromUrl(url: String): String {
|
||||
val path = URI(url).path ?: ""
|
||||
val path = Url(url).encodedPath.decodeURLPart()
|
||||
return path.trimEnd('/').substringAfterLast('/')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.decodeUri
|
||||
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
|
||||
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.decodeUri()
|
||||
a = a.decodeUrl()
|
||||
a = a.map { char ->
|
||||
if (char.code in 33..126) {
|
||||
return@map (33 + (char.code + 14) % 94).toChar().toString()
|
||||
|
|
|
|||
|
|
@ -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 okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import io.ktor.http.Url
|
||||
|
||||
// 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.toHttpUrl().encodedPath
|
||||
val videoId = Url(url).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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ 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 java.net.URI
|
||||
|
||||
|
||||
import io.ktor.http.Url
|
||||
import io.ktor.http.decodeURLPart
|
||||
|
||||
class Geodailymotion : Dailymotion() {
|
||||
override val name = "GeoDailymotion"
|
||||
|
|
@ -57,7 +56,6 @@ 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")) {
|
||||
|
|
@ -67,9 +65,8 @@ open class Dailymotion : ExtractorApi() {
|
|||
return null
|
||||
}
|
||||
|
||||
|
||||
private fun getVideoId(url: String): String? {
|
||||
val path = URI(url).path
|
||||
val path = Url(url).encodedPath.decodeURLPart()
|
||||
val id = path.substringAfter("/video/")
|
||||
return if (id.matches(videoIdRegex)) id else null
|
||||
}
|
||||
|
|
@ -82,7 +79,6 @@ open class Dailymotion : ExtractorApi() {
|
|||
return generateM3u8(name, streamLink, "").forEach(callback)
|
||||
}
|
||||
|
||||
|
||||
data class MetaData(
|
||||
val qualities: Map<String, List<Quality>>?,
|
||||
val subtitles: SubtitlesWrapper?
|
||||
|
|
@ -102,5 +98,4 @@ open class Dailymotion : ExtractorApi() {
|
|||
val label: String,
|
||||
val urls: List<String>
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -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 java.net.URI
|
||||
import io.ktor.http.Url
|
||||
|
||||
class Doodspro : DoodLaExtractor() {
|
||||
override var mainUrl = "https://doods.pro"
|
||||
|
|
@ -138,8 +138,6 @@ open class DoodLaExtractor : ExtractorApi() {
|
|||
}
|
||||
|
||||
private fun getBaseUrl(url: String): String {
|
||||
return URI(url).let {
|
||||
"${it.scheme}://${it.host}"
|
||||
}
|
||||
return Url(url).let { "${it.protocol.name}://${it.host}" }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
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
|
||||
)
|
||||
}
|
||||
|
|
@ -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 java.net.URI
|
||||
import io.ktor.http.Url
|
||||
|
||||
class Techinmind: GDMirrorbot() {
|
||||
override var name = "Techinmind Cloud AIO"
|
||||
|
|
@ -103,7 +103,7 @@ open class GDMirrorbot : ExtractorApi() {
|
|||
}
|
||||
|
||||
private fun getBaseUrl(url: String): String {
|
||||
return URI(url).let { "${it.scheme}://${it.host}" }
|
||||
return Url(url).let { "${it.protocol.name}://${it.host}" }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 java.net.URI
|
||||
import io.ktor.http.Url
|
||||
|
||||
class HubCloud : ExtractorApi() {
|
||||
override val name = "Hub-Cloud"
|
||||
|
|
@ -24,7 +24,7 @@ class HubCloud : ExtractorApi() {
|
|||
) {
|
||||
val tag = "HubCloud"
|
||||
val realUrl = url.takeIf {
|
||||
try { URI(it).toURL(); true } catch (e: Exception) { Log.e(tag, "Invalid URL: ${e.message}"); false }
|
||||
try { Url(it); 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 {
|
||||
URI(url).let { "${it.scheme}://${it.host}" }
|
||||
Url(url).let { "${it.protocol.name}://${it.host}" }
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.decodeUri
|
||||
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
|
||||
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.decodeUri().substringBeforeLast('.')
|
||||
val fileNameCleaned = fileName.decodeUrl().substringBeforeLast('.')
|
||||
"$fileNameCleaned ($fileExtension)"
|
||||
} else this.name
|
||||
callback(
|
||||
|
|
|
|||
|
|
@ -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 java.net.URI
|
||||
import io.ktor.http.Url
|
||||
|
||||
open class Streamplay : ExtractorApi() {
|
||||
override val name = "Streamplay"
|
||||
|
|
@ -22,9 +22,7 @@ open class Streamplay : ExtractorApi() {
|
|||
) {
|
||||
val request = app.get(url, referer = referer)
|
||||
val redirectUrl = request.url
|
||||
val mainServer = URI(redirectUrl).let {
|
||||
"${it.scheme}://${it.host}"
|
||||
}
|
||||
val mainServer = Url(redirectUrl).let { "${it.protocol.name}://${it.host}" }
|
||||
val key = redirectUrl.substringAfter("embed-").substringBefore(".html")
|
||||
val token =
|
||||
request.document.select("script").find { it.data().contains("sitekey:") }?.data()
|
||||
|
|
@ -79,4 +77,4 @@ open class Streamplay : ExtractorApi() {
|
|||
@JsonProperty("label") val label: String? = null,
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 java.net.URI
|
||||
import io.ktor.http.Url
|
||||
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 {
|
||||
URI(url).let { "${it.scheme}://${it.host}" }
|
||||
Url(url).let { "${it.protocol.name}://${it.host}" }
|
||||
} catch (e: Exception) {
|
||||
Log.e("Vidstack", "getBaseUrl fallback: ${e.message}")
|
||||
mainUrl
|
||||
|
|
|
|||
|
|
@ -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 uri = URI(iframeUrl)
|
||||
val mainUrl = "https://" + uri.host
|
||||
val url = Url(iframeUrl)
|
||||
val mainUrl = "https://${url.host}"
|
||||
|
||||
val encryptedId = cryptoHandler(id, foundIv, foundKey)
|
||||
val encryptRequestData = if (isUsingAdaptiveData) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.lagradost.cloudstream3.extractors.helper
|
||||
|
||||
import com.lagradost.cloudstream3.utils.StringUtils.decodeUri
|
||||
import com.lagradost.cloudstream3.utils.StringUtils.encodeUri
|
||||
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
|
||||
import com.lagradost.cloudstream3.utils.StringUtils.encodeUrl
|
||||
|
||||
// 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,8 +108,6 @@ object NineAnimeHelper {
|
|||
}
|
||||
}
|
||||
|
||||
fun encode(input: String): String =
|
||||
input.encodeUri().replace("+", "%20")
|
||||
|
||||
private fun decode(input: String): String = input.decodeUri()
|
||||
fun encode(input: String): String = input.encodeUrl()
|
||||
private fun decode(input: String): String = input.decodeUrl()
|
||||
}
|
||||
|
|
@ -60,8 +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>() }.getOrNull()
|
||||
?: json.serializersModule.getContextual(T::class)
|
||||
val serializer = runCatching { serializer<T>() }
|
||||
.recoverCatching { json.serializersModule.getContextual(T::class) }
|
||||
.getOrNull()
|
||||
|
||||
// Prefer Kotlin Serialization over Jackson
|
||||
if (serializer != null) {
|
||||
|
|
@ -69,6 +70,8 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ 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
|
||||
|
|
@ -312,11 +313,12 @@ 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
|
||||
|
|
@ -420,7 +422,7 @@ enum class ExtractorLinkType {
|
|||
|
||||
private fun inferTypeFromUrl(url: String): ExtractorLinkType {
|
||||
val path = try {
|
||||
URI(url).path
|
||||
Url(url).encodedPath.decodeURLPart()
|
||||
} catch (_: Throwable) {
|
||||
// don't log magnet links as errors
|
||||
null
|
||||
|
|
@ -819,7 +821,7 @@ constructor(
|
|||
|
||||
/**
|
||||
* Removes https:// and www.
|
||||
* To match urls regardless of schema, perhaps Uri() can be used?
|
||||
* To match urls regardless of schema, perhaps Url() can be used?
|
||||
*/
|
||||
val schemaStripRegex = Regex("""^(https:|)//(www\.|)""")
|
||||
|
||||
|
|
@ -1297,6 +1299,7 @@ val extractorApis: AtomicMutableList<ExtractorApi> = atomicListOf(
|
|||
GUpload(),
|
||||
HlsWish(),
|
||||
ByseQekaho(),
|
||||
Flyfile()
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -19,12 +19,11 @@
|
|||
*/
|
||||
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 {
|
||||
|
|
@ -276,29 +275,29 @@ object HlsPlaylistParser {
|
|||
}
|
||||
}
|
||||
|
||||
object UriUtil {
|
||||
fun resolveToUri(baseUri: String?, referenceUri: String?): URI {
|
||||
return URI.create(resolve(baseUri, referenceUri))
|
||||
object UrlUtil {
|
||||
fun resolveToUrl(baseUrl: String?, referenceUrl: String?): Url {
|
||||
return Url(resolve(baseUrl, referenceUrl))
|
||||
}
|
||||
|
||||
|
||||
/** The length of arrays returned by [.getUriIndices]. */
|
||||
/** The length of arrays returned by [.getUrlIndices]. */
|
||||
private
|
||||
const val INDEX_COUNT: Int = 4
|
||||
|
||||
/**
|
||||
* An index into an array returned by [.getUriIndices].
|
||||
* An index into an array returned by [.getUrlIndices].
|
||||
*
|
||||
*
|
||||
* The value at this position in the array is the index of the ':' after the scheme. Equals -1
|
||||
* if the URI is a relative reference (no scheme). The hier-part starts at (schemeColon + 1),
|
||||
* including when the URI has no scheme.
|
||||
* if the URL is a relative reference (no scheme). The hier-part starts at (schemeColon + 1),
|
||||
* including when the URL has no scheme.
|
||||
*/
|
||||
private
|
||||
const val SCHEME_COLON: Int = 0
|
||||
|
||||
/**
|
||||
* An index into an array returned by [.getUriIndices].
|
||||
* An index into an array returned by [.getUrlIndices].
|
||||
*
|
||||
*
|
||||
* The value at this position in the array is the index of the path part. Equals (schemeColon +
|
||||
|
|
@ -310,7 +309,7 @@ object HlsPlaylistParser {
|
|||
const val PATH: Int = 1
|
||||
|
||||
/**
|
||||
* An index into an array returned by [.getUriIndices].
|
||||
* An index into an array returned by [.getUrlIndices].
|
||||
*
|
||||
*
|
||||
* The value at this position in the array is the index of the query part, including the '?'
|
||||
|
|
@ -321,87 +320,87 @@ object HlsPlaylistParser {
|
|||
const val QUERY: Int = 2
|
||||
|
||||
/**
|
||||
* An index into an array returned by [.getUriIndices].
|
||||
* An index into an array returned by [.getUrlIndices].
|
||||
*
|
||||
*
|
||||
* 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 URI if no fragment part, and (length - 1) if
|
||||
* before the fragment. Equal to the length of the URL 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 `referenceUri` with respect to a `baseUri`.
|
||||
* Performs relative resolution of a `referenceUrl` with respect to a `baseUrl`.
|
||||
*
|
||||
*
|
||||
* The resolution is performed as specified by RFC-3986.
|
||||
*
|
||||
* @param baseUri The base URI.
|
||||
* @param referenceUri The reference URI to resolve.
|
||||
* @param baseUrl The base URL.
|
||||
* @param referenceUrl The reference URL to resolve.
|
||||
*/
|
||||
private fun resolve(baseUri: String?, referenceUri: String?): String {
|
||||
var baseUri = baseUri
|
||||
var referenceUri = referenceUri
|
||||
val uri = StringBuilder()
|
||||
private fun resolve(baseUrl: String?, referenceUrl: String?): String {
|
||||
var baseUrl = baseUrl
|
||||
var referenceUrl = referenceUrl
|
||||
val url = StringBuilder()
|
||||
|
||||
// Map null onto empty string, to make the following logic simpler.
|
||||
baseUri = baseUri ?: ""
|
||||
referenceUri = referenceUri ?: ""
|
||||
baseUrl = baseUrl ?: ""
|
||||
referenceUrl = referenceUrl ?: ""
|
||||
|
||||
val refIndices: IntArray = getUriIndices(referenceUri)
|
||||
val refIndices: IntArray = getUrlIndices(referenceUrl)
|
||||
if (refIndices[SCHEME_COLON] != -1) {
|
||||
// The reference is absolute. The target Uri is the reference.
|
||||
uri.append(referenceUri)
|
||||
removeDotSegments(uri, refIndices[PATH], refIndices[QUERY])
|
||||
return uri.toString()
|
||||
// The reference is absolute. The target Url is the reference.
|
||||
url.append(referenceUrl)
|
||||
removeDotSegments(url, refIndices[PATH], refIndices[QUERY])
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
val baseIndices: IntArray = getUriIndices(baseUri)
|
||||
val baseIndices: IntArray = getUrlIndices(baseUrl)
|
||||
if (refIndices[FRAGMENT] == 0) {
|
||||
// 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()
|
||||
// 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()
|
||||
}
|
||||
|
||||
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 uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString()
|
||||
return url.append(baseUrl, 0, baseIndices[QUERY]).append(referenceUrl).toString()
|
||||
}
|
||||
|
||||
if (refIndices[PATH] != 0) {
|
||||
// The reference has authority. The target is the base scheme plus the reference.
|
||||
val baseLimit = baseIndices[SCHEME_COLON] + 1
|
||||
uri.append(baseUri, 0, baseLimit).append(referenceUri)
|
||||
url.append(baseUrl, 0, baseLimit).append(referenceUrl)
|
||||
return removeDotSegments(
|
||||
uri,
|
||||
url,
|
||||
baseLimit + refIndices[PATH],
|
||||
baseLimit + refIndices[QUERY]
|
||||
)
|
||||
}
|
||||
|
||||
if (referenceUri[refIndices[PATH]] == '/') {
|
||||
if (referenceUrl[refIndices[PATH]] == '/') {
|
||||
// The reference path is rooted. The target is the base scheme and authority (if any), plus
|
||||
// the reference.
|
||||
uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri)
|
||||
url.append(baseUrl, 0, baseIndices[PATH]).append(referenceUrl)
|
||||
return removeDotSegments(
|
||||
uri,
|
||||
url,
|
||||
baseIndices[PATH],
|
||||
baseIndices[PATH] + refIndices[QUERY]
|
||||
)
|
||||
}
|
||||
|
||||
// The target Uri is the concatenation of the base Uri up to (but excluding) the last segment,
|
||||
// The target Url is the concatenation of the base Url 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.
|
||||
uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri)
|
||||
url.append(baseUrl, 0, baseIndices[PATH]).append('/').append(referenceUrl)
|
||||
return removeDotSegments(
|
||||
uri,
|
||||
url,
|
||||
baseIndices[PATH],
|
||||
baseIndices[PATH] + refIndices[QUERY] + 1
|
||||
)
|
||||
|
|
@ -410,22 +409,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 = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1)
|
||||
val lastSlashIndex = baseUrl.lastIndexOf('/', baseIndices[QUERY] - 1)
|
||||
val baseLimit = if (lastSlashIndex == -1) baseIndices[PATH] else lastSlashIndex + 1
|
||||
uri.append(baseUri, 0, baseLimit).append(referenceUri)
|
||||
return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY])
|
||||
url.append(baseUrl, 0, baseLimit).append(referenceUrl)
|
||||
return removeDotSegments(url, baseIndices[PATH], baseLimit + refIndices[QUERY])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes dot segments from the path of a URI.
|
||||
* Removes dot segments from the path of a 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`.
|
||||
* @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`.
|
||||
*/
|
||||
private fun removeDotSegments(
|
||||
uri: StringBuilder,
|
||||
url: StringBuilder,
|
||||
offset: Int,
|
||||
limit: Int
|
||||
): String {
|
||||
|
|
@ -433,9 +432,9 @@ object HlsPlaylistParser {
|
|||
var limit = limit
|
||||
if (offset >= limit) {
|
||||
// Nothing to do.
|
||||
return uri.toString()
|
||||
return url.toString()
|
||||
}
|
||||
if (uri[offset] == '/') {
|
||||
if (url[offset] == '/') {
|
||||
// If the path starts with a /, always retain it.
|
||||
offset++
|
||||
}
|
||||
|
|
@ -445,7 +444,7 @@ object HlsPlaylistParser {
|
|||
while (i <= limit) {
|
||||
val nextSegmentStart = if (i == limit) {
|
||||
i
|
||||
} else if (uri[i] == '/') {
|
||||
} else if (url[i] == '/') {
|
||||
i + 1
|
||||
} else {
|
||||
i++
|
||||
|
|
@ -453,16 +452,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 && uri[segmentStart] == '.') {
|
||||
if (i == segmentStart + 1 && url[segmentStart] == '.') {
|
||||
// Given "abc/def/./ghi", remove "./" to get "abc/def/ghi".
|
||||
uri.delete(segmentStart, nextSegmentStart)
|
||||
url.delete(segmentStart, nextSegmentStart)
|
||||
limit -= nextSegmentStart - segmentStart
|
||||
i = segmentStart
|
||||
} else if (i == segmentStart + 2 && uri[segmentStart] == '.' && uri[segmentStart + 1] == '.') {
|
||||
} else if (i == segmentStart + 2 && url[segmentStart] == '.' && url[segmentStart + 1] == '.') {
|
||||
// Given "abc/def/../ghi", remove "def/../" to get "abc/ghi".
|
||||
val prevSegmentStart = uri.lastIndexOf("/", segmentStart - 2) + 1
|
||||
val prevSegmentStart = url.lastIndexOf("/", segmentStart - 2) + 1
|
||||
val removeFrom = if (prevSegmentStart > offset) prevSegmentStart else offset
|
||||
uri.delete(removeFrom, nextSegmentStart)
|
||||
url.delete(removeFrom, nextSegmentStart)
|
||||
limit -= nextSegmentStart - removeFrom
|
||||
segmentStart = prevSegmentStart
|
||||
i = prevSegmentStart
|
||||
|
|
@ -471,41 +470,41 @@ object HlsPlaylistParser {
|
|||
segmentStart = i
|
||||
}
|
||||
}
|
||||
return uri.toString()
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates indices of the constituent components of a URI.
|
||||
* Calculates indices of the constituent components of a URL.
|
||||
*
|
||||
* @param uriString The URI as a string.
|
||||
* @param urlString The URL as a string.
|
||||
* @return The corresponding indices.
|
||||
*/
|
||||
private fun getUriIndices(uriString: String?): IntArray {
|
||||
private fun getUrlIndices(urlString: String?): IntArray {
|
||||
val indices = IntArray(INDEX_COUNT)
|
||||
if (uriString.isNullOrEmpty()) {
|
||||
if (urlString.isNullOrEmpty()) {
|
||||
indices[SCHEME_COLON] = -1
|
||||
return indices
|
||||
}
|
||||
|
||||
// Determine outer structure from right to left.
|
||||
// Uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
|
||||
val length = uriString.length
|
||||
var fragmentIndex = uriString.indexOf('#')
|
||||
// Url = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
|
||||
val length = urlString.length
|
||||
var fragmentIndex = urlString.indexOf('#')
|
||||
if (fragmentIndex == -1) {
|
||||
fragmentIndex = length
|
||||
}
|
||||
var queryIndex = uriString.indexOf('?')
|
||||
var queryIndex = urlString.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 = uriString.indexOf('/')
|
||||
var schemeIndexLimit = urlString.indexOf('/')
|
||||
if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) {
|
||||
schemeIndexLimit = queryIndex
|
||||
}
|
||||
var schemeIndex = uriString.indexOf(':')
|
||||
var schemeIndex = urlString.indexOf(':')
|
||||
if (schemeIndex > schemeIndexLimit) {
|
||||
// '/' before ':'
|
||||
schemeIndex = -1
|
||||
|
|
@ -514,10 +513,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 && uriString[schemeIndex + 1] == '/' && uriString[schemeIndex + 2] == '/'
|
||||
schemeIndex + 2 < queryIndex && urlString[schemeIndex + 1] == '/' && urlString[schemeIndex + 2] == '/'
|
||||
var pathIndex: Int
|
||||
if (hasAuthority) {
|
||||
pathIndex = uriString.indexOf('/', schemeIndex + 3) // find first '/' after "://"
|
||||
pathIndex = urlString.indexOf('/', schemeIndex + 3) // find first '/' after "://"
|
||||
if (pathIndex == -1 || pathIndex > queryIndex) {
|
||||
pathIndex = queryIndex
|
||||
}
|
||||
|
|
@ -806,7 +805,7 @@ object HlsPlaylistParser {
|
|||
|
||||
const val APPLICATION_MEDIA3_CUES: String = "$BASE_TYPE_APPLICATION/x-media3-cues"
|
||||
|
||||
/** MIME type for an image URI loaded from an external image management framework. */
|
||||
/** MIME type for an image URL loaded from an external image management framework. */
|
||||
const val APPLICATION_EXTERNALLY_LOADED_IMAGE: String = "$BASE_TYPE_APPLICATION/x-image-uri"
|
||||
|
||||
|
||||
|
|
@ -1169,7 +1168,6 @@ 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>
|
||||
|
|
@ -1177,11 +1175,11 @@ object HlsPlaylistParser {
|
|||
val keyFormatVersions =
|
||||
parseOptionalStringAttr(line, REGEX_KEYFORMATVERSIONS, "1", variableDefinitions)
|
||||
if (KEYFORMAT_WIDEVINE_PSSH_BINARY == keyFormat) {
|
||||
val uriString = parseStringAttr(line, REGEX_URI, variableDefinitions)
|
||||
val urlString = parseStringAttr(line, REGEX_URI, variableDefinitions)
|
||||
return SchemeData(
|
||||
uuid = C.WIDEVINE_UUID,
|
||||
mimeType = MimeTypes.VIDEO_MP4,
|
||||
data = Base64.Default.decode(uriString.substring(uriString.indexOf(',')))
|
||||
data = base64DecodeArray(urlString.substring(urlString.indexOf(',')))
|
||||
)
|
||||
} else if (KEYFORMAT_WIDEVINE_PSSH_JSON == keyFormat) {
|
||||
return SchemeData(
|
||||
|
|
@ -1190,9 +1188,9 @@ object HlsPlaylistParser {
|
|||
data = line.encodeToByteArray()
|
||||
)
|
||||
} else if (KEYFORMAT_PLAYREADY == keyFormat && "1" == keyFormatVersions) {
|
||||
val uriString = parseStringAttr(line, REGEX_URI, variableDefinitions)
|
||||
val urlString = parseStringAttr(line, REGEX_URI, variableDefinitions)
|
||||
val data: ByteArray =
|
||||
Base64.Default.decode(uriString.substring(uriString.indexOf(',')))
|
||||
base64DecodeArray(urlString.substring(urlString.indexOf(',')))
|
||||
val psshData: ByteArray =
|
||||
PsshAtomUtil.buildPsshAtom(
|
||||
systemId = C.PLAYREADY_UUID,
|
||||
|
|
@ -1270,7 +1268,7 @@ object HlsPlaylistParser {
|
|||
}
|
||||
|
||||
data class Variant(
|
||||
val url: URI,
|
||||
val url: Url,
|
||||
val format: Format,
|
||||
val videoGroupId: String?,
|
||||
val audioGroupId: String?,
|
||||
|
|
@ -1323,7 +1321,7 @@ object HlsPlaylistParser {
|
|||
|
||||
data class Rendition(
|
||||
/** The rendition's url, or null if the tag does not have a URI attribute. */
|
||||
val url: URI?,
|
||||
val url: Url?,
|
||||
|
||||
/** Format information associated with this rendition. */
|
||||
val format: Format,
|
||||
|
|
@ -1336,14 +1334,14 @@ object HlsPlaylistParser {
|
|||
)
|
||||
|
||||
data class HlsMultivariantPlaylist(
|
||||
/** The base uri. Used to resolve relative paths. */
|
||||
/** The base url. 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<URI>,
|
||||
//val mediaPlaylistUrls: List<Url>,
|
||||
|
||||
/** The variants declared by the playlist. */
|
||||
val variants: List<Variant>,
|
||||
|
|
@ -1729,8 +1727,8 @@ object HlsPlaylistParser {
|
|||
private fun parseMultivariantPlaylist(
|
||||
iterator: Iterator<String>, baseUri: String
|
||||
): HlsMultivariantPlaylist {
|
||||
val urlToVariantInfos: HashMap<URI, ArrayList<VariantInfo>?> =
|
||||
HashMap<URI, ArrayList<VariantInfo>?>()
|
||||
val urlToVariantInfos: HashMap<Url, ArrayList<VariantInfo>?> =
|
||||
HashMap<Url, ArrayList<VariantInfo>?>()
|
||||
val variableDefinitions = HashMap<String, String>()
|
||||
val variants: ArrayList<Variant> = ArrayList<Variant>()
|
||||
val videos: ArrayList<Rendition> = ArrayList<Rendition>()
|
||||
|
|
@ -1853,10 +1851,10 @@ object HlsPlaylistParser {
|
|||
parseOptionalStringAttr(line, REGEX_SUBTITLES, variableDefinitions)
|
||||
val closedCaptionsGroupId: String? =
|
||||
parseOptionalStringAttr(line, REGEX_CLOSED_CAPTIONS, variableDefinitions)
|
||||
val uri: URI
|
||||
val url: Url
|
||||
if (isIFrameOnlyVariant) {
|
||||
uri =
|
||||
UriUtil.resolveToUri(
|
||||
url =
|
||||
UrlUtil.resolveToUrl(
|
||||
baseUri,
|
||||
parseStringAttr(line, REGEX_URI, variableDefinitions)
|
||||
)
|
||||
|
|
@ -1865,14 +1863,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 URI.
|
||||
// The following line contains #EXT-X-STREAM-INF's URL.
|
||||
line = replaceVariableReferences(iterator.next(), variableDefinitions)
|
||||
uri = UriUtil.resolveToUri(baseUri, line)
|
||||
url = UrlUtil.resolveToUrl(baseUri, line)
|
||||
}
|
||||
|
||||
val variant =
|
||||
Variant(
|
||||
url = uri,
|
||||
url = url,
|
||||
format = Format(
|
||||
id = variants.size.toString(),
|
||||
containerMimeType = MimeTypes.APPLICATION_M3U8,
|
||||
|
|
@ -1890,10 +1888,10 @@ object HlsPlaylistParser {
|
|||
captionGroupId = closedCaptionsGroupId
|
||||
)
|
||||
variants.add(variant)
|
||||
var variantInfosForUrl: ArrayList<VariantInfo>? = urlToVariantInfos[uri]
|
||||
var variantInfosForUrl: ArrayList<VariantInfo>? = urlToVariantInfos[url]
|
||||
if (variantInfosForUrl == null) {
|
||||
variantInfosForUrl = ArrayList()
|
||||
urlToVariantInfos[uri] = variantInfosForUrl
|
||||
urlToVariantInfos[url] = variantInfosForUrl
|
||||
}
|
||||
variantInfosForUrl.add(
|
||||
VariantInfo(
|
||||
|
|
@ -1911,7 +1909,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<URI>()
|
||||
val urlsInDeduplicatedVariants = HashSet<Url>()
|
||||
for (i in variants.indices) {
|
||||
val variant: Variant = variants[i]
|
||||
if (urlsInDeduplicatedVariants.add(variant.url)) {
|
||||
|
|
@ -1945,10 +1943,10 @@ object HlsPlaylistParser {
|
|||
containerMimeType = MimeTypes.APPLICATION_M3U8,
|
||||
)
|
||||
|
||||
val referenceUri: String? =
|
||||
val referenceUrl: String? =
|
||||
parseOptionalStringAttr(line, REGEX_URI, variableDefinitions)
|
||||
val uri: URI? =
|
||||
if (referenceUri == null) null else UriUtil.resolveToUri(baseUri, referenceUri)
|
||||
val url: Url? =
|
||||
if (referenceUrl == null) null else UrlUtil.resolveToUrl(baseUri, referenceUrl)
|
||||
//val metadata =
|
||||
// Metadata(HlsTrackMetadataEntry(groupId, name, emptyList<T>()))
|
||||
when (parseStringAttr(line, REGEX_TYPE, variableDefinitions)) {
|
||||
|
|
@ -1963,11 +1961,11 @@ object HlsPlaylistParser {
|
|||
codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO)
|
||||
)
|
||||
}
|
||||
if (uri == null) {
|
||||
// TODO: Remove this case and add a Rendition with a null uri to videos.
|
||||
if (url == null) {
|
||||
// TODO: Remove this case and add a Rendition with a null url to videos.
|
||||
} else {
|
||||
//formatBuilder.setMetadata(metadata)
|
||||
videos.add(Rendition(url = uri, format = formatBuilder, groupId, name))
|
||||
videos.add(Rendition(url = url, format = formatBuilder, groupId, name))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1995,11 +1993,11 @@ object HlsPlaylistParser {
|
|||
}
|
||||
}
|
||||
val format = formatBuilder.copy(sampleMimeType = sampleMimeType)
|
||||
if (uri != null) {
|
||||
if (url != null) {
|
||||
//formatBuilder.setMetadata(metadata)
|
||||
audios.add(Rendition(uri, format, groupId, name))
|
||||
audios.add(Rendition(url, format, groupId, name))
|
||||
} else if (variant != null) {
|
||||
// TODO: Remove muxedAudioFormat and add a Rendition with a null uri to audios.
|
||||
// TODO: Remove muxedAudioFormat and add a Rendition with a null url to audios.
|
||||
muxedAudioFormat = format
|
||||
}
|
||||
}
|
||||
|
|
@ -2018,10 +2016,10 @@ object HlsPlaylistParser {
|
|||
if (sampleMimeType == null) {
|
||||
sampleMimeType = MimeTypes.TEXT_VTT
|
||||
}
|
||||
if (uri != null) {
|
||||
if (url != null) {
|
||||
subtitles.add(
|
||||
Rendition(
|
||||
uri,
|
||||
url,
|
||||
formatBuilder.copy(sampleMimeType = sampleMimeType),
|
||||
groupId,
|
||||
name
|
||||
|
|
|
|||
|
|
@ -112,8 +112,8 @@ object M3u8Helper2 {
|
|||
return c.doFinal(data)
|
||||
}
|
||||
|
||||
private fun getParentLink(uri: String): String {
|
||||
val split = uri.split("/").toMutableList()
|
||||
private fun getParentLink(url: String): String {
|
||||
val split = url.split("/").toMutableList()
|
||||
split.removeAt(split.lastIndex)
|
||||
return split.joinToString("/")
|
||||
}
|
||||
|
|
@ -322,15 +322,15 @@ object M3u8Helper2 {
|
|||
|
||||
if (!match.isNullOrEmpty()) {
|
||||
encryptionState = true
|
||||
var encryptionUri = match[2]
|
||||
var encryptionUrl = match[2]
|
||||
|
||||
if (isNotCompleteUrl(encryptionUri)) {
|
||||
encryptionUri = "${getParentLink(playlistStream.streamUrl)}/$encryptionUri"
|
||||
if (isNotCompleteUrl(encryptionUrl)) {
|
||||
encryptionUrl = "${getParentLink(playlistStream.streamUrl)}/$encryptionUrl"
|
||||
}
|
||||
|
||||
encryptionIv = match[3].encodeToByteArray()
|
||||
val encryptionKeyResponse =
|
||||
app.get(encryptionUri, headers = playlistStream.headers, verify = false)
|
||||
app.get(encryptionUrl, headers = playlistStream.headers, verify = false)
|
||||
val body = encryptionKeyResponse.body
|
||||
encryptionData = body.bytes()
|
||||
body.close()
|
||||
|
|
|
|||
|
|
@ -1,14 +1,32 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import com.lagradost.cloudstream3.Prerelease
|
||||
import io.ktor.http.decodeURLQueryComponent
|
||||
import io.ktor.http.encodeURLParameter
|
||||
|
||||
object StringUtils {
|
||||
fun String.encodeUri(): String {
|
||||
return URLEncoder.encode(this, "UTF-8")
|
||||
@Prerelease
|
||||
fun String.decodeUrl(): String {
|
||||
return this.decodeURLQueryComponent()
|
||||
}
|
||||
|
||||
fun String.decodeUri(): String {
|
||||
return URLDecoder.decode(this, "UTF-8")
|
||||
@Prerelease
|
||||
fun String.encodeUrl(): String {
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.decodeUri
|
||||
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
|
||||
import com.lagradost.nicehttp.NiceResponse
|
||||
import java.net.URI
|
||||
import io.ktor.http.Url
|
||||
|
||||
// Code heavily based on unshortenit.py form kodiondemand /addon
|
||||
|
||||
|
|
@ -48,8 +48,8 @@ object ShortLink {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun unshorten(uri: String, type: String? = null): String {
|
||||
var currentUrl = uri
|
||||
suspend fun unshorten(url: String, type: String? = null): String {
|
||||
var currentUrl = url
|
||||
|
||||
val visitedUrls = mutableSetOf<String>()
|
||||
var count = 10
|
||||
|
|
@ -57,9 +57,7 @@ object ShortLink {
|
|||
visitedUrls += currentUrl
|
||||
count -= 1
|
||||
|
||||
val domain =
|
||||
URI(currentUrl.trim()).host
|
||||
?: throw IllegalArgumentException("No domain found in URI!")
|
||||
val domain = Url(currentUrl.trim()).host
|
||||
currentUrl = shortList.firstOrNull {
|
||||
it.regex.find(domain) != null || type == it.type
|
||||
}?.function?.let { it(currentUrl) } ?: break
|
||||
|
|
@ -67,8 +65,8 @@ object ShortLink {
|
|||
return currentUrl.trim()
|
||||
}
|
||||
|
||||
suspend fun unshortenAdfly(uri: String): String {
|
||||
val html = app.get(uri).text
|
||||
suspend fun unshortenAdfly(url: String): String {
|
||||
val html = app.get(url).text
|
||||
val ysmm = Regex("""var ysmm =.*;?""").find(html)!!.value
|
||||
|
||||
if (ysmm.isNotEmpty()) {
|
||||
|
|
@ -81,46 +79,46 @@ object ShortLink {
|
|||
left += c[0]
|
||||
right = c[1] + right
|
||||
}
|
||||
val encodedUri = (left + right).toMutableList()
|
||||
val encodedUrl = (left + right).toMutableList()
|
||||
val numbers =
|
||||
encodedUri.mapIndexed { i, n -> Pair(i, n) }.filter { it.second.isDigit() }
|
||||
encodedUrl.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) {
|
||||
encodedUri[el[0].first] = xor.digitToChar()
|
||||
encodedUrl[el[0].first] = xor.digitToChar()
|
||||
}
|
||||
}
|
||||
val encodedbytearray = encodedUri.map { it.code.toByte() }.toByteArray()
|
||||
var decodedUri =
|
||||
val encodedbytearray = encodedUrl.map { it.code.toByte() }.toByteArray()
|
||||
var decodedUrl =
|
||||
base64Decode(encodedbytearray.toString()).dropLast(16)
|
||||
.drop(16)
|
||||
|
||||
if (Regex("""go\.php\?u=""").find(decodedUri) != null) {
|
||||
decodedUri =
|
||||
base64Decode(decodedUri.replace(Regex("""(.*?)u="""), ""))
|
||||
if (Regex("""go\.php\?u=""").find(decodedUrl) != null) {
|
||||
decodedUrl =
|
||||
base64Decode(decodedUrl.replace(Regex("""(.*?)u="""), ""))
|
||||
}
|
||||
|
||||
return decodedUri
|
||||
return decodedUrl
|
||||
} else {
|
||||
return uri
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unshortenLinkup(uri: String): String {
|
||||
suspend fun unshortenLinkup(url: String): String {
|
||||
var r: NiceResponse? = null
|
||||
var uri = uri
|
||||
var url = url
|
||||
when {
|
||||
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("/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("/speedx/") -> uri =
|
||||
uri.replace("http://linkup.pro/speedx", "http://speedvideo.net")
|
||||
url.contains("/speedx/") -> url =
|
||||
url.replace("http://linkup.pro/speedx", "http://speedvideo.net")
|
||||
|
||||
else -> {
|
||||
r = app.get(uri, allowRedirects = true)
|
||||
uri = r.url
|
||||
r = app.get(url, allowRedirects = true)
|
||||
url = r.url
|
||||
val link =
|
||||
Regex("<iframe[^<>]*src=\\'([^'>]*)\\'[^<>]*>").find(r.text)?.value
|
||||
?: Regex("""action="(?:[^/]+.*?/[^/]+/([a-zA-Z0-9_]+))">""").find(r.text)?.value
|
||||
|
|
@ -128,40 +126,40 @@ object ShortLink {
|
|||
.elementAtOrNull(1)?.groupValues?.get(1)
|
||||
|
||||
if (link != null) {
|
||||
uri = link
|
||||
url = link
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val short = Regex("""^https?://.*?(https?://.*)""").find(uri)?.value
|
||||
val short = Regex("""^https?://.*?(https?://.*)""").find(url)?.value
|
||||
if (short != null) {
|
||||
uri = short
|
||||
url = short
|
||||
}
|
||||
if (r == null) {
|
||||
r = app.get(
|
||||
uri,
|
||||
url,
|
||||
allowRedirects = false
|
||||
)
|
||||
if (r.headers["location"] != null) {
|
||||
uri = r.headers["location"].toString()
|
||||
url = r.headers["location"].toString()
|
||||
}
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
return uri
|
||||
return url
|
||||
}
|
||||
|
||||
fun unshortenLinksafe(uri: String): String {
|
||||
return base64Decode(uri.split("?url=").last())
|
||||
fun unshortenLinksafe(url: String): String {
|
||||
return base64Decode(url.split("?url=").last())
|
||||
}
|
||||
|
||||
suspend fun unshortenNuovoIndirizzo(uri: String): String {
|
||||
val soup = app.get(uri, allowRedirects = true)
|
||||
suspend fun unshortenNuovoIndirizzo(url: String): String {
|
||||
val soup = app.get(url, allowRedirects = true)
|
||||
val header = soup.headers["refresh"]
|
||||
val link: String = if (header != null) {
|
||||
soup.headers["refresh"]!!.substringAfter("=")
|
||||
|
|
@ -171,29 +169,29 @@ object ShortLink {
|
|||
return link
|
||||
}
|
||||
|
||||
suspend fun unshortenNuovoLink(uri: String): String {
|
||||
return app.get(uri, allowRedirects = true).document.selectFirst("a")!!.attr("href")
|
||||
suspend fun unshortenNuovoLink(url: String): String {
|
||||
return app.get(url, allowRedirects = true).document.selectFirst("a")!!.attr("href")
|
||||
|
||||
}
|
||||
|
||||
suspend fun unshortenUprot(uri: String): String {
|
||||
val page = app.get(uri).text
|
||||
suspend fun unshortenUprot(url: String): String {
|
||||
val page = app.get(url).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 != uri) {
|
||||
if (link.contains("https://maxstream.video") || link.contains("https://uprot.net") && link != url) {
|
||||
return link
|
||||
}
|
||||
}
|
||||
return uri
|
||||
return url
|
||||
}
|
||||
|
||||
fun unshortenDavisonbarker(uri: String): String {
|
||||
return uri.substringAfter("dest=").decodeUri()
|
||||
fun unshortenDavisonbarker(url: String): String {
|
||||
return url.substringAfter("dest=").decodeUrl()
|
||||
}
|
||||
|
||||
suspend fun unshortenIsecure(uri: String): String {
|
||||
val doc = app.get(uri).document
|
||||
return doc.selectFirst("iframe")?.attr("src")?.trim() ?: uri
|
||||
suspend fun unshortenIsecure(url: String): String {
|
||||
val doc = app.get(url).document
|
||||
return doc.selectFirst("iframe")?.attr("src")?.trim() ?: url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue