From adf2ed6df3f5f3275e0f91da882d3adfc815edb6 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:36:31 +0000 Subject: [PATCH 01/60] Fix livestreams (#2627) --- .../cloudstream3/ui/player/CS3IPlayer.kt | 47 ++++++++- .../ui/player/FullScreenPlayer.kt | 17 +++- .../cloudstream3/ui/player/live/LiveHelper.kt | 77 +++++++++++++++ .../ui/player/live/LiveManager.kt | 97 +++++++++++++++++++ .../ui/player/live/LivePreviewTimeBar.kt | 38 ++++++++ .../main/res/layout/player_custom_layout.xml | 20 +++- .../res/layout/player_custom_layout_tv.xml | 22 ++++- .../main/res/layout/trailer_custom_layout.xml | 19 +++- app/src/main/res/values/strings.xml | 2 + 9 files changed, 333 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LiveHelper.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LiveManager.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LivePreviewTimeBar.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 43b281a2855..60c87532b14 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -42,6 +42,7 @@ import androidx.media3.datasource.cronet.CronetDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.DecoderCounters import androidx.media3.exoplayer.DecoderReuseEvaluation +import androidx.media3.exoplayer.DefaultLivePlaybackSpeedControl import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer @@ -54,6 +55,7 @@ import androidx.media3.exoplayer.drm.DefaultDrmSessionManager import androidx.media3.exoplayer.drm.FrameworkMediaDrm import androidx.media3.exoplayer.drm.HttpMediaDrmCallback import androidx.media3.exoplayer.drm.LocalMediaDrmCallback +import androidx.media3.exoplayer.hls.playlist.HlsPlaylistTracker import androidx.media3.exoplayer.source.ClippingMediaSource import androidx.media3.exoplayer.source.ConcatenatingMediaSource import androidx.media3.exoplayer.source.ConcatenatingMediaSource2 @@ -83,6 +85,8 @@ import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safe import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.fixSubtitleAlignment +import com.lagradost.cloudstream3.ui.player.live.LiveHelper +import com.lagradost.cloudstream3.ui.player.live.PREFERRED_LIVE_OFFSET import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout @@ -272,6 +276,10 @@ class CS3IPlayer : IPlayer { } override fun hasPreview(): Boolean { + // No previews on livestreams because the previews get outdated + if (exoPlayer?.isCurrentMediaItemDynamic == true) { + return false + } return imageGenerator.hasPreview() } @@ -399,7 +407,12 @@ class CS3IPlayer : IPlayer { ?.let { group -> exoPlayer?.trackSelectionParameters ?.buildUpon() - ?.setOverrideForType(TrackSelectionOverride(group.mediaTrackGroup, trackFormatIndex)) + ?.setOverrideForType( + TrackSelectionOverride( + group.mediaTrackGroup, + trackFormatIndex + ) + ) ?.build() } ?.let { newParams -> @@ -516,10 +529,12 @@ class CS3IPlayer : IPlayer { Log.i(TAG, "setPreferredSubtitles REQUIRES_RELOAD") return true } + SubtitleStatus.NOT_FOUND -> { Log.i(TAG, "setPreferredSubtitles NOT_FOUND") return true } + SubtitleStatus.IS_ACTIVE -> { Log.i(TAG, "setPreferredSubtitles IS_ACTIVE") exoPlayer?.currentTracks?.groups @@ -1067,6 +1082,17 @@ class CS3IPlayer : IPlayer { ): ExoPlayer { val exoPlayerBuilder = ExoPlayer.Builder(context) + .setMediaSourceFactory( + DefaultMediaSourceFactory(context).setLiveTargetOffsetMs( + PREFERRED_LIVE_OFFSET + ) + ) + .setLivePlaybackSpeedControl( + DefaultLivePlaybackSpeedControl.Builder() + .setFallbackMaxPlaybackSpeed(1.03f) + .setFallbackMinPlaybackSpeed(0.97f) + .build() + ) .setRenderersFactory { eventHandler, videoRendererEventListener, audioRendererEventListener, textRendererOutput, metadataRendererOutput -> val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) val current = settingsManager.getInt( @@ -1398,6 +1424,8 @@ class CS3IPlayer : IPlayer { return } + LiveHelper.registerPlayer(exoPlayer) + exoPlayer?.addListener(object : Player.Listener { override fun onTracksChanged(tracks: Tracks) { safe { @@ -1506,6 +1534,23 @@ class CS3IPlayer : IPlayer { exoPlayer?.prepare() } + // PlaylistStuckException usually happens when the player position is ahead of the live window. + // Seek to the default location in that case + error.cause is HlsPlaylistTracker.PlaylistStuckException -> { + val position = exoPlayer?.currentPosition ?: exoPlayer?.duration ?: 0 + + // Seek to live head + val aheadOfLive = LiveHelper.getLiveManager(exoPlayer)?.getTimeAheadOfLive(position) ?: 0 + + if (aheadOfLive > 100) { + exoPlayer?.seekTo(position - aheadOfLive) + } else { + exoPlayer?.seekToDefaultPosition() + } + exoPlayer?.prepare() + } + + else -> { event(ErrorEvent(error)) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 4bec57f9c5f..8699202b9b0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -631,7 +631,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { override fun subtitlesChanged() { val tracks = player.getVideoTracks() val isBuiltinSubtitles = tracks.currentTextTracks.all { track -> - track.sampleMimeType == MimeTypes.APPLICATION_MEDIA3_CUES + track.sampleMimeType == MimeTypes.APPLICATION_MEDIA3_CUES } // Subtitle offset is not possible on built-in media3 tracks playerBinding?.playerSubtitleOffsetBtt?.isGone = @@ -738,6 +738,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { activity?.window?.attributes = lp activity?.showSystemUI() } + private fun resetZoomToDefault() { if (zoomMatrix != null) resize(PlayerResize.Fit, false) } @@ -2648,6 +2649,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } + exoProgress.registerPlayerView(playerView) + exoProgress.setOnTouchListener { _, event -> // this makes the bar not disappear when sliding when (event.action) { @@ -2720,10 +2723,20 @@ open class FullScreenPlayer : AbstractPlayerFragment() { val duration = player.getDuration() val position = player.getPosition() + if (playerBinding?.exoProgress?.isAtLiveEdge() == true) { + // Hide using a parentView instead? + playerBinding?.timeLeft?.alpha = 0f + playerBinding?.exoDuration?.alpha = 0f + playerBinding?.timeLive?.isVisible = true + } else { + playerBinding?.timeLeft?.alpha = 1f + playerBinding?.exoDuration?.alpha = 1f + playerBinding?.timeLive?.isVisible = false + } + if (duration != null && duration > 1 && position != null) { val remainingTimeSeconds = (duration - position + 500) / 1000 val formattedTime = "-${DateUtils.formatElapsedTime(remainingTimeSeconds)}" - playerBinding?.timeLeft?.text = formattedTime } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LiveHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LiveHelper.kt new file mode 100644 index 00000000000..52cd4361bab --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LiveHelper.kt @@ -0,0 +1,77 @@ +package com.lagradost.cloudstream3.ui.player.live + +import androidx.annotation.OptIn +import androidx.media3.common.Player +import androidx.media3.common.Timeline +import androidx.media3.common.util.UnstableApi +import com.lagradost.cloudstream3.mvvm.debugWarning +import java.util.WeakHashMap + +object LiveHelper { + private val liveManagers = WeakHashMap>() + + @OptIn(UnstableApi::class) + fun registerPlayer(player: Player?) { + if (player == null) { + debugWarning { "LiveHelper registerPlayer called with null player!" } + return + } + + // Prevent duplicates + if (liveManagers.contains(player)) { + return + } + + val liveManager = LiveManager(player) + val listener = object : Player.Listener { + override fun onTimelineChanged(timeline: Timeline, reason: Int) { + val window = Timeline.Window() + timeline.getWindow(player.currentMediaItemIndex, window) + if (window.isDynamic) { + liveManager.submitLivestreamChunk(LivestreamChunk(window.durationMs)) + } + super.onTimelineChanged(timeline, reason) + } + + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + super.onPositionDiscontinuity(oldPosition, newPosition, reason) + val timeAheadOfLive = liveManager.getTimeAheadOfLive(newPosition.positionMs) + + // Seek back to the optimal live spot + if (timeAheadOfLive > 100) { + player.seekTo(newPosition.positionMs - timeAheadOfLive) + } + } + } + + synchronized(liveManagers) { + player.addListener(listener) + liveManagers[player] = liveManager to listener + } + } + + fun unregisterPlayer(player: Player?) { + if (player == null) { + debugWarning { "LiveHelper unregisterPlayer called with null player!" } + return + } + + // Prevent duplicates + if (!liveManagers.contains(player)) { + return + } + + synchronized(liveManagers) { + liveManagers[player]?.let { (_, listener) -> + player.removeListener(listener) + } + liveManagers.remove(player) + } + } + + fun getLiveManager(player: Player?) = liveManagers[player]?.first +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LiveManager.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LiveManager.kt new file mode 100644 index 00000000000..8d848d46aa9 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LiveManager.kt @@ -0,0 +1,97 @@ +package com.lagradost.cloudstream3.ui.player.live + +import androidx.media3.common.C +import androidx.media3.common.Player +import java.lang.ref.WeakReference + +// How much margin from the live point is still considered "live" +const val LIVE_MARGIN = 6_000L + +// How many ms should we be behind the real live point? +// Too low, and we cannot pre-buffer +// Too high, and we are no longer live +const val PREFERRED_LIVE_OFFSET = 5_000L + +// An extra offset from the optimal calculated timestamp +// This is to account for chunk updates not always being the same size +const val CHUNK_VARIANCE = 3000L + +// A livestream chunk from the player, the time we get it and the duration can be used to calculate +// the expected live timestamp. +class LivestreamChunk( + durationMs: Long, val receiveTimeMs: Long = System.currentTimeMillis() +) { + // We want to be PREFERRED_LIVE_OFFSET ms after the latest update, but we cannot be ahead of the middle point. + // If we are ahead of the middle point we will reach the end before the new chunk is expected to be released. + val targetPosition = maxOf(0,minOf( + durationMs - PREFERRED_LIVE_OFFSET, + durationMs / 2 - CHUNK_VARIANCE + )) + + fun isPositionLive(position: Long): Boolean { + val currentTime = System.currentTimeMillis() + val livePosition = targetPosition + (currentTime - receiveTimeMs) + val withinLive = position + LIVE_MARGIN > livePosition - PREFERRED_LIVE_OFFSET + // println("Position: $position, livePosition: ${livePosition}, Behind live: ${livePosition-position}, within live: $withinLive") + return withinLive + } + + fun getTimeAheadOfLive(position: Long): Long { + val currentTime = System.currentTimeMillis() + val livePosition = targetPosition + (currentTime - receiveTimeMs) + // println("Ahead of live: ${position-livePosition}") + return position - livePosition + } +} + +// There are two types of livestreams we need to manage +// 1. A livestream with no history, a continually sliding window. +// This livestream has no currentLiveOffset, which means we need to calculate +// the real live point based on when we receive the latest update and the size of that update. +// 2. A livestream with history. +// This livestream has a currentLiveOffset and therefore requires no calculation to get the live point. +// currentLiveOffset can however be inaccurate, and we need to be able to fall back to manual calculations. +class LiveManager { + private var _currentPlayer: WeakReference? = null + val currentPlayer: Player? get() = _currentPlayer?.get() + + constructor(player: Player?) { + _currentPlayer = WeakReference(player) + } + + private var lastLivestreamChunk: LivestreamChunk? = null + + fun submitLivestreamChunk(chunk: LivestreamChunk) { + lastLivestreamChunk = chunk + } + + /** Returns how much a position is ahead of the calculated live window. Returns 0 if not ahead of live window */ + fun getTimeAheadOfLive(position: Long): Long { + val player = currentPlayer ?: return 0 + if (!player.isCurrentMediaItemDynamic || player.duration == C.TIME_UNSET) return 0 + + // If the currentLiveOffset is wrong we fall back to manual calculations + val ahead = if (player.currentLiveOffset != C.TIME_UNSET && player.currentLiveOffset < player.duration) { + val relativeOffset = player.currentLiveOffset - player.currentPosition + position + PREFERRED_LIVE_OFFSET - relativeOffset + } else { + lastLivestreamChunk?.getTimeAheadOfLive(position) ?: 0 + } + + // Ensure min of 0 + return maxOf(0, ahead) + } + + /** Check if the stream is currently at the expected live edge, with margins */ + fun isAtLiveEdge(): Boolean { + val player = currentPlayer ?: return false + if (!player.isCurrentMediaItemDynamic || player.duration == C.TIME_UNSET) return false + + // If the currentLiveOffset is wrong we fall back to manual calculations + return if (player.currentLiveOffset != C.TIME_UNSET && player.currentLiveOffset < player.duration) { + player.currentLiveOffset < LIVE_MARGIN + PREFERRED_LIVE_OFFSET + } else { + lastLivestreamChunk?.isPositionLive(player.currentPosition) == true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LivePreviewTimeBar.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LivePreviewTimeBar.kt new file mode 100644 index 00000000000..3001281fd45 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LivePreviewTimeBar.kt @@ -0,0 +1,38 @@ +package com.lagradost.cloudstream3.ui.player.live + +import android.content.Context +import android.util.AttributeSet +import androidx.annotation.OptIn +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.ui.PlayerControlView +import androidx.media3.ui.PlayerView +import androidx.media3.ui.R +import com.github.rubensousa.previewseekbar.media3.PreviewTimeBar +import java.lang.ref.WeakReference + + +@OptIn(UnstableApi::class) +class LivePreviewTimeBar(val ctx: Context, attrs: AttributeSet) : PreviewTimeBar(ctx, attrs) { + + private var _currentPlayerView: WeakReference? = null + val currentPlayer: Player? get() = _currentPlayerView?.get()?.player + + fun registerPlayerView(player: PlayerView?) { + _currentPlayerView = WeakReference(player) + val controller = + _currentPlayerView?.get()?.findViewById(R.id.exo_controller) + + controller?.setProgressUpdateListener { position, bufferedPosition -> + currentPlayer?.let { player -> + if (isAtLiveEdge()) { + setPosition(player.duration) + } + } + } + } + + fun isAtLiveEdge(): Boolean { + return LiveHelper.getLiveManager(currentPlayer)?.isAtLiveEdge() == true + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 72024a918d5..407de4a3f1b 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -759,7 +759,7 @@ android:scaleType="centerCrop" /> - + + - + + + - + %d download queued %d downloads queued + Live + From 14d56de61ea89b422e2335bcb180188da735aac3 Mon Sep 17 00:00:00 2001 From: Phisher98 <153359846+phisher98@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:10:05 +0530 Subject: [PATCH 02/60] Adding a subtle shadow and minor adjustments to make the description stand out more on a white background. (#2648) --- app/src/main/res/layout/player_custom_layout_tv.xml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/player_custom_layout_tv.xml b/app/src/main/res/layout/player_custom_layout_tv.xml index 41d45b6bb50..2c536c825b9 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -10,7 +10,7 @@ @@ -74,8 +78,12 @@ android:textColor="#E6FFFFFF" android:textSize="16sp" android:lineSpacingExtra="8dp" + android:shadowColor="@android:color/black" + android:shadowDx="2" + android:shadowDy="2" + android:shadowRadius="4" android:maxLines="5" - tools:text="Brave rabbit cop Judy Hopps and her friend, the fox Nick Wilde, team up again to crack a new case."/> + tools:text="Brave rabbit cop Judy Hopps..."/> Date: Sun, 12 Apr 2026 14:46:11 -0600 Subject: [PATCH 03/60] Upgrade media3 to 1.10.0 (#2608) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c92a937bd9c..5a46edf3d68 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,10 +27,10 @@ kotlinGradlePlugin = "2.3.0" kotlinxCoroutinesCore = "1.10.2" lifecycleKtx = "2.10.0" material = "1.14.0-beta01" -media3 = "1.9.2" +media3 = "1.10.0" navigationKtx = "2.9.7" newpipeextractor = "v0.26.0" -nextlibMedia3 = "1.9.1-0.11.0" +nextlibMedia3 = "1.9.3-0.12.0" nicehttp = "0.4.17" overlappingpanels = "0.1.5" paletteKtx = "1.0.0" From 2eb63dc334b19e5a1463158fd1f79572bfebdcf9 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:47:12 +0000 Subject: [PATCH 04/60] Change default installer to legacy (#2653) Switching the default to the more reliable legacy installer until we fix the new installer. --- .../com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt | 3 ++- .../main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index 118d89ac482..9250f6f6f2b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -205,8 +205,9 @@ class SettingsUpdates : BasePreferenceFragmentCompat() { val prefNames = resources.getStringArray(R.array.apk_installer_pref) val prefValues = resources.getIntArray(R.array.apk_installer_values) + // Use legacy installer as default until we make the new installer completely reliable val currentInstaller = - settingsManager.getInt(getString(R.string.apk_installer_key), 0) + settingsManager.getInt(getString(R.string.apk_installer_key), 1) activity?.showBottomDialog( prefNames.toList(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index 9380285ca44..8bcd1b88e70 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -308,7 +308,7 @@ object InAppUpdater { } val currentInstaller = settingsManager.getInt( - getString(R.string.apk_installer_key), 0 + getString(R.string.apk_installer_key), 1 ) when (currentInstaller) { From 1b0fdb57a8298477cf68a321e19abed02417c8f7 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:50:57 -0600 Subject: [PATCH 05/60] Add permissions to workflows (#2654) https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions --- .github/workflows/build_to_archive.yml | 3 +++ .github/workflows/generate_dokka.yml | 3 +++ .github/workflows/issue_action.yml | 4 ++++ .github/workflows/prerelease.yml | 3 +++ .github/workflows/pull_request.yml | 3 +++ .github/workflows/update_locales.yml | 3 +++ 6 files changed, 19 insertions(+) diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml index f72dd10c62d..b5960d5d942 100644 --- a/.github/workflows/build_to_archive.yml +++ b/.github/workflows/build_to_archive.yml @@ -9,6 +9,9 @@ on: - '**/wcokey.txt' workflow_dispatch: +permissions: + contents: read + concurrency: group: "Archive-build" cancel-in-progress: true diff --git a/.github/workflows/generate_dokka.yml b/.github/workflows/generate_dokka.yml index 91f03a43485..8ca1f9688a7 100644 --- a/.github/workflows/generate_dokka.yml +++ b/.github/workflows/generate_dokka.yml @@ -6,6 +6,9 @@ on: paths-ignore: - '*.md' +permissions: + contents: read + concurrency: group: "dokka" cancel-in-progress: true diff --git a/.github/workflows/issue_action.yml b/.github/workflows/issue_action.yml index 4286e6b683e..a410fcfff00 100644 --- a/.github/workflows/issue_action.yml +++ b/.github/workflows/issue_action.yml @@ -4,6 +4,10 @@ on: issues: types: [opened] +permissions: + contents: read + issues: write + jobs: issue-moderator: runs-on: ubuntu-latest diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 03cb68cbc46..d9a20a04b2b 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -12,6 +12,9 @@ concurrency: group: "pre-release" cancel-in-progress: true +permissions: + contents: write + jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index a5a7d56e37b..675ce3b2f77 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -2,6 +2,9 @@ name: Artifact Build on: [pull_request] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/update_locales.yml b/.github/workflows/update_locales.yml index 5b170d540f7..0a538d5d4da 100644 --- a/.github/workflows/update_locales.yml +++ b/.github/workflows/update_locales.yml @@ -11,6 +11,9 @@ concurrency: group: "locale" cancel-in-progress: true +permissions: + contents: read + jobs: create: runs-on: ubuntu-latest From 788189c80cd2b6bba8184843d382477091dcb638 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:52:57 -0600 Subject: [PATCH 06/60] Bump github-script action (#2642) --- .github/workflows/issue_action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue_action.yml b/.github/workflows/issue_action.yml index a410fcfff00..e354d657d50 100644 --- a/.github/workflows/issue_action.yml +++ b/.github/workflows/issue_action.yml @@ -33,7 +33,7 @@ jobs: - name: Label if possible duplicate if: steps.similarity.outputs.similar-issues-found =='true' - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: github-token: ${{ steps.generate_token.outputs.token }} script: | @@ -79,7 +79,7 @@ jobs: - name: Label if mentions provider if: steps.provider_check.outputs.name != 'none' - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: github-token: ${{ steps.generate_token.outputs.token }} script: | From fb54d02979c39b66b6423cf2c3239743e9b48a85 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:21:52 +0000 Subject: [PATCH 07/60] Fix SSL issues (#2655) --- .../lagradost/cloudstream3/MainActivity.kt | 7 +++-- .../cloudstream3/network/RequestsHelper.kt | 27 ++++++++++++++----- .../com/lagradost/cloudstream3/MainAPI.kt | 7 +++++ .../lagradost/cloudstream3/MainActivity.kt | 17 +++++++++--- 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 709e92a41fa..071ce6c897b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1169,7 +1169,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } override fun onCreate(savedInstanceState: Bundle?) { - app.initClient(this) + app.initClient(this, ignoreSSL = false) + @OptIn(UnsafeSSL::class) + insecureApp.initClient(this, ignoreSSL = true) + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) setLastError(this) @@ -2059,4 +2062,4 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa false } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt index ec486d61db6..6234297d080 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.network import android.content.Context import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.Prerelease import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.mvvm.safe @@ -15,11 +16,26 @@ import org.conscrypt.Conscrypt import java.io.File import java.security.Security +// Backwards compatible constructor, mark as deprecated later fun Requests.initClient(context: Context) { this.baseClient = buildDefaultClient(context) } +/** Only use ignoreSSL if you know what you are doing*/ +@Prerelease +fun Requests.initClient(context: Context, ignoreSSL: Boolean = false) { + this.baseClient = buildDefaultClient(context, ignoreSSL) +} + + +// Backwards compatible constructor, mark as deprecated later fun buildDefaultClient(context: Context): OkHttpClient { + return buildDefaultClient(context, false) +} + +/** Only use ignoreSSL if you know what you are doing*/ +@Prerelease +fun buildDefaultClient(context: Context, ignoreSSL: Boolean = false): OkHttpClient { safe { Security.insertProviderAt(Conscrypt.newProvider(), 1) } val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) @@ -27,7 +43,11 @@ fun buildDefaultClient(context: Context): OkHttpClient { val baseClient = OkHttpClient.Builder() .followRedirects(true) .followSslRedirects(true) - .ignoreAllSSLErrors() + .apply { + if (ignoreSSL) { + ignoreAllSSLErrors() + } + } .cache( // Note that you need to add a ResponseInterceptor to make this 100% active. // The server response dictates if and when stuff should be cached. @@ -52,11 +72,6 @@ fun buildDefaultClient(context: Context): OkHttpClient { return baseClient } -//val Request.cookies: Map -// get() { -// return this.headers.getCookies("Cookie") -// } - private val DEFAULT_HEADERS = mapOf("user-agent" to USER_AGENT) /** diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index 81eabbe77f8..aeab8ef9f1f 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -55,6 +55,13 @@ annotation class Prerelease ) annotation class InternalAPI +@Retention(AnnotationRetention.BINARY) // This is only an IDE hint, and will not be used in the runtime +@RequiresOptIn( + message = "Only use this if you know what you are doing and you need to bypass the SSL certificate checks. Never use this for sensitive network requests such as logins.", + level = RequiresOptIn.Level.WARNING +) +annotation class UnsafeSSL + /** * Defines the constant for the all languages preference, if this is set then it is * the equivalent of all languages being set diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt index 6502cc83166..4b163867de3 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt @@ -8,8 +8,7 @@ import com.lagradost.nicehttp.ResponseParser import kotlin.reflect.KClass // Short name for requests client to make it nicer to use - -var app = Requests(responseParser = object : ResponseParser { +private val jacksonResponseParser = object : ResponseParser { val mapper: ObjectMapper = jacksonObjectMapper().configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false @@ -30,6 +29,18 @@ var app = Requests(responseParser = object : ResponseParser { override fun writeValueAsString(obj: Any): String { return mapper.writeValueAsString(obj) } -}).apply { +} + +/** The default networking helper. This helper performs SSL checks. + * If you need to make requests to websites with invalid SSL certificates use insecureApp instead. */ +var app = Requests(responseParser = jacksonResponseParser).apply { + defaultHeaders = mapOf("user-agent" to USER_AGENT) +} + +/** Same as the default app networking helper, but this instance ignores SSL certificates. + * This should NEVER be used for sensitive networking operations such as logins. Only use this when required. */ +@Prerelease +@UnsafeSSL +var insecureApp = Requests(responseParser = jacksonResponseParser).apply { defaultHeaders = mapOf("user-agent" to USER_AGENT) } \ No newline at end of file From cfce80e93e181eb2fd7517438e2d77d1a802781f Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:27:27 -0600 Subject: [PATCH 08/60] Bump DGP and KGP libs (#2582) Final compatibility with AGP 9 --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5a46edf3d68..fc9926c93dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ conscryptAndroid = { strictly = "2.5.2" } # 2.5.3 crashes everything constraintlayout = "2.2.1" coreKtx = "1.18.0" desugar_jdk_libs_nio = "2.1.5" -dokkaGradlePlugin = "2.1.0" +dokkaGradlePlugin = "2.2.0" espressoCore = "3.7.0" fragmentKtx = "1.8.9" fuzzywuzzy = "1.4.0" @@ -23,7 +23,7 @@ junit = "4.13.2" junitKtx = "1.3.0" junitVersion = "1.3.0" juniversalchardet = "2.5.0" -kotlinGradlePlugin = "2.3.0" +kotlinGradlePlugin = "2.3.20" kotlinxCoroutinesCore = "1.10.2" lifecycleKtx = "2.10.0" material = "1.14.0-beta01" From 0bb932227622431304c47a50418c92b4a4971bd3 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:47:38 -0600 Subject: [PATCH 09/60] Don't explicitly enable WebContentsDebugging (#2657) "this is enabled automatically if the app is declared as `android:debuggable="true"` in its manifest; otherwise, the default is false." - which we set on CloudStream Debug but not release flavors. "Enabling web contents debugging allows the state of any WebView in the app to be inspected and modified by the user via adb. This is a security liability and should not be enabled in production builds of apps unless this is an explicitly intended use of the app." --- .../lagradost/cloudstream3/network/WebViewResolver.android.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt index a99d0a16a7b..60a4d045346 100644 --- a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt +++ b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt @@ -123,8 +123,6 @@ actual class WebViewResolver actual constructor( val extraRequestList = threadSafeListOf() main { - // Useful for debugging - WebView.setWebContentsDebuggingEnabled(true) try { webView = WebView( (getContext() as? Context) From 8d416fa2fc48ff9a4fca242b3c82243e441a198e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:48:23 -0600 Subject: [PATCH 10/60] Remove commented android.enableJetifier from gradle.properties (#2662) It is now deprecated anyway. We will never use it now, so we can just fully remove it. --- gradle.properties | 2 -- 1 file changed, 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 0168ae437bd..b6d502f9a2a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,8 +15,6 @@ org.gradle.jvmargs=-Xmx4G -Dfile.encoding=UTF-8 # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -# android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official android.nonTransitiveRClass=true From 7925e714e7a97dadf7a7e70a89a4cc45f6e914ea Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:39:31 -0600 Subject: [PATCH 11/60] Fix editing accounts from MainActivity (#2663) --- .../ui/account/AccountSelectActivity.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt index 29c35dea6c0..ad323c7d124 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -48,10 +48,16 @@ class AccountSelectActivity : FragmentActivity(), BiometricCallback { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // Are we editing and coming from MainActivity? + val isEditingFromMainActivity = intent.getBooleanExtra( + "isEditingFromMainActivity", + false + ) + // Sometimes we start this activity when we have already logged in // For example when using cloudstreamsearch:// // In those cases we want to just go to the main activity instantly - if (hasLoggedIn) { + if (hasLoggedIn && !isEditingFromMainActivity) { navigateToMainActivity() return } @@ -61,12 +67,6 @@ class AccountSelectActivity : FragmentActivity(), BiometricCallback { enableEdgeToEdgeCompat() setNavigationBarColorCompat(R.attr.primaryBlackBackground) - // Are we editing and coming from MainActivity? - val isEditingFromMainActivity = intent.getBooleanExtra( - "isEditingFromMainActivity", - false - ) - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val skipStartup = settingsManager.getBoolean( getString(R.string.skip_startup_account_select_key), false @@ -216,4 +216,4 @@ class AccountSelectActivity : FragmentActivity(), BiometricCallback { override fun onAuthenticationError() { finish() } -} \ No newline at end of file +} From c31c5764ea649f85365d5cf9c42e19557abc9ef4 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:22:38 -0600 Subject: [PATCH 12/60] Bump nextlibMedia3 (#2658) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc9926c93dd..f997e4f6e5d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ material = "1.14.0-beta01" media3 = "1.10.0" navigationKtx = "2.9.7" newpipeextractor = "v0.26.0" -nextlibMedia3 = "1.9.3-0.12.0" +nextlibMedia3 = "1.10.0-0.12.1" nicehttp = "0.4.17" overlappingpanels = "0.1.5" paletteKtx = "1.0.0" From cd033923642f3d146500de707ed117dc8afce543 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:26:52 -0600 Subject: [PATCH 13/60] Remove setup-android action from Dokka action (#2666) It shouldn't be necessary with setup-gradle. --- .github/workflows/generate_dokka.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/generate_dokka.yml b/.github/workflows/generate_dokka.yml index 8ca1f9688a7..d67b8a519d7 100644 --- a/.github/workflows/generate_dokka.yml +++ b/.github/workflows/generate_dokka.yml @@ -54,9 +54,6 @@ jobs: with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - - name: Set up Android SDK - uses: android-actions/setup-android@v4 - - name: Generate Dokka run: | cd $GITHUB_WORKSPACE/src/ From 636d2507f72ce571b37bce8c30766658309074c0 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:28:16 -0600 Subject: [PATCH 14/60] Add missing OptIn (#2668) This an error level opt in introduced in media3 1.10.0. --- .../java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 60c87532b14..8a643cc69b8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -29,6 +29,7 @@ import androidx.media3.common.TrackGroup import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.Tracks import androidx.media3.common.VideoSize +import androidx.media3.common.util.ExperimentalApi import androidx.media3.common.util.UnstableApi import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.datasource.DataSource @@ -1195,6 +1196,7 @@ class CS3IPlayer : IPlayer { CustomDecoder.subtitleOffset = subtitleOffset val decoder = CustomSubtitleDecoderFactory() + @OptIn(ExperimentalApi::class) val currentTextRenderer = TextRenderer( customTextOutput, eventHandler.looper, From 63368379031281d5c228f6a1ed5279b0c27cf794 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sat, 18 Apr 2026 07:40:05 -0600 Subject: [PATCH 15/60] Revert media3 to 1.9.3 (#2693) --- .../java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 4 ++-- gradle/libs.versions.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 8a643cc69b8..887777934a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -29,7 +29,7 @@ import androidx.media3.common.TrackGroup import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.Tracks import androidx.media3.common.VideoSize -import androidx.media3.common.util.ExperimentalApi +// import androidx.media3.common.util.ExperimentalApi import androidx.media3.common.util.UnstableApi import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.datasource.DataSource @@ -1196,7 +1196,7 @@ class CS3IPlayer : IPlayer { CustomDecoder.subtitleOffset = subtitleOffset val decoder = CustomSubtitleDecoderFactory() - @OptIn(ExperimentalApi::class) + // @OptIn(ExperimentalApi::class) val currentTextRenderer = TextRenderer( customTextOutput, eventHandler.looper, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f997e4f6e5d..a4f9219525b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,10 +27,10 @@ kotlinGradlePlugin = "2.3.20" kotlinxCoroutinesCore = "1.10.2" lifecycleKtx = "2.10.0" material = "1.14.0-beta01" -media3 = "1.10.0" +media3 = "1.9.3" navigationKtx = "2.9.7" newpipeextractor = "v0.26.0" -nextlibMedia3 = "1.10.0-0.12.1" +nextlibMedia3 = "1.9.3-0.12.0" nicehttp = "0.4.17" overlappingpanels = "0.1.5" paletteKtx = "1.0.0" From c67ba2b4859d7d1d2077c721cce28f42ddf33e10 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sat, 18 Apr 2026 07:45:55 -0600 Subject: [PATCH 16/60] Add explicit permission checks for notifications in downloader (#2667) --- .../lagradost/cloudstream3/services/DownloadQueueService.kt | 6 ++++++ .../cloudstream3/utils/downloader/DownloadManager.kt | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt b/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt index 37b9a100228..028356e76cc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt @@ -1,8 +1,10 @@ package com.lagradost.cloudstream3.services +import android.Manifest import android.app.Service import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.os.Build.VERSION.SDK_INT import android.os.IBinder @@ -104,6 +106,10 @@ class DownloadQueueService : Service() { private fun updateNotification(context: Context, downloads: Int, queued: Int) { + if (context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED + ) return + val activeDownloads = resources.getQuantityString(R.plurals.downloads_active, downloads).format(downloads) val activeQueue = diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt index 11c35e9ec04..94962de13f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadManager.kt @@ -1734,6 +1734,10 @@ object VideoDownloadManager { companion object { private fun displayNotification(context: Context, id: Int, notification: Notification) { safe { + if (context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED + ) return@safe + NotificationManagerCompat.from(context) .notify(DOWNLOAD_NOTIFICATION_TAG, id, notification) } From e55794c200e71f10f83c86d6a7189b03679e704a Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sat, 18 Apr 2026 07:52:14 -0600 Subject: [PATCH 17/60] Bump buildkonfig lib (#2643) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a4f9219525b..c02e79398c1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ activityKtx = "1.13.0" androidGradlePlugin = "8.13.2" appcompat = "1.7.1" biometric = "1.4.0-alpha06" -buildkonfigGradlePlugin = "0.17.1" +buildkonfigGradlePlugin = "0.18.0" coil = { strictly = "3.3.0" } # Later versions require jvmTarget 11 or later colorpicker = "6b46b49" conscryptAndroid = { strictly = "2.5.2" } # 2.5.3 crashes everything From f175beb51b727132589d4acbeed0dbad7366e591 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Sun, 19 Apr 2026 12:05:24 +0000 Subject: [PATCH 18/60] Fix concurrent plugin loading (#2700) --- .../cloudstream3/plugins/PluginManager.kt | 17 +++++++++++++---- .../cloudstream3/plugins/BasePlugin.kt | 4 +++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index feb0ba6d458..0dc65358a50 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -691,16 +691,25 @@ object PluginManager { APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename } } - extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename } + synchronized(extractorApis) { + extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename } + } synchronized(VideoClickActionHolder.allVideoClickActions) { VideoClickActionHolder.allVideoClickActions.removeIf { action: VideoClickAction -> action.sourcePlugin == plugin.filename } } - classLoaders.values.removeIf { v -> v == plugin } + synchronized(classLoaders) { + classLoaders.values.removeIf { v -> v == plugin } + } - plugins.remove(absolutePath) - urlPlugins.values.removeIf { v -> v == plugin } + synchronized(plugins) { + plugins.remove(absolutePath) + } + + synchronized(urlPlugins) { + urlPlugins.values.removeIf { v -> v == plugin } + } } /** diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt index c917a55ae42..61f87b8bab9 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/BasePlugin.kt @@ -31,7 +31,9 @@ abstract class BasePlugin { fun registerExtractorAPI(element: ExtractorApi) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi") element.sourcePlugin = this.filename - extractorApis.add(element) + synchronized(extractorApis) { + extractorApis.add(element) + } } /** From c1eef1de1de12e8da1873418b005a90caeb9961a Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 19 Apr 2026 06:41:05 -0600 Subject: [PATCH 19/60] Add new URL for Voe (#2701) --- .../kotlin/com/lagradost/cloudstream3/extractors/Voe.kt | 4 ++++ .../kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt | 2 ++ 2 files changed, 6 insertions(+) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Voe.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Voe.kt index 860f9b540ce..67eb49c9a55 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Voe.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Voe.kt @@ -45,6 +45,10 @@ class Voe1 : Voe() { override val mainUrl = "https://donaldlineelse.com" } +class Voe2 : Voe() { + override val mainUrl = "https://charlestoughrace.com" +} + open class Voe : ExtractorApi() { override val name = "Voe" override val mainUrl = "https://voe.sx" diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 4f3f05df6e7..1fd39943cbd 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -288,6 +288,7 @@ import com.lagradost.cloudstream3.extractors.Vidsonic import com.lagradost.cloudstream3.extractors.VkExtractor import com.lagradost.cloudstream3.extractors.Voe import com.lagradost.cloudstream3.extractors.Voe1 +import com.lagradost.cloudstream3.extractors.Voe2 import com.lagradost.cloudstream3.extractors.Vtbe import com.lagradost.cloudstream3.extractors.Wibufile import com.lagradost.cloudstream3.extractors.WishembedPro @@ -1097,6 +1098,7 @@ val extractorApis: MutableList = arrayListOf( Vidmolybiz(), Voe(), Voe1(), + Voe2(), Tubeless(), Moviehab(), MoviehabNet(), From ee6a9af217ee0cad9f5076a121355b375c67b11c Mon Sep 17 00:00:00 2001 From: hrisabhy <87358494+hrisabhy@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:29:44 +0530 Subject: [PATCH 20/60] Improve subtitle selection UX: Move "No Subtitles" option to bottom (#2523) --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 16b03e4f61c..c3d8306e672 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -1163,7 +1163,6 @@ class GeneratorPlayer : FullScreenPlayer() { val subsArrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) - subsArrayAdapter.add(ctx.getString(R.string.no_subtitles).html()) val subtitlesGrouped = currentSubtitles.groupBy { it.originalName }.map { (key, value) -> @@ -1173,8 +1172,13 @@ class GeneratorPlayer : FullScreenPlayer() { val subtitles = subtitlesGrouped.map { it.key.html() } - val subtitleGroupIndexStart = - subtitlesGrouped.keys.indexOf(currentSelectedSubtitles?.originalName) + 1 + val realIndex = subtitlesGrouped.keys.indexOf(currentSelectedSubtitles?.originalName) + val subtitleGroupIndexStart = if (realIndex == -1) { + // The "No Subtitles" option is outside the subtitlesGrouped list. + subtitlesGrouped.size + } else { + realIndex + } var subtitleGroupIndex = subtitleGroupIndexStart val subtitleOptionIndexStart = @@ -1183,6 +1187,7 @@ class GeneratorPlayer : FullScreenPlayer() { var subtitleOptionIndex = subtitleOptionIndexStart subsArrayAdapter.addAll(subtitles) + subsArrayAdapter.add(ctx.getString(R.string.no_subtitles).html()) subtitleList.adapter = subsArrayAdapter subtitleList.choiceMode = AbsListView.CHOICE_MODE_SINGLE @@ -1201,7 +1206,7 @@ class GeneratorPlayer : FullScreenPlayer() { val subtitleOptions = subtitlesGroupedList - .getOrNull(subtitleGroupIndex - 1)?.value?.map { subtitle -> + .getOrNull(subtitleGroupIndex)?.value?.map { subtitle -> val nameSuffix = subtitle.nameSuffix.html() nameSuffix.ifBlank { when (subtitle.origin) { @@ -1253,7 +1258,7 @@ class GeneratorPlayer : FullScreenPlayer() { } subtitleOptionList.setOnItemClickListener { _, _, which, _ -> - if (which >= (subtitlesGroupedList.getOrNull(subtitleGroupIndex - 1)?.value?.size + if (which >= (subtitlesGroupedList.getOrNull(subtitleGroupIndex)?.value?.size ?: -1) ) { val child = subtitleOptionList.adapter.getView(which, null, subtitleList) @@ -1340,10 +1345,10 @@ class GeneratorPlayer : FullScreenPlayer() { binding.applyBtt.setOnClickListener { var init = sourceIndex != startSource if (subtitleGroupIndex != subtitleGroupIndexStart || subtitleOptionIndex != subtitleOptionIndexStart) { - init = init or if (subtitleGroupIndex <= 0) { + init = init or if (subtitleGroupIndex >= subtitlesGrouped.size) { noSubtitles() } else { - subtitlesGroupedList.getOrNull(subtitleGroupIndex - 1)?.value?.getOrNull( + subtitlesGroupedList.getOrNull(subtitleGroupIndex)?.value?.getOrNull( subtitleOptionIndex )?.let { setSubtitles(it, true) From 68a1d0856c708c0c6e6faf0314296374152abc77 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 19 Apr 2026 07:09:19 -0600 Subject: [PATCH 21/60] Fix STATE_IDLE issues in player (#2691) --- .../cloudstream3/ui/player/CS3IPlayer.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 887777934a1..29a77883beb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -961,6 +961,22 @@ class CS3IPlayer : IPlayer { when (event) { CSPlayerEvent.Play -> { event(PlayEvent(source)) + // If the player was stopped (e.g. notification dismissed) it lands in + // STATE_IDLE. A bare play() call is a no-op in that state, re-prepare and + // then resume to the current position once we are in STATE_READY again. + if (playbackState == Player.STATE_IDLE) { + val seekPosition = currentPosition + exoPlayer?.addListener(object : Player.Listener { + private var seekApplied = false + override fun onPlaybackStateChanged(playbackState: Int) { + if (seekApplied || playbackState != Player.STATE_READY) return + seekApplied = true + exoPlayer?.seekTo(currentWindow, seekPosition) + exoPlayer?.removeListener(this) + } + }) + prepare() + } play() } From 7926e60fb00d93062acee671a088f37209edb4cb Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:29:37 +0000 Subject: [PATCH 22/60] Add plugin hash validation (#2644) --- .../cloudstream3/plugins/PluginManager.kt | 20 +++-- .../cloudstream3/plugins/RepositoryManager.kt | 87 ++++++++++++++----- .../settings/extensions/PluginsViewModel.kt | 2 + 3 files changed, 83 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 0dc65358a50..eae14a6c0c3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -13,6 +13,7 @@ import android.os.Build import android.os.Environment import android.util.Log import android.widget.Toast +import androidx.annotation.WorkerThread import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -45,6 +46,7 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDE import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins +import com.lagradost.cloudstream3.plugins.RepositoryManager.sha256 import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings @@ -78,6 +80,7 @@ data class PluginData( @JsonProperty("filePath") val filePath: String, @JsonProperty("version") val version: Int, ) { + @WorkerThread fun toSitePlugin(): SitePlugin { return SitePlugin( this.filePath, @@ -92,7 +95,9 @@ data class PluginData( null, null, null, - File(this.filePath).length() + File(this.filePath).length(), + // No file hash for local plugins. Local plugins have no use for the hash, and it's expensive to compute. + null ) } } @@ -302,6 +307,7 @@ object PluginManager { downloadPlugin( activity, pluginData.onlineData.second.url, + pluginData.onlineData.second.fileHash, pluginData.savedData.internalName, File(pluginData.savedData.filePath), true @@ -413,6 +419,7 @@ object PluginManager { downloadPlugin( activity, pluginData.onlineData.second.url, + pluginData.onlineData.second.fileHash, pluginData.savedData.internalName, pluginData.onlineData.first, !pluginData.isDisabled @@ -739,25 +746,27 @@ object PluginManager { suspend fun downloadPlugin( activity: Activity, pluginUrl: String, + pluginHash: String?, internalName: String, repositoryUrl: String, loadPlugin: Boolean ): Boolean { val file = getPluginPath(activity, internalName, repositoryUrl) - return downloadPlugin(activity, pluginUrl, internalName, file, loadPlugin) + return downloadPlugin(activity, pluginUrl, pluginHash, internalName, file, loadPlugin) } suspend fun downloadPlugin( activity: Activity, pluginUrl: String, + pluginHash: String?, internalName: String, file: File, - loadPlugin: Boolean + loadPlugin: Boolean, ): Boolean { try { Log.d(TAG, "Downloading plugin: $pluginUrl to ${file.absolutePath}") // The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names - val newFile = downloadPluginToFile(pluginUrl, file) ?: return false + val newFile = downloadPluginToFile(activity, pluginUrl, file, pluginHash) ?: return false val data = PluginData( internalName, @@ -845,6 +854,7 @@ object PluginManager { if (downloadPlugin( activity, pluginData.onlineData.second.url, + pluginData.onlineData.second.fileHash, pluginData.savedData.internalName, existingFile, true @@ -943,4 +953,4 @@ object PluginManager { return null } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index 45ed65611e7..07d6aaa37bc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.plugins import android.content.Context +import androidx.annotation.WorkerThread import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.CloudStreamApp.Companion.context import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey @@ -18,10 +19,12 @@ import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import java.io.BufferedInputStream import java.io.File -import java.io.InputStream -import java.io.OutputStream +import java.nio.file.AtomicMoveNotSupportedException +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import java.security.MessageDigest +import java.util.concurrent.atomic.AtomicInteger /** * Comes with the app, always available in the app, non removable. @@ -67,6 +70,7 @@ data class SitePlugin( @JsonProperty("iconUrl") val iconUrl: String?, // Automatically generated by the gradle plugin @JsonProperty("fileSize") val fileSize: Long?, + @JsonProperty("fileHash") val fileHash: String?, ) @@ -75,7 +79,26 @@ object RepositoryManager { val PREBUILT_REPOSITORIES: Array by lazy { getKey("PREBUILT_REPOSITORIES") ?: emptyArray() } - private val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$") + private val GH_REGEX = + Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$") + + + /** Returns a SHA-256 string of the file content. + * Example: "sha256-b70462c264cb7f90fc2860a8e58d7544ce747ff347d1d11fa093623901853573" **/ + @WorkerThread + fun sha256(file: File): String { + val digest = MessageDigest.getInstance("SHA-256") + + file.inputStream().use { fis -> + val buffer = ByteArray(8192) + var read = fis.read(buffer) + while (read != -1) { + digest.update(buffer, 0, read) + read = fis.read(buffer) + } + } + return "sha256-" + digest.digest().joinToString("") { "%02x".format(it) } + } /* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */ fun convertRawGitUrl(url: String): String { @@ -140,21 +163,52 @@ object RepositoryManager { }.flatten() } + suspend fun downloadPluginToFile( + context: Context, pluginUrl: String, - file: File + file: File, + expectedFileHash: String? ): File? { return safeAsync { - file.mkdirs() + val parentDir = file.parentFile ?: return@safeAsync null + parentDir.mkdirs() - // Overwrite if exists - if (file.exists()) { - file.delete() - } - file.createNewFile() + // Prevent corrupting the plugin file if the operation fails + val tempFile = File.createTempFile(file.name, ".tmp", context.cacheDir) val body = app.get(convertRawGitUrl(pluginUrl)).okhttpResponse.body - write(body.byteStream(), file.outputStream()) + + body.byteStream().use { body -> + tempFile.outputStream().use { fileSteam -> + body.copyTo(fileSteam) + } + } + + if (expectedFileHash != null) { + val downloadHash = sha256(tempFile) + if (expectedFileHash != downloadHash) { + tempFile.delete() + throw IllegalStateException("Extension hash mismatch when validating '${file.name}'! Expected: '$expectedFileHash', got: '$downloadHash'.") + } + } + + // We prefer the operation to be atomic + try { + Files.move( + tempFile.toPath(), + file.toPath(), + StandardCopyOption.REPLACE_EXISTING, + StandardCopyOption.ATOMIC_MOVE + ) + } catch (_: AtomicMoveNotSupportedException) { + Files.move( + tempFile.toPath(), + file.toPath(), + StandardCopyOption.REPLACE_EXISTING + ) + } + file } } @@ -202,13 +256,4 @@ object RepositoryManager { PluginManager.deleteRepositoryData(file.absolutePath) } - - private fun write(stream: InputStream, output: OutputStream) { - val input = BufferedInputStream(stream) - val dataBuffer = ByteArray(512) - var readBytes: Int - while (input.read(dataBuffer).also { readBytes = it } != -1) { - output.write(dataBuffer, 0, readBytes) - } - } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt index e0fd906b49f..dfc61eba54c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsViewModel.kt @@ -128,6 +128,7 @@ class PluginsViewModel : ViewModel() { PluginManager.downloadPlugin( activity, metadata.url, + metadata.fileHash, metadata.internalName, repo, metadata.status != PROVIDER_STATUS_DOWN @@ -179,6 +180,7 @@ class PluginsViewModel : ViewModel() { PluginManager.downloadPlugin( activity, metadata.url, + metadata.fileHash, metadata.internalName, repo, isEnabled From 7c1554a479a8e0679c235b2bae5d293b4fb7bd46 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:00:47 -0600 Subject: [PATCH 23/60] AGP 9! (#2604) --- app/build.gradle.kts | 5 +++-- build.gradle.kts | 1 - gradle/libs.versions.toml | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0b201d1cb35..0ea37a0257e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,7 +8,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile plugins { alias(libs.plugins.android.application) alias(libs.plugins.dokka) - alias(libs.plugins.kotlin.android) } val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) @@ -314,8 +313,10 @@ tasks.withType { dokka { moduleName = "App" dokkaSourceSets { - main { + configureEach { + suppress = name != "prereleaseDebug" analysisPlatform = KotlinPlatform.JVM + displayName = "JVM" documentedVisibilities( VisibilityModifier.Public, VisibilityModifier.Protected diff --git a/build.gradle.kts b/build.gradle.kts index cca263dd422..e35c1f61148 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,6 @@ plugins { alias(libs.plugins.android.multiplatform.library) apply false alias(libs.plugins.buildkonfig) apply false // Universal build config alias(libs.plugins.dokka) apply false - alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.multiplatform) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c02e79398c1..e304fd57f91 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # https://docs.gradle.org/current/userguide/dependency_versions.html#sec:strict-version [versions] activityKtx = "1.13.0" -androidGradlePlugin = "8.13.2" +androidGradlePlugin = "9.1.1" appcompat = "1.7.1" biometric = "1.4.0-alpha06" buildkonfigGradlePlugin = "0.18.0" @@ -117,7 +117,6 @@ android-lint = { id = "com.android.lint", version.ref = "androidGradlePlugin" } android-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "androidGradlePlugin" } buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfigGradlePlugin" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokkaGradlePlugin" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinGradlePlugin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm" , version.ref = "kotlinGradlePlugin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlinGradlePlugin" } From e3e995b2227ffffcbd22d2350c00f3def0fbf4a7 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:01:50 -0600 Subject: [PATCH 24/60] Add lint ignore (#2669) We only care about the source language with this, not translations which would mostly be false positives. --- app/lint.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/lint.xml b/app/lint.xml index 48cdec04ac3..b2f5e8f2bc3 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -5,4 +5,9 @@ + + + + + From 0ed6fd8fef2e119a64193fcdddaadc5eb081a7ca Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 19 Apr 2026 15:39:12 -0600 Subject: [PATCH 25/60] Bump jsoup and zipline libs (#2517) --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e304fd57f91..6aaa0c43f97 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ fragmentKtx = "1.8.9" fuzzywuzzy = "1.4.0" jacksonModuleKotlin = { strictly = "2.13.1" } # Later versions don't support minSdk <26 (Crashes on Android TV's and FireSticks) json = "20251224" -jsoup = "1.21.2" +jsoup = "1.22.1" junit = "4.13.2" junitKtx = "1.3.0" junitVersion = "1.3.0" @@ -45,7 +45,7 @@ torrentserver = "7861970" tvprovider = "1.1.0" video = "1.0.0" workRuntimeKtx = "2.11.2" -zipline = "1.24.0" +zipline = "1.27.0" jvmTarget = "1.8" jdkToolchain = "17" From 2264b903963e1928cd440cee468743c8570e0634 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 19 Apr 2026 15:39:22 -0600 Subject: [PATCH 26/60] Bump nicehttp (#2697) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6aaa0c43f97..19be8d6ada4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ media3 = "1.9.3" navigationKtx = "2.9.7" newpipeextractor = "v0.26.0" nextlibMedia3 = "1.9.3-0.12.0" -nicehttp = "0.4.17" +nicehttp = "0.4.18" overlappingpanels = "0.1.5" paletteKtx = "1.0.0" preferenceKtx = "1.2.1" From 590a94e3188c38b01a55a25956290c84a5207966 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 19 Apr 2026 15:40:12 -0600 Subject: [PATCH 27/60] Fix typo in credits (#2703) --- .../java/com/lagradost/cloudstream3/utils/videoskip/SkipAPI.kt | 2 +- app/src/main/res/values-b+apc/strings.xml | 2 +- app/src/main/res/values-b+ar/strings.xml | 2 +- app/src/main/res/values-b+as/strings.xml | 2 +- app/src/main/res/values-b+bg/strings.xml | 2 +- app/src/main/res/values-b+cs/strings.xml | 2 +- app/src/main/res/values-b+de/strings.xml | 2 +- app/src/main/res/values-b+el/strings.xml | 2 +- app/src/main/res/values-b+es/strings.xml | 2 +- app/src/main/res/values-b+fr/strings.xml | 2 +- app/src/main/res/values-b+hr/strings.xml | 2 +- app/src/main/res/values-b+hu/strings.xml | 2 +- app/src/main/res/values-b+in/strings.xml | 2 +- app/src/main/res/values-b+it/strings.xml | 2 +- app/src/main/res/values-b+iw/strings.xml | 2 +- app/src/main/res/values-b+ja/strings.xml | 2 +- app/src/main/res/values-b+ko/strings.xml | 2 +- app/src/main/res/values-b+lv/strings.xml | 2 +- app/src/main/res/values-b+mk/strings.xml | 2 +- app/src/main/res/values-b+ms/strings.xml | 2 +- app/src/main/res/values-b+my/strings.xml | 2 +- app/src/main/res/values-b+nl/strings.xml | 2 +- app/src/main/res/values-b+no/strings.xml | 2 +- app/src/main/res/values-b+or/strings.xml | 2 +- app/src/main/res/values-b+pl/strings.xml | 2 +- app/src/main/res/values-b+pt+BR/strings.xml | 2 +- app/src/main/res/values-b+pt/strings.xml | 2 +- app/src/main/res/values-b+qt/strings.xml | 2 +- app/src/main/res/values-b+ro/strings.xml | 2 +- app/src/main/res/values-b+ru/strings.xml | 2 +- app/src/main/res/values-b+so/strings.xml | 2 +- app/src/main/res/values-b+sv/strings.xml | 2 +- app/src/main/res/values-b+ta/strings.xml | 2 +- app/src/main/res/values-b+tr/strings.xml | 2 +- app/src/main/res/values-b+uk/strings.xml | 2 +- app/src/main/res/values-b+ur/strings.xml | 2 +- app/src/main/res/values-b+vi/strings.xml | 2 +- app/src/main/res/values-b+zh+TW/strings.xml | 2 +- app/src/main/res/values-b+zh/strings.xml | 2 +- app/src/main/res/values-be/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 41 files changed, 41 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/SkipAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/SkipAPI.kt index cd6727a24a4..6c712604903 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/SkipAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/SkipAPI.kt @@ -16,7 +16,7 @@ enum class SkipType(@StringRes val res: Int) { Recap(R.string.skip_type_recap), MixedOpening(R.string.skip_type_mixed_op), MixedEnding(R.string.skip_type_mixed_ed), - Credits(R.string.skip_type_creddits), + Credits(R.string.skip_type_credits), Intro(R.string.skip_type_intro), Preview(R.string.skip_type_preview), } diff --git a/app/src/main/res/values-b+apc/strings.xml b/app/src/main/res/values-b+apc/strings.xml index 9bc697acf26..365a317e30d 100644 --- a/app/src/main/res/values-b+apc/strings.xml +++ b/app/src/main/res/values-b+apc/strings.xml @@ -562,7 +562,7 @@ /%d @string/home_play شيلو من لايحة المحتوى الحاضرينو - الإعتمادات + الإعتمادات فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يللي عنده أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يللي بتفضلوها. \n \nمتلًا: diff --git a/app/src/main/res/values-b+ar/strings.xml b/app/src/main/res/values-b+ar/strings.xml index 17e809d8d0b..158748bbf7e 100644 --- a/app/src/main/res/values-b+ar/strings.xml +++ b/app/src/main/res/values-b+ar/strings.xml @@ -454,7 +454,7 @@ مشغل داخلي لم يتم العثور على التطبيق جميع اللغات - الإعتمادات + الإعتمادات ‌تنزيل تحديث التطبيق… ‏تثبيت تحديث التطبيق… %d دقيقة diff --git a/app/src/main/res/values-b+as/strings.xml b/app/src/main/res/values-b+as/strings.xml index eb6ad4aa444..f5338a9e5ef 100644 --- a/app/src/main/res/values-b+as/strings.xml +++ b/app/src/main/res/values-b+as/strings.xml @@ -493,7 +493,7 @@ মিশ্ৰিত সমাপ্তি মিশ্ৰিত উদ্‌ঘাটনী ইতিহাস পৰিস্কাৰ কৰক - স্বীকৃতি + স্বীকৃতি ভূমিকা ইতিহাস উদ্‌ঘাটনী/সমাপ্তিৰ বাবে এৰি দিয়াৰ পপআপ দেখুৱাওক diff --git a/app/src/main/res/values-b+bg/strings.xml b/app/src/main/res/values-b+bg/strings.xml index 096e9f66b3d..6c8e3872274 100644 --- a/app/src/main/res/values-b+bg/strings.xml +++ b/app/src/main/res/values-b+bg/strings.xml @@ -451,7 +451,7 @@ Изтегля се актуализация на приложението… Смесено отваряне Смесено затваряне - Кредити + Кредити въведение Изчистване на историята Автоматично инсталиране на всички все още неинсталирани добавки от добавени хранилища. diff --git a/app/src/main/res/values-b+cs/strings.xml b/app/src/main/res/values-b+cs/strings.xml index 96110d9c1e1..71ddc8697e5 100644 --- a/app/src/main/res/values-b+cs/strings.xml +++ b/app/src/main/res/values-b+cs/strings.xml @@ -438,7 +438,7 @@ Vymazat historii Všechny jazyky Smíšený úvod - Poděkování + Poděkování Znělka Zobrazit vyskakovací okna pro přeskočení úvodu/konce Stahování aktualizace aplikace… diff --git a/app/src/main/res/values-b+de/strings.xml b/app/src/main/res/values-b+de/strings.xml index 9a67f9d204b..dfcf97ce2fd 100644 --- a/app/src/main/res/values-b+de/strings.xml +++ b/app/src/main/res/values-b+de/strings.xml @@ -13,7 +13,7 @@ Chromecast-Mirror In App wiedergeben Gemischte Openings - Abspann + Abspann Intro Verlauf löschen Verlauf diff --git a/app/src/main/res/values-b+el/strings.xml b/app/src/main/res/values-b+el/strings.xml index 4b671644bda..6839d994459 100644 --- a/app/src/main/res/values-b+el/strings.xml +++ b/app/src/main/res/values-b+el/strings.xml @@ -389,7 +389,7 @@ Web HDR Ανάμεικτοι τίτλοι αρχής - Εύσημα + Εύσημα Εισαγωγή +30 Ολοκληρώθηκε diff --git a/app/src/main/res/values-b+es/strings.xml b/app/src/main/res/values-b+es/strings.xml index 5e59477cef5..e692ecc4aca 100644 --- a/app/src/main/res/values-b+es/strings.xml +++ b/app/src/main/res/values-b+es/strings.xml @@ -66,7 +66,7 @@ Final Apertura mixta Resumen - Créditos + Créditos Final mixto Póster del episodio Siguiente episodio diff --git a/app/src/main/res/values-b+fr/strings.xml b/app/src/main/res/values-b+fr/strings.xml index 1cbee687f54..10b8cf9ef26 100644 --- a/app/src/main/res/values-b+fr/strings.xml +++ b/app/src/main/res/values-b+fr/strings.xml @@ -302,7 +302,7 @@ Ignorer %s Ouverture Récap - Crédits + Crédits Intro Effacer l\'historique Oui diff --git a/app/src/main/res/values-b+hr/strings.xml b/app/src/main/res/values-b+hr/strings.xml index 8b3a6fbf339..c629c492ff1 100644 --- a/app/src/main/res/values-b+hr/strings.xml +++ b/app/src/main/res/values-b+hr/strings.xml @@ -436,7 +436,7 @@ Jezik HLS playlista Automatski instaliraj dodatke - Zasluge + Zasluge Automatski instaliraj sve neinstalirane dodatke iz dodanih repozitorija. Preferirani video player Interni player diff --git a/app/src/main/res/values-b+hu/strings.xml b/app/src/main/res/values-b+hu/strings.xml index 8bd2ac7ac6c..bffc0a86a3b 100644 --- a/app/src/main/res/values-b+hu/strings.xml +++ b/app/src/main/res/values-b+hu/strings.xml @@ -469,7 +469,7 @@ Repó URL Bővítmény betöltve Bővítmény letöltve - Közreműködők + Közreműködők Betűrendben (Z-től az A-ig) Könyvtár kiválasztása Biztonságos módú fájlba ütköztünk! diff --git a/app/src/main/res/values-b+in/strings.xml b/app/src/main/res/values-b+in/strings.xml index d5bf2d4b012..8309725866a 100644 --- a/app/src/main/res/values-b+in/strings.xml +++ b/app/src/main/res/values-b+in/strings.xml @@ -467,7 +467,7 @@ Sesi Akhir Sinopsis Sesi akhir ganda - Sesi Kredit + Sesi Kredit Sesi Intro Terlalu banyak teks. Tidak dapat menyalin ke papan klip. Yakin ingin keluar? diff --git a/app/src/main/res/values-b+it/strings.xml b/app/src/main/res/values-b+it/strings.xml index 08a1572d6de..c96299c7208 100644 --- a/app/src/main/res/values-b+it/strings.xml +++ b/app/src/main/res/values-b+it/strings.xml @@ -447,7 +447,7 @@ Riassunto - Crediti + Crediti Cancella cronologia Cronologia diff --git a/app/src/main/res/values-b+iw/strings.xml b/app/src/main/res/values-b+iw/strings.xml index ef4cb9202ed..0b047967905 100644 --- a/app/src/main/res/values-b+iw/strings.xml +++ b/app/src/main/res/values-b+iw/strings.xml @@ -422,7 +422,7 @@ כל %s כבר הורד מחברים שפה - קרדיטים + קרדיטים מיין בחר ספרייה נראה שהספרייה שלכם ריקה :( diff --git a/app/src/main/res/values-b+ja/strings.xml b/app/src/main/res/values-b+ja/strings.xml index 0b66ca8b2ec..b489db37ddc 100644 --- a/app/src/main/res/values-b+ja/strings.xml +++ b/app/src/main/res/values-b+ja/strings.xml @@ -469,7 +469,7 @@ 無効: %d 優先ビデオプレーヤー %s をスキップ - クレジット + クレジット アプリのバッテリー使用はすでに無制限に設定されています 並べ替え 元に戻す diff --git a/app/src/main/res/values-b+ko/strings.xml b/app/src/main/res/values-b+ko/strings.xml index 256fc26e9f5..efe03425891 100644 --- a/app/src/main/res/values-b+ko/strings.xml +++ b/app/src/main/res/values-b+ko/strings.xml @@ -443,7 +443,7 @@ 엔딩 혼합 엔딩 혼합 오프닝 - 크레딧 + 크레딧 소개 기록 삭제 기록 diff --git a/app/src/main/res/values-b+lv/strings.xml b/app/src/main/res/values-b+lv/strings.xml index 89003317a51..101498b8364 100644 --- a/app/src/main/res/values-b+lv/strings.xml +++ b/app/src/main/res/values-b+lv/strings.xml @@ -436,7 +436,7 @@ Kopsavilkums Jauktas beigas Jauktais sākums - Kredīts + Kredīts Notīrīt vēsturi Vēsture Rādīt izlaižamos uznirstošos logus atvēršanai/beigšanai diff --git a/app/src/main/res/values-b+mk/strings.xml b/app/src/main/res/values-b+mk/strings.xml index 6998c49dbec..4af4995ea4c 100644 --- a/app/src/main/res/values-b+mk/strings.xml +++ b/app/src/main/res/values-b+mk/strings.xml @@ -260,7 +260,7 @@ Подреди Внатрешен плеер Резолуција - Кредити + Кредити Пребарај %s… Приклучокот е избришан Статус diff --git a/app/src/main/res/values-b+ms/strings.xml b/app/src/main/res/values-b+ms/strings.xml index 83492a5fff3..9ec0192cf86 100644 --- a/app/src/main/res/values-b+ms/strings.xml +++ b/app/src/main/res/values-b+ms/strings.xml @@ -5,7 +5,7 @@ Sejarah Kosongkan sejarah Pengenalan - Kredit + Kredit Pembukaan bercampur Penamat Pembukaan diff --git a/app/src/main/res/values-b+my/strings.xml b/app/src/main/res/values-b+my/strings.xml index 4a7a50aa73d..0938e4f982b 100644 --- a/app/src/main/res/values-b+my/strings.xml +++ b/app/src/main/res/values-b+my/strings.xml @@ -336,7 +336,7 @@ အစမှပြန်စ ရောထားသောအဆုံးပိုင်း ရောထားသောအစပိုင်း - ခရက်ဒစ်များ + ခရက်ဒစ်များ အစ သေချာသည် သမားရိုးကျ diff --git a/app/src/main/res/values-b+nl/strings.xml b/app/src/main/res/values-b+nl/strings.xml index 30b8b2def99..3cdea9d8f35 100644 --- a/app/src/main/res/values-b+nl/strings.xml +++ b/app/src/main/res/values-b+nl/strings.xml @@ -514,7 +514,7 @@ Veilige mode aan Herstart Beschrijving - Waardering + Waardering Wis geschiedenis Ingeschreven Wis repository diff --git a/app/src/main/res/values-b+no/strings.xml b/app/src/main/res/values-b+no/strings.xml index 374b033c6af..55b5303eb12 100644 --- a/app/src/main/res/values-b+no/strings.xml +++ b/app/src/main/res/values-b+no/strings.xml @@ -381,7 +381,7 @@ Bruk dette hvis undertekster vises %d ms for sent Programtillegg innlastet Lydspor - Rulletekst + Rulletekst Introduksjon Lagringstilgang mangler. Prøv igjen. Vis trailere diff --git a/app/src/main/res/values-b+or/strings.xml b/app/src/main/res/values-b+or/strings.xml index 8c9379f5bab..40a2915fd84 100644 --- a/app/src/main/res/values-b+or/strings.xml +++ b/app/src/main/res/values-b+or/strings.xml @@ -61,7 +61,7 @@ ସବୁ ଭାଷା ମିଶ୍ରିତ ପ୍ରାନ୍ତ ମିଶ୍ରିତ ଆଦ୍ୟ - ଶ୍ରେୟ + ଶ୍ରେୟ ଉପକ୍ରମ ଏହି ଭାଷାଗୁଡ଼ିକରେ ଵିଡ଼ିଓ ଦେଖନ୍ତୁ ସଂସ୍କରଣ diff --git a/app/src/main/res/values-b+pl/strings.xml b/app/src/main/res/values-b+pl/strings.xml index c8126f2fe42..0536e680775 100644 --- a/app/src/main/res/values-b+pl/strings.xml +++ b/app/src/main/res/values-b+pl/strings.xml @@ -452,7 +452,7 @@ Opening Ending Mixed opening - Napisy końcowe + Napisy końcowe Intro Mixed ending Pokaż wyskakujące okienka pomijania dla niektórych segmentów diff --git a/app/src/main/res/values-b+pt+BR/strings.xml b/app/src/main/res/values-b+pt+BR/strings.xml index 58d59870892..72ecbf3d9cd 100644 --- a/app/src/main/res/values-b+pt+BR/strings.xml +++ b/app/src/main/res/values-b+pt+BR/strings.xml @@ -510,7 +510,7 @@ Versão Autores Instale a extensão primeiro - Créditos + Créditos Historico Limpar historico Tem Muito texto. Não é possível salvar no clipboard. diff --git a/app/src/main/res/values-b+pt/strings.xml b/app/src/main/res/values-b+pt/strings.xml index a1abfa33836..88eccbeac6f 100644 --- a/app/src/main/res/values-b+pt/strings.xml +++ b/app/src/main/res/values-b+pt/strings.xml @@ -410,7 +410,7 @@ Sim Baixando atualização do app… Episódio %d lançado! - Créditos + Créditos Descrição Tamanho Parar diff --git a/app/src/main/res/values-b+qt/strings.xml b/app/src/main/res/values-b+qt/strings.xml index d60a4e32c0d..8a43e97d78b 100644 --- a/app/src/main/res/values-b+qt/strings.xml +++ b/app/src/main/res/values-b+qt/strings.xml @@ -607,7 +607,7 @@ aaaagg aahh oooohh uuuuuk aaagg aaaahhh - ooooggg + ooooggg oh oooogg ooooggguuuugg aaaahhhug ooh aaaaggguuuuuk ooh aaaagggoog uk shows, aaaaggguugg aaagg uuuuggguug ug uug oh aaaahhhooh ah eeeeeekh OK, youl og aaaaaakg uh uuk aagg There, aaaagg uk oog uuuuhhh ooohh uug uuh oooohhh aaagg ug uuuuhhhooohh oooohh note, aagg aaaahhhaak oohh uug uugg CS3 aagg eeeek aahh uuuuhhh uh uugg oogg ooooggg ek uug aaaahhhaak oohh necessary, uugg oh uuhh uuuuhhhuh uuuuuukaaaahh ah oooohhhaagg uuuuuk oogg aaaagggh oooogggoog uh uuh aaaahh ug cancel, uuh uug aaaahh uugg uuuuggg ooogg uh aaaahhh uuuugggg uuuuggg (Old og New) uuuuggguuuhh (Z ak A) diff --git a/app/src/main/res/values-b+ro/strings.xml b/app/src/main/res/values-b+ro/strings.xml index dbd6076665c..bb49563ece3 100644 --- a/app/src/main/res/values-b+ro/strings.xml +++ b/app/src/main/res/values-b+ro/strings.xml @@ -522,7 +522,7 @@ Afișează opțiunea de omitere a ferestrelor pop-up pentru început/sfârșit Toate limbile Deschidere mixat - Credite + Credite Limbă plugin plugin-uri diff --git a/app/src/main/res/values-b+ru/strings.xml b/app/src/main/res/values-b+ru/strings.xml index 9f6b53aa75f..1a5db40a19c 100644 --- a/app/src/main/res/values-b+ru/strings.xml +++ b/app/src/main/res/values-b+ru/strings.xml @@ -303,7 +303,7 @@ Приложение не найдено Все языки Вступление - Титры + Титры Отметить как просмотренное Показывать информацию про видеоплеер Предпочтительное качество видео (WiFi) diff --git a/app/src/main/res/values-b+so/strings.xml b/app/src/main/res/values-b+so/strings.xml index 09499af0038..9e4e9f9f119 100644 --- a/app/src/main/res/values-b+so/strings.xml +++ b/app/src/main/res/values-b+so/strings.xml @@ -471,5 +471,5 @@ Dhamaad isku qasan Bilowga Bilow isku qasan - Qoraalka dhamaadka + Qoraalka dhamaadka diff --git a/app/src/main/res/values-b+sv/strings.xml b/app/src/main/res/values-b+sv/strings.xml index e388b67e1ff..69505033621 100644 --- a/app/src/main/res/values-b+sv/strings.xml +++ b/app/src/main/res/values-b+sv/strings.xml @@ -549,7 +549,7 @@ Ladda ner listan över webbplatser du vill använda %s (Inaktiverad) Beskrivning - Eftertexter + Eftertexter Introduktion Favoriter Ange standard diff --git a/app/src/main/res/values-b+ta/strings.xml b/app/src/main/res/values-b+ta/strings.xml index 626554c1865..5cdbeaa3711 100644 --- a/app/src/main/res/values-b+ta/strings.xml +++ b/app/src/main/res/values-b+ta/strings.xml @@ -537,7 +537,7 @@ உள் வீரர் திறப்பு கலப்பு திறப்பு - வரவு + வரவு வரலாறு சரி கிளவுட்ச்ட்ரீமின் பயன்பாட்டுத் தகவலைத் திறக்க முடியவில்லை. diff --git a/app/src/main/res/values-b+tr/strings.xml b/app/src/main/res/values-b+tr/strings.xml index e84d5271e37..47db0e97e62 100644 --- a/app/src/main/res/values-b+tr/strings.xml +++ b/app/src/main/res/values-b+tr/strings.xml @@ -477,7 +477,7 @@ İzlenenlerden kaldır Karışık son Karışık başlangıç - Katkıda Bulunanlar + Katkıda Bulunanlar Giriş Eklenti İndirildi Eylemler diff --git a/app/src/main/res/values-b+uk/strings.xml b/app/src/main/res/values-b+uk/strings.xml index 2eb6e24518e..74acb48ae62 100644 --- a/app/src/main/res/values-b+uk/strings.xml +++ b/app/src/main/res/values-b+uk/strings.xml @@ -435,7 +435,7 @@ Коротке повторення Пропустити %s Змішаний ендінґ - Подяки + Подяки Опенінґ Вступ Очистити історію diff --git a/app/src/main/res/values-b+ur/strings.xml b/app/src/main/res/values-b+ur/strings.xml index 5f6d8aa1473..acd6e40975b 100644 --- a/app/src/main/res/values-b+ur/strings.xml +++ b/app/src/main/res/values-b+ur/strings.xml @@ -346,7 +346,7 @@ %d / 10 اٹھایا اگر سب ٹائٹلز %d ms بہت جلد دکھائے جائیں تو اسے استعمال کریں - کریڈٹس + کریڈٹس اضافی مرکزی ترتیب diff --git a/app/src/main/res/values-b+vi/strings.xml b/app/src/main/res/values-b+vi/strings.xml index a51a3551db0..d0d6059aa12 100644 --- a/app/src/main/res/values-b+vi/strings.xml +++ b/app/src/main/res/values-b+vi/strings.xml @@ -470,7 +470,7 @@ Điểm lại nội dung Kết thúc hỗn hợp Mở đầu hỗn hợp - Danh đề + Danh đề Giới thiệu Xoá lịch sử Hiện các popup bỏ qua cho mở đầu/kết thúc diff --git a/app/src/main/res/values-b+zh+TW/strings.xml b/app/src/main/res/values-b+zh+TW/strings.xml index 78ba573100a..cb58e96f580 100644 --- a/app/src/main/res/values-b+zh+TW/strings.xml +++ b/app/src/main/res/values-b+zh+TW/strings.xml @@ -448,7 +448,7 @@ 前情回顧 混合片尾 混合片頭 - 致謝名單 + 致謝名單 介紹 清除歷史紀錄 歷史紀錄 diff --git a/app/src/main/res/values-b+zh/strings.xml b/app/src/main/res/values-b+zh/strings.xml index bc7c2ca0e30..496afe81c9d 100644 --- a/app/src/main/res/values-b+zh/strings.xml +++ b/app/src/main/res/values-b+zh/strings.xml @@ -449,7 +449,7 @@ 前情回顾 混合片尾 混合片头 - 致谢名单 + 致谢名单 介绍 清除历史记录 历史记录 diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index ee2c24972b8..374de33d2c1 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -537,7 +537,7 @@ Зводка Змешанае заканчэнне Змешаны опенінг - Удзельнікі + Удзельнікі Застаўка Ачысціць гісторыю Гісторыя diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d59065a64e3..e41c01fda69 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -559,7 +559,7 @@ Recap Mixed ending Mixed opening - Credits + Credits Preview Intro Clear history From f7494f20e17a3731ea298588af62f4fc9e714f77 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 19 Apr 2026 15:42:46 -0600 Subject: [PATCH 28/60] Support resuming fragmented MP4s (#2690) --- .../cloudstream3/ui/player/CS3IPlayer.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 29a77883beb..4323c98fde5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -1430,6 +1430,23 @@ class CS3IPlayer : IPlayer { event(PlayerAttachedEvent(exoPlayer)) exoPlayer?.prepare() + // For offline fragmented MP4s, FLAG_MERGE_FRAGMENTED_SIDX builds the SIDX seek map + // incrementally as data is buffered. The initial seek resolves to the nearest merged + // entry (~first fragment, 3 s). On STATE_READY, re-seek to the actual saved position. + // This may only be reproducible on large and fairly long fragmented MP4 files with + // multiple sidx boxes. + if (onlineSource == null && playbackPosition > (exoPlayer?.duration ?: 0L)) { + exoPlayer?.addListener(object : Player.Listener { + private var seekApplied = false + override fun onPlaybackStateChanged(playbackState: Int) { + if (seekApplied || playbackState != Player.STATE_READY) return + seekApplied = true + exoPlayer?.seekTo(currentWindow, playbackPosition) + exoPlayer?.removeListener(this) + } + }) + } + exoPlayer?.let { exo -> event(StatusEvent(CSPlayerLoading.IsBuffering, CSPlayerLoading.IsBuffering)) isPlaying = exo.isPlaying From 18ee71664ff6b7592e45e85c8ad6c758d64472f6 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:24:37 +0000 Subject: [PATCH 29/60] Feat: Offline filler database (#2704) --- app/build.gradle.kts | 3 + .../ui/result/ResultViewModel2.kt | 18 +- .../cloudstream3/utils/FillerEpisodeCheck.kt | 236 +++++++++++------- gradle/libs.versions.toml | 2 + 4 files changed, 160 insertions(+), 99 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0ea37a0257e..3684450977a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -234,6 +234,9 @@ dependencies { // FFmpeg Decoding implementation(libs.bundles.nextlib) + // Anime-db for filler + implementation(libs.anime.db) + // PlayBack implementation(libs.colorpicker) // Subtitle Color Picker implementation(libs.newpipeextractor) // For Trailers diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 6eab987fc6e..cc48f65494a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -113,12 +113,14 @@ import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.txt import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.isActive import kotlinx.coroutines.job import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.concurrent.TimeUnit /** This starts at 1 */ @@ -452,7 +454,7 @@ class ResultViewModel2 : ViewModel() { private var currentShowFillers: Boolean = false var currentRepo: APIRepository? = null private var currentId: Int? = null - private var fillers: Map = emptyMap() + private var fillers: HashSet = hashSetOf() private var generator: IGenerator? = null private var preferDubStatus: DubStatus? = null private var preferStartEpisode: Int? = null @@ -1806,11 +1808,11 @@ class ResultViewModel2 : ViewModel() { } - private suspend fun updateFillers(name: String) { + private suspend fun updateFillers(data : LoadResponse) { fillers = - ioWorkSafe { - FillerEpisodeCheck.getFillerEpisodes(name) - } ?: emptyMap() + withContext(Dispatchers.IO) { + safe { FillerEpisodeCheck.getFillerEpisodes(data) } + } ?: hashSetOf() } fun changeDubStatus(status: DubStatus) { @@ -2147,8 +2149,8 @@ class ResultViewModel2 : ViewModel() { ) { _episodes.postValue(Resource.Loading()) - if (updateFillers && loadResponse is AnimeLoadResponse) { - updateFillers(loadResponse.name) + if (updateFillers) { + updateFillers(loadResponse) } val allEpisodes = when (loadResponse) { @@ -2189,7 +2191,7 @@ class ResultViewModel2 : ViewModel() { index, i.score, i.description, - fillers.getOrDefault(episode, false), + fillers.contains(episode), loadResponse.type, mainId, totalIndex, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt index 09d4683bc20..8456094d1e9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/FillerEpisodeCheck.kt @@ -1,112 +1,166 @@ package com.lagradost.cloudstream3.utils -import com.lagradost.cloudstream3.app +import androidx.annotation.WorkerThread +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.getKitsuId +import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.getTMDbId +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.ui.result.getId import com.lagradost.cloudstream3.utils.Coroutines.main -import org.jsoup.Jsoup import java.lang.Thread.sleep import java.util.* import kotlin.concurrent.thread +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import java.io.InputStream +import kotlin.let object FillerEpisodeCheck { - private const val MAIN_URL = "https://www.animefillerlist.com" + fun String?.toClassDir(): String { + val q = this ?: "null" + val z = (6..10).random().calc() + return q + "cache" + z + } - var list: HashMap? = null - var cache: HashMap> = hashMapOf() + data class Show( + @JsonProperty("slug") + val slug: String, + @JsonProperty("title") + val title: String, + @JsonProperty("filler") + val filler: ArrayList, + @JsonProperty("mixedCanon") + val mixedCanon: ArrayList, + @JsonProperty("mangaCanon") + val mangaCanon: ArrayList, + @JsonProperty("animeCanon") + val animeCanon: ArrayList, + ) - private fun fixName(name: String): String { - return name.lowercase(Locale.ROOT)/*.replace(" ", "")*/.replace("-", " ") - .replace("[^a-zA-Z0-9 ]".toRegex(), "") - } + data class MappingRoot( + @JsonProperty("type") + val type: String?, + @JsonProperty("anidb_id") + val anidbId: Long?, + @JsonProperty("anilist_id") + val anilistId: Long?, + @JsonProperty("animecountdown_id") + val animecountdownId: Long?, + @JsonProperty("animenewsnetwork_id") + val animenewsnetworkId: Long?, + @JsonProperty("anime-planet_id") + val animePlanetId: String?, + @JsonProperty("anisearch_id") + val anisearchId: Long?, + @JsonProperty("imdb_id") + val imdbId: String?, + @JsonProperty("kitsu_id") + val kitsuId: Long?, + @JsonProperty("livechart_id") + val livechartId: Long?, + @JsonProperty("mal_id") + val malId: Long?, + @JsonProperty("simkl_id") + val simklId: Long?, + @JsonProperty("themoviedb_id") + val themoviedbId: Long?, + @JsonProperty("tvdb_id") + val tvdbId: Long?, + @JsonProperty("season") + val season: Season?, + ) - private suspend fun getFillerList(): Boolean { - if (list != null) return true - try { - val result = app.get("$MAIN_URL/shows").text - val documented = Jsoup.parse(result) - val localHTMLList = documented.select("div#ShowList > div.Group > ul > li > a") - val localList = HashMap() - for (i in localHTMLList) { - val name = i.text() - - if (name.lowercase(Locale.ROOT).contains("manga only")) continue - - val href = i.attr("href") - if (name.isNullOrEmpty() || href.isNullOrEmpty()) { - continue - } + data class Season( + @JsonProperty("tvdb") + val tvdb: Long?, + @JsonProperty("tmdb") + val tmdb: Long?, + ) - val values = "(.*) \\((.*)\\)".toRegex().matchEntire(name)?.groups - if (values != null) { - for (index in 1 until values.size) { - val localName = values[index]?.value ?: continue - localList[fixName(localName)] = href - } - } else { - localList[fixName(name)] = href - } - } - if (localList.size > 0) { - list = localList - return true - } - } catch (e: Exception) { - e.printStackTrace() + data class CombinedMedia( + @JsonProperty("mapping") + val mapping: MappingRoot?, + @JsonProperty("show") + val show: Show + ) + + data class Database( + val mal: HashMap = hashMapOf(), + val anilist: HashMap = hashMapOf(), + val kitsu: HashMap = hashMapOf(), + val tmdb: HashMap = hashMapOf(), + val imdb: HashMap = hashMapOf(), + val name: HashMap = hashMapOf(), + ) + + private var database: Database? = null + + private val strip = Regex("[ :\\-.!]") + + /** Makes names more uniform to make partial matches more still give a result */ + fun stripName(name: String): String = + name.replace(strip, "").lowercase() + + + @Synchronized + @Throws + @WorkerThread + fun loadJson(): Database { + database?.let { + return it } - return false - } + + /** The entire "database" is stored as a json file we can parse */ + val stream: InputStream = com.lagradost.AnimeDB.getDatabaseStream()!! + val text = stream.reader().readText() - fun String?.toClassDir(): String { - val q = this ?: "null" - val z = (6..10).random().calc() - return q + "cache" + z + val allMedia = parseJson>(text) + val pending = Database() + for (media in allMedia) { + val lowercase = stripName(media.show.title) + pending.name[lowercase] = media + val map = media.mapping ?: continue + + map.imdbId?.let { id -> pending.imdb[id] = media } + map.malId?.let { id -> pending.mal[id] = media } + map.anilistId?.let { id -> pending.anilist[id] = media } + map.kitsuId?.let { id -> pending.kitsu[id] = media } + map.season?.tmdb?.let { id -> pending.tmdb[id] = media } + } + database = pending + return pending } - suspend fun getFillerEpisodes(query: String): HashMap? { - try { - cache[query]?.let { - return it - } - if (!getFillerList()) return null - val localList = list ?: return null - - // Strips these from the name - val blackList = listOf( - "TV Dubbed", - "(Dub)", - "Subbed", - "(TV)", - "(Uncensored)", - "(Censored)", - "(\\d+)" // year - ) - val blackListRegex = - Regex( - """ (${ - blackList.joinToString(separator = "|").replace("(", "\\(") - .replace(")", "\\)") - })""" - ) - - val realQuery = - fixName(query.replace(blackListRegex, "")).replace("shippuuden", "shippuden") - if (!localList.containsKey(realQuery)) return null - val href = localList[realQuery]?.replace(MAIN_URL, "") ?: return null // JUST IN CASE - val result = app.get("$MAIN_URL$href").text - val documented = Jsoup.parse(result) - val hashMap = HashMap() - documented.select("table.EpisodeList > tbody > tr").forEach { - val type = it.selectFirst("td.Type > span")?.text() == "Filler" - val episodeNumber = it.selectFirst("td.Number")?.text()?.toIntOrNull() - if (episodeNumber != null) { - hashMap[episodeNumber] = type - } - } - cache[query] = hashMap - return hashMap - } catch (e: Exception) { - e.printStackTrace() + val loadCache: HashMap?> = hashMapOf() + + @Synchronized + @Throws + @WorkerThread + fun getFillerEpisodes(data: LoadResponse): HashSet? { + /** Only for anime */ + if (data.type != TvType.Anime) { return null } + /** Try to hit the cache for this entry, to avoid recreating the hashset */ + loadCache[data.getId()]?.let { cachedResponse -> + return cachedResponse + } + val db = loadJson() + + val media = + db.mal[data.getMalId()?.toLongOrNull()] + ?: db.anilist[data.getAniListId()?.toLongOrNull()] + ?: db.kitsu[data.getKitsuId()?.toLongOrNull()] + ?: db.imdb[data.getImdbId()] + ?: db.tmdb[data.getTMDbId()?.toLongOrNull()] + ?: db.name[stripName(data.name)] + + return media?.show?.filler?.toHashSet().also { response -> + loadCache[data.getId()] = response + } } private fun Int.calc(): Int { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 19be8d6ada4..f0b24c8e7ca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ [versions] activityKtx = "1.13.0" androidGradlePlugin = "9.1.1" +animeDb = "1.0.2" appcompat = "1.7.1" biometric = "1.4.0-alpha06" buildkonfigGradlePlugin = "0.18.0" @@ -55,6 +56,7 @@ targetSdk = "36" [libraries] activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" } +anime-db = { module = "com.github.recloudstream:anime-db", version.ref = "animeDb" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" } coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } From d4899536d3391b1228afdc0572310c23b7b296ea Mon Sep 17 00:00:00 2001 From: Bnyro Date: Wed, 22 Apr 2026 02:45:04 +0200 Subject: [PATCH 30/60] refactor(extractors): simplify and combine jwplayer extraction (#2398) --- .../cloudstream3/extractors/Bigwarp.kt | 51 ------ .../cloudstream3/extractors/Fastream.kt | 52 +++--- .../cloudstream3/extractors/Filegram.kt | 52 ++++++ .../cloudstream3/extractors/Filemoon.kt | 40 ++--- .../cloudstream3/extractors/Filesim.kt | 13 +- .../cloudstream3/extractors/GamoVideo.kt | 29 ++-- .../cloudstream3/extractors/Hxfile.kt | 68 ++------ .../cloudstream3/extractors/JWPlayer.kt | 73 ++++----- .../cloudstream3/extractors/Jeniusplay.kt | 38 +---- .../cloudstream3/extractors/LuluStream.kt | 21 +-- .../cloudstream3/extractors/Minoplres.kt | 38 ----- .../cloudstream3/extractors/MultiQuality.kt | 63 ------- .../cloudstream3/extractors/Pelisplus.kt | 101 ------------ .../extractors/StreamWishExtractor.kt | 15 +- .../cloudstream3/extractors/StreamoUpload.kt | 36 ++-- .../cloudstream3/extractors/Supervideo.kt | 45 ++--- .../cloudstream3/extractors/Up4Stream.kt | 27 ++- .../cloudstream3/extractors/VidHidePro.kt | 23 +-- .../cloudstream3/extractors/VidNest.kt | 45 ----- .../cloudstream3/extractors/Vidmoly.kt | 49 +----- .../cloudstream3/extractors/Vidstream.kt | 104 ------------ .../lagradost/cloudstream3/extractors/Vtbe.kt | 33 ++-- .../extractors/helper/JWPlayerHelper.kt | 155 ++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 2 - 24 files changed, 379 insertions(+), 794 deletions(-) delete mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Bigwarp.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filegram.kt delete mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt delete mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt delete mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt delete mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidNest.kt delete mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/JWPlayerHelper.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Bigwarp.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Bigwarp.kt deleted file mode 100644 index 50a68c62f27..00000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Bigwarp.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.lagradost.cloudstream3.extractors - -import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.Qualities -import com.lagradost.cloudstream3.utils.newExtractorLink - -open class BigwarpIO : ExtractorApi() { - override var name = "Bigwarp" - override var mainUrl = "https://bigwarp.io" - override val requiresReferer = false - - private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""") - private val qualityRegex = Regex("""\d+x(\d+) .*""") - - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val resp = app.get(url).text - - for (sourceMatch in sourceRegex.findAll(resp)) { - val label = sourceMatch.groupValues[2] - - callback.invoke( - newExtractorLink( - name, - "$name ${label.split(" ", limit = 2).getOrNull(1)}", - sourceMatch.groupValues[1], // streams are usually in mp4 format - ) { - this.referer = url - this.quality = - qualityRegex.find(label)?.groupValues?.getOrNull(1)?.toIntOrNull() - ?: Qualities.Unknown.value - } - ) - } - } -} - -class BgwpCC : BigwarpIO() { - override var mainUrl = "https://bgwp.cc" -} - -class BigwarpArt : BigwarpIO() { - override var mainUrl = "https://bigwarp.art" -} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt index e8f8c49aca0..94ddaf61e0e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt @@ -1,54 +1,44 @@ package com.lagradost.cloudstream3.extractors +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import com.lagradost.cloudstream3.utils.getAndUnpack -import org.jsoup.nodes.Document +import com.lagradost.cloudstream3.utils.getPacked -open class Fastream: ExtractorApi() { +open class Fastream : ExtractorApi() { override var mainUrl = "https://fastream.to" override var name = "Fastream" override val requiresReferer = false - suspend fun getstream( - response: Document, - sources: ArrayList): Boolean{ - response.select("script").amap { script -> - if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) { - val unpacked = getAndUnpack(script.data()) - //val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)") - val newm3u8link = unpacked.substringAfter("file:\"").substringBefore("\"") - //val m3u8link = m3u8regex.find(unpacked)?.value ?: return@forEach - generateM3u8( - name, - newm3u8link, - mainUrl - ).forEach { link -> - sources.add(link) - } - } - } - return true - } - override suspend fun getUrl(url: String, referer: String?): List { - val sources = ArrayList() + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { val idregex = Regex("emb.html\\?(.*)=") - if (url.contains(Regex("(emb.html.*fastream)"))) { + val response = if (url.contains(Regex("(emb.html.*fastream)"))) { val id = idregex.find(url)?.destructured?.component1() ?: "" - val response = app.post("https://fastream.to/dl", allowRedirects = false, + app.post( + "$mainUrl/dl", allowRedirects = false, data = mapOf( "op" to "embed", "file_code" to id, "auto" to "1" ) ).document - getstream(response, sources) + } else { + app.get(url, referer = url).document + } + response.select("script").amap { script -> + if (getPacked(script.data()) != null) { + val unPacked = getAndUnpack(script.data()) + JwPlayerHelper.extractStreamLinks(unPacked, name, mainUrl, callback, subtitleCallback) + } } - val response = app.get(url, referer = url).document - getstream(response, sources) - return sources } } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filegram.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filegram.kt new file mode 100644 index 00000000000..7756f729087 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filegram.kt @@ -0,0 +1,52 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getAndUnpack +import org.jsoup.nodes.Element + +open class Filegram : ExtractorApi() { + override val name = "Filegram" + override val mainUrl = "https://filegram.to" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val header = mapOf( + "Accept" to "*/*", + "Accept-language" to "en-US,en;q=0.9", + "Origin" to mainUrl, + "Accept-Encoding" to "gzip, deflate, br, zstd", + "Connection" to "keep-alive", + "Sec-Fetch-Dest" to "empty", + "Sec-Fetch-Mode" to "cors", + "Sec-Fetch-Site" to "same-site", + "user-agent" to USER_AGENT, + ) + + val doc = app.get(getEmbedUrl(url), referer = referer).document + val unpackedJs = unpackJs(doc).toString() + + JwPlayerHelper.extractStreamLinks(unpackedJs, name, mainUrl, callback, subtitleCallback, headers = header) + } + + private fun unpackJs(script: Element): String? { + return script.select("script").find { it.data().contains("eval(function(p,a,c,k,e,d)") } + ?.data()?.let { getAndUnpack(it) } + } + + private fun getEmbedUrl(url: String): String { + return if (!url.contains("/embed-")) { + val videoId = url.substringAfter("$mainUrl/") + "$mainUrl/embed-$videoId" + } else url + } +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filemoon.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filemoon.kt index 6c10a92d935..ad4def1defc 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filemoon.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filemoon.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.api.Log import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink @@ -54,18 +55,16 @@ open class FilemoonV2 : ExtractorApi() { ?.data().orEmpty() val unpackedScript = JsUnpacker(fallbackScriptData).unpack() - val videoUrl = unpackedScript?.let { - Regex("""sources:\[\{file:"(.*?)"""").find(it)?.groupValues?.get(1) - } + val linkFound = JwPlayerHelper.extractStreamLinks( + unpackedScript.orEmpty(), + name, + mainUrl, + callback, + subtitleCallback, + defaultHeaders + ) - if (!videoUrl.isNullOrEmpty()) { - M3u8Helper.generateM3u8( - name, - videoUrl, - mainUrl, - headers = defaultHeaders - ).forEach(callback) - } else { + if (!linkFound) { Log.d("FilemoonV2", "No iframe and no video URL found in script fallback.") } return @@ -81,18 +80,15 @@ open class FilemoonV2 : ExtractorApi() { val unpackedScript = JsUnpacker(iframeScriptData).unpack() - val videoUrl = unpackedScript?.let { - Regex("""sources:\[\{file:"(.*?)"""").find(it)?.groupValues?.get(1) - } + val linkFound = JwPlayerHelper.extractStreamLinks( + unpackedScript.orEmpty(), + name, + mainUrl, + callback, + subtitleCallback + ) - if (!videoUrl.isNullOrEmpty()) { - M3u8Helper.generateM3u8( - name, - videoUrl, - mainUrl, - headers = defaultHeaders - ).forEach(callback) - } else { + if (!linkFound) { // Last-resort fallback using WebView interception val resolver = WebViewResolver( interceptUrl = Regex("""(m3u8|master\.txt)"""), diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt index 8c0cbec3252..51e127e3fef 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt @@ -4,6 +4,7 @@ import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* import com.lagradost.api.Log +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper import com.lagradost.cloudstream3.network.WebViewResolver class Multimoviesshg : Filesim() { @@ -78,17 +79,9 @@ open class Filesim : ExtractorApi() { pageResponse.document.selectFirst("script:containsData(sources:)")?.data() } - val m3u8Url = scriptData?.let { - Regex("""file:\s*"(.*?m3u8.*?)"""").find(it)?.groupValues?.getOrNull(1) - } + val linkFound = JwPlayerHelper.extractStreamLinks(scriptData.orEmpty(), name, mainUrl, callback, subtitleCallback) - if (!m3u8Url.isNullOrEmpty()) { - M3u8Helper.generateM3u8( - name, - m3u8Url, - mainUrl - ).forEach(callback) - } else { + if (!linkFound) { // Fallback using WebViewResolver val resolver = WebViewResolver( interceptUrl = Regex("""(m3u8|master\.txt)"""), diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GamoVideo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GamoVideo.kt index 7e00dbf9596..85212e6bb5b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GamoVideo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GamoVideo.kt @@ -1,7 +1,10 @@ package com.lagradost.cloudstream3.extractors +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink open class GamoVideo : ExtractorApi() { @@ -11,21 +14,13 @@ open class GamoVideo : ExtractorApi() { override suspend fun getUrl( url: String, - referer: String? - ): List? { - return app.get(url, referer = referer).document.select("script") - .firstOrNull { it.html().contains("sources:") }!!.html().substringAfter("file: \"") - .substringBefore("\",").let { - listOf( - newExtractorLink( - name, - name, - it, - ) { - this.referer = url - this.quality = Qualities.Unknown.value - } - ) - } + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + app.get(url, referer = referer).document.select("script") + .firstOrNull { JwPlayerHelper.canParseJwScript(it.data()) }!!.let { + JwPlayerHelper.extractStreamLinks(it.data(), name, mainUrl, callback, subtitleCallback) + } } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt index 8a56783b1f6..8f8a0c0cec2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt @@ -1,9 +1,9 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson class Neonime7n : Hxfile() { override val name = "Neonime7n" @@ -39,64 +39,22 @@ open class Hxfile : ExtractorApi() { override val requiresReferer = false open val redirect = true - override suspend fun getUrl(url: String, referer: String?): List? { - val sources = mutableListOf() + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { val document = app.get(url, allowRedirects = redirect, referer = referer).document with(document) { this.select("script").map { script -> - if (script.data().contains("eval(function(p,a,c,k,e,d)")) { - val data = - getAndUnpack(script.data()).substringAfter("sources:[").substringBefore("]") - tryParseJson>("[$data]")?.map { - sources.add( - newExtractorLink( - name, - name, - it.file, - ) { - this.referer = mainUrl - this.quality = when { - url.contains("hxfile.co") -> getQualityFromName( - Regex("\\d\\.(.*?).mp4").find( - document.select("title").text() - )?.groupValues?.get(1).toString() - ) - else -> getQualityFromName(it.label) - } - } - ) - } - } else if (script.data().contains("\"sources\":[")) { - val data = script.data().substringAfter("\"sources\":[").substringBefore("]") - tryParseJson>("[$data]")?.map { - sources.add( - newExtractorLink( - name, - name, - it.file, - ) { - this.referer = mainUrl - this.quality = when { - it.label?.contains("HD") == true -> Qualities.P720.value - it.label?.contains("SD") == true -> Qualities.P480.value - else -> getQualityFromName(it.label) - } - } - ) - } - } - else { - null + if (getPacked(script.data()) != null) { + val data = getAndUnpack(script.data()) + JwPlayerHelper.extractStreamLinks(data, name, mainUrl, callback, subtitleCallback) + } else if (JwPlayerHelper.canParseJwScript(script.data())) { + JwPlayerHelper.extractStreamLinks(script.data(), name, mainUrl, callback, subtitleCallback) } } } - return sources } - - private data class ResponseSource( - @JsonProperty("file") val file: String, - @JsonProperty("type") val type: String?, - @JsonProperty("label") val label: String? - ) - } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt index e744fdb3977..324640355c4 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt @@ -1,13 +1,10 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.Qualities -import com.lagradost.cloudstream3.utils.getQualityFromName -import com.lagradost.cloudstream3.utils.newExtractorLink class Meownime : JWPlayer() { override val name = "Meownime" @@ -34,50 +31,36 @@ class DesuOdvip : JWPlayer() { override val mainUrl = "https://desustream.me/odvip/" } +class VidNest : JWPlayer() { + override var name = "Vidnest" + override var mainUrl = "https://vidnest.io" +} + +open class BigwarpIO : JWPlayer() { + override var name = "Bigwarp" + override var mainUrl = "https://bigwarp.io" +} + +class BgwpCC : BigwarpIO() { + override var mainUrl = "https://bgwp.cc" +} + +class BigwarpArt : BigwarpIO() { + override var mainUrl = "https://bigwarp.art" +} + open class JWPlayer : ExtractorApi() { override val name = "JWPlayer" override val mainUrl = "https://www.jwplayer.com" override val requiresReferer = false - override suspend fun getUrl(url: String, referer: String?): List? { - val sources = mutableListOf() - with(app.get(url).document) { - val data = this.select("script").mapNotNull { script -> - if (script.data().contains("sources: [")) { - script.data().substringAfter("sources: [") - .substringBefore("],").replace("'", "\"") - } else if (script.data().contains("otakudesu('")) { - script.data().substringAfter("otakudesu('") - .substringBefore("');") - } else { - null - } - } - - tryParseJson>("$data")?.map { - sources.add( - newExtractorLink( - name, - name, - it.file, - ) { - this.referer = url - this.quality = getQualityFromName( - Regex("(\\d{3,4}p)").find(it.file)?.groupValues?.get( - 1 - ) - ) - } - ) - } - } - return sources + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val script = app.get(url).document.selectFirst("script:containsData(sources:)") ?: return + JwPlayerHelper.extractStreamLinks(script.data(), name, mainUrl, callback, subtitleCallback) } - - private data class ResponseSource( - @JsonProperty("file") val file: String, - @JsonProperty("type") val type: String?, - @JsonProperty("label") val label: String? - ) - } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt index f64863a9f3a..896228b5110 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt @@ -3,9 +3,12 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.newSubtitleFile -import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.getAndUnpack +import com.lagradost.cloudstream3.utils.getPacked open class Jeniusplay : ExtractorApi() { override val name = "Jeniusplay" @@ -34,40 +37,17 @@ open class Jeniusplay : ExtractorApi() { url, ).forEach(callback) - document.select("script").map { script -> - if (script.data().contains("eval(function(p,a,c,k,e,d)")) { - val subData = - getAndUnpack(script.data()).substringAfter("\"tracks\":[").substringBefore("],") - tryParseJson>("[$subData]")?.map { subtitle -> - subtitleCallback.invoke( - newSubtitleFile( - getLanguage(subtitle.label ?: ""), - subtitle.file - ) - ) - } + if (getPacked(script.data()) != null) { + val unpacked = getAndUnpack(script.data()) + JwPlayerHelper.extractStreamLinks(unpacked, name, mainUrl, callback, subtitleCallback) } } } - private fun getLanguage(str: String): String { - return when { - str.contains("indonesia", true) || str - .contains("bahasa", true) -> "Indonesian" - else -> str - } - } - data class ResponseSource( @JsonProperty("hls") val hls: Boolean, @JsonProperty("videoSource") val videoSource: String, @JsonProperty("securedLink") val securedLink: String?, ) - - data class Tracks( - @JsonProperty("kind") val kind: String?, - @JsonProperty("file") val file: String, - @JsonProperty("label") val label: String?, - ) } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/LuluStream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/LuluStream.kt index c7b6586065f..dec67959410 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/LuluStream.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/LuluStream.kt @@ -1,12 +1,10 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.INFER_TYPE -import com.lagradost.cloudstream3.utils.Qualities -import com.lagradost.cloudstream3.utils.newExtractorLink class Luluvdoo : LuluStream() { @@ -47,18 +45,7 @@ open class LuluStream : ExtractorApi() { ).document post.selectFirst("script:containsData(vplayer)")?.data() ?.let { script -> - Regex("file:\"(.*)\"").find(script)?.groupValues?.get(1)?.let { link -> - callback( - newExtractorLink( - name, - name, - link, - ) { - this.referer = mainUrl - this.quality = Qualities.P1080.value - } - ) - } + JwPlayerHelper.extractStreamLinks(script, name, mainUrl, callback, subtitleCallback) } } -} +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt deleted file mode 100644 index 702501a1e0c..00000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.lagradost.cloudstream3.extractors - -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.M3u8Helper - -open class Minoplres : ExtractorApi() { - - override val name = "Minoplres" // formerly SpeedoStream - override val requiresReferer = true - override val mainUrl = "https://minoplres.xyz" // formerly speedostream.bond - private val hostUrl = "https://minoplres.xyz" - - override suspend fun getUrl(url: String, referer: String?): List { - val sources = mutableListOf() - app.get(url, referer = referer).document.select("script").map { script -> - if (script.data().contains("jwplayer(\"vplayer\").setup(")) { - val data = script.data().substringAfter("sources: [") - .substringBefore("],").replace("file", "\"file\"").trim() - tryParseJson(data)?.let { - M3u8Helper.generateM3u8( - name, - it.file, - "$hostUrl/", - ).forEach { m3uData -> sources.add(m3uData) } - } - } - } - return sources - } - - private data class File( - @JsonProperty("file") val file: String, - ) -} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt deleted file mode 100644 index 802d9ea3af6..00000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.lagradost.cloudstream3.extractors - -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.Qualities -import com.lagradost.cloudstream3.utils.getQualityFromName -import com.lagradost.cloudstream3.utils.newExtractorLink -import java.net.URI - -open class MultiQuality : ExtractorApi() { - override var name = "MultiQuality" - override var mainUrl = "https://anihdplay.com" - private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""") - private val m3u8Regex = Regex(""".*?(\d*).m3u8""") - private val urlRegex = Regex("""(.*?)([^/]+$)""") - override val requiresReferer = false - - override fun getExtractorUrl(id: String): String { - return "$mainUrl/loadserver.php?id=$id" - } - - override suspend fun getUrl(url: String, referer: String?): List { - val extractedLinksList: MutableList = mutableListOf() - with(app.get(url)) { - sourceRegex.findAll(this.text).forEach { sourceMatch -> - val extractedUrl = sourceMatch.groupValues[1] - // Trusting this isn't mp4, may fuck up stuff - if (URI(extractedUrl).path.endsWith(".m3u8")) { - with(app.get(extractedUrl)) { - m3u8Regex.findAll(this.text).forEach { match -> - extractedLinksList.add( - newExtractorLink( - source = name, - name = name, - url = urlRegex.find(this.url)!!.groupValues[1] + match.groupValues[0], - type = ExtractorLinkType.M3U8 - ) { - this.referer = url - this.quality = getQualityFromName(match.groupValues[1]) - } - ) - } - - } - } else if (extractedUrl.endsWith(".mp4")) { - extractedLinksList.add( - newExtractorLink( - name, - "$name ${sourceMatch.groupValues[2]}", - extractedUrl, - ) { - this.referer = url.replace(" ", "%20") - this.quality = Qualities.Unknown.value - } - ) - } - } - return extractedLinksList - } - } -} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt deleted file mode 100644 index e2588feb63f..00000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.lagradost.cloudstream3.extractors - -import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.amap -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.safeAsync -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.INFER_TYPE -import com.lagradost.cloudstream3.utils.extractorApis -import com.lagradost.cloudstream3.utils.getQualityFromName -import com.lagradost.cloudstream3.utils.loadExtractor -import com.lagradost.cloudstream3.utils.newExtractorLink -import org.jsoup.Jsoup - -/** - * overrideMainUrl is necessary for for other vidstream clones like vidembed.cc - * If they diverge it'd be better to make them separate. - * */ -open class Pelisplus(val mainUrl: String) { - val name: String = "Vidstream" - - private fun getExtractorUrl(id: String): String { - return "$mainUrl/play?id=$id" - } - - private fun getDownloadUrl(id: String): String { - return "$mainUrl/download?id=$id" - } - - private val normalApis = arrayListOf(MultiQuality()) - - // https://gogo-stream.com/streaming.php?id=MTE3NDg5 - suspend fun getUrl( - id: String, - isCasting: Boolean = false, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ): Boolean { - try { - normalApis.amap { api -> - val url = api.getExtractorUrl(id) - api.getSafeUrl(url, subtitleCallback = subtitleCallback, callback = callback) - } - val extractorUrl = getExtractorUrl(id) - - /** Stolen from GogoanimeProvider.kt extractor */ - safeAsync { - val link = getDownloadUrl(id) - println("Generated vidstream download link: $link") - val page = app.get(link, referer = extractorUrl) - - val pageDoc = Jsoup.parse(page.text) - val qualityRegex = Regex("(\\d+)P") - - //a[download] - pageDoc.select(".dowload > a").amap { element -> - val href = element.attr("href") - val qual = if (element.text() - .contains("HDP") - ) "1080" else qualityRegex.find(element.text())?.destructured?.component1() - .toString() - - if (!loadExtractor(href, link, subtitleCallback, callback)) { - callback.invoke( - newExtractorLink( - this.name, - name = this.name, - href - ) { - this.referer = page.url - this.quality = getQualityFromName(qual) - } - ) - } - } - } - - with(app.get(extractorUrl)) { - val document = Jsoup.parse(this.text) - val primaryLinks = document.select("ul.list-server-items > li.linkserver") - //val extractedLinksList: MutableList = mutableListOf() - - // All vidstream links passed to extractors - primaryLinks.distinctBy { it.attr("data-video") }.forEach { element -> - val link = element.attr("data-video") - //val name = element.text() - - // Matches vidstream links with extractors - extractorApis.filter { !it.requiresReferer || !isCasting }.amap { api -> - if (link.startsWith(api.mainUrl)) { - api.getSafeUrl(link, extractorUrl, subtitleCallback, callback) - } - } - } - return true - } - } catch (e: Exception) { - return false - } - } -} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt index db883d6afb5..c721db6b95f 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt @@ -4,6 +4,7 @@ import com.lagradost.api.Log import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper @@ -12,7 +13,6 @@ import com.lagradost.cloudstream3.utils.getPacked import com.lagradost.cloudstream3.network.WebViewResolver - class Mwish : StreamWishExtractor() { override val name = "Mwish" override val mainUrl = "https://mwish.pro" @@ -180,18 +180,9 @@ open class StreamWishExtractor : ExtractorApi() { else -> pageResponse.document.selectFirst("script:containsData(sources:)")?.data() } - val directStreamUrl = playerScriptData?.let { - Regex("""file:\s*"(.*?m3u8.*?)"""").find(it)?.groupValues?.getOrNull(1) - } + val linkFound = JwPlayerHelper.extractStreamLinks(playerScriptData.orEmpty(), name, mainUrl, callback, subtitleCallback, headers) - if (!directStreamUrl.isNullOrEmpty()) { - M3u8Helper.generateM3u8( - name, - directStreamUrl, - mainUrl, - headers = headers - ).forEach(callback) - } else { + if (!linkFound) { val webViewM3u8Resolver = WebViewResolver( interceptUrl = Regex("""txt|m3u8"""), additionalUrls = listOf(Regex("""txt|m3u8""")), diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt index 7fafe05bec7..b7f618e9553 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt @@ -1,42 +1,30 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.getAndUnpack -import com.lagradost.cloudstream3.utils.Qualities -import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.getPacked open class StreamoUpload : ExtractorApi() { override val name = "StreamoUpload" override val mainUrl = "https://streamoupload.xyz" override val requiresReferer = true - override suspend fun getUrl(url: String, referer: String?): List { - val sources = mutableListOf() + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { val response = app.get(url, referer = referer) - val scriptElements = response.document.select("script").map { script -> - if (script.data().contains("eval(function(p,a,c,k,e,d)")) { + response.document.select("script").map { script -> + if (getPacked(script.data()) != null) { val data = getAndUnpack(script.data()) - .substringAfter("sources:[") - .substringBefore("],") - .replace("file", "\"file\"") - .trim() - tryParseJson(data)?.let { - M3u8Helper.generateM3u8( - name, - it.file, - "$mainUrl/", - ).forEach { m3uData -> sources.add(m3uData) } - } + JwPlayerHelper.extractStreamLinks(data, name, mainUrl, callback, subtitleCallback) } } - return sources } - - private data class File( - @JsonProperty("file") val file: String, - ) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt index e70cae6bdf8..5e47dd2decf 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt @@ -1,42 +1,27 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppUtils.parseJson - -data class Files( - @JsonProperty("file") val id: String, - @JsonProperty("label") val label: String? = null, -) +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.JsUnpacker open class Supervideo : ExtractorApi() { override var name = "Supervideo" override var mainUrl = "https://supervideo.cc" override val requiresReferer = false - override suspend fun getUrl(url: String, referer: String?): List? { - val extractedLinksList: MutableList = mutableListOf() + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { val response = app.get(url).text val jstounpack = Regex("eval((.|\\n)*?)").find(response)?.groups?.get(1)?.value - val unpacjed = JsUnpacker(jstounpack).unpack() - val extractedUrl = - unpacjed?.let { Regex("""sources:((.|\n)*?)image""").find(it) }?.groups?.get(1)?.value.toString() - .replace("file", """"file"""").replace("label", """"label"""") - .substringBeforeLast(",") - val parsedlinks = parseJson>(extractedUrl) - parsedlinks.forEach { data -> - if (data.label.isNullOrBlank()) { // mp4 links (with labels) are slow. Use only m3u8 link. - M3u8Helper.generateM3u8( - name, - data.id, - url, - headers = mapOf("referer" to url) - ).forEach { link -> - extractedLinksList.add(link) - } - } - } - return extractedLinksList + val unpacked = JsUnpacker(jstounpack).unpack() + + JwPlayerHelper.extractStreamLinks(unpacked.orEmpty(), name, mainUrl, callback, subtitleCallback) } } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Up4Stream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Up4Stream.kt index 91150992b3a..b72213e66ea 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Up4Stream.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Up4Stream.kt @@ -1,13 +1,13 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.api.Log +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.JsUnpacker -import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.fixUrl -import com.lagradost.cloudstream3.utils.newExtractorLink import kotlinx.coroutines.delay class Up4FunTop : Up4Stream() { @@ -19,12 +19,17 @@ open class Up4Stream : ExtractorApi() { override var mainUrl = "https://up4stream.com" override val requiresReferer = true - override suspend fun getUrl(url: String, referer: String?): List? { + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { val movieId = url.substringAfterLast("/").substringBefore(".html") // redirect from "wait 5 seconds" page to actual movie page val redirectResponse = app.get(url, cookies = mapOf("id" to movieId)) - val redirectForm = redirectResponse.document.selectFirst("form[method=POST]") ?: return null + val redirectForm = redirectResponse.document.selectFirst("form[method=POST]") ?: return val redirectUrl = fixUrl(redirectForm.attr("action")) val redirectParams = redirectForm.select("input[type=hidden]").associate { input -> input.attr("name") to input.attr("value") @@ -42,19 +47,7 @@ open class Up4Stream : ExtractorApi() { } JsUnpacker(extractedpack).unpack()?.let { unPacked -> - Regex("sources:\\[\\{file:\"(.*?)\"").find(unPacked)?.groupValues?.get(1)?.let { link -> - return listOf( - newExtractorLink( - this.name, - this.name, - link, - ) { - this.referer = referer.orEmpty() - this.quality = Qualities.Unknown.value - } - ) - } + JwPlayerHelper.extractStreamLinks(unPacked, name, mainUrl, callback, subtitleCallback) } - return null } } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidHidePro.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidHidePro.kt index 469efc5ec96..849b2b6d96b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidHidePro.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidHidePro.kt @@ -3,8 +3,11 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.getAndUnpack +import com.lagradost.cloudstream3.utils.getPacked class Ryderjet: VidHidePro() { override var mainUrl = "https://ryderjet.com" @@ -74,24 +77,12 @@ open class VidHidePro : ExtractorApi() { val response = app.get(getEmbedUrl(url), referer = referer) val script = if (!getPacked(response.text).isNullOrEmpty()) { - var result = getAndUnpack(response.text) - if(result.contains("var links")){ - result = result.substringAfter("var links") - } - result + getAndUnpack(response.text) } else { response.document.selectFirst("script:containsData(sources:)")?.data() } ?: return - // m3u8 urls could be prefixed by 'file:', 'hls2:' or 'hls4:', so we just match ':' - Regex(":\\s*\"(.*?m3u8.*?)\"").findAll(script).forEach { m3u8Match -> - generateM3u8( - name, - fixUrl(m3u8Match.groupValues[1]), - referer = "$mainUrl/", - headers = headers - ).forEach(callback) - } + JwPlayerHelper.extractStreamLinks(script, name, mainUrl, callback, subtitleCallback, headers) } private fun getEmbedUrl(url: String): String { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidNest.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidNest.kt deleted file mode 100644 index f9d45ebb8b2..00000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidNest.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.lagradost.cloudstream3.extractors - -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.M3u8Helper -import com.lagradost.cloudstream3.utils.newExtractorLink - -open class VidNest : ExtractorApi() { - override var name = "VidNest" - override var mainUrl = "https://vidnest.io" - override val requiresReferer = true - - private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""") - override suspend fun getUrl(url: String, referer: String?): List { - val extractedLinksList: MutableList = mutableListOf() - with(app.get(url, referer = referer)) { - sourceRegex.findAll(this.text).forEach { sourceMatch -> - val extractedUrl = sourceMatch.groupValues[1] - // Trusting this isn't mp4, may fuck up stuff - if (extractedUrl.contains(".m3u8")) { - M3u8Helper.generateM3u8( - name, - extractedUrl, - url, - headers = mapOf("referer" to this.url) - ).forEach { link -> - extractedLinksList.add(link) - } - } else if (extractedUrl.contains(".mp4")) { - extractedLinksList.add( - newExtractorLink( - source = name, - name = name, - url = extractedUrl, - ) { - this.referer = url.replace(" ", "%20") - } - ) - } - } - return extractedLinksList - } - } -} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt index bd259b17583..11927c50752 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt @@ -1,13 +1,11 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.newSubtitleFile -import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import kotlinx.coroutines.delay +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink class Vidmolyme : Vidmoly() { override val mainUrl = "https://vidmoly.me" @@ -26,20 +24,6 @@ open class Vidmoly : ExtractorApi() { override val mainUrl = "https://vidmoly.net" override val requiresReferer = true - private fun String.addMarks(str: String): String { - return this.replace(Regex("\"?$str\"?"), "\"$str\"") - } - - private data class Source( - @JsonProperty("file") val file: String? = null, - ) - - private data class SubSource( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, - @JsonProperty("kind") val kind: String? = null, - ) - override suspend fun getUrl( url: String, referer: String?, @@ -54,34 +38,13 @@ open class Vidmoly : ExtractorApi() { val newUrl = if (url.contains("/w/")) url.replaceFirst("/w/", "/embed-") + ".html" else url + val script = app.get(newUrl, headers = headers, referer = referer) .document.select("script") .firstOrNull { it.data().contains("sources:") } ?.data() + // Extracts and parses videoData - script?.substringAfter("sources: [") - ?.substringBefore("]") - ?.addMarks("file") - ?.replace("'","\"") - ?.let { videoData -> - tryParseJson(videoData)?.file?.let { m3uLink -> - M3u8Helper.generateM3u8(name, m3uLink, "$mainUrl/") - .forEach(callback) - } - } - // Extracts and parses captions - script?.substringAfter("tracks: [") - ?.substringBefore("]") - ?.addMarks("file")?.addMarks("label")?.addMarks("kind") - ?.replace("'","\"") - ?.let { subData -> - tryParseJson>("[$subData]") - ?.filter { it.kind == "captions" } - ?.forEach { - subtitleCallback( - newSubtitleFile(it.label.toString(), fixUrl(it.file.toString())) - ) - } - } + JwPlayerHelper.extractStreamLinks(script.orEmpty(), name, mainUrl, callback, subtitleCallback) } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt deleted file mode 100644 index ab228ee3c27..00000000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.lagradost.cloudstream3.extractors - -import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.amap -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.runAllAsync -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.INFER_TYPE -import com.lagradost.cloudstream3.utils.extractorApis -import com.lagradost.cloudstream3.utils.getQualityFromName -import com.lagradost.cloudstream3.utils.loadExtractor -import com.lagradost.cloudstream3.utils.newExtractorLink -import org.jsoup.Jsoup - -/** - * overrideMainUrl is necessary for for other vidstream clones like vidembed.cc - * If they diverge it'd be better to make them separate. - * */ -class Vidstream(val mainUrl: String) { - val name: String = "Vidstream" - - private fun getExtractorUrl(id: String): String { - return "$mainUrl/streaming.php?id=$id" - } - - private fun getDownloadUrl(id: String): String { - return "$mainUrl/download?id=$id" - } - - private val normalApis = arrayListOf(MultiQuality()) - - // https://gogo-stream.com/streaming.php?id=MTE3NDg5 - suspend fun getUrl( - id: String, - isCasting: Boolean = false, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit, - ): Boolean { - val extractorUrl = getExtractorUrl(id) - runAllAsync( - { - normalApis.amap { api -> - val url = api.getExtractorUrl(id) - api.getSafeUrl( - url, - callback = callback, - subtitleCallback = subtitleCallback - ) - } - }, { - /** Stolen from GogoanimeProvider.kt extractor */ - val link = getDownloadUrl(id) - println("Generated vidstream download link: $link") - val page = app.get(link, referer = extractorUrl) - - val pageDoc = Jsoup.parse(page.text) - val qualityRegex = Regex("(\\d+)P") - - //a[download] - pageDoc.select(".dowload > a").amap { element -> - val href = element.attr("href") - val qual = if (element.text() - .contains("HDP") - ) "1080" else qualityRegex.find(element.text())?.destructured?.component1() - .toString() - - if (!loadExtractor(href, link, subtitleCallback, callback)) { - callback.invoke( - newExtractorLink( - this.name, - name = this.name, - href, - type = INFER_TYPE - ) { - this.referer = page.url - this.quality = getQualityFromName(qual) - } - ) - } - } - }, { - with(app.get(extractorUrl)) { - val document = Jsoup.parse(this.text) - val primaryLinks = document.select("ul.list-server-items > li.linkserver") - //val extractedLinksList: MutableList = mutableListOf() - - // All vidstream links passed to extractors - primaryLinks.distinctBy { it.attr("data-video") }.forEach { element -> - val link = element.attr("data-video") - //val name = element.text() - - // Matches vidstream links with extractors - extractorApis.filter { !it.requiresReferer || !isCasting }.amap { api -> - if (link.startsWith(api.mainUrl)) { - api.getSafeUrl(link, extractorUrl, subtitleCallback, callback) - } - } - } - } - } - ) - return true - } -} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vtbe.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vtbe.kt index 37b8ecb239e..2fdd7082a34 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vtbe.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vtbe.kt @@ -1,15 +1,11 @@ package com.lagradost.cloudstream3.extractors -import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import com.lagradost.cloudstream3.utils.JsUnpacker +import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.Qualities -import com.lagradost.cloudstream3.utils.getQualityFromName -import java.net.URI +import com.lagradost.cloudstream3.utils.JsUnpacker open class Vtbe : ExtractorApi() { @@ -17,23 +13,16 @@ open class Vtbe : ExtractorApi() { override var mainUrl = "https://vtbe.to" override val requiresReferer = true - override suspend fun getUrl(url: String, referer: String?): List? { + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { val response = app.get(url,referer=mainUrl).document - val extractedpack =response.selectFirst("script:containsData(function(p,a,c,k,e,d))")?.data().toString() + val extractedpack = response.selectFirst("script:containsData(function(p,a,c,k,e,d))")?.data().toString() JsUnpacker(extractedpack).unpack()?.let { unPacked -> - Regex("sources:\\[\\{file:\"(.*?)\"").find(unPacked)?.groupValues?.get(1)?.let { link -> - return listOf( - newExtractorLink( - this.name, - this.name, - link, - ) { - this.referer = referer ?: "" - this.quality = Qualities.Unknown.value - } - ) - } + JwPlayerHelper.extractStreamLinks(unPacked, name, mainUrl, callback, subtitleCallback) } - return null } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/JWPlayerHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/JWPlayerHelper.kt new file mode 100644 index 00000000000..43ceb2314cf --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/JWPlayerHelper.kt @@ -0,0 +1,155 @@ +package com.lagradost.cloudstream3.extractors.helper + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.api.Log +import com.lagradost.cloudstream3.Prerelease +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.newSubtitleFile +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.newExtractorLink +import kotlin.collections.orEmpty + +@Prerelease +object JwPlayerHelper { + private val sourceRegex = Regex(""""?sources"?:\s*(\[.*?\])""") + private val tracksRegex = Regex(""""?tracks"?:\s*(\[.*?\])""") + private val m3u8Regex = Regex("""[:=]\s*\"([^\"\s]+(\.m3u8|master\.txt)[^\"\s]*)""") + + /** + * Get stream links the "sources" attribute inside a JWPlayer script, e.g. + * + * ```js + *