Compare commits

...

20 commits

Author SHA1 Message Date
KingLucius
3c6bf2984e
SyncApi Search query fix (#2932) 2026-06-19 00:06:46 +02:00
Luna712
6f458fc9b5
Remove unused classgraph dependency (#2924) 2026-06-17 23:41:02 +00:00
Bnyro
b4100dbfca
feat(extractors): add flyfile.app extractor (#2925) 2026-06-17 23:40:30 +00:00
Luna712
943bc551e9
[skip ci] HlsPlaylistParser: use base64DecodeArray (#2929) 2026-06-17 23:34:25 +00:00
Luna712
c045bfdc0d
[skip ci] MainAPI: remove @OptIn(ExperimentalEncodingApi::class) (#2930)
It is stable since Kotlin 2.2.0
2026-06-17 23:03:28 +00:00
firelight
2c03a3d976
fix gradient (#2912) 2026-06-13 02:59:02 +02:00
firelight
3417fe0160
Feat: OnlyPlayer (#2905) 2026-06-13 02:58:43 +02:00
firelight
55450a02fa
Merge pull request #2904 from recloudstream/mpvrx
Feat: MpvRx
2026-06-13 02:56:45 +02:00
Osten
6f9646e52f
Fix one last issue in JSON parsing :I 2026-06-11 15:06:23 +02:00
Luna712
b222911e29
Fix parseJson inline on stable once again! (#2908) 2026-06-11 12:29:29 +02:00
firelight
5667f52648
Translated using Weblate (German) (#2876)
Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (German)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Macedonian)

Currently translated at 100.0% (729 of 729 strings)





Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/
Translation: Cloudstream/App

Co-authored-by: Deleted User <pandoroo@users.noreply.hosted.weblate.org>
Co-authored-by: WertZuz <97708601+wertzuz@users.noreply.github.com>
Co-authored-by: stojkovskistefan <stefanstojkovski@gmail.com>
2026-06-11 02:28:26 +02:00
Hosted Weblate
b2a02a174f
Merge remote-tracking branch 'origin/master' 2026-06-11 02:26:56 +02:00
Luna712
18a857723b
Replace java.net.uri in library (#2839) 2026-06-11 00:26:49 +00:00
Hosted Weblate
292d3f1442
Merge remote-tracking branch 'origin/master' 2026-06-11 02:24:56 +02:00
Luna712
8012c58069
Set explicitNulls = false for kotlinx serialization (#2897)
This is more inline with Jackson's behavior otherwise for nullable types with non default values, and they don't exist, gives a Missingfield exception whereas it worked on Jackson. We may need coerceInputValues = true also but I am unsure of that right now.
2026-06-11 00:24:50 +00:00
Hosted Weblate
4f8a79669c
Merge remote-tracking branch 'origin/master' 2026-06-11 02:09:06 +02:00
Luna712
2181243dd1
Bump NewPipeExtractor to fix trailers and other YouTube videos (#2906) 2026-06-11 00:08:58 +00:00
Hosted Weblate
eae18bb50d
Merge remote-tracking branch 'origin/master' 2026-06-09 21:03:54 +00:00
Luna712
f7cbf25b30
Replace EnumSet for dubStatus (#2845) 2026-06-09 23:03:48 +02:00
Hosted Weblate
fd579fcc18
Translated using Weblate (German)
Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (German)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Macedonian)

Currently translated at 100.0% (729 of 729 strings)

Co-authored-by: Deleted User <pandoroo@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: WertZuz <97708601+wertzuz@users.noreply.github.com>
Co-authored-by: stojkovskistefan <stefanstojkovski@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/
Translation: Cloudstream/App
2026-06-08 22:22:51 +02:00
36 changed files with 499 additions and 277 deletions

View file

@ -207,7 +207,6 @@ 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() 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") @Suppress("DEPRECATION")
private fun findSerializableClasses(packageName: String): List<KClass<*>> { private fun findSerializableClasses(packageName: String): List<KClass<*>> {
val context = InstrumentationRegistry val context = InstrumentationRegistry
@ -109,7 +109,6 @@ 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

@ -20,8 +20,10 @@ import com.lagradost.cloudstream3.actions.temp.MpvExPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPackage import com.lagradost.cloudstream3.actions.temp.MpvKtPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage
import com.lagradost.cloudstream3.actions.temp.MpvPackage 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.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
@ -64,6 +66,8 @@ object VideoClickActionHolder {
MpvYTDLPackage(), MpvYTDLPackage(),
MpvKtPackage(), MpvKtPackage(),
MpvKtPreviewPackage(), MpvKtPreviewPackage(),
OnlyPlayer(),
MpvRxPackage(),
// Always Ask option // Always Ask option
AlwaysAskAction(), AlwaysAskAction(),
// added by plugins // added by plugins

View file

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

View file

@ -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 */
}
}

View file

@ -50,7 +50,8 @@ 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"] ?: throw ErrorLoadingException("No access token"), accessToken = sanitizer["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(),
) )
@ -83,8 +84,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(name) ?: return null val data = searchShows(query) ?: 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,
@ -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) 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
@ -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 internalId = id.toIntOrNull() ?: return null
val data = getDataAboutId(auth ?: return null, internalId) ?: 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 = 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)
@ -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( return app.post(
"https://graphql.anilist.co/", "https://graphql.anilist.co/",
headers = mapOf( 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 { val list = getAniListAnimeListSmart(auth ?: return null)?.groupBy {
convertAniListStringToStatus(it.status ?: "").stringRes convertAniListStringToStatus(it.status ?: "").stringRes
}?.mapValues { group -> }?.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 userID = auth.user.id
val mediaType = "ANIME" val mediaType = "ANIME"
@ -714,7 +715,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 {
@ -737,7 +738,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?,
@ -786,7 +787,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=$name&limit=$MAL_MAX_SEARCH_LIMIT" val url = "$apiUrl/v2/anime?q=$query&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 name) "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to query)
).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() } ).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() }
} }

View file

@ -11,6 +11,7 @@
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,6 +12,7 @@
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,6 +11,7 @@
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">Playerinformationen anzeigen</string> <string name="limit_title_rez">Zeige Playerinformationen</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">Cast-Panel zeigen</string> <string name="show_cast_in_details">Zeige Cast-Panel</string>
<string name="video_info">Medieninfo</string> <string name="video_info">Mediainfo</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,4 +731,8 @@
<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 3 не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив!</string> <string name="download_all_plugins_from_repo">Предупредување: CloudStream не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив!</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,4 +705,37 @@
<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,7 +8,6 @@ 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
@ -32,11 +31,12 @@ 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.2" newpipeextractor = "v0.26.3"
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,7 +69,6 @@ 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" }
@ -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-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,6 +61,7 @@ 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,12 +15,13 @@ 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(...)
@ -211,7 +212,7 @@ actual class WebViewResolver actual constructor(
* */ * */
return@runBlocking try { return@runBlocking try {
when { when {
blacklistedFiles.any { URI(webViewUrl).path.contains(it) } || webViewUrl.endsWith( blacklistedFiles.any { Url(webViewUrl).encodedPath.decodeURLPart().contains(it) } || webViewUrl.endsWith(
"/favicon.ico" "/favicon.ico"
) -> WebResourceResponse( ) -> WebResourceResponse(
"image/png", "image/png",

View file

@ -22,6 +22,10 @@ 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
@ -35,11 +39,8 @@ 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
@ -90,6 +91,7 @@ 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
} }
@ -176,9 +178,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 uri = URI.create(url) val _url = Url(url)
val domain = base64Encode( val domain = base64Encode(
(uri.scheme + "://" + uri.host + ":443").encodeToByteArray(), (_url.protocol.name + "://" + _url.host + ":443").encodeToByteArray(),
).replace("\n", "").replace("=", ".") ).replace("\n", "").replace("=", ".")
val vToken = val vToken =
@ -713,12 +715,10 @@ 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 {
try { return try {
val original = URI(url) val original = Url(url)
val updated = URI(mainUrl) val updated = Url(mainUrl)
// URI(String scheme, String userInfo, String host, int port, String path, String query, String fragment) URLBuilder().apply {
return URI( takeFrom(updated)
updated.scheme, user = original.user
original.userInfo, password = original.password
updated.host, encodedPath = original.encodedPath
updated.port, fragment = original.fragment
original.path,
original.query, parameters.clear()
original.fragment parameters.appendAll(original.parameters)
).toString() }.buildString()
} catch (t: Throwable) { } catch (t: Throwable) {
logError(t) logError(t)
return url 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: EnumSet<DubStatus>? = null, var dubStatus: MutableSet<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) } ?: EnumSet.of(status) this.dubStatus = dubStatus?.also { it.add(status) } ?: mutableSetOf(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,7 +8,8 @@ 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 java.net.URI import io.ktor.http.Url
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
@ -45,11 +46,11 @@ open class ByseSX : ExtractorApi() {
} }
private fun getBaseUrl(url: String): String { 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 { private fun getCodeFromUrl(url: String): String {
val path = URI(url).path ?: "" val path = Url(url).encodedPath.decodeURLPart()
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.decodeUri import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
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.decodeUri() a = a.decodeUrl()
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 okhttp3.HttpUrl.Companion.toHttpUrl import io.ktor.http.Url
// 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.toHttpUrl().encodedPath val videoId = Url(url).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

View file

@ -7,9 +7,8 @@ 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 java.net.URI import io.ktor.http.Url
import io.ktor.http.decodeURLPart
class Geodailymotion : Dailymotion() { class Geodailymotion : Dailymotion() {
override val name = "GeoDailymotion" override val name = "GeoDailymotion"
@ -57,7 +56,6 @@ 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")) {
@ -67,9 +65,8 @@ open class Dailymotion : ExtractorApi() {
return null return null
} }
private fun getVideoId(url: String): String? { private fun getVideoId(url: String): String? {
val path = URI(url).path val path = Url(url).encodedPath.decodeURLPart()
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
} }
@ -82,7 +79,6 @@ 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?
@ -102,5 +98,4 @@ 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 java.net.URI import io.ktor.http.Url
class Doodspro : DoodLaExtractor() { class Doodspro : DoodLaExtractor() {
override var mainUrl = "https://doods.pro" override var mainUrl = "https://doods.pro"
@ -138,8 +138,6 @@ open class DoodLaExtractor : ExtractorApi() {
} }
private fun getBaseUrl(url: String): String { private fun getBaseUrl(url: String): String {
return URI(url).let { return Url(url).let { "${it.protocol.name}://${it.host}" }
"${it.scheme}://${it.host}"
}
} }
} }

View file

@ -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
)
}

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 java.net.URI import io.ktor.http.Url
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 URI(url).let { "${it.scheme}://${it.host}" } return Url(url).let { "${it.protocol.name}://${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 java.net.URI import io.ktor.http.Url
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 { 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 } ?: 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 {
URI(url).let { "${it.scheme}://${it.host}" } Url(url).let { "${it.protocol.name}://${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.decodeUri import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
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.decodeUri().substringBeforeLast('.') val fileNameCleaned = fileName.decodeUrl().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 java.net.URI import io.ktor.http.Url
open class Streamplay : ExtractorApi() { open class Streamplay : ExtractorApi() {
override val name = "Streamplay" override val name = "Streamplay"
@ -22,9 +22,7 @@ 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 = URI(redirectUrl).let { val mainServer = Url(redirectUrl).let { "${it.protocol.name}://${it.host}" }
"${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()

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 java.net.URI import io.ktor.http.Url
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 {
URI(url).let { "${it.scheme}://${it.host}" } Url(url).let { "${it.protocol.name}://${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 uri = URI(iframeUrl) val url = Url(iframeUrl)
val mainUrl = "https://" + uri.host val mainUrl = "https://${url.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.decodeUri import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
import com.lagradost.cloudstream3.utils.StringUtils.encodeUri 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 // 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,8 +108,6 @@ object NineAnimeHelper {
} }
} }
fun encode(input: String): String = fun encode(input: String): String = input.encodeUrl()
input.encodeUri().replace("+", "%20") private fun decode(input: String): String = input.decodeUrl()
private fun decode(input: String): String = input.decodeUri()
} }

View file

@ -60,8 +60,9 @@ 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>() }.getOrNull() val serializer = runCatching { serializer<T>() }
?: json.serializersModule.getContextual(T::class) .recoverCatching { json.serializersModule.getContextual(T::class) }
.getOrNull()
// Prefer Kotlin Serialization over Jackson // Prefer Kotlin Serialization over Jackson
if (serializer != null) { if (serializer != null) {
@ -69,6 +70,8 @@ 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,6 +81,7 @@ 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
@ -312,11 +313,12 @@ 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
@ -420,7 +422,7 @@ enum class ExtractorLinkType {
private fun inferTypeFromUrl(url: String): ExtractorLinkType { private fun inferTypeFromUrl(url: String): ExtractorLinkType {
val path = try { val path = try {
URI(url).path Url(url).encodedPath.decodeURLPart()
} catch (_: Throwable) { } catch (_: Throwable) {
// don't log magnet links as errors // don't log magnet links as errors
null null
@ -819,7 +821,7 @@ constructor(
/** /**
* Removes https:// and www. * 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\.|)""") val schemaStripRegex = Regex("""^(https:|)//(www\.|)""")
@ -1297,6 +1299,7 @@ val extractorApis: AtomicMutableList<ExtractorApi> = atomicListOf(
GUpload(), GUpload(),
HlsWish(), HlsWish(),
ByseQekaho(), ByseQekaho(),
Flyfile()
) )

View file

@ -19,12 +19,11 @@
*/ */
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 {
@ -276,29 +275,29 @@ object HlsPlaylistParser {
} }
} }
object UriUtil { object UrlUtil {
fun resolveToUri(baseUri: String?, referenceUri: String?): URI { fun resolveToUrl(baseUrl: String?, referenceUrl: String?): Url {
return URI.create(resolve(baseUri, referenceUri)) return Url(resolve(baseUrl, referenceUrl))
} }
/** The length of arrays returned by [.getUriIndices]. */ /** The length of arrays returned by [.getUrlIndices]. */
private private
const val INDEX_COUNT: Int = 4 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 * 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), * if the URL is a relative reference (no scheme). The hier-part starts at (schemeColon + 1),
* including when the URI has no scheme. * including when the URL has no scheme.
*/ */
private private
const val SCHEME_COLON: Int = 0 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 + * 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 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 '?' * 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 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 '#' * 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. * 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 `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. * The resolution is performed as specified by RFC-3986.
* *
* @param baseUri The base URI. * @param baseUrl The base URL.
* @param referenceUri The reference URI to resolve. * @param referenceUrl The reference URL to resolve.
*/ */
private fun resolve(baseUri: String?, referenceUri: String?): String { private fun resolve(baseUrl: String?, referenceUrl: String?): String {
var baseUri = baseUri var baseUrl = baseUrl
var referenceUri = referenceUri var referenceUrl = referenceUrl
val uri = StringBuilder() val url = StringBuilder()
// Map null onto empty string, to make the following logic simpler. // Map null onto empty string, to make the following logic simpler.
baseUri = baseUri ?: "" baseUrl = baseUrl ?: ""
referenceUri = referenceUri ?: "" referenceUrl = referenceUrl ?: ""
val refIndices: IntArray = getUriIndices(referenceUri) val refIndices: IntArray = getUrlIndices(referenceUrl)
if (refIndices[SCHEME_COLON] != -1) { if (refIndices[SCHEME_COLON] != -1) {
// The reference is absolute. The target Uri is the reference. // The reference is absolute. The target Url is the reference.
uri.append(referenceUri) url.append(referenceUrl)
removeDotSegments(uri, refIndices[PATH], refIndices[QUERY]) removeDotSegments(url, refIndices[PATH], refIndices[QUERY])
return uri.toString() return url.toString()
} }
val baseIndices: IntArray = getUriIndices(baseUri) val baseIndices: IntArray = getUrlIndices(baseUrl)
if (refIndices[FRAGMENT] == 0) { if (refIndices[FRAGMENT] == 0) {
// The reference is empty or contains just the fragment part, then the target Uri is the // The reference is empty or contains just the fragment part, then the target Url is the
// concatenation of the base Uri without its fragment, and the reference. // concatenation of the base Url without its fragment, and the reference.
return uri.append(baseUri, 0, baseIndices[FRAGMENT]).append(referenceUri).toString() return url.append(baseUrl, 0, baseIndices[FRAGMENT]).append(referenceUrl).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 uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString() return url.append(baseUrl, 0, baseIndices[QUERY]).append(referenceUrl).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
uri.append(baseUri, 0, baseLimit).append(referenceUri) url.append(baseUrl, 0, baseLimit).append(referenceUrl)
return removeDotSegments( return removeDotSegments(
uri, url,
baseLimit + refIndices[PATH], baseLimit + refIndices[PATH],
baseLimit + refIndices[QUERY] 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 path is rooted. The target is the base scheme and authority (if any), plus
// the reference. // the reference.
uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri) url.append(baseUrl, 0, baseIndices[PATH]).append(referenceUrl)
return removeDotSegments( return removeDotSegments(
uri, url,
baseIndices[PATH], baseIndices[PATH],
baseIndices[PATH] + refIndices[QUERY] 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: // 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.
uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri) url.append(baseUrl, 0, baseIndices[PATH]).append('/').append(referenceUrl)
return removeDotSegments( return removeDotSegments(
uri, url,
baseIndices[PATH], baseIndices[PATH],
baseIndices[PATH] + refIndices[QUERY] + 1 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 // 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 = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1) val lastSlashIndex = baseUrl.lastIndexOf('/', baseIndices[QUERY] - 1)
val baseLimit = if (lastSlashIndex == -1) baseIndices[PATH] else lastSlashIndex + 1 val baseLimit = if (lastSlashIndex == -1) baseIndices[PATH] else lastSlashIndex + 1
uri.append(baseUri, 0, baseLimit).append(referenceUri) url.append(baseUrl, 0, baseLimit).append(referenceUrl)
return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY]) 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 url A [StringBuilder] containing the URL.
* @param offset The index of the start of the path in `uri`. * @param offset The index of the start of the path in `url`.
* @param limit The limit (exclusive) of the path in `uri`. * @param limit The limit (exclusive) of the path in `url`.
*/ */
private fun removeDotSegments( private fun removeDotSegments(
uri: StringBuilder, url: StringBuilder,
offset: Int, offset: Int,
limit: Int limit: Int
): String { ): String {
@ -433,9 +432,9 @@ object HlsPlaylistParser {
var limit = limit var limit = limit
if (offset >= limit) { if (offset >= limit) {
// Nothing to do. // Nothing to do.
return uri.toString() return url.toString()
} }
if (uri[offset] == '/') { if (url[offset] == '/') {
// If the path starts with a /, always retain it. // If the path starts with a /, always retain it.
offset++ offset++
} }
@ -445,7 +444,7 @@ object HlsPlaylistParser {
while (i <= limit) { while (i <= limit) {
val nextSegmentStart = if (i == limit) { val nextSegmentStart = if (i == limit) {
i i
} else if (uri[i] == '/') { } else if (url[i] == '/') {
i + 1 i + 1
} else { } else {
i++ 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 // 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 && uri[segmentStart] == '.') { if (i == segmentStart + 1 && url[segmentStart] == '.') {
// Given "abc/def/./ghi", remove "./" to get "abc/def/ghi". // Given "abc/def/./ghi", remove "./" to get "abc/def/ghi".
uri.delete(segmentStart, nextSegmentStart) url.delete(segmentStart, nextSegmentStart)
limit -= nextSegmentStart - segmentStart limit -= nextSegmentStart - segmentStart
i = 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". // 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 val removeFrom = if (prevSegmentStart > offset) prevSegmentStart else offset
uri.delete(removeFrom, nextSegmentStart) url.delete(removeFrom, nextSegmentStart)
limit -= nextSegmentStart - removeFrom limit -= nextSegmentStart - removeFrom
segmentStart = prevSegmentStart segmentStart = prevSegmentStart
i = prevSegmentStart i = prevSegmentStart
@ -471,41 +470,41 @@ object HlsPlaylistParser {
segmentStart = i 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. * @return The corresponding indices.
*/ */
private fun getUriIndices(uriString: String?): IntArray { private fun getUrlIndices(urlString: String?): IntArray {
val indices = IntArray(INDEX_COUNT) val indices = IntArray(INDEX_COUNT)
if (uriString.isNullOrEmpty()) { if (urlString.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.
// Uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ] // Url = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
val length = uriString.length val length = urlString.length
var fragmentIndex = uriString.indexOf('#') var fragmentIndex = urlString.indexOf('#')
if (fragmentIndex == -1) { if (fragmentIndex == -1) {
fragmentIndex = length fragmentIndex = length
} }
var queryIndex = uriString.indexOf('?') var queryIndex = urlString.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 = uriString.indexOf('/') var schemeIndexLimit = urlString.indexOf('/')
if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) { if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) {
schemeIndexLimit = queryIndex schemeIndexLimit = queryIndex
} }
var schemeIndex = uriString.indexOf(':') var schemeIndex = urlString.indexOf(':')
if (schemeIndex > schemeIndexLimit) { if (schemeIndex > schemeIndexLimit) {
// '/' before ':' // '/' before ':'
schemeIndex = -1 schemeIndex = -1
@ -514,10 +513,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 && uriString[schemeIndex + 1] == '/' && uriString[schemeIndex + 2] == '/' schemeIndex + 2 < queryIndex && urlString[schemeIndex + 1] == '/' && urlString[schemeIndex + 2] == '/'
var pathIndex: Int var pathIndex: Int
if (hasAuthority) { if (hasAuthority) {
pathIndex = uriString.indexOf('/', schemeIndex + 3) // find first '/' after "://" pathIndex = urlString.indexOf('/', schemeIndex + 3) // find first '/' after "://"
if (pathIndex == -1 || pathIndex > queryIndex) { if (pathIndex == -1 || pathIndex > queryIndex) {
pathIndex = queryIndex pathIndex = queryIndex
} }
@ -806,7 +805,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 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" 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) 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>
@ -1177,11 +1175,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 uriString = parseStringAttr(line, REGEX_URI, variableDefinitions) val urlString = 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 = Base64.Default.decode(uriString.substring(uriString.indexOf(','))) data = base64DecodeArray(urlString.substring(urlString.indexOf(',')))
) )
} else if (KEYFORMAT_WIDEVINE_PSSH_JSON == keyFormat) { } else if (KEYFORMAT_WIDEVINE_PSSH_JSON == keyFormat) {
return SchemeData( return SchemeData(
@ -1190,9 +1188,9 @@ object HlsPlaylistParser {
data = line.encodeToByteArray() data = line.encodeToByteArray()
) )
} else if (KEYFORMAT_PLAYREADY == keyFormat && "1" == keyFormatVersions) { } else if (KEYFORMAT_PLAYREADY == keyFormat && "1" == keyFormatVersions) {
val uriString = parseStringAttr(line, REGEX_URI, variableDefinitions) val urlString = parseStringAttr(line, REGEX_URI, variableDefinitions)
val data: ByteArray = val data: ByteArray =
Base64.Default.decode(uriString.substring(uriString.indexOf(','))) base64DecodeArray(urlString.substring(urlString.indexOf(',')))
val psshData: ByteArray = val psshData: ByteArray =
PsshAtomUtil.buildPsshAtom( PsshAtomUtil.buildPsshAtom(
systemId = C.PLAYREADY_UUID, systemId = C.PLAYREADY_UUID,
@ -1270,7 +1268,7 @@ object HlsPlaylistParser {
} }
data class Variant( data class Variant(
val url: URI, val url: Url,
val format: Format, val format: Format,
val videoGroupId: String?, val videoGroupId: String?,
val audioGroupId: String?, val audioGroupId: String?,
@ -1323,7 +1321,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: URI?, val url: Url?,
/** Format information associated with this rendition. */ /** Format information associated with this rendition. */
val format: Format, val format: Format,
@ -1336,14 +1334,14 @@ object HlsPlaylistParser {
) )
data class HlsMultivariantPlaylist( data class HlsMultivariantPlaylist(
/** The base uri. Used to resolve relative paths. */ /** The base url. 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<URI>, //val mediaPlaylistUrls: List<Url>,
/** The variants declared by the playlist. */ /** The variants declared by the playlist. */
val variants: List<Variant>, val variants: List<Variant>,
@ -1729,8 +1727,8 @@ object HlsPlaylistParser {
private fun parseMultivariantPlaylist( private fun parseMultivariantPlaylist(
iterator: Iterator<String>, baseUri: String iterator: Iterator<String>, baseUri: String
): HlsMultivariantPlaylist { ): HlsMultivariantPlaylist {
val urlToVariantInfos: HashMap<URI, ArrayList<VariantInfo>?> = val urlToVariantInfos: HashMap<Url, ArrayList<VariantInfo>?> =
HashMap<URI, ArrayList<VariantInfo>?>() HashMap<Url, 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>()
@ -1853,10 +1851,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 uri: URI val url: Url
if (isIFrameOnlyVariant) { if (isIFrameOnlyVariant) {
uri = url =
UriUtil.resolveToUri( UrlUtil.resolveToUrl(
baseUri, baseUri,
parseStringAttr(line, REGEX_URI, variableDefinitions) parseStringAttr(line, REGEX_URI, variableDefinitions)
) )
@ -1865,14 +1863,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 URI. // The following line contains #EXT-X-STREAM-INF's URL.
line = replaceVariableReferences(iterator.next(), variableDefinitions) line = replaceVariableReferences(iterator.next(), variableDefinitions)
uri = UriUtil.resolveToUri(baseUri, line) url = UrlUtil.resolveToUrl(baseUri, line)
} }
val variant = val variant =
Variant( Variant(
url = uri, url = url,
format = Format( format = Format(
id = variants.size.toString(), id = variants.size.toString(),
containerMimeType = MimeTypes.APPLICATION_M3U8, containerMimeType = MimeTypes.APPLICATION_M3U8,
@ -1890,10 +1888,10 @@ object HlsPlaylistParser {
captionGroupId = closedCaptionsGroupId captionGroupId = closedCaptionsGroupId
) )
variants.add(variant) variants.add(variant)
var variantInfosForUrl: ArrayList<VariantInfo>? = urlToVariantInfos[uri] var variantInfosForUrl: ArrayList<VariantInfo>? = urlToVariantInfos[url]
if (variantInfosForUrl == null) { if (variantInfosForUrl == null) {
variantInfosForUrl = ArrayList() variantInfosForUrl = ArrayList()
urlToVariantInfos[uri] = variantInfosForUrl urlToVariantInfos[url] = variantInfosForUrl
} }
variantInfosForUrl.add( variantInfosForUrl.add(
VariantInfo( VariantInfo(
@ -1911,7 +1909,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<URI>() val urlsInDeduplicatedVariants = HashSet<Url>()
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)) {
@ -1945,10 +1943,10 @@ object HlsPlaylistParser {
containerMimeType = MimeTypes.APPLICATION_M3U8, containerMimeType = MimeTypes.APPLICATION_M3U8,
) )
val referenceUri: String? = val referenceUrl: String? =
parseOptionalStringAttr(line, REGEX_URI, variableDefinitions) parseOptionalStringAttr(line, REGEX_URI, variableDefinitions)
val uri: URI? = val url: Url? =
if (referenceUri == null) null else UriUtil.resolveToUri(baseUri, referenceUri) if (referenceUrl == null) null else UrlUtil.resolveToUrl(baseUri, referenceUrl)
//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)) {
@ -1963,11 +1961,11 @@ object HlsPlaylistParser {
codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO) codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO)
) )
} }
if (uri == null) { if (url == null) {
// TODO: Remove this case and add a Rendition with a null uri to videos. // TODO: Remove this case and add a Rendition with a null url to videos.
} else { } else {
//formatBuilder.setMetadata(metadata) //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) val format = formatBuilder.copy(sampleMimeType = sampleMimeType)
if (uri != null) { if (url != null) {
//formatBuilder.setMetadata(metadata) //formatBuilder.setMetadata(metadata)
audios.add(Rendition(uri, format, groupId, name)) audios.add(Rendition(url, format, groupId, name))
} else if (variant != null) { } 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 muxedAudioFormat = format
} }
} }
@ -2018,10 +2016,10 @@ object HlsPlaylistParser {
if (sampleMimeType == null) { if (sampleMimeType == null) {
sampleMimeType = MimeTypes.TEXT_VTT sampleMimeType = MimeTypes.TEXT_VTT
} }
if (uri != null) { if (url != null) {
subtitles.add( subtitles.add(
Rendition( Rendition(
uri, url,
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(uri: String): String { private fun getParentLink(url: String): String {
val split = uri.split("/").toMutableList() val split = url.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 encryptionUri = match[2] var encryptionUrl = match[2]
if (isNotCompleteUrl(encryptionUri)) { if (isNotCompleteUrl(encryptionUrl)) {
encryptionUri = "${getParentLink(playlistStream.streamUrl)}/$encryptionUri" encryptionUrl = "${getParentLink(playlistStream.streamUrl)}/$encryptionUrl"
} }
encryptionIv = match[3].encodeToByteArray() encryptionIv = match[3].encodeToByteArray()
val encryptionKeyResponse = val encryptionKeyResponse =
app.get(encryptionUri, headers = playlistStream.headers, verify = false) app.get(encryptionUrl, 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,14 +1,32 @@
package com.lagradost.cloudstream3.utils package com.lagradost.cloudstream3.utils
import java.net.URLDecoder import com.lagradost.cloudstream3.Prerelease
import java.net.URLEncoder import io.ktor.http.decodeURLQueryComponent
import io.ktor.http.encodeURLParameter
object StringUtils { object StringUtils {
fun String.encodeUri(): String { @Prerelease
return URLEncoder.encode(this, "UTF-8") fun String.decodeUrl(): String {
return this.decodeURLQueryComponent()
} }
fun String.decodeUri(): String { @Prerelease
return URLDecoder.decode(this, "UTF-8") 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()
} }

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.decodeUri import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
import com.lagradost.nicehttp.NiceResponse import com.lagradost.nicehttp.NiceResponse
import java.net.URI import io.ktor.http.Url
// 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(uri: String, type: String? = null): String { suspend fun unshorten(url: String, type: String? = null): String {
var currentUrl = uri var currentUrl = url
val visitedUrls = mutableSetOf<String>() val visitedUrls = mutableSetOf<String>()
var count = 10 var count = 10
@ -57,9 +57,7 @@ object ShortLink {
visitedUrls += currentUrl visitedUrls += currentUrl
count -= 1 count -= 1
val domain = val domain = Url(currentUrl.trim()).host
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
@ -67,8 +65,8 @@ object ShortLink {
return currentUrl.trim() return currentUrl.trim()
} }
suspend fun unshortenAdfly(uri: String): String { suspend fun unshortenAdfly(url: String): String {
val html = app.get(uri).text val html = app.get(url).text
val ysmm = Regex("""var ysmm =.*;?""").find(html)!!.value val ysmm = Regex("""var ysmm =.*;?""").find(html)!!.value
if (ysmm.isNotEmpty()) { if (ysmm.isNotEmpty()) {
@ -81,46 +79,46 @@ object ShortLink {
left += c[0] left += c[0]
right = c[1] + right right = c[1] + right
} }
val encodedUri = (left + right).toMutableList() val encodedUrl = (left + right).toMutableList()
val numbers = 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 }) { 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) {
encodedUri[el[0].first] = xor.digitToChar() encodedUrl[el[0].first] = xor.digitToChar()
} }
} }
val encodedbytearray = encodedUri.map { it.code.toByte() }.toByteArray() val encodedbytearray = encodedUrl.map { it.code.toByte() }.toByteArray()
var decodedUri = var decodedUrl =
base64Decode(encodedbytearray.toString()).dropLast(16) base64Decode(encodedbytearray.toString()).dropLast(16)
.drop(16) .drop(16)
if (Regex("""go\.php\?u=""").find(decodedUri) != null) { if (Regex("""go\.php\?u=""").find(decodedUrl) != null) {
decodedUri = decodedUrl =
base64Decode(decodedUri.replace(Regex("""(.*?)u="""), "")) base64Decode(decodedUrl.replace(Regex("""(.*?)u="""), ""))
} }
return decodedUri return decodedUrl
} else { } else {
return uri return url
} }
} }
suspend fun unshortenLinkup(uri: String): String { suspend fun unshortenLinkup(url: String): String {
var r: NiceResponse? = null var r: NiceResponse? = null
var uri = uri var url = url
when { when {
uri.contains("/tv/") -> uri = uri.replace("/tv/", "/tva/") url.contains("/tv/") -> url = url.replace("/tv/", "/tva/")
uri.contains("delta") -> uri = uri.replace("/delta/", "/adelta/") url.contains("delta") -> url = url.replace("/delta/", "/adelta/")
(uri.contains("/ga/") || uri.contains("/ga2/")) -> uri = (url.contains("/ga/") || url.contains("/ga2/")) -> url =
base64Decode(uri.split('/').last()).trim() base64Decode(url.split('/').last()).trim()
uri.contains("/speedx/") -> uri = url.contains("/speedx/") -> url =
uri.replace("http://linkup.pro/speedx", "http://speedvideo.net") url.replace("http://linkup.pro/speedx", "http://speedvideo.net")
else -> { else -> {
r = app.get(uri, allowRedirects = true) r = app.get(url, allowRedirects = true)
uri = r.url url = 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
@ -128,40 +126,40 @@ object ShortLink {
.elementAtOrNull(1)?.groupValues?.get(1) .elementAtOrNull(1)?.groupValues?.get(1)
if (link != null) { 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) { if (short != null) {
uri = short url = short
} }
if (r == null) { if (r == null) {
r = app.get( r = app.get(
uri, url,
allowRedirects = false allowRedirects = false
) )
if (r.headers["location"] != null) { if (r.headers["location"] != null) {
uri = r.headers["location"].toString() url = r.headers["location"].toString()
} }
} }
if (uri.contains("snip.")) { if (url.contains("snip.")) {
if (uri.contains("out_generator")) { if (url.contains("out_generator")) {
uri = Regex("url=(.*)\$").find(uri)!!.value url = Regex("url=(.*)\$").find(url)!!.value
} else if (uri.contains("/decode/")) { } else if (url.contains("/decode/")) {
uri = app.get(uri, allowRedirects = true).url url = app.get(url, allowRedirects = true).url
} }
} }
return uri return url
} }
fun unshortenLinksafe(uri: String): String { fun unshortenLinksafe(url: String): String {
return base64Decode(uri.split("?url=").last()) return base64Decode(url.split("?url=").last())
} }
suspend fun unshortenNuovoIndirizzo(uri: String): String { suspend fun unshortenNuovoIndirizzo(url: String): String {
val soup = app.get(uri, allowRedirects = true) val soup = app.get(url, 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("=")
@ -171,29 +169,29 @@ object ShortLink {
return link return link
} }
suspend fun unshortenNuovoLink(uri: String): String { suspend fun unshortenNuovoLink(url: String): String {
return app.get(uri, allowRedirects = true).document.selectFirst("a")!!.attr("href") return app.get(url, allowRedirects = true).document.selectFirst("a")!!.attr("href")
} }
suspend fun unshortenUprot(uri: String): String { suspend fun unshortenUprot(url: String): String {
val page = app.get(uri).text val page = app.get(url).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 != uri) { if (link.contains("https://maxstream.video") || link.contains("https://uprot.net") && link != url) {
return link return link
} }
} }
return uri return url
} }
fun unshortenDavisonbarker(uri: String): String { fun unshortenDavisonbarker(url: String): String {
return uri.substringAfter("dest=").decodeUri() return url.substringAfter("dest=").decodeUrl()
} }
suspend fun unshortenIsecure(uri: String): String { suspend fun unshortenIsecure(url: String): String {
val doc = app.get(uri).document val doc = app.get(url).document
return doc.selectFirst("iframe")?.attr("src")?.trim() ?: uri return doc.selectFirst("iframe")?.attr("src")?.trim() ?: url
} }
} }