From 6bf20a1ade53545884ec93352b67b9ce4c32c3d2 Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Sun, 8 Mar 2026 01:18:04 +0100 Subject: [PATCH 001/177] Fixed shared pool, closes #2082 --- .../lagradost/cloudstream3/CommonActivity.kt | 11 ------- .../lagradost/cloudstream3/ui/BaseAdapter.kt | 29 +++++++++++++++++++ .../ui/home/HomeChildItemAdapter.kt | 7 ++--- .../cloudstream3/ui/home/HomeFragment.kt | 1 + .../ui/home/HomeParentItemAdapter.kt | 6 ++-- .../ui/home/HomeParentItemAdapterPreview.kt | 1 + .../cloudstream3/ui/home/HomeViewModel.kt | 5 ---- .../cloudstream3/ui/player/GeneratorPlayer.kt | 2 +- .../ui/quicksearch/QuickSearchFragment.kt | 1 + .../cloudstream3/ui/result/ActorAdaptor.kt | 4 +-- .../cloudstream3/ui/result/EpisodeAdapter.kt | 13 ++++----- .../cloudstream3/ui/result/ImageAdapter.kt | 4 +-- .../ui/result/ResultFragmentPhone.kt | 4 +-- .../ui/result/ResultFragmentTv.kt | 2 +- .../cloudstream3/ui/search/SearchAdaptor.kt | 4 +-- .../cloudstream3/ui/search/SearchFragment.kt | 1 + .../cloudstream3/ui/settings/SettingsUI.kt | 2 +- .../ui/settings/extensions/PluginAdapter.kt | 7 ++--- .../ui/settings/extensions/PluginsFragment.kt | 1 + 19 files changed, 57 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index abf56dcbd..c806cac63 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -234,19 +234,8 @@ object CommonActivity { fun init(act: Activity) { setActivityInstance(act) ioSafe { Torrent.deleteAllFiles() } - - // Clear all pools to apply the correct theme - for (pool in arrayOf( - PluginAdapter.sharedPool, HomeChildItemAdapter.sharedPool, - ParentItemAdapter.sharedPool, ActorAdaptor.sharedPool, EpisodeAdapter.sharedPool, - SearchAdapter.sharedPool, ImageAdapter.sharedPool - )) { - pool.clear() - } - val componentActivity = activity as? ComponentActivity ?: return - componentActivity.updateLocale() componentActivity.updateTv() AccountManager.initMainAPI() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt index 2bc1af833..4ebb7564c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui +import android.content.Context import android.view.View import android.view.ViewGroup import android.widget.ImageView @@ -11,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.viewbinding.ViewBinding import coil3.dispose +import java.util.WeakHashMap import java.util.concurrent.CopyOnWriteArrayList open class ViewHolderState(val view: ViewBinding) : ViewHolder(view.root) { @@ -22,6 +24,33 @@ abstract class NoStateAdapter( diffCallback: DiffUtil.ItemCallback = BaseDiffCallback() ) : BaseAdapter(0, diffCallback) +/** Creates a new shared pool, using the supplied lambda as a constructor. + * + * The reason for this complicated structure is that a pool should not be shared between contexts + * as it makes coil fuck up, and theming. + * */ +fun newSharedPool(lambda: RecyclerView.RecycledViewPool.() -> Unit = { }): Pair, RecyclerView.RecycledViewPool.() -> Unit> = + WeakHashMap() to lambda + +/** Sets the shared pool of the recyclerview */ +fun RecyclerView.setRecycledViewPool(pool: Pair, RecyclerView.RecycledViewPool.() -> Unit>) { + val ctx = context ?: return + synchronized(pool.first) { + this.setRecycledViewPool(pool.first.getOrPut(ctx) { + RecyclerView.RecycledViewPool().apply(pool.second) + }) + } +} + +/** Clears the shared pool of views */ +fun Pair, RecyclerView.RecycledViewPool.() -> Unit>.clear() { + synchronized(this.first) { + for (pool in this.first.values) { + pool?.clear() + } + } +} + /** * BaseAdapter is a persistent state stored adapter that supports headers and footers. * This should be used for restoring eg scroll or focus related to a view when it is recreated. diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index 4cd4197df..43f6d19ff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -5,12 +5,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import android.widget.ImageView -import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding -import coil3.load import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.databinding.HomeRemoveGridBinding @@ -20,6 +16,7 @@ import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding import com.lagradost.cloudstream3.ui.BaseAdapter import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder @@ -165,7 +162,7 @@ open class HomeChildItemAdapter( // The vast majority of the lag comes from creating the view // This simply shares the views between all HomeChildItemAdapter val sharedPool = - RecyclerView.RecycledViewPool().apply { this.setMaxRecycledViews(CONTENT, 20) } + newSharedPool { setMaxRecycledViews(CONTENT, 20) } var minPosterSize: Int = 0 var maxPosterSize: Int = 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index a254e1aec..375b2313f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -52,6 +52,7 @@ import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_PLAY_FILE import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index 0d08dc898..6bdd1bf49 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -6,10 +6,8 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding -import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.HomepageParentBinding @@ -17,9 +15,11 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.BaseAdapter import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SearchClickCallback +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -48,7 +48,7 @@ open class ParentItemAdapter( ) { companion object { val sharedPool = - RecyclerView.RecycledViewPool().apply { this.setMaxRecycledViews(CONTENT, 4) } + newSharedPool { setMaxRecycledViews(CONTENT, 4) } } data class ParentItemHolder(val binding: ViewBinding) : ViewHolderState(binding) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 26e3477ef..a292c2da2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -60,6 +60,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarMargin import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView import com.lagradost.cloudstream3.utils.UIHelper.populateChips import androidx.core.graphics.toColorInt +import com.lagradost.cloudstream3.ui.setRecycledViewPool class HomeParentItemAdapterPreview( val fragment: LifecycleOwner, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index a066bf151..e0609c0e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.MainActivity -import com.lagradost.cloudstream3.MainActivity.Companion.lastError import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.mvvm.Resource @@ -57,7 +56,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.withContext import java.util.EnumSet import java.util.concurrent.CopyOnWriteArrayList -import kotlin.collections.set class HomeViewModel : ViewModel() { companion object { @@ -518,9 +516,6 @@ class HomeViewModel : ViewModel() { return@ioSafe } - HomeChildItemAdapter.sharedPool.clear() - ParentItemAdapter.sharedPool.clear() - val api = getApiFromNameNull(preferredApiName) if (preferredApiName == noneApi.name) { // just set to random 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 7138e8dad..d20e85707 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 @@ -90,6 +90,7 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.SyncViewModel import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -128,7 +129,6 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.io.Serializable import java.util.Calendar -import kotlin.math.abs @OptIn(UnstableApi::class) class GeneratorPlayer : FullScreenPlayer() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index 724276ab7..cf9bc9975 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -32,6 +32,7 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 733933913..056588d0b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -6,7 +6,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.ActorData import com.lagradost.cloudstream3.ActorRole import com.lagradost.cloudstream3.R @@ -14,6 +13,7 @@ import com.lagradost.cloudstream3.databinding.CastItemBinding import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.NoStateAdapter import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.ImageLoader.loadImage @@ -26,7 +26,7 @@ class ActorAdaptor( })) { companion object { val sharedPool = - RecyclerView.RecycledViewPool().apply { this.setMaxRecycledViews(CONTENT, 10) } + newSharedPool { setMaxRecycledViews(CONTENT, 10) } } // Easier to store it here than to store it in the ActorData diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 7ff3904d8..5e5504164 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.result -import android.annotation.SuppressLint import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup @@ -8,8 +7,6 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.setPadding import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView import coil3.dispose import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.CommonActivity @@ -24,6 +21,7 @@ import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK import com.lagradost.cloudstream3.ui.download.DownloadClickEvent +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -93,11 +91,10 @@ class EpisodeAdapter( } val sharedPool = - RecyclerView.RecycledViewPool() - .apply { - this.setMaxRecycledViews(HAS_POSTER or CONTENT, 10) - this.setMaxRecycledViews(HAS_NO_POSTER or CONTENT, 10) - } + newSharedPool { + setMaxRecycledViews(HAS_POSTER or CONTENT, 10) + setMaxRecycledViews(HAS_NO_POSTER or CONTENT, 10) + } } override fun onClearView(holder: ViewHolderState) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt index 0513564fe..54657ed57 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt @@ -2,11 +2,11 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.NoStateAdapter import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.ImageLoader.loadImage @@ -27,7 +27,7 @@ class ImageAdapter( ) { companion object { val sharedPool = - RecyclerView.RecycledViewPool().apply { this.setMaxRecycledViews(CONTENT, 10) } + newSharedPool { setMaxRecycledViews(CONTENT, 10) } } override fun onCreateContent(parent: ViewGroup): ViewHolderState { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index 74285f552..c9da385f6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -30,7 +30,6 @@ import com.discord.panels.PanelState import com.discord.panels.PanelsChildGestureRegionObserver import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastContext -import com.google.android.gms.cast.framework.CastState import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.APIHolder @@ -66,6 +65,7 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache @@ -88,9 +88,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.populateChips import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes -import com.lagradost.cloudstream3.utils.downloader.DownloadObjects import com.lagradost.cloudstream3.utils.UIHelper.setListViewHeightBasedOnItems import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat +import com.lagradost.cloudstream3.utils.downloader.DownloadObjects import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager import com.lagradost.cloudstream3.utils.getImageFromDrawable import com.lagradost.cloudstream3.utils.setText diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 19f85bf3e..70ca11743 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -42,6 +42,7 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout @@ -61,7 +62,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.populateChips import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat -import com.lagradost.cloudstream3.utils.UiImage import com.lagradost.cloudstream3.utils.getImageFromDrawable import com.lagradost.cloudstream3.utils.setText import com.lagradost.cloudstream3.utils.setTextHtml diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt index 9338d4942..7b63b6ede 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt @@ -4,7 +4,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.databinding.SearchResultGridBinding import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding @@ -12,6 +11,7 @@ import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.NoStateAdapter import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.utils.UIHelper.isBottomLayout import kotlin.math.roundToInt @@ -43,7 +43,7 @@ class SearchAdapter( })) { companion object { val sharedPool = - RecyclerView.RecycledViewPool().apply { this.setMaxRecycledViews(CONTENT, 10) } + newSharedPool { setMaxRecycledViews(CONTENT, 10) } } var hasNext: Boolean = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 6bbd569b7..b79ba1707 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -56,6 +56,7 @@ import com.lagradost.cloudstream3.ui.home.HomeViewModel import com.lagradost.cloudstream3.ui.home.ParentItemAdapter import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt index 9e61a0b40..f4c522bf9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt @@ -12,13 +12,13 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.BasePreferenceFragmentCompat +import com.lagradost.cloudstream3.ui.clear import com.lagradost.cloudstream3.ui.home.HomeChildItemAdapter import com.lagradost.cloudstream3.ui.home.ParentItemAdapter import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE -import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.updateTv import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index 47b0b3da3..d0f9ff565 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -8,7 +8,6 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN @@ -16,15 +15,13 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.plugins.VotingApi.getVotes import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.NoStateAdapter import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppContextUtils.html -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.ImageLoader.loadImage import com.lagradost.cloudstream3.utils.SubtitleHelper.getNameNextToFlagEmoji import com.lagradost.cloudstream3.utils.UIHelper.toPx @@ -204,7 +201,7 @@ class PluginAdapter( companion object { // A high count as we can render in the entire list as the same time val sharedPool = - RecyclerView.RecycledViewPool().apply { this.setMaxRecycledViews(CONTENT, 15) } + newSharedPool { setMaxRecycledViews(CONTENT, 15) } private tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { if (current >= max) return max diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index ee333abad..534ffa62a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.ui.BaseFragment import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout From 618f9cde65d8db19bf6ebfe0cf275555995daba4 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 9 Mar 2026 19:09:54 +0100 Subject: [PATCH 002/177] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 99.8% (724 of 725 strings) Translated using Weblate (Hungarian) Currently translated at 82.0% (595 of 725 strings) Translated using Weblate (Korean) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Belarusian) Currently translated at 99.5% (722 of 725 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Translated using Weblate (Belarusian) Currently translated at 99.5% (722 of 725 strings) Translated using Weblate (Filipino) Currently translated at 21.2% (154 of 725 strings) Translated using Weblate (Hungarian) Currently translated at 77.3% (561 of 725 strings) Translated using Weblate (Dutch) Currently translated at 89.1% (646 of 725 strings) Translated using Weblate (Tamil) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (German) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Tamil) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Hungarian) Currently translated at 75.4% (547 of 725 strings) Co-authored-by: Aron Folkerts Co-authored-by: David Hermann Co-authored-by: Hosted Weblate Co-authored-by: Nguyễn Tiến Đạt Co-authored-by: Romhányi-Kakucska Viktor Co-authored-by: Sasha Glazko Co-authored-by: Wacky Wars Co-authored-by: clearstripe Co-authored-by: தமிழ்நேரம் Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fil/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/ Translation: Cloudstream/App --- app/src/main/res/values-b+de/strings.xml | 12 ++- app/src/main/res/values-b+fil/strings.xml | 104 ++++++++++++++++++++++ app/src/main/res/values-b+hu/strings.xml | 62 +++++++++++++ app/src/main/res/values-b+ko/strings.xml | 61 +++++++++---- app/src/main/res/values-b+nl/strings.xml | 30 ++++++- app/src/main/res/values-b+ta/strings.xml | 65 +++++++++++++- app/src/main/res/values-b+vi/strings.xml | 14 +++ app/src/main/res/values-be/strings.xml | 26 +++++- 8 files changed, 346 insertions(+), 28 deletions(-) diff --git a/app/src/main/res/values-b+de/strings.xml b/app/src/main/res/values-b+de/strings.xml index 8989ce795..9a67f9d20 100644 --- a/app/src/main/res/values-b+de/strings.xml +++ b/app/src/main/res/values-b+de/strings.xml @@ -744,9 +744,19 @@ Medieninfo Quellname Alle herunterladen - Möchtest du Episode%s herunter laden? + Möchtest du Episode %s herunter laden? %d aktiver Download %d aktive Downloads + Möchtest du alle Downloads in der Warteschlange abbrechen? + Alles abbrechen + + %d Download in Warteschlange + %d Downloads in Warteschlange + + Download in Warteschlange + Es befinden sich keine Downloads in der Warteschlange. + Quellpriorität + Entscheide, wie Videoquellen im Player sortiert werden sollen diff --git a/app/src/main/res/values-b+fil/strings.xml b/app/src/main/res/values-b+fil/strings.xml index d4844d1d7..be54aa959 100644 --- a/app/src/main/res/values-b+fil/strings.xml +++ b/app/src/main/res/values-b+fil/strings.xml @@ -52,4 +52,108 @@ Itago ang napiling quality ng video sa mga resulta ng paghahanap Pumili ng mode upang i-filter ang pag-download ng mga plugin Awtomatikong i-install ang lahat ng hindi pa naka-install na plugin mula sa mga idinagdag na repository. + Tauhan: %s + Kabanata %d ay ipapalabas sa + Season %1$d Kabanata %2$d ay ipapalabas sa + %1$da %2$do %3$dm + %1$do %2$dm + %dm + %1$do %2$dm %3$ds + %1$dm %2$ds + %1$ds + Poster ng Kabanata + Susunod na Kahit Ano + Bumalik + Ipatugtog mula sa Simula + Baguhin ang Tagatustos + Background ng Pasilip + Bilis (%.2fx) + Na-rate: %.1f + May nakitang bagong update!\n%1$s -> %2$s + %d min + Ipatugtog sa CloudStream + Bahay + Maghanap + Mga Download + Nakapila na download + Mga pagpipilian + Maghanap… + Maghanap %s… + Magsimulang magsalita… + Walang Data + Iba pang Mga Pagpipilian + Susunod na kabanata + Mga Genre + Ibahagi + Browser + Laktawan ang paglo-load + Naglo-load… + Pinapanood + Nakabinbin + Nakumpleto + Pinaplanong Panoorin + Pinapanood muli + Ipatugtog ang Pelikula + Ipatugtog ang Trailer + Ipatugtog ang Livestream + Ipatugtog ang Buong Serye + Mga Mapagkukunan + Mga saling-teksto + Subukang muli kumonekta… + Bumalik + Ipatugtog ang Kabanata + I-stream ang Torrent + I-download + Na-download + Dina-download + Hininto ang pag-download + Sinimulan ang pag-download + Nabigong ma-download + Kinansela ang pag-download + Natapos ang pag-download + Kasalukuyang walang nakapilang mga download. + Sinimulan ang pag-update + Buksan ang local video + Nagkamali sa paglo-load ng mga link + Ini-load muli ang mga link + Imbakan + Dub + Sub + Burahin ang File + Ipatugtog ang File + Ipagpatuloy ang pag-download + Ihinto ang pag-download + Higit pang impormasyon + Itago + Ipatugtog + Impormasyon + I-filter ang Mga Bookmark + Mga Bookmark + Alisin + Itakda ang kapalaran ng panonood + Ilapat + Ikopya + Isara + Alisin + I-save + Pangalan ng repository at URL + nakopya! + Bagong notipikasyon ng kabanata + Maghanap sa iba pang mga extension + Ipakita ang mga rekomendasyon + Bilis ng Manlalaro + Pagpipilian sa Saling-teksto + Kulay ng Teksto + Kulay ng Balangkas + Kulay ng Likuran + Kulay ng Bintana + Kataasan ng Saling-teksto + Font + Laki ng Font + Maghanap gamit ang mga tagatustos + Maghanap gamit ang mga uri + %d Benenes naibigay sa mga dev + Walang Benenes na binigay + Awtomatikong Piliin ang Wika + Mag-download ng Mga Wika diff --git a/app/src/main/res/values-b+hu/strings.xml b/app/src/main/res/values-b+hu/strings.xml index ae018207b..8bd2ac7ac 100644 --- a/app/src/main/res/values-b+hu/strings.xml +++ b/app/src/main/res/values-b+hu/strings.xml @@ -591,4 +591,66 @@ Elérhető offline megtekintésre Mindet Kiválaszt Mindent Kijelölés Eltávolítása + %1$dh %2$dm %3$ds + %1$dm %2$ds + %1$ds + Letöltési sor + Beszédfelismerés nem elérhető + Kezdjen beszélni… + Teljes sorozat lejátszása + Jelenleg nincs sorban álló letöltés. + Extra fényerő + A fényerő szűrő engedélyezése, ha a kijelző fényereje meghaladja a 100% -ot + extra_brightness_enabled + Keresési javaslatok + Mutasson keresési javaslatok gépelés közben + Javaslatok törlése + Szereplők panel megjelenítése + Telepít kiadás előtti verziót + Az előzetes verzió már telepítve van. + Az előzetes verzió telepítése sikertelen. + Fájlok törlése + Biztosan törölni szeretné az alábbi sorozat összes megjelenését?\n\n%s + %s \nmaradék + Zene + Hangoskönyv + Média + Hang + Podcast + Kódolási hiba + Hiba nem támogatott + Törlés (%1$d | %2$s) + Figyelem + Biztosan véglegesen szeretné törölni a következő tételeket?\n\n%s + Biztosan véglegesen szeretné törölni a következő epizódokat?\n\n%2$s + A következő sorozatok összes epizódját is véglegesen törli:\n\n%s + Lejátszás tükrözve" + Értékelési címke + Epizód szövege + Biztonság + Fiókok + Helyi hitelesítés + Töltse be az első létezőt + QR kód képe + Média információ + Plugin törlése + Mindig kérdezzen + Válassza ki a lejátszó eszközt + Hiba történt a vágólap elérésénél, kérjük, próbálkozzon újra. + Hiba történt a másolás során. Kérjük, másolja a logcat fájlt, és vegye fel a kapcsolatot az alkalmazás ügyfélszolgálatával. + Rendben + Elutasítás + Jelszó/PIN-kód hitelesítés + A biometrikus hitelesítés nem támogatott ezen az eszközön + Az alkalmazás feloldása ujjlenyomat, arcfelismerés, PIN-kód, minta vagy jelszó segítségével. + Néhány sikertelen kísérlet után a parancssor bezárul. Egyszerűen indítsa újra az alkalmazást, és próbálja meg újra. + Visszaállítás + CloudStream Wiki + Látogasson el %s okostelefonján vagy számítógépén, és írja be a fenti kódot + Nem működik az eszköz PIN-kódja, próbálkozzon helyi hitelesítéssel + A PIN-kód most lejárt! + A kód %1$dm %2$ds után lejár + Kiadás dátuma (újabbaktól régebbi felé) + Kiadás dátuma (régitől újig) + A lejátszó vezérlői neveinek elrejtése diff --git a/app/src/main/res/values-b+ko/strings.xml b/app/src/main/res/values-b+ko/strings.xml index 90504ec95..04c113b5b 100644 --- a/app/src/main/res/values-b+ko/strings.xml +++ b/app/src/main/res/values-b+ko/strings.xml @@ -109,7 +109,7 @@ 플레이어 자막 설정 Chromecast 자막 Chromecast 자막 설정 - 배속 모드 + Playback 속도 스와이프하여 탐색 좌우로 스와이프하여 동영상 위치 제어하기 스와이프하여 설정 변경 @@ -142,7 +142,7 @@ 추가된 저장소에서 아직 설치되지 않은 모든 플러그인을 자동으로 설치합니다. 앱 업데이트 표시 앱을 시작한 후 새 업데이트를 자동으로 검색합니다. - 일부 휴대폰은 새 패키지 설치 프로그램을 지원하지 않습니다. 업데이트가 설치되지 않으면 레거시 옵션을 사용해 보세요. + 일부 장치는 새 패키지 설치 프로그램을 지원하지 않습니다. 업데이트가 설치되지 않으면 레거시 옵션을 사용해보십시오. 같은 개발자가 만든 라이트 노벨 앱 같은 개발자가 만든 애니메이션 앱 Discord에 참여하기 @@ -198,12 +198,12 @@ 이 업데이트 건너뛰기 선호하는 화질 (WiFi) 선호하는 화질 (모바일 데이터) - 동영상 플레이어 해상도 + 본문 바로가기 동영상 버퍼 크기 동영상 및 이미지 캐시 지우기 DNS over HTTPS GitHub에 연결할 수 없습니다. jsDelivr 프록시를 켜는 중… - jsDelivr을 사용하여 GitHub 차단을 우회합니다. 업데이트가 며칠 지연될 수 있습니다. + JsDelivr를 사용하여 원시 github URL 차단을 우회하십시오. 몇 일 지연 될 업데이트가 발생할 수 있습니다. 복제 사이트 사이트 삭제 다른 URL을 사용하여 기존 사이트의 복제본을 추가합니다 @@ -217,10 +217,10 @@ 일반 플레이어 기능 기능 - 소스 언어 + 확장 언어 앱 레이아웃 선호하는 미디어 - 지원되는 공급업체에서 19금 사용 설정 + 지원된 연장에 NSFW 활성화 자막 인코딩 소스 소스 테스트 @@ -305,11 +305,11 @@ 커뮤니티 저장소 보기 공개 목록 모든 자막 대문자화 - 이 저장소에서 모든 플러그인을 다운로드하시겠습니까?경고: CloudStream 3은 타 사 확장 프로그램 사용에 대해 책임을 지지 않으며 이를 지원하지 않습니다! + 경고: CloudStream은 제3자 확장을 이용하여 어떠한 책임도 지지 않습니다! %s (사용불가) 저장소 추가 - 저장소 이름 - 저장소 URL + 저장소 이름 (선택 사항) + 저장소 URL 또는 단축 코드 플러그인이 로드됨 플러그인 다운로드 플러그인 삭제됨 @@ -380,7 +380,7 @@ 다시 표시하지 않음 업데이트를 찾을 수 없음 업데이트 - raw.githubusercontent.com 프록시 + GitHub 프록시 동영상 버퍼 길이 저장소에 동영상 캐시 Android TV와 같이 메모리가 부족한 디바이스에서 너무 높게 설정하면 충돌이 발생할 수 있습니다. @@ -397,12 +397,12 @@ 앱 업데이트 확장 기능 로그아웃 - 사이트 URL + https://example.com 비밀번호 계정 사용자 이름 언어 코드 (ko) - 사이트 이름 + 새사이트이름 %1$s %2$s 자막이 %d ms 너무 늦게 표시되는 경우, 사용하세요 자막 지연 없음 @@ -497,16 +497,16 @@ 동작 외형 랜덤 버튼 - 홈페이지에 랜덤 버튼 표시 + 홈페이지 및 도서관에서 임의 버튼 표시 포스터 아래에 제목을 이동 내려감 올라감 다람쥐 헌 쳇바퀴에 타고파 자막에서 부풀림 제거 엑스트라 - 스트림 링크 + https://example.com/example.mp4 트랙 - 레퍼러 + Referer (선택) 요약 시청함으로 표시 되돌리기 @@ -600,7 +600,7 @@ 플러그인 다운로드를 필터링할 모드 선택 데이터가 백업되었습니다. 장치에 따라 동작이 다를 수 있으며 앱 접근이 차단될 경우 앱 데이터를 완전히 지우고 백업에서 복원하세요. 이로 인해 발생하는 불편을 사과드립니다. 스마트폰이나 컴퓨터에서 %s를 방문하여 위의 코드를 입력하세요 - 구독 TV 프로그램에 대한 중단 없는 다운로드 및 알림을 보장하기 위해 CloudStream은 백그라운드에서 실행할 수 있는 권한이 필요합니다. 확인을 누르면 App info로 이동합니다. 거기서 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚로 스크롤하여 배터리 사용량을 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙로 설정합니다. 이 권한은 CS3가 배터리를 소모한다는 의미가 아닙니다. 알림을 받거나 공식 확장에서 동영상을 다운로드하는 등 필요할 때만 백그라운드에서 작동합니다. 취소를 선택한 경우 나중에 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨에서 이 설정을 조정할 수 있습니다. + 구독 된 TV 프로그램에 대한 특이성 다운로드 및 알림을 보려면 클라우드 스트림은 배경에서 오른쪽으로 실행할 권리가 필요합니다. 확인을 눌러 요청 대화 상자를 표시하십시오. 필요한 경우 필요에 따라 CP3에 제한되지 않고 공식을 다운로드하거나 공식화에서 확대를 누릴 필요가 있습니다. 여기서 소스의 순서를 변경할 수 있습니다. 비디오의 우선 순위가 높은 경우에는 소스 선택에 더 높게 나타납니다. 소스 우선 순위와 품질 우선 순위의 합이 비디오 우선 순위입니다. \n \n참고 A: 3 @@ -651,7 +651,7 @@ 이 동영상은 토렌트이므로 동영상 활동이 추적될 수 있습니다.\n계속하기 전에 토렌트에 대해 충분히 이해했는지 확인하세요. 오디오 팟캐스트 - 음성 시작… + 시작하기 … 인코딩 오류 지원되지 않는 오류 음성 인식 사용 불가 @@ -732,4 +732,31 @@ 왼쪽 위 중앙 위 오른쪽 위 + 비디오 소스가 플레이어에서 정렬되어야하는 방법 결정 + 100% 표시 광도가 초과될 때 Enable 광도 여과기 + 모든 퀴즈 다운로드를 취소하시겠습니까? + 에피소드를 다운로드 하시겠습니까 %s? + 현재 누락된 다운로드가 없습니다. + + %d 활성 다운로드 + + + %d 다운로드 + + 검색 결과 표시 + 회사 소개 + 배경 반경 + Reload 공급자 + 검색 제안 + 자주 묻는 질문 + 미디어 정보 + 근원 이름 + 추가 밝기 + 다운로드 queue + 다운로드 + 모든 것 + 근원 우선권 + 구름 많음 + 관련 상품 + 추가_brightness_enabled diff --git a/app/src/main/res/values-b+nl/strings.xml b/app/src/main/res/values-b+nl/strings.xml index 10588a3fc..30b8b2def 100644 --- a/app/src/main/res/values-b+nl/strings.xml +++ b/app/src/main/res/values-b+nl/strings.xml @@ -249,7 +249,7 @@ Update Voorkeurskwaliteit voor kijken (WiFi) Maximaal aantal tekens voor titel van videospeler - Videospeler Resolutie + Toon spelerinformatie Grootte videobuffer Lengte videobuffer Video cache op schijf @@ -289,7 +289,7 @@ Gebruikersnaam hello@Wereld.com 127.0.0.1 - MyCoolSite + NieuweSiteNaam https://voorbeeld.com Taalcode (nl) %1$s %2$s @@ -432,7 +432,7 @@ Repository naam (Optioneel) Plugin Gedownload Mislukt - Omzeilt de blokkering van GitHub met behulp van jsDelivr, waardoor updates enkele dagen vertraging kunnen oplopen. + Omzeil de blokkering van ruwe GitHub-URL’s via jsDelivr. Hierdoor kunnen updates enkele dagen vertraagd zijn. Repository URL of Shortcode Download %1$d %2$s voltooid HLS Afspeellijst @@ -658,4 +658,28 @@ Ga naar %s op je smartphone of computer en voeren de bovenstaande code in PIN-code is nu verlopen! Code verloopt in %1$dm %2$ds + Downloadwachtrij + Er staan momenteel geen downloads in de wachtrij. + Extra helderheid + Schakel het helderheidsfilter in zodra de schermhelderheid boven 100% komt + extra_helderheid_ingeschakeld + Zoeksuggesties + Zoeksuggesties laten zien tijdens het typen + Suggesties verwijderen + Castpaneel weergeven + Pre-releaseversie installeren + Pre-releaseversie is al geïnstaleerd. + Installatie van de pre-release is mislukt. + Schermspiegeling + Spiegeling spelen" + Beoordelingslabel + Afleveringstekst + Deze test is alleen bedoeld voor ontwikkelaars en bevestigt noch ontkent de werking van een extensie. + Lokale authenticatie + Media info + Waarschuwing: Cloudstream is niet verandwoordelijk voor het gebruik van extensies van derden en biedt hier ook geen ondersteuning voor! + Cast apparaat selecteren + Fout bij toegang tot het Klembord, Probeer het opnieuw. + Fout bij het kopiëren. Kopieer alsjeblieft de logcat en neem contact op met de app-ondersteuning. + Afwijzen diff --git a/app/src/main/res/values-b+ta/strings.xml b/app/src/main/res/values-b+ta/strings.xml index e223f6c60..626554c18 100644 --- a/app/src/main/res/values-b+ta/strings.xml +++ b/app/src/main/res/values-b+ta/strings.xml @@ -278,7 +278,7 @@ தற்குறிப்பு -30 ஒளிதோற்றம் - வீடியோ பிளேயர் தீர்மானம் + பிளேயர் தகவலைக் காட்டு வீடியோ இடையக அளவு நகலி தளம் அறிவிலிமையம் பதிலாள் @@ -286,7 +286,7 @@ களஞ்சியம் கிடைக்கவில்லை, முகவரி ஐ சரிபார்த்து VPN ஐ முயற்சிக்கவும் தொகுதி பதிவிறக்கம் சொருகு - எச்சரிக்கை: கிளவுட்ச்ட்ரீம் 3 மூன்றாம் தரப்பு நீட்டிப்புகளைப் பயன்படுத்துவதற்கான எந்தப் பொறுப்பையும் ஏற்காது, அவர்களுக்கு எந்த ஆதரவையும் வழங்காது! + எச்சரிக்கை: மூன்றாம் தரப்பு நீட்டிப்புகளைப் பயன்படுத்துவதற்கு CloudStream எந்தப் பொறுப்பையும் ஏற்காது, அவற்றிற்கு எந்த ஆதரவையும் வழங்காது! மொழி திரும்பவும் %s இலிருந்து குழுவிலகப்பட்டது @@ -555,7 +555,7 @@ புதுப்பிப்பு விடுபதிகை விரலிடைத் தோல் - சில தொலைபேசிகள் புதிய தொகுப்பு நிறுவியை ஆதரிக்கவில்லை. புதுப்பிப்புகள் நிறுவப்படாவிட்டால் மரபு விருப்பத்தை முயற்சிக்கவும். + சில சாதனங்கள் புதிய தொகுப்பு நிறுவியை ஆதரிக்கவில்லை. புதுப்பிப்புகள் நிறுவப்படவில்லை என்றால், மரபு விருப்பத்தை முயற்சிக்கவும். சந்தா தொலைக்காட்சி நிகழ்ச்சிகளுக்கான தடையற்ற பதிவிறக்கங்கள் மற்றும் அறிவிப்புகளை உறுதிப்படுத்த, கிளவுட்ச்ட்ரீம் பின்னணியில் இயங்க இசைவு தேவை. சரி என்பதை அழுத்துவதன் மூலம், உங்களுக்கு கோரிக்கை உரையாடல் காண்பிக்கப்படும். தயவுசெய்து \'இசைவு\' என்பதை அழுத்தவும். \n\nதயவுசெய்து கவனிக்கவும், இந்த இசைவு CS3 உங்கள் பேட்டரியை வெளியேற்றும் என்று அர்த்தமல்ல. அறிவிப்புகளைப் பெறும்போது அல்லது உத்தியோகபூர்வ நீட்டிப்புகளிலிருந்து வீடியோக்களைப் பதிவிறக்குவது போன்ற பின்னணியில் மட்டுமே இது செயல்படும். பயன்பாட்டு பேட்டரி பயன்பாடு ஏற்கனவே கட்டுப்பாடற்றதாக அமைக்கப்பட்டுள்ளது பயன்பாட்டு புதுப்பிப்பை நிறுவுகிறது… @@ -648,7 +648,7 @@ அமைப்புகள்/வழங்குநர்கள்/விருப்பமான ஊடகங்களில் டொரெண்டை இயக்கவும் பயன்பாட்டை மறுதொடக்கம் செய்து, தொடர ச்ட்ரீம் டொரண்ட் பாப்-அப் ஏற்றுக்கொள்ளுங்கள். மென்பொருள் டிகோடிங் - மென்பொருள் டிகோடிங் உங்கள் தொலைபேசியால் ஆதரிக்கப்படாத வீடியோ கோப்புகளை இயக்க பிளேயருக்கு உதவுகிறது, ஆனால் உயர் தெளிவுத்திறனில் பின்னடைவு அல்லது நிலையற்ற பின்னணியை ஏற்படுத்தக்கூடும் + மென்பொருள் டிகோடிங் உங்கள் சாதனத்தால் ஆதரிக்கப்படாத வீடியோ கோப்புகளை பிளேயருக்கு இயக்க உதவுகிறது, ஆனால் உயர் தெளிவுத்திறனில் பின்னடைவு அல்லது நிலையற்ற பிளேபேக்கை ஏற்படுத்தலாம். தொகுதி 100% ஐ தாண்டியுள்ளது 100% க்கு அப்பால் செல்ல மீண்டும் சறுக்கவும் செருகுநிரல்களைப் புதுப்பிக்கவும் @@ -676,4 +676,61 @@ %1$d மணி %2$d நிமிடம் %3$d விநாடி %1$d நிமிடம் %2$d விநாடி %1$d விநாடி + பதிவிறக்க வரிசை + முழு தொடரையும் விளையாடு + தற்போது வரிசைப்படுத்தப்பட்ட பதிவிறக்கங்கள் எதுவும் இல்லை. + கூடுதல் ஒளி + 100% காட்சி பிரகாசத்தை மீறும் போது பிரகாச வடிப்பானை இயக்கவும் + கூடுதல்_பிரகாசம்_செயல்படுத்தப்பட்டது + தேடல் பரிந்துரைகள் + தட்டச்சு செய்யும் போது தேடல் பரிந்துரைகளைக் காட்டு + தெளிவான பரிந்துரைகள் + காச்ட் பேனலைக் காட்டு + வெளியீட்டிற்கு முந்தைய பதிப்பை நிறுவவும் + முன் வெளியீடு ஏற்கனவே நிறுவப்பட்டுள்ளது. + முன் வெளியீட்டை நிறுவுவதில் தோல்வி. + கண்ணாடியை விளையாடு" + மதிப்பீடு சிட்டை + எபிசோட் உரை + மீடியா செய்தி + எப்போதும் கேளுங்கள் + மூல முன்னுரிமை + பிளேயரில் வீடியோ ஆதாரங்கள் எவ்வாறு வரிசைப்படுத்தப்பட வேண்டும் என்பதைத் தீர்மானிக்கவும் + கணக்கு இல்லை + LongPress விரைவு மாற்று + 2x வேகத்தைப் பெற அழுத்திப் பிடிக்கவும் + சுயவிவரப் படத்தைத் திருத்து + சுயவிவரப் பட முகவரி ஐ உள்ளிடவும் + முகவரி இல்லை + தவறான முகவரி அல்லது படம் + படம் வெற்றிகரமாக புதுப்பிக்கப்பட்டது + இந்த எபிசோட் வரை பார்த்ததாகக் குறிக்கவும் + இந்த எபிசோட் வரை பார்த்ததை அகற்று + மீண்டும் ஏற்றப்பட்டது + மறுஏற்றம் வழங்குநர் + பெயர் + மூலப் பெயர் + தீர்மானம் மற்றும் பெயர் + அனைத்தையும் பதிவிறக்கவும் + அனைத்தையும் ரத்து வெற்றி + அத்தியாயம் %s ஐ பதிவிறக்க விரும்புகிறீர்களா? + வரிசைப்படுத்தப்பட்ட அனைத்து பதிவிறக்கங்களையும் ரத்துசெய்ய விரும்புகிறீர்களா? + வசன சீரமைப்பு + கீழே இடது + கீழ் நடுவண் + கீழ் வலது + நடுத்தர இடது + நடுத்தர நடுவண் + நடுத்தர வலது + மேல் இடது + மேல் நடுவண் + மேல் வலது + + %d செயலில் பதிவிறக்கம் + %d செயலில் உள்ள பதிவிறக்கங்கள் + + + %d பதிவிறக்கம் வரிசையில் உள்ளது + %d பதிவிறக்கங்கள் வரிசையில் உள்ளன + diff --git a/app/src/main/res/values-b+vi/strings.xml b/app/src/main/res/values-b+vi/strings.xml index a4804c9d4..7999feb99 100644 --- a/app/src/main/res/values-b+vi/strings.xml +++ b/app/src/main/res/values-b+vi/strings.xml @@ -757,4 +757,18 @@ Thông tin âm thanh Độ sáng bổ sung Tên nguồn + Hàng đợi tải xuống + Hiện tại không có tệp nào đang chờ tải xuống. + Hãy quyết định cách sắp xếp các nguồn video trong trình phát. + Ưu tiên nguồn + Tải xuống tất cả + Hủy tất cả + Bạn có muốn tải xuống tập %s không? + Bạn có muốn hủy tất cả các lượt tải xuống đang chờ xử lý không? + + %d đang tải xuống + + + %d lượt tải xuống đang chờ xử lý + diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 4c9a88313..690296ca6 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -179,10 +179,10 @@ Паказваць плакаты з Kitsu Схаваць выбраную якасць відэа з вынікаў пошуку Аўтаматычнае абнаўленне пашырэнняў - Аўтаматычная спампоўка ўбудоваў + Аўтаматычная спампоўка ўбудоў Некаторыя прылады не падтрымліваюць новы ўсталёўшчык пакетаў. Калі абнаўленні не ўсталёўваюцца, паспрабуйце ранейшую версію. Github - Выберыце рэжым фільтравання спампоўвання убудоваў + Выберыце рэжым фільтравання спампоўвання убудоў Прапановы пошуку Паказваць прапановы пошуку падчас уводу тэксту Ачысціць прапановы @@ -673,7 +673,7 @@ Абнавіць убудовы ўручную Пачынаецца абнаўленне ўбудоў! Абноўлена %d убудова(ы/ў)! - Не абнавілася ніводная ўбудова. + Ніводнай убудовы не было абноўлена. Апавяшчэнні прайгравальніка Апавяшчэнне прайгравальніка для кіравання прайграваннем у фонавым рэжыме Убудаваны @@ -715,4 +715,24 @@ Зверху злева Зверху па цэнтру Зверху справа + Чагра спампоўванняў + У чарзе пакуль што няма спампоўванняў. + Прыярытэт крыніц + Выберыце, як сартаваць крыніцы відэа ў прайгравальніку + Спампаваць усё + Скасаваць усё + Спампаваць %s серыю? + Скасаваць усе спампоўванні ў чарзе? + + %d актыўнае спампоўванне + %d актыўных спампоўвання + %d актыўных спампоўванняў + %d актыўных спампоўванняў + + + %d спампоўванне ў чарзе + %d спампоўвання ў чарзе + %d спампоўванняў у чарзе + %d спампоўванняў у чарзе + From b0d3731faa16cabaa51b6d01ce6b500ebdcfcf82 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Wed, 11 Mar 2026 15:19:57 +0100 Subject: [PATCH 003/177] feat(extractors): add vide0 doodstream mirror (#2558) --- .../com/lagradost/cloudstream3/extractors/DoodExtractor.kt | 6 ++++-- .../kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index 0cef9eb4c..25c8ea9e7 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -4,11 +4,9 @@ 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.INFER_TYPE import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.newExtractorLink import java.net.URI -import kotlin.random.Random class Doodspro : DoodLaExtractor() { override var mainUrl = "https://doods.pro" @@ -81,6 +79,10 @@ class Ds2video : DoodLaExtractor() { override var mainUrl = "https://ds2video.com" } +class Vide0Net: DoodLaExtractor() { + override var mainUrl = "https://vide0.net" +} + class MyVidPlay : DoodLaExtractor() { override var mainUrl = "https://myvidplay.com" } 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 f796f3fdd..3dc0cc7ff 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -281,6 +281,7 @@ import com.lagradost.cloudstream3.extractors.Vidoza import com.lagradost.cloudstream3.extractors.VinovoSi import com.lagradost.cloudstream3.extractors.VinovoTo import com.lagradost.cloudstream3.extractors.VidNest +import com.lagradost.cloudstream3.extractors.Vide0Net import com.lagradost.cloudstream3.extractors.VkExtractor import com.lagradost.cloudstream3.extractors.Voe import com.lagradost.cloudstream3.extractors.Voe1 @@ -1227,6 +1228,7 @@ val extractorApis: MutableList = arrayListOf( ByseVepoin(), ByseBuho(), MyVidPlay(), + Vide0Net(), Up4Stream(), Up4FunTop(), GUpload(), From ccc0a45065de49c888651b7dac23608413b9eb0d Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:54:11 +0100 Subject: [PATCH 004/177] Fixed shared pool, closes #2082 (#2553) --- .../lagradost/cloudstream3/CommonActivity.kt | 11 ------- .../lagradost/cloudstream3/ui/BaseAdapter.kt | 29 +++++++++++++++++++ .../ui/home/HomeChildItemAdapter.kt | 7 ++--- .../cloudstream3/ui/home/HomeFragment.kt | 1 + .../ui/home/HomeParentItemAdapter.kt | 6 ++-- .../ui/home/HomeParentItemAdapterPreview.kt | 1 + .../cloudstream3/ui/home/HomeViewModel.kt | 5 ---- .../cloudstream3/ui/player/GeneratorPlayer.kt | 2 +- .../ui/quicksearch/QuickSearchFragment.kt | 1 + .../cloudstream3/ui/result/ActorAdaptor.kt | 4 +-- .../cloudstream3/ui/result/EpisodeAdapter.kt | 13 ++++----- .../cloudstream3/ui/result/ImageAdapter.kt | 4 +-- .../ui/result/ResultFragmentPhone.kt | 4 +-- .../ui/result/ResultFragmentTv.kt | 2 +- .../cloudstream3/ui/search/SearchAdaptor.kt | 4 +-- .../cloudstream3/ui/search/SearchFragment.kt | 1 + .../cloudstream3/ui/settings/SettingsUI.kt | 2 +- .../ui/settings/extensions/PluginAdapter.kt | 7 ++--- .../ui/settings/extensions/PluginsFragment.kt | 1 + 19 files changed, 57 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index abf56dcbd..c806cac63 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -234,19 +234,8 @@ object CommonActivity { fun init(act: Activity) { setActivityInstance(act) ioSafe { Torrent.deleteAllFiles() } - - // Clear all pools to apply the correct theme - for (pool in arrayOf( - PluginAdapter.sharedPool, HomeChildItemAdapter.sharedPool, - ParentItemAdapter.sharedPool, ActorAdaptor.sharedPool, EpisodeAdapter.sharedPool, - SearchAdapter.sharedPool, ImageAdapter.sharedPool - )) { - pool.clear() - } - val componentActivity = activity as? ComponentActivity ?: return - componentActivity.updateLocale() componentActivity.updateTv() AccountManager.initMainAPI() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt index 2bc1af833..4ebb7564c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui +import android.content.Context import android.view.View import android.view.ViewGroup import android.widget.ImageView @@ -11,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.viewbinding.ViewBinding import coil3.dispose +import java.util.WeakHashMap import java.util.concurrent.CopyOnWriteArrayList open class ViewHolderState(val view: ViewBinding) : ViewHolder(view.root) { @@ -22,6 +24,33 @@ abstract class NoStateAdapter( diffCallback: DiffUtil.ItemCallback = BaseDiffCallback() ) : BaseAdapter(0, diffCallback) +/** Creates a new shared pool, using the supplied lambda as a constructor. + * + * The reason for this complicated structure is that a pool should not be shared between contexts + * as it makes coil fuck up, and theming. + * */ +fun newSharedPool(lambda: RecyclerView.RecycledViewPool.() -> Unit = { }): Pair, RecyclerView.RecycledViewPool.() -> Unit> = + WeakHashMap() to lambda + +/** Sets the shared pool of the recyclerview */ +fun RecyclerView.setRecycledViewPool(pool: Pair, RecyclerView.RecycledViewPool.() -> Unit>) { + val ctx = context ?: return + synchronized(pool.first) { + this.setRecycledViewPool(pool.first.getOrPut(ctx) { + RecyclerView.RecycledViewPool().apply(pool.second) + }) + } +} + +/** Clears the shared pool of views */ +fun Pair, RecyclerView.RecycledViewPool.() -> Unit>.clear() { + synchronized(this.first) { + for (pool in this.first.values) { + pool?.clear() + } + } +} + /** * BaseAdapter is a persistent state stored adapter that supports headers and footers. * This should be used for restoring eg scroll or focus related to a view when it is recreated. diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index 4cd4197df..43f6d19ff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -5,12 +5,8 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import android.widget.ImageView -import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding -import coil3.load import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.databinding.HomeRemoveGridBinding @@ -20,6 +16,7 @@ import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding import com.lagradost.cloudstream3.ui.BaseAdapter import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder @@ -165,7 +162,7 @@ open class HomeChildItemAdapter( // The vast majority of the lag comes from creating the view // This simply shares the views between all HomeChildItemAdapter val sharedPool = - RecyclerView.RecycledViewPool().apply { this.setMaxRecycledViews(CONTENT, 20) } + newSharedPool { setMaxRecycledViews(CONTENT, 20) } var minPosterSize: Int = 0 var maxPosterSize: Int = 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index a254e1aec..375b2313f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -52,6 +52,7 @@ import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_PLAY_FILE import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index 0d08dc898..6bdd1bf49 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -6,10 +6,8 @@ import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding -import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.HomepageParentBinding @@ -17,9 +15,11 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.BaseAdapter import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SearchClickCallback +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -48,7 +48,7 @@ open class ParentItemAdapter( ) { companion object { val sharedPool = - RecyclerView.RecycledViewPool().apply { this.setMaxRecycledViews(CONTENT, 4) } + newSharedPool { setMaxRecycledViews(CONTENT, 4) } } data class ParentItemHolder(val binding: ViewBinding) : ViewHolderState(binding) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index 26e3477ef..a292c2da2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -60,6 +60,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarMargin import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView import com.lagradost.cloudstream3.utils.UIHelper.populateChips import androidx.core.graphics.toColorInt +import com.lagradost.cloudstream3.ui.setRecycledViewPool class HomeParentItemAdapterPreview( val fragment: LifecycleOwner, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index a066bf151..e0609c0e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.MainActivity -import com.lagradost.cloudstream3.MainActivity.Companion.lastError import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.mvvm.Resource @@ -57,7 +56,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.withContext import java.util.EnumSet import java.util.concurrent.CopyOnWriteArrayList -import kotlin.collections.set class HomeViewModel : ViewModel() { companion object { @@ -518,9 +516,6 @@ class HomeViewModel : ViewModel() { return@ioSafe } - HomeChildItemAdapter.sharedPool.clear() - ParentItemAdapter.sharedPool.clear() - val api = getApiFromNameNull(preferredApiName) if (preferredApiName == noneApi.name) { // just set to random 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 7138e8dad..d20e85707 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 @@ -90,6 +90,7 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.SyncViewModel import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -128,7 +129,6 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.io.Serializable import java.util.Calendar -import kotlin.math.abs @OptIn(UnstableApi::class) class GeneratorPlayer : FullScreenPlayer() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index 724276ab7..cf9bc9975 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -32,6 +32,7 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 733933913..056588d0b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -6,7 +6,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.ActorData import com.lagradost.cloudstream3.ActorRole import com.lagradost.cloudstream3.R @@ -14,6 +13,7 @@ import com.lagradost.cloudstream3.databinding.CastItemBinding import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.NoStateAdapter import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.ImageLoader.loadImage @@ -26,7 +26,7 @@ class ActorAdaptor( })) { companion object { val sharedPool = - RecyclerView.RecycledViewPool().apply { this.setMaxRecycledViews(CONTENT, 10) } + newSharedPool { setMaxRecycledViews(CONTENT, 10) } } // Easier to store it here than to store it in the ActorData diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 7ff3904d8..5e5504164 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.result -import android.annotation.SuppressLint import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup @@ -8,8 +7,6 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.view.setPadding import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView import coil3.dispose import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.CommonActivity @@ -24,6 +21,7 @@ import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK import com.lagradost.cloudstream3.ui.download.DownloadClickEvent +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -93,11 +91,10 @@ class EpisodeAdapter( } val sharedPool = - RecyclerView.RecycledViewPool() - .apply { - this.setMaxRecycledViews(HAS_POSTER or CONTENT, 10) - this.setMaxRecycledViews(HAS_NO_POSTER or CONTENT, 10) - } + newSharedPool { + setMaxRecycledViews(HAS_POSTER or CONTENT, 10) + setMaxRecycledViews(HAS_NO_POSTER or CONTENT, 10) + } } override fun onClearView(holder: ViewHolderState) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt index 0513564fe..54657ed57 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt @@ -2,11 +2,11 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.NoStateAdapter import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.ImageLoader.loadImage @@ -27,7 +27,7 @@ class ImageAdapter( ) { companion object { val sharedPool = - RecyclerView.RecycledViewPool().apply { this.setMaxRecycledViews(CONTENT, 10) } + newSharedPool { setMaxRecycledViews(CONTENT, 10) } } override fun onCreateContent(parent: ViewGroup): ViewHolderState { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index 74285f552..c9da385f6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -30,7 +30,6 @@ import com.discord.panels.PanelState import com.discord.panels.PanelsChildGestureRegionObserver import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastContext -import com.google.android.gms.cast.framework.CastState import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.APIHolder @@ -66,6 +65,7 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache @@ -88,9 +88,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.populateChips import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes -import com.lagradost.cloudstream3.utils.downloader.DownloadObjects import com.lagradost.cloudstream3.utils.UIHelper.setListViewHeightBasedOnItems import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat +import com.lagradost.cloudstream3.utils.downloader.DownloadObjects import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager import com.lagradost.cloudstream3.utils.getImageFromDrawable import com.lagradost.cloudstream3.utils.setText diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 19f85bf3e..70ca11743 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -42,6 +42,7 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout @@ -61,7 +62,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.populateChips import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat -import com.lagradost.cloudstream3.utils.UiImage import com.lagradost.cloudstream3.utils.getImageFromDrawable import com.lagradost.cloudstream3.utils.setText import com.lagradost.cloudstream3.utils.setTextHtml diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt index 9338d4942..7b63b6ede 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchAdaptor.kt @@ -4,7 +4,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.databinding.SearchResultGridBinding import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding @@ -12,6 +11,7 @@ import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.NoStateAdapter import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.utils.UIHelper.isBottomLayout import kotlin.math.roundToInt @@ -43,7 +43,7 @@ class SearchAdapter( })) { companion object { val sharedPool = - RecyclerView.RecycledViewPool().apply { this.setMaxRecycledViews(CONTENT, 10) } + newSharedPool { setMaxRecycledViews(CONTENT, 10) } } var hasNext: Boolean = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 6bbd569b7..b79ba1707 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -56,6 +56,7 @@ import com.lagradost.cloudstream3.ui.home.HomeViewModel import com.lagradost.cloudstream3.ui.home.ParentItemAdapter import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt index 9e61a0b40..f4c522bf9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt @@ -12,13 +12,13 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.BasePreferenceFragmentCompat +import com.lagradost.cloudstream3.ui.clear import com.lagradost.cloudstream3.ui.home.HomeChildItemAdapter import com.lagradost.cloudstream3.ui.home.ParentItemAdapter import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE -import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.updateTv import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index 47b0b3da3..d0f9ff565 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -8,7 +8,6 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN @@ -16,15 +15,13 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.plugins.VotingApi.getVotes import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.NoStateAdapter import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.newSharedPool import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppContextUtils.html -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.ImageLoader.loadImage import com.lagradost.cloudstream3.utils.SubtitleHelper.getNameNextToFlagEmoji import com.lagradost.cloudstream3.utils.UIHelper.toPx @@ -204,7 +201,7 @@ class PluginAdapter( companion object { // A high count as we can render in the entire list as the same time val sharedPool = - RecyclerView.RecycledViewPool().apply { this.setMaxRecycledViews(CONTENT, 15) } + newSharedPool { setMaxRecycledViews(CONTENT, 15) } private tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { if (current >= max) return max diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index ee333abad..534ffa62a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.ui.BaseFragment import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.setRecycledViewPool import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout From 8d3846d2a3ead72efdef26982d5e0f76e14842f7 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Fri, 13 Mar 2026 22:59:59 +0100 Subject: [PATCH 005/177] feat(extractors): add support for vidara.to (#2556) * feat(extractors): add support for vidara.to * Allow soft subtitle failure in Streamup --- .../cloudstream3/extractors/Streamup.kt | 28 ++++++++++++++++--- .../cloudstream3/utils/ExtractorApi.kt | 2 ++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamup.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamup.kt index b043186ed..ea85a005e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamup.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamup.kt @@ -3,6 +3,7 @@ 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.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType @@ -13,10 +14,17 @@ class Streamix(): Streamup() { override val mainUrl = "https://streamix.so" } +class Vidara(): Streamup() { + override val name: String = "Vidara" + override val mainUrl = "https://vidara.to" + override val apiPath: String = "/api/stream" +} + open class Streamup() : ExtractorApi() { override val name: String = "Streamup" override val mainUrl: String = "https://strmup.to" override val requiresReferer: Boolean = false + open val apiPath: String = "/ajax/stream" override suspend fun getUrl( url: String, @@ -25,7 +33,7 @@ open class Streamup() : ExtractorApi() { callback: (ExtractorLink) -> Unit ) { val fileCode = url.substringAfterLast("/") - val fileInfo = app.get("$mainUrl/ajax/stream?filecode=$fileCode") + val fileInfo = app.get("$mainUrl$apiPath?filecode=$fileCode") .parsed() callback.invoke( @@ -36,6 +44,12 @@ open class Streamup() : ExtractorApi() { type = ExtractorLinkType.M3U8 ) ) + + fileInfo.subtitles?.forEach { subtitle -> + subtitleCallback.invoke( + newSubtitleFile(subtitle.language, subtitle.filePath) + ) + } } private data class StreamUpFileInfo( @@ -43,7 +57,13 @@ open class Streamup() : ExtractorApi() { val thumbnail: String, @JsonProperty("streaming_url") val streamingUrl: String, - // subtitles seems to always be empty - // val subtitles: List + val subtitles: List? ) -} \ No newline at end of file + + private data class StreamUpSubtitle( + @JsonProperty("file_path") + val filePath: String, + @JsonProperty("language") + val language: String, + ) +} 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 3dc0cc7ff..0a3c33ff2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -281,6 +281,7 @@ import com.lagradost.cloudstream3.extractors.Vidoza import com.lagradost.cloudstream3.extractors.VinovoSi import com.lagradost.cloudstream3.extractors.VinovoTo import com.lagradost.cloudstream3.extractors.VidNest +import com.lagradost.cloudstream3.extractors.Vidara import com.lagradost.cloudstream3.extractors.Vide0Net import com.lagradost.cloudstream3.extractors.VkExtractor import com.lagradost.cloudstream3.extractors.Voe @@ -1101,6 +1102,7 @@ val extractorApis: MutableList = arrayListOf( StreamoUpload(), Streamup(), Streamix(), + Vidara(), GamoVideo(), Gdriveplayerapi(), From 904dda0c60fff774ea94c4cf69897cc62dc59be9 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Fri, 13 Mar 2026 23:03:27 +0100 Subject: [PATCH 006/177] feat(extractors): add new extractor for vidsonic.net (#2557) --- .../cloudstream3/extractors/Vidsonic.kt | 61 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 2 + 2 files changed, 63 insertions(+) create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidsonic.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidsonic.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidsonic.kt new file mode 100644 index 000000000..5c871b54b --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidsonic.kt @@ -0,0 +1,61 @@ +package com.lagradost.cloudstream3.extractors + +import com.lagradost.api.Log +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkType +import com.lagradost.cloudstream3.utils.newExtractorLink + +class Vidsonic() : ExtractorApi() { + override val name: String = "Vidsonic" + override val mainUrl: String = "https://vidsonic.net" + override val requiresReferer: Boolean = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + // Extracted JavaScript code that decodes the encrypted m3u8 stream URL: + // + // const _0x1 = '3363616238|3638666534|6264323565|3666616366|6636333662|6230626339|30613d3564|6d26743130|6b74693170|336563793d|64695f656c|6966263634|3332363033|3737313d73|6572697078|6526333d64|695f726576|7265733f38|75336d2e72|657473616d|2f7431306b|7469317033|6563792f38|392f657275|6365732f74|656e2e6369|6e6f736469|762e31302d|73752d7473|2f2f3a7370|747468'; + // const _0x2 = function(_0x3) { + // const _0x4 = _0x3.split('|').join(''); + // let _0x5 = ''; + // for (let _0x6 = 0; _0x6 < _0x4.length; _0x6 += 2) { + // _0x5 += String.fromCharCode(parseInt(_0x4.substr(_0x6, 2), 16)); + // } + // return _0x5.split('').reverse().join(''); + // }; + // const _0x7 = _0x2(_0x1); <-- now contains the stream URL + + val response = app.get(url).text + val encodedStreamUrl = response + .substringAfter("const _0x1 = ") + .substringBefore(";") + .replace("'", "") + + // (improved) Java implementation of the JavaScript code from above + val streamUrl = encodedStreamUrl + .replace("|", "") + // always two base16 digits together build one ASCII char + .chunked(2) + .map { + Integer.parseInt(it, 16).toChar() + } + .joinToString("") + .reversed() + + callback.invoke( + newExtractorLink( + source = name, + name = name, + url = streamUrl, + type = ExtractorLinkType.M3U8 + ) + ) + } +} \ No newline at end of file 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 0a3c33ff2..a21087601 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -283,6 +283,7 @@ import com.lagradost.cloudstream3.extractors.VinovoTo import com.lagradost.cloudstream3.extractors.VidNest import com.lagradost.cloudstream3.extractors.Vidara import com.lagradost.cloudstream3.extractors.Vide0Net +import com.lagradost.cloudstream3.extractors.Vidsonic import com.lagradost.cloudstream3.extractors.VkExtractor import com.lagradost.cloudstream3.extractors.Voe import com.lagradost.cloudstream3.extractors.Voe1 @@ -1213,6 +1214,7 @@ val extractorApis: MutableList = arrayListOf( Wishonly(), Ds2play(), Ds2video(), + Vidsonic(), InternetArchive(), VidStack(), GDMirrorbot(), From ef07f761d73711ad45d98c1dc1e668be9f75fb2b Mon Sep 17 00:00:00 2001 From: Nivin <89772187+NivinCNC@users.noreply.github.com> Date: Sat, 14 Mar 2026 03:47:53 +0530 Subject: [PATCH 007/177] Prefer player default live position for HLS/DASH (#2547) --- .../com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 7 +++++++ 1 file changed, 7 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 7f6586912..a134ae911 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 @@ -1868,6 +1868,13 @@ class CS3IPlayer : IPlayer { ) } + // For DASH or HLS single streams (non-playlist), prefer the player's default + // live position instead of starting at 0. Use TIME_UNSET to let ExoPlayer pick + // the live/default position when no explicit start position was provided. + if (playbackPosition == 0L && (link.type == ExtractorLinkType.M3U8 || link.type == ExtractorLinkType.DASH)) { + playbackPosition = TIME_UNSET + } + val provider = getApiFromNameNull(link.source) val interceptor: Interceptor? = provider?.getVideoInterceptor(link) From 86cca03dd7d8c2669ce377b90b8c7da31397e859 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:39:34 -0600 Subject: [PATCH 008/177] Use a new method to pass if debug to fix debug logging in library (#2330) * Use a new method to pass if debug to fix debug logging in library --- app/build.gradle.kts | 16 +++++----------- .../com/lagradost/cloudstream3/CloudStreamApp.kt | 4 ++++ library/build.gradle.kts | 13 ++++--------- .../kotlin/com/lagradost/cloudstream3/MainAPI.kt | 9 +++++++++ .../cloudstream3/mvvm/ArchComponentExt.kt | 12 ++++++------ .../com/lagradost/cloudstream3/utils/AppDebug.kt | 9 +++++++++ 6 files changed, 37 insertions(+), 26 deletions(-) create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppDebug.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5f6f55575..722fcf58e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -226,16 +226,7 @@ dependencies { implementation(libs.work.runtime.ktx) implementation(libs.nicehttp) // HTTP Lib - implementation(project(":library") { - // There does not seem to be a good way of getting the android flavor. - val isDebug = gradle.startParameter.taskRequests.any { task -> - task.args.any { arg -> - arg.contains("debug", true) - } - } - - this.extra.set("isDebug", isDebug) - }) + implementation(project(":library")) } tasks.register("androidSourcesJar") { @@ -272,8 +263,11 @@ tasks.withType { compilerOptions { jvmTarget.set(javaTarget) jvmDefault.set(JvmDefaultMode.ENABLE) - optIn.add("com.lagradost.cloudstream3.Prerelease") freeCompilerArgs.add("-Xannotation-default-target=param-property") + optIn.addAll( + "com.lagradost.cloudstream3.InternalAPI", + "com.lagradost.cloudstream3.Prerelease", + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt index b78327998..ffd5ea812 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt @@ -13,6 +13,7 @@ import coil3.ImageLoader import coil3.PlatformContext import coil3.SingletonImageLoader import com.lagradost.api.setContext +import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.mvvm.safe import com.lagradost.cloudstream3.mvvm.safeAsync import com.lagradost.cloudstream3.plugins.PluginManager @@ -20,6 +21,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser +import com.lagradost.cloudstream3.utils.AppDebug import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys @@ -81,6 +83,8 @@ class CloudStreamApp : Application(), SingletonImageLoader.Factory { exceptionHandler = it Thread.setDefaultUncaughtExceptionHandler(it) } + + AppDebug.isDebug = BuildConfig.DEBUG } override fun attachBaseContext(base: Context?) { diff --git a/library/build.gradle.kts b/library/build.gradle.kts index e73ed970d..14ef644f0 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -46,7 +46,10 @@ kotlin { sourceSets { all { - languageSettings.optIn("com.lagradost.cloudstream3.Prerelease") + languageSettings { + optIn("com.lagradost.cloudstream3.InternalAPI") + optIn("com.lagradost.cloudstream3.Prerelease") + } } commonMain.dependencies { @@ -73,14 +76,6 @@ buildkonfig { exposeObjectWithName = "BuildConfig" defaultConfigs { - val isDebug = kotlin.runCatching { extra.get("isDebug") }.getOrNull() == true - if (isDebug) { - logger.quiet("Compiling library with debug flag") - } else { - logger.quiet("Compiling library with release flag") - } - buildConfigField(FieldSpec.Type.BOOLEAN, "DEBUG", isDebug.toString()) - // Reads local.properties val localProperties = gradleLocalProperties(rootDir, project.providers) buildConfigField( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index 91cd375db..2be9f61fd 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -46,6 +46,15 @@ import kotlin.math.roundToInt ) annotation class Prerelease +@Retention(AnnotationRetention.BINARY) // This is only an IDE hint, and will not be used in the runtime +@RequiresOptIn( + message = "This API is marked as internal and should not be used by extensions. " + + "Using it could cause catastrophic build or runtime errors and may " + + "be changed or removed at any time.", + level = RequiresOptIn.Level.ERROR +) +annotation class InternalAPI + /** * 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/mvvm/ArchComponentExt.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index 97aaf357d..e13bcf5ec 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -1,8 +1,8 @@ package com.lagradost.cloudstream3.mvvm -import com.lagradost.api.BuildConfig import com.lagradost.api.Log import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.utils.AppDebug import kotlinx.coroutines.* import java.io.InterruptedIOException import java.net.SocketTimeoutException @@ -18,31 +18,31 @@ const val DEBUG_PRINT = "DEBUG PRINT" class DebugException(message: String) : Exception("$DEBUG_EXCEPTION\n$message") inline fun debugException(message: () -> String) { - if (BuildConfig.DEBUG) { + if (AppDebug.isDebug) { throw DebugException(message.invoke()) } } inline fun debugPrint(tag: String = DEBUG_PRINT, message: () -> String) { - if (BuildConfig.DEBUG) { + if (AppDebug.isDebug) { Log.d(tag, message.invoke()) } } inline fun debugWarning(message: () -> String) { - if (BuildConfig.DEBUG) { + if (AppDebug.isDebug) { logError(DebugException(message.invoke())) } } inline fun debugAssert(assert: () -> Boolean, message: () -> String) { - if (BuildConfig.DEBUG && assert.invoke()) { + if (AppDebug.isDebug && assert.invoke()) { throw DebugException(message.invoke()) } } inline fun debugWarning(assert: () -> Boolean, message: () -> String) { - if (BuildConfig.DEBUG && assert.invoke()) { + if (AppDebug.isDebug && assert.invoke()) { logError(DebugException(message.invoke())) } } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppDebug.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppDebug.kt new file mode 100644 index 000000000..e07f32c0a --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppDebug.kt @@ -0,0 +1,9 @@ +package com.lagradost.cloudstream3.utils + +import com.lagradost.cloudstream3.InternalAPI + +@InternalAPI +object AppDebug { + @Volatile + var isDebug: Boolean = false +} From 19efb1ffc3f7516e3d80284335d0b8646e99a5d7 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:21:12 -0600 Subject: [PATCH 009/177] Fix BuildConfig import (#2566) --- .../com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2dfbb5598..bc10285e3 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 @@ -54,7 +54,7 @@ import androidx.media3.ui.AspectRatioFrameLayout import androidx.preference.PreferenceManager import androidx.recyclerview.widget.SimpleItemAnimator import com.google.android.material.button.MaterialButton -import com.lagradost.api.BuildConfig +import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.screenHeightWithOrientation From 51bd1c4a6c85bbc170e8ce7dead5542adae07108 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 20 Mar 2026 10:09:56 +0100 Subject: [PATCH 010/177] Translated using Weblate (Slovak) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 62.6% (454 of 725 strings) Translated using Weblate (Esperanto) Currently translated at 23.7% (172 of 725 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Bulgarian) Currently translated at 99.1% (719 of 725 strings) Translated using Weblate (Latvian) Currently translated at 81.2% (589 of 725 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Translated using Weblate (Esperanto) Currently translated at 17.5% (127 of 725 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Vietnamese) Currently translated at 99.8% (724 of 725 strings) Translated using Weblate (Hungarian) Currently translated at 82.0% (595 of 725 strings) Translated using Weblate (Korean) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Belarusian) Currently translated at 99.5% (722 of 725 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Translated using Weblate (Belarusian) Currently translated at 99.5% (722 of 725 strings) Translated using Weblate (Filipino) Currently translated at 21.2% (154 of 725 strings) Translated using Weblate (Hungarian) Currently translated at 77.3% (561 of 725 strings) Translated using Weblate (Dutch) Currently translated at 89.1% (646 of 725 strings) Translated using Weblate (Tamil) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (German) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Tamil) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Hungarian) Currently translated at 75.4% (547 of 725 strings) Co-authored-by: Aron Folkerts Co-authored-by: Daniel Konstantinov Co-authored-by: David Hermann Co-authored-by: Hosted Weblate Co-authored-by: Jen Xie Co-authored-by: Nguyễn Tiến Đạt Co-authored-by: Romhányi-Kakucska Viktor Co-authored-by: Sasha Glazko Co-authored-by: Wacky Wars Co-authored-by: clearstripe Co-authored-by: jpkaster 77 Co-authored-by: programutox Co-authored-by: tomas293 Co-authored-by: தமிழ்நேரம் Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bg/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/eo/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fil/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hant/ Translation: Cloudstream/App --- app/src/main/res/values-b+bg/strings.xml | 60 +++++++++- app/src/main/res/values-b+eo/strings.xml | 57 ++++++++- app/src/main/res/values-b+lv/strings.xml | 29 +++-- app/src/main/res/values-b+pt/strings.xml | 31 ++++- app/src/main/res/values-b+sk/strings.xml | 44 +++++-- app/src/main/res/values-b+zh+TW/strings.xml | 123 +++++++++++++++++--- 6 files changed, 303 insertions(+), 41 deletions(-) diff --git a/app/src/main/res/values-b+bg/strings.xml b/app/src/main/res/values-b+bg/strings.xml index 2c238b968..096e9f66b 100644 --- a/app/src/main/res/values-b+bg/strings.xml +++ b/app/src/main/res/values-b+bg/strings.xml @@ -252,7 +252,7 @@ Актуализация Предпочитано качество за гледане (през WiFi) Максимален брой знаци за заглавие във видеоплейъра - Разделителна способност на видео плейъра + Покажи информация за плейъра Размер на видео буфера Дължина на видео буфера Видео кеш на диск @@ -411,7 +411,7 @@ Вижте хранилищата на общността Публичен списък Всички субтитри с главни букви - Предупреждение: CloudStream 3 не носи отговорност за използването на трети страни разширения и не предоставя поддръжка за тях! + Предупреждение: CloudStream не носи отговорност за използването на трети страни разширения и не предоставя поддръжка за тях! %s (Деактивиран) Потоци Аудио потоци @@ -456,7 +456,7 @@ Изчистване на историята Автоматично инсталиране на всички все още неинсталирани добавки от добавени хранилища. APK Инсталатор - Някои телефони не поддържат новия пакет за инсталиране. Опитайте предпоследената опция, ако актуализациите не се инсталират. + Някои устройства не поддържат новия пакет за инсталиране. Опитайте предпоследената опция, ако актуализациите не се инсталират. Пусни трейлър Връзки Актуализации на приложението @@ -639,7 +639,7 @@ Няма актуализирани плъгини. Are you sure you wСигурни ли сте, че искате трайно да изтриете всички епизоди от тази поредица?\n\n%s Рестартирайте приложението и приемете изскачащото съобщение за Stream Torrent, за да продължите. - Софтуерното декодиране позволява на плейъра да възпроизвежда видео файлове, които не се поддържат от телефона ви, но може да причини забавяне или нестабилно възпроизвеждане при висока резолюция + Софтуерното декодиране позволява на плейъра да възпроизвежда видео файлове, които не се поддържат от устройството ви, но може да причини забавяне или нестабилно възпроизвеждане при висока резолюция Известия на плейъра Епизод (низходящо) Започва процесът на актуализация на плъгините! @@ -700,4 +700,56 @@ Винаги изпращай запитване Задръжте, за да удвоите скоростта Дълго задържане за смяна на скоростта + Опашка за изтегляне + Пусни цялата поредица + В момента няма изтегляния в опашката. + Допълнителна яркост + Активирай филтъра за яркост, когато яркостта на екрана надвиши 100% + Предложения за търсене + Показвай предложения за търсене по време на писане + Изтрий предложения + Покажи панела за излъчване + Инсталирай предварителна версия + Предварителна версия вече е изтеглена. + Неуспешна инсталация на предварителната версия. + Етикет за рейтинг + Текст на епизода + Информация за медията + Приоритет на източника + Определи как източниците на видеото да се сортират в плейъра + Няма акаунт + Редактирай профилната снимка + Въведи линк за профилна снимка + Няма намерен URL + Невалиден URL или снимка + Успешно актуализирана снимка + Маркирай като гледано до този епизод + Премахни маркирането като гледано до този епизод + Презаредено + Презареди доставчика + Име + Име на източника + Резолюция и име + Изтегли всички + Откажи всички + Искаш ли да изтеглиш епизод %s? + Искаш ли да отмениш всички изтегляния в опашката? + Подравняване на субтитрите + Долу вляво + Долу в центъра + Долу вдясно + В средата вляво + В средата в центъра + В средата вдясно + Горе вляво + Горе в центъра + Горе вдясно + + %d активно изтегляне + %d активни изтегляния + + + %d изтегляне в опашката + %d изтегляния в опашката + diff --git a/app/src/main/res/values-b+eo/strings.xml b/app/src/main/res/values-b+eo/strings.xml index f957da076..6809ceb7d 100644 --- a/app/src/main/res/values-b+eo/strings.xml +++ b/app/src/main/res/values-b+eo/strings.xml @@ -49,7 +49,7 @@ Ĝenroj Ĉiuj lingvoj Serĉi - Kontoj + Kontoj kaj Sekureco GitHub Sezono Epizodo @@ -127,4 +127,59 @@ Elŝutite Elŝutante Elŝuto Malsukcesite + %1$dh %2$dm %3$ds + %1$dm %2$ds + %1$ds + Sezono %1$d epizodo %2$d publikiĝos en + Legi ekde la komenco + Elsûtovico + La parolrekono ne haveblas + Komencu paroli… + Ne estas elŝuto ĉimomente. + Selekti Ĉion + Malselekti Ĉion + Malfermi lokan videon + Forigi Dosieron + Daŭrigi Elŝuton + Paŭzigi Elŝuton + Pli da informoj + Kaŝi + Filtri Legosignojn + Legosignoj + Nomo kaj URL de la deponejo + Kopiita! + Sciigo de novo epizodo + Serĉi en aliaj kromprogramoj + Montri la rekomendojn + Subtekstaj Agordoj + Kontura Koloro + Fona Koloro + Randa Tipo + Serĉi uzante provizantojn + Serĉi uzante tipojn + Subteksta Lingvo + Pli da informoj + \@string/home_play + Priskribo + Neniu Priskribo Trovita + Forigi nigrajn borderaĵojn + Subtekstoj + Serĉaj Sugestoj + Aligi Diskordon + Neniu Ligilo Trovita + %1$s %2$d%3$s + Neniu Sezono + %1$d-%2$d + %1$d %2$s + Neniu Epizodo Trovita + Forigi + Forigi Dosieron + Forigi Dosierojn + Forigi (%1$d | %2$s) + Nuligi + Paŭzigi + Daŭrigi + Daŭro + Sinoptiko + Neniu Subteksto diff --git a/app/src/main/res/values-b+lv/strings.xml b/app/src/main/res/values-b+lv/strings.xml index 18d3177f5..055732644 100644 --- a/app/src/main/res/values-b+lv/strings.xml +++ b/app/src/main/res/values-b+lv/strings.xml @@ -199,16 +199,16 @@ \natlikušas Pabeigts Statuss - gads - Reitings + Gads + Vērtējums Ilgums - Saite - Synopsis - Gaida + Vietne + Konspekts + ievietots rindā Lietotie Aplikācija Filmas - Seriāli + Seriāli, raidījumi Animācija Anime Torrenti @@ -219,7 +219,7 @@ NSFW Citi Filmas - Sērijas + Seriāls, raidījums Animācija Anime OVA @@ -464,7 +464,7 @@ Anulēts %s abonements %d sērija izlaista! Apk insteletājs - Github + GitHub Nav subtitru Atskaņot epizodi Iet @@ -596,4 +596,17 @@ Padarīt visus subtitrus slīprakstā Pievieno atskaņošanas ātruma izvēli atskaņotājā Dzēst (%1$d | %2$s) + + %d aktīvas lejupielādes + %d aktīva lejupielāde + %d aktīvas lejupielādes + + Lejupielādes rinda + Automātiski pagriezt + Atbloķēt CloudStream + Paroles/PIN autentifikācija + Atskaņot visas epizodes + Vai tiešām vēlaties neatgriezeniski dzēst šīs %1$s epizodes?\n\n%2$s + Jūs arī neatgriezeniski izdzēsīsiet visas šī seriāla, raidījuma epizodes:\n\n%s + Vai tiešām vēlaties neatgriezeniski dzēst visas šī seriāla, raidījuma epizodes?\n\n%s diff --git a/app/src/main/res/values-b+pt/strings.xml b/app/src/main/res/values-b+pt/strings.xml index e7b3623e6..a1abfa338 100644 --- a/app/src/main/res/values-b+pt/strings.xml +++ b/app/src/main/res/values-b+pt/strings.xml @@ -242,7 +242,7 @@ Atualizar Qualidade Preferida (WiFi) Máximo de caracteres do título no player de video - Resolução do player de vídeo + Mostrar informações do player de vídeo Tamanho do buffer do vídeo Comprimento do buffer do vídeo Cache do vídeo em disco @@ -663,7 +663,7 @@ Ativar torrent nas Configurações/Provedores/Mídia preferida Reinicie a aplicação e aceite o pop-up do Stream Torrent para continuar. Descodificação por software - Descodificação por software permite que o leitor reproduza ficheiros não suportados pelo seu dispositivo, mas pode resultar numa reprodução desfasada ou instável em altas resoluções + Descodificação por software permite que o leitor reproduza ficheiros não suportados pelo seu dispositivo, mas pode resultar numa reprodução desfasada ou instável em altas resoluções. Incorporada Online Episódio (Ascendente) @@ -734,4 +734,31 @@ Versão de pré-lançamento já instalada. Falha ao instalar pré-lançamento. Texto do Episódio + Fila de downloads + Não há downloads na fila no momento. + Brilho extra + Ativar filtro de brilho quando o brilho da tela exceder 100% + extra_brightness_enabled + Sugestões de pesquisa + Mostrar sugestões de pesquisa enquanto digita + Limpar Sugestões + Mostrar Painel de Elenco + Informações da mídia + Prioridade da fonte + Decida como as fontes de vídeo devem ser classificadas no player + Nome da fonte + Baixar tudo + Cancelar tudo + Deseja baixar o episódio %s? + Deseja cancelar todos os downloads em fila? + + %d download ativo + %d downloads ativos + %d downloads ativos + + + %d download na fila + %d downloads na fila + %d downloads na fila + diff --git a/app/src/main/res/values-b+sk/strings.xml b/app/src/main/res/values-b+sk/strings.xml index 93505971c..462a02ad5 100644 --- a/app/src/main/res/values-b+sk/strings.xml +++ b/app/src/main/res/values-b+sk/strings.xml @@ -118,7 +118,7 @@ Prehliadač Zhrnutie sa nenašlo Dvojitým ťuknutím pozastaviť - Aktualizácie a zálohovanie + Aktualizácie a Zálohovanie Informácie Rozšírené vyhľadávanie Zobraziť plagáty z Kitsu @@ -126,7 +126,7 @@ Skryť vybranú kvalitu videa vo výsledkoch vyhľadávania Zobraziť výplňovú epizódu pre anime APK inštalátor - Niektoré telefóny nepodporujú nový inštalátor balíčkov. Ak sa aktualizácie nenainštalujú, skúste použiť staršiu možnosť. + Niektoré zariadenia nepodporujú nový inštalátor balíčkov. Ak sa aktualizácie nenainštalujú, skúste použiť staršiu možnosť. Nenáročná aplikácia pre romány od rovnakých vývojárov Jazyk aplikácie Nenašli sa žiadne odkazy @@ -157,7 +157,7 @@ Nastavenia titulkov prehrávača Spustiť ďalšiu epizódu po skončení aktuálnej Chromecast titulky - Eigengravy režim + Rýchlosť prehrávania (Eigengravy režim) Potiahnutím pretočiť Automaticky prehrať ďalšiu epizódu Aktualizovať priebeh sledovania @@ -168,7 +168,7 @@ Knižnica GitHub Hľadať - Účty + Účty a Zabezpečenie Nastavenia Chromecast titulkov Potiahnutím zo strany na stranu môžete ovládať svoju pozíciu vo videu Nepodarilo sa obnoviť dáta zo súboru %s @@ -203,7 +203,7 @@ Rozloženie aplikácie ahoj@svet.sk Úspešné - MojeSuperMeno + Meno Seriály Seriál E @@ -227,7 +227,7 @@ Prehrať v aplikácii %1$d-%2$d Spôsobuje zlyhania, ak je nastavená príliš vysoko na zariadeniach s nízkou pamäťou, ako je Android TV. - raw.githubusercontent.com Proxy + GitHub Proxy Trvanie Aplikácia /%d @@ -242,7 +242,7 @@ %d / 10 Priblížiť Torrenty - Rozlíšenie prehrávača + Informácie o prehrávači Umiestniť názov pod plagát Preferovaná kvalita sledovania (WiFi) Rozšírenia @@ -260,11 +260,11 @@ Žiadna sezóna Epizóda Znova načítať odkazy - Jazyky poskytovateľa + Jazyky rozšírenia Spustiť Živé prenosy Stiahnuť titulky - Povoliť NSFW u podporovaných poskytovateľov + Povoliť NSFW u podporovaných rozšírení Obchádzanie ISP Prepnúť UI prvky na plagáte Rozloženie @@ -290,7 +290,7 @@ Vzhľad %1$s %2$d%3$s Obchádza blokovanie GitHubu pomocou jsDelivr. Môže spôsobiť oneskorenie aktualizácií o niekoľko dní. - Zobraziť náhodné tlačidlo na domovskej stránke + Zobraziť náhodné tlačidlo na domovskej stránke a Knižnici Odhlásiť sa Aktualizovať Stránka @@ -305,7 +305,7 @@ Klonovať stránku OVA Filmy - príklad.sk + https://example.com Vyrovnávacia pamäť Nepodarilo sa pripojiť na GitHub. Zapína sa proxy jsDelivr… Nenašli sa žiadne epizódy @@ -328,7 +328,7 @@ Poskytovatelia TV rozloženie Kód jazyka (sk) - MôjSuperWeb + NovyNazovWebu %1$s %2$s Vylúčenie zodpovednosti NSFW @@ -457,4 +457,24 @@ Podcast Všetko Chyba kódovania + %1$dh %2$dm %3$ds + Poradie sťahovania + %1$ds + %1$dm %2$ds + Rozpoznanie reči nie je k dispozícii + Začnite hovoriť… + Momentálne nie sú žiadne sťahovania v poradí. + Vyhľadávanie v iných rozšíreniach + Extra Jas + Spustiť filter jasu, keď je prekročený 100% jas displeja + extra_jas_zapnuty + Návrhy vyhľadávania + Zobraziť návrhy vyhľadávania pri písaní + Vyčistiť Návrhy + Nainštalujte pre-release verziu + Pre-release je už nainštalovaný. + Nepodarilo sa nainštalovať pre-release. + Nepodporovaná chyba + Text Epizódy + Lokálna verifikácia 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 7dc4b48f2..c8f9df9a2 100644 --- a/app/src/main/res/values-b+zh+TW/strings.xml +++ b/app/src/main/res/values-b+zh+TW/strings.xml @@ -155,7 +155,7 @@ 顯示預告片 顯示來自 Kitsu 的封面 在搜尋結果中隱藏所選的影片畫質 - 自動更新外掛程式 + 自動更新外掛 顯示應用程式更新 啟動應用程式後自動搜尋更新。 Github @@ -258,7 +258,7 @@ 更新 偏好播放畫質 (WiFi) 影片播放器標題最大字數 - 影片播放器標題 + 顯示播放器資訊 影片緩衝大小 影片緩衝長度 磁碟上的影片快取 @@ -397,16 +397,16 @@ 資源庫名稱(選填) 資源庫 URL 或簡碼 外掛程式已載入 - 外掛程式已刪除 + 外掛已刪除 無法載入 %s 18+ 開始下載 %1$d %2$s … 已下載 %1$d %2$s 全部 %s 已經下載 批次下載 - 外掛程式 - 外掛程式 - 這也將刪除所有在資源庫中的外掛程式 + 外掛 + 外掛 + 這也將刪除所有在資源庫中的外掛 刪除資源庫 下載你所需的片源 已下載:%d @@ -419,14 +419,14 @@ 查看 公開清單 字幕全大寫 - 警告:CloudStream 3 不對使用第三方擴充功能承擔任何責任,也不提供任何支援! + 警告:CloudStream 不對使用第三方擴充功能承擔任何責任,也不提供任何支援! %s (停用) 軌道 音頻軌道 影片軌道 重新啟動應用程式以查看變更。 安全模式已啟用 - 由於程式崩潰,所有外掛程式皆已關閉,以協助您找到導致問題的程式。 + 由於程式當掉,為了協助您找到導致問題的程式,所有外掛皆已關閉。 查看程式崩潰資訊 評分:%s 簡介 @@ -436,7 +436,7 @@ 作者 類型 語言 - 請先安裝外掛程式 + 請先安裝外掛 HLS 播放清單 偏好影片播放器 內部播放器 @@ -452,13 +452,13 @@ 介紹 清除歷史紀錄 歷史紀錄 - 自動下載外掛程式 + 自動下載外掛 你確定要離開? 從新增的資源庫自動安裝所有尚未安裝的外掛程式。 在開始/結束顯示跳過彈出視窗 無法安裝新版本的應用程式 APK 安裝器 - 有些手機不支援新的軟體包安裝程式。 如果未安裝更新,請嘗試使用舊版選項。 + 部分裝置不支援新的軟體安裝程式。 如果未安裝更新,請嘗試使用舊版選項。 文字太多。 無法儲存到剪貼簿。 正在下載應用程式更新… @@ -619,7 +619,7 @@ 無法開啟 CloudStream 的應用程式資訊頁面。 您的 CloudStream 資料已完成備份。儘管可能性非常低,但因不同裝置的行為都有所不同,在極少數情況下,您可能會無法存取本應用程式。此時請完全清除本應用程式的資料,再使用已有的備份進行還原。若因此造成任何不便,我們深感抱歉。 此測試是供開發人員參考,而不是用以驗證任何擴充功能的正常運作與否。 - 為了確保下載與通知已訂閱的電視節目的不間斷,CloudStream 需要取得在背景執行的權限。若點選「確定」,將移至「應用程式資訊」,請找到「應用程式電池使用」並將電池用量設置為「無限制」。請注意,取得此權限並不表示 CS3 會明顯增加電池用量,而是只在必要時在背景執行,例如取得通知或使用官方擴充功能下載影片時。若選擇「取消」,您可以稍後在「一般設定」中調整此設定。 + 為了確保已訂閱的電視節目的不間斷下載與通知,CloudStream 需要取得在背景執行的權限。若點選「確定」,將顯示「請求權限」,請選擇「允許」。\n\n請注意,取得此權限並不表示本程式會明顯增加電池用量,而是只在必要時在背景執行,例如取得通知或使用擴充功能下載影片。 CloudStream Wiki 此裝置不支援生物特徵認證 無法取得裝置 PIN 碼,請嘗試本機驗證 @@ -663,9 +663,104 @@ subs_edge_size 音樂 編碼錯誤 - 因為不支持造成的錯誤 + 因為不支援造成錯誤 播客 軟體解碼 - 不接受的種子 + 重開程式並「同意」線上播放 Torrent 視窗。 載入第一個可用的 + %1$d小時 %2$d分鐘 %3$d秒 + %1$d分鐘 %2$d秒 + %1$d秒 + 下載佇列 + 不支援語音辨識 + 開始說話…… + 播放全劇 + 目前佇列中無下載影片。 + 額外亮度 + 超過100%亮度時開啟亮度調整 + 已開啟額外亮度 + 搜尋建議 + 輸入時顯示搜尋建議 + 清除建議 + 顯示投影控制板 + 安裝提前發行版 + 已安裝提前發行版。 + 提前發行版安裝失敗。 + 鏡像播放" + 評分標籤 + 影劇文本 + 媒體資訊 + 總是詢問 + 成功更新了 %d 外掛! + 沒有需要更新的外掛。 + 播放器通知 + 控制背景播放的通知 + 內嵌 + 線上 + 所有字幕變為粗體 + 所有字幕變為斜體 + 背景寬度 + 允許同時下載幾個項目 + 同時下載 + 同時連接量 + 下載時,每個下載可同時使用的連接量 + 前往下載 + 無網際網路。\n\n請連接網路後重試,或者以離線模式觀看已下載項目。 + 更改螢幕邊界 + 放大符合螢幕 + 更改圖片尺寸 + 圖片尺寸 + 長按以切換速度 + 2 倍速(按住) + 編輯帳戶圖片 + 輸入需使用的網址 URL 以更改帳戶圖片 + 未找到該網址 + 無效網址或圖片 + 已成功更新圖片 + 將這集及之前集數標示為「已觀看」 + 移除至此前所有集數「已觀看」狀態 + 已重新載入 + 重新載入來源 + 名稱 + 來源名 + 解析度與名稱 + 全部下載 + 全部取消 + 需要下載第 %s 集嗎? + 需取消佇列中的所有下載嗎? + 字幕對齊 + 左下 + 底部置中 + 右下 + 左中 + 正中 + 右中 + 左上 + 頂部置中 + 右上 + + %d 個正在下載 + + + 佇列中有 %d 個下載 + + 集數(由舊至新) + 集數(由新至舊) + 評分(最高) + 評分(最低) + 播放日期(最新) + 播放日期(最舊) + 第 %s 集 + 評分 %s + 日期 %s + 讓音量超過 100%(再次上滑) + 更新外掛 + 手動更新外掛 + 正在更新外掛! + 來源優先順序 + 決定影片來源的排列順序 + 帳戶不存在 + 在「設定/影片來源/首選媒體」中啟用 Torrent下載 + 軟體解碼使程式可以播放裝置不支援的影片,但可能導致播放高解析的影片時的延遲或不穩定。 + 音量已超過 100% From ee1e90e0f476823d5c4a15059ed172d3c4839d7f Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Sun, 22 Mar 2026 08:34:43 +0100 Subject: [PATCH 011/177] Emergency fix for OOM and leaks --- .../lagradost/cloudstream3/CommonActivity.kt | 12 ++++++++++++ .../cloudstream3/extractors/Videa.kt | 18 ++++++++++++++---- .../cloudstream3/utils/UnshortenUrl.kt | 19 ++++++++++++------- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index c806cac63..dddcd4892 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -9,6 +9,8 @@ import android.content.res.Configuration import android.content.res.Resources import android.Manifest import android.os.Build +import android.os.Handler +import android.os.Looper import android.util.DisplayMetrics import android.util.Log import android.view.Gravity @@ -191,6 +193,16 @@ object CommonActivity { currentToast = toast toast.show() + val handler = Handler(Looper.getMainLooper()) + val ref = WeakReference(toast) + + /* Clean up activity leak */ + handler.postDelayed({ + if (ref.get() == currentToast) { + currentToast = null + } + }, 10_000) + } catch (e: Exception) { logError(e) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt index 121e221ad..bbcce2755 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt @@ -25,12 +25,17 @@ class Videa : ExtractorApi() { ) { var currentUrl = url var key = "" - var lastUrl: String? = null // Handle redirect loop until we get valid XML - while (true) { + val visitedUrls = mutableSetOf() + var count = 10 + while (!visitedUrls.contains(currentUrl) && count > 0) { + visitedUrls += currentUrl + count -= 1 + val webUrl = getXmlUrl(currentUrl) { cookie -> /* no-op, cookie not used */ } ?: return val response = app.get(webUrl) - val rawBytes = response.body.bytes() + val body = response.body // TODO CLOSE? + val rawBytes = body.bytes() // Check if response starts with XML declaration val isXml = rawBytes.size >= 5 && @@ -53,7 +58,6 @@ class Videa : ExtractorApi() { val redirectMatch = """(.*)""".toRegex().find(videaXml) if (redirectMatch != null && redirectMatch.groupValues[1] != currentUrl) { - lastUrl = currentUrl currentUrl = redirectMatch.groupValues[1] } else { parseVideoSources(videaXml, callback) @@ -64,6 +68,12 @@ class Videa : ExtractorApi() { private suspend fun getXmlUrl(url: String, cookieCallback: (String) -> Unit = {}): String? { val response = app.get(url) + val size = response.size + /* OOM Protection */ + if(size != null && size > 5_000_000) { + // You tried to use a video here + return null + } val html = response.text // Extract sl cookie if present diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt index 1a9867f50..206b0f29f 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt @@ -51,16 +51,18 @@ object ShortLink { suspend fun unshorten(uri: String, type: String? = null): String { var currentUrl = uri - while (true) { - val oldurl = currentUrl + val visitedUrls = mutableSetOf() + var count = 10 + while (!visitedUrls.contains(currentUrl) && count > 0) { + visitedUrls += currentUrl + count -= 1 + val domain = - URI(currentUrl.trim()).host ?: throw IllegalArgumentException("No domain found in URI!") + URI(currentUrl.trim()).host + ?: throw IllegalArgumentException("No domain found in URI!") currentUrl = shortList.firstOrNull { it.regex.find(domain) != null || type == it.type }?.function?.let { it(currentUrl) } ?: break - if (oldurl == currentUrl) { - break - } } return currentUrl.trim() } @@ -112,8 +114,10 @@ object ShortLink { uri.contains("delta") -> uri = uri.replace("/delta/", "/adelta/") (uri.contains("/ga/") || uri.contains("/ga2/")) -> uri = base64Decode(uri.split('/').last()).trim() + uri.contains("/speedx/") -> uri = uri.replace("http://linkup.pro/speedx", "http://speedvideo.net") + else -> { r = app.get(uri, allowRedirects = true) uri = r.url @@ -187,8 +191,9 @@ object ShortLink { fun unshortenDavisonbarker(uri: String): String { return URLDecoder.decode(uri.substringAfter("dest="), "UTF-8") } + suspend fun unshortenIsecure(uri: String): String { val doc = app.get(uri).document - return doc.selectFirst("iframe")?.attr("src")?.trim()?: uri + return doc.selectFirst("iframe")?.attr("src")?.trim() ?: uri } } \ No newline at end of file From a74a0840d60925fbc7bb2b72d7f0ca8d1ea726c8 Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:04:22 +0100 Subject: [PATCH 012/177] Fixed BackPressedCallbackHelper activity leak --- .../lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt index d68b254b0..10736e13e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt @@ -56,7 +56,7 @@ object BackPressedCallbackHelper { fun ComponentActivity.detachBackPressedCallback(id: String) { val callbackMap = backPressedCallbacks[this] ?: return callbackMap[id]?.let { callback -> - callback.isEnabled = false + callback.remove() callbackMap.remove(id) } From 45699b72a8c8d0a0a74867ff75ef5be78c2221df Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:41:12 +0100 Subject: [PATCH 013/177] Added .close to m3u8 hslLazy --- .../kotlin/com/lagradost/cloudstream3/extractors/Videa.kt | 3 ++- .../kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt index bbcce2755..47840a08a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Videa.kt @@ -34,8 +34,9 @@ class Videa : ExtractorApi() { val webUrl = getXmlUrl(currentUrl) { cookie -> /* no-op, cookie not used */ } ?: return val response = app.get(webUrl) - val body = response.body // TODO CLOSE? + val body = response.body val rawBytes = body.bytes() + body.close() // Check if response starts with XML declaration val isXml = rawBytes.size >= 5 && diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt index a852bdc03..23226418b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -233,7 +233,9 @@ object M3u8Helper2 { val ts = allTsLinks[index] val tsResponse = app.get(ts.url, headers = headers, verify = false) - val tsData = tsResponse.body.bytes() + val body = tsResponse.body + val tsData = body.bytes() + body.close() if (tsData.isEmpty()) throw ErrorLoadingException("no data") return if (isEncrypted) { @@ -329,7 +331,9 @@ object M3u8Helper2 { encryptionIv = match[3].toByteArray() val encryptionKeyResponse = app.get(encryptionUri, headers = playlistStream.headers, verify = false) - encryptionData = encryptionKeyResponse.body.bytes() + val body = encryptionKeyResponse.body + encryptionData = body.bytes() + body.close() } else { encryptionState = false } From f5b46949ecd15ac5c37ea2762cc02e32b54de4e7 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 22 Mar 2026 17:16:39 -0600 Subject: [PATCH 014/177] Add support for configuration cache with keystore (#2328) --- app/build.gradle.kts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 722fcf58e..20ce54077 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,8 +12,6 @@ plugins { } val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) -val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/" -val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first() fun getGitCommitHash(): String { return try { @@ -46,9 +44,14 @@ android { } signingConfigs { - if (prereleaseStoreFile != null) { + // We just use SIGNING_KEY_ALIAS here since it won't change + // so won't kill the configuration cache. + if (System.getenv("SIGNING_KEY_ALIAS") != null) { create("prerelease") { - storeFile = file(prereleaseStoreFile) + val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/" + val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first() + + storeFile = prereleaseStoreFile?.let { file(it) } storePassword = System.getenv("SIGNING_STORE_PASSWORD") keyAlias = System.getenv("SIGNING_KEY_ALIAS") keyPassword = System.getenv("SIGNING_KEY_PASSWORD") From f674b427ac709d56a6a0ab4414f7f1b013063701 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:48:48 -0600 Subject: [PATCH 015/177] Use assemblePrereleaseRelease for building prerelease (#2365) --- .github/workflows/build_to_archive.yml | 3 ++- .github/workflows/prerelease.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml index 07096014a..f72dd10c6 100644 --- a/.github/workflows/build_to_archive.yml +++ b/.github/workflows/build_to_archive.yml @@ -61,13 +61,14 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Run Gradle - run: ./gradlew assemblePrerelease + run: ./gradlew assemblePrereleaseRelease env: SIGNING_KEY_ALIAS: "key0" SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} + MDL_API_KEY: ${{ secrets.MDL_API_KEY }} - uses: actions/checkout@v6 with: diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index c7dee13eb..03cb68cbc 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -52,7 +52,7 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Run Gradle - run: ./gradlew assemblePrerelease build androidSourcesJar makeJar + run: ./gradlew assemblePrereleaseRelease androidSourcesJar makeJar env: SIGNING_KEY_ALIAS: "key0" SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} From d06afa32fdeab8264d5cf6c7880b07d5ad0dc7ad Mon Sep 17 00:00:00 2001 From: Phisher98 <153359846+phisher98@users.noreply.github.com> Date: Tue, 24 Mar 2026 03:44:48 +0530 Subject: [PATCH 016/177] Tracks naming fix and minor UI improvements (#2480) --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 140 ++++++++++++------ 1 file changed, 97 insertions(+), 43 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 d20e85707..a2cef1122 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 @@ -1434,45 +1434,42 @@ class GeneratorPlayer : FullScreenPlayer() { val audioArrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) - audioArrayAdapter.addAll(currentAudioTracks.mapIndexed { index, track -> - val language = track.language?.let { fromTagToLanguageName(it) ?: it } - ?: track.label - ?: "Audio" - - val codec = track.sampleMimeType?.let { mimeType -> - when { - mimeType.contains("mp4a") || mimeType.contains("aac") -> "aac" - mimeType.contains("ac-3") || mimeType.contains("ac3") -> "ac3" - mimeType.contains("eac3-joc") -> "Dolby Atmos" - mimeType.contains("eac3") -> "eac3" - mimeType.contains("opus") -> "opus" - mimeType.contains("vorbis") -> "vorbis" - mimeType.contains("mp3") || mimeType.contains("mpeg") -> "mp3" - mimeType.contains("flac") -> "flac" - mimeType.contains("dts") -> "dts" - else -> mimeType.substringAfter("/") + audioArrayAdapter.addAll( + currentAudioTracks.mapIndexed { _, track -> + + val language = ( + track.language?.trim()?.let { raw -> + fromTagToLanguageName(raw) + ?: fromTagToLanguageName(raw.replace('_','-').substringBefore('-').lowercase()) + ?: raw + } + ?: track.label + ?: "Audio" + ).replaceFirstChar { it.uppercaseChar() } + + val codec = audioCodecName(track.sampleMimeType) + + val channelCount = track.channelCount + + val channels = when { + // May be below 1 or null when unknown + channelCount == null || channelCount <= 0 -> "" + channelCount == 1 -> "Mono" + channelCount == 2 -> "Stereo" + channelCount == 6 -> "5.1" + channelCount == 8 -> "7.1" + else -> "${channelCount}ch" } - } ?: "codec?" - - val channels: Int = track.channelCount ?: 0 - val channelConfig = when (channels) { - 1 -> "mono" - 2 -> "stereo" - 6 -> "5.1" - 8 -> "7.1" - else -> "${channels}Ch" + listOfNotNull( + language.takeIf { it.isNotBlank() }?.replaceFirstChar { it.uppercaseChar() }, + channels.takeIf { it.isNotBlank() }, + codec.takeIf { it.isNotBlank() }?.uppercase() + ).joinToString(" • ") + + } - - listOfNotNull( - "[$index]", - language.replaceFirstChar { it.uppercaseChar() }, - codec.uppercase(), - channelConfig.replaceFirstChar { it.uppercaseChar() } - ).joinToString(" • ") - - "[$index] $language $codec $channelConfig" - }) + ) audioList.adapter = audioArrayAdapter audioList.choiceMode = AbsListView.CHOICE_MODE_SINGLE @@ -1830,6 +1827,42 @@ class GeneratorPlayer : FullScreenPlayer() { } } + + private fun videoCodecName(mime: String?): String? { + val m = mime?.lowercase() ?: return null + return when { + m.contains("avc") || m.contains("h264") -> "AVC" + m.contains("hevc") || m.contains("h265") -> "HEVC" + m.contains("av1") -> "AV1" + m.contains("vp9") -> "VP9" + m.contains("vp8") -> "VP8" + "/" in m -> m.substringAfter("/").uppercase() + else -> m.uppercase() + } + } + + private fun audioCodecName(mime: String?): String { + val m = mime?.lowercase()?.trim().orEmpty() + if (m.isBlank()) return "" + return when { + m.contains("eac3-joc") -> "Dolby Atmos" + m.contains("truehd") -> "TrueHD" + m.contains("eac3") -> "E-AC3" + m.contains("ac-3") || m.contains("ac3") -> "AC3" + m.contains("aac") || m.contains("mp4a") -> "AAC" + m.contains("opus") -> "Opus" + m.contains("vorbis") -> "Vorbis" + m.contains("mp3") -> "MP3" + m.contains("flac") -> "FLAC" + m.contains("dts") -> "DTS" + m.contains("pcm") -> "PCM" + m.contains("alac") -> "ALAC" + m.contains("amr") -> "AMR" + m.contains("/") -> m.substringAfter("/").uppercase().takeIf { it.isNotBlank() } ?: "" + else -> "" + } + } + private fun updatePlayerInfo() { val tracks = player.getVideoTracks() @@ -1840,14 +1873,35 @@ class GeneratorPlayer : FullScreenPlayer() { val prefs = PreferenceManager.getDefaultSharedPreferences(ctx) showMediaInfo = prefs.getBoolean(ctx.getString(R.string.show_media_info_key), false) - val videoCodec = videoTrack?.sampleMimeType?.substringAfterLast('/')?.uppercase() - val audioCodec = audioTrack?.sampleMimeType?.substringAfterLast('/')?.uppercase() - val language = listOfNotNull( - audioTrack?.label, - fromTagToLanguageName(audioTrack?.language)?.let { "[$it]" } - ).joinToString(" ") + val videoCodec = videoCodecName(videoTrack?.sampleMimeType) + val audioCodec = audioCodecName(audioTrack?.sampleMimeType) + val languageName = fromTagToLanguageName(audioTrack?.language) + val label = audioTrack?.label - val stats = arrayOf(videoCodec, audioCodec, language).filter { !it.isNullOrBlank() }.joinToString(" • ") + val channelCount = audioTrack?.channelCount + + val channels = when { + // May be below 1 or null when unknown + channelCount == null || channelCount <= 0 -> "" + channelCount == 1 -> "Mono" + channelCount == 2 -> "Stereo" + channelCount == 6 -> "5.1" + channelCount == 8 -> "7.1" + else -> "${channelCount}ch" + } + + val language = languageName?.takeIf { it.isNotBlank() }?.let { lang -> + label?.takeIf { it.isNotBlank() && !it.equals(lang, true) } + ?.let { lang } + ?: lang + } ?: label?.takeIf { it.isNotBlank() } + + val stats = arrayOf( + videoCodec, + language, + channels, + audioCodec + ).filter { !it.isNullOrBlank() }.joinToString(" • ") playerBinding?.playerVideoInfo?.apply { text = stats From 89400be5e5e7f49ac1fbb14f163d18ecdd68d467 Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Wed, 25 Mar 2026 18:31:57 +0100 Subject: [PATCH 017/177] Remove google dependenciesInfo + bumb nicehttp --- app/build.gradle.kts | 8 ++++++++ gradle/libs.versions.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 20ce54077..3be4e2ea7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -39,6 +39,14 @@ android { unitTests.isReturnDefaultValues = true } + // Looks like google likes to add metadata only they can read https://gitlab.com/IzzyOnDroid/repo/-/work_items/491 + dependenciesInfo { + // Disables dependency metadata when building APKs. + includeInApk = false + // Disables dependency metadata when building Android App Bundles. + includeInBundle = false + } + viewBinding { enable = true } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1e5a2ade9..c79726d01 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ media3 = "1.9.2" navigationKtx = "2.9.7" newpipeextractor = "v0.25.2" nextlibMedia3 = "1.9.1-0.11.0" -nicehttp = "0.4.16" +nicehttp = "0.4.17" overlappingpanels = "0.1.5" paletteKtx = "1.0.0" preferenceKtx = "1.2.1" From a23c136d81d12035091751d36a070e7b68ef6896 Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Thu, 26 Mar 2026 01:13:13 +0100 Subject: [PATCH 018/177] Fixed partial downloads + resume bugs --- .../utils/downloader/DownloadManager.kt | 67 +++++++++++++++---- .../utils/downloader/DownloadObjects.kt | 4 +- 2 files changed, 57 insertions(+), 14 deletions(-) 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 fd1715e22..a561dbbe8 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 @@ -21,6 +21,7 @@ import androidx.core.net.toUri import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey @@ -182,6 +183,13 @@ object VideoDownloadManager { /** the process failed due to some reason, so we retry and also try the next mirror */ private val DOWNLOAD_FAILED = DownloadStatus(retrySame = true, tryNext = true, success = false) + /** The download only downloaded partial */ + private val DOWNLOAD_PARTIAL_SUCCESS = + DownloadStatus(retrySame = true, tryNext = false, success = true) + + /** 10MB minimum size */ + const val DOWNLOAD_PARTIAL_MIN_SIZE = 1_048_576L * 10L + /** bad config, skip all mirrors as every call to download will have the same bad config */ private val DOWNLOAD_BAD_CONFIG = DownloadStatus(retrySame = false, tryNext = false, success = false) @@ -523,6 +531,7 @@ object VideoDownloadManager { /** This class handles the notifications, as well as the relevant key */ data class DownloadMetaData( private val id: Int?, + private val linkHash : Int, var bytesDownloaded: Long = 0, var bytesWritten: Long = 0, @@ -534,7 +543,7 @@ object VideoDownloadManager { private val createNotificationCallback: (CreateNotificationMetadata) -> Unit, private var internalType: DownloadType = DownloadType.IsPending, - + val isHLS : Boolean, // how many segments that we have downloaded var hlsProgress: Int = 0, // how many segments that exist @@ -552,13 +561,17 @@ object VideoDownloadManager { lastDownloadedBytes = length } + /** Returns the appropriate failed status based on download progress */ + fun failedStatus() = if (this.bytesWritten > DOWNLOAD_PARTIAL_MIN_SIZE) + DOWNLOAD_PARTIAL_SUCCESS + else + DOWNLOAD_FAILED + val approxTotalBytes: Long get() = totalBytes ?: hlsTotal?.let { total -> (bytesDownloaded * (total / hlsProgress.toFloat())).toLong() } ?: bytesDownloaded - private val isHLS get() = hlsTotal != null - private var stopListener: (() -> Unit)? = null /** on cancel button pressed or failed invoke this once and only once */ @@ -593,11 +606,32 @@ object VideoDownloadManager { private fun updateFileInfo() { if (id == null) return downloadFileInfoTemplate?.let { template -> + /** This looks strange, but fixes an issue where we do an instant retry, and it fails immediately, + * eg. by turning off wifi */ + val totalBytesValue = if (approxTotalBytes <= bytesDownloaded) { + val prevInfo = getKey( + KEY_DOWNLOAD_INFO, + id.toString() + ) + + /** If this link is the same as the last cached video link metadata */ + if (prevInfo != null && prevInfo.linkHash == linkHash) { + /** Try to use totalBytes if it exists, otherwise the max of the prev data, + * and download size to ensure total >= downloaded */ + totalBytes ?: maxOf(prevInfo.totalBytes, bytesDownloaded) + } else { + approxTotalBytes + } + } else { + approxTotalBytes + } + setKey( KEY_DOWNLOAD_INFO, id.toString(), template.copy( - totalBytes = approxTotalBytes, + linkHash = linkHash, + totalBytes = totalBytesValue, extraInfo = if (isHLS) hlsWrittenProgress.toString() else null ) ) @@ -982,6 +1016,8 @@ object VideoDownloadManager { bytesDownloaded = 0, createNotificationCallback = createNotificationCallback, id = parentId, + linkHash = link.url.hashCode(), + isHLS = false ) try { // get the file path @@ -1171,7 +1207,7 @@ object VideoDownloadManager { if (!stream.exists) metadata.type = DownloadType.IsStopped if (metadata.type == DownloadType.IsFailed) { - return@withContext DOWNLOAD_FAILED + return@withContext metadata.failedStatus() } if (metadata.type == DownloadType.IsStopped) { @@ -1201,11 +1237,11 @@ object VideoDownloadManager { throw e } catch (t: Throwable) { // some sort of network error, will error - + logError(t) // note that when failing we don't want to delete the file, // only user interaction has that power metadata.type = DownloadType.IsFailed - return@withContext DOWNLOAD_FAILED + return@withContext metadata.failedStatus() } finally { fileStream?.closeQuietly() //requestStream?.closeQuietly() @@ -1227,7 +1263,9 @@ object VideoDownloadManager { val metadata = DownloadMetaData( createNotificationCallback = createNotificationCallback, - id = parentId + id = parentId, + linkHash = link.url.hashCode(), + isHLS = true ) var fileStream: OutputStream? = null try { @@ -1385,7 +1423,7 @@ object VideoDownloadManager { if (!stream.exists) metadata.type = DownloadType.IsStopped if (metadata.type == DownloadType.IsFailed) { - return@withContext DOWNLOAD_FAILED + return@withContext metadata.failedStatus() } if (metadata.type == DownloadType.IsStopped) { @@ -1401,7 +1439,7 @@ object VideoDownloadManager { } catch (t: Throwable) { logError(t) metadata.type = DownloadType.IsFailed - return@withContext DOWNLOAD_FAILED + return@withContext metadata.failedStatus() } finally { fileStream?.closeQuietly() metadata.close() @@ -1983,7 +2021,8 @@ object VideoDownloadManager { linkLoadingJob?.join() // Remove link loading notification - NotificationManagerCompat.from(context).cancel(DOWNLOAD_NOTIFICATION_TAG, downloadItem.episode.id) + NotificationManagerCompat.from(context) + .cancel(DOWNLOAD_NOTIFICATION_TAG, downloadItem.episode.id) if (linkLoadingJob?.isCancelled == true) { // Same as if no links, but no toast. @@ -2009,8 +2048,10 @@ object VideoDownloadManager { } // Profiles should always contain a download type - val profile = QualityDataHelper.getProfiles().first { it.types.contains( - QualityDataHelper.QualityProfileType.Download) + val profile = QualityDataHelper.getProfiles().first { + it.types.contains( + QualityDataHelper.QualityProfileType.Download + ) } val sortedLinks = currentLinks.sortedBy { link -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt index 1d945a6b4..25a9fdf2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadObjects.kt @@ -133,7 +133,9 @@ object DownloadObjects { @JsonProperty("relativePath") val relativePath: String, @JsonProperty("displayName") val displayName: String, @JsonProperty("extraInfo") val extraInfo: String? = null, - @JsonProperty("basePath") val basePath: String? = null // null is for legacy downloads. See getBasePath() + @JsonProperty("basePath") val basePath: String? = null, // null is for legacy downloads. See getBasePath() + // Hash of the link associated with this DownloadFile, used so not override old data in the DownloadedFileInfo + @JsonProperty("linkHash") val linkHash : Int? = null ) data class DownloadedFileInfoResult( From 07eb9973f8f32dcba0756c1d55e8740a8345441c Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Sun, 29 Mar 2026 13:54:51 +0200 Subject: [PATCH 019/177] Increased DOWNLOAD_PARTIAL_MIN_SIZE to 50MB --- .../cloudstream3/utils/downloader/DownloadManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a561dbbe8..11c35e9ec 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 @@ -187,8 +187,8 @@ object VideoDownloadManager { private val DOWNLOAD_PARTIAL_SUCCESS = DownloadStatus(retrySame = true, tryNext = false, success = true) - /** 10MB minimum size */ - const val DOWNLOAD_PARTIAL_MIN_SIZE = 1_048_576L * 10L + /** 50MB minimum size */ + const val DOWNLOAD_PARTIAL_MIN_SIZE = 1_048_576L * 50L /** bad config, skip all mirrors as every call to download will have the same bad config */ private val DOWNLOAD_BAD_CONFIG = From 235863f9d29b8c8d78ea603fcfe261712860182c Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Sun, 29 Mar 2026 14:06:17 +0200 Subject: [PATCH 020/177] Bump to 4.7.0 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3be4e2ea7..97d24c4fb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -73,8 +73,8 @@ android { applicationId = "com.lagradost.cloudstream3" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = 67 - versionName = "4.6.2" + versionCode = 68 + versionName = "4.7.0" resValue("string", "commit_hash", getGitCommitHash()) From 81c7d90a5f30015749bccbb0126433bd5959df3b Mon Sep 17 00:00:00 2001 From: Nguyen Van Nam Date: Mon, 30 Mar 2026 01:15:34 +0700 Subject: [PATCH 021/177] Fix: Subtitle deletion matches on substring extension, can delete non-subtitle files (#2584) --- .../java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt index 18d465e3c..c0068f91a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt @@ -43,7 +43,7 @@ object SubtitleUtils { cleanDisplay: String ): Boolean { // Check if the file has a valid subtitle extension - val hasValidExtension = allowedExtensions.any { name.contains(it, ignoreCase = true) } + val hasValidExtension = allowedExtensions.any { name.endsWith(it, ignoreCase = true) } // We can't have the exact same file as a subtitle val isNotDisplayName = !name.equals(display, ignoreCase = true) @@ -57,4 +57,4 @@ object SubtitleUtils { fun cleanDisplayName(name: String): String { return name.substringBeforeLast('.').trim() } -} \ No newline at end of file +} From 76a2feb79ce6a56bbedbae265e81742790c1e218 Mon Sep 17 00:00:00 2001 From: PiterDev <71133634+PiterWeb@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:33:36 +0200 Subject: [PATCH 022/177] Add fallback url for kitsu sync (#2552) --- .../syncproviders/providers/KitsuApi.kt | 82 ++++++++++++++----- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt index 3f079d9d5..29c3c0c17 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/KitsuApi.kt @@ -5,11 +5,11 @@ import androidx.annotation.StringRes import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey -import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.Score import com.lagradost.cloudstream3.ShowStatus import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AuthData import com.lagradost.cloudstream3.syncproviders.AuthLoginRequirement @@ -22,18 +22,16 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.txt -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.withIndex +import okhttp3.Interceptor +import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response import java.text.SimpleDateFormat import java.time.Instant import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.Date import java.util.Locale -import kotlin.collections.set const val KITSU_MAX_SEARCH_LIMIT = 20 @@ -42,7 +40,9 @@ class KitsuApi: SyncAPI() { override val idPrefix = "kitsu" private val apiUrl = "https://kitsu.io/api/edge" + private val fallbackApiUrl = "https://kitsu.app/api/edge" private val oauthUrl = "https://kitsu.io/api/oauth" + private val fallbackOauthUrl = "https://kitsu.app/api/oauth" override val hasInApp = true override val mainUrl = "https://kitsu.app" override val icon = R.drawable.kitsu_icon @@ -63,6 +63,33 @@ class KitsuApi: SyncAPI() { email = true ) + private class FallbackInterceptor(private val apiUrl: String, private val fallbackApiUrl: String) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request: Request = chain.request() + + try { + + val response = chain.proceed(request); + + if (response.isSuccessful) return response + + response.close() + + } catch (_: Exception) { + } + + val fallbackRequest: Request = request.newBuilder() + .url(request.url.toString().replaceFirst(apiUrl, fallbackApiUrl)) + .build() + + return chain.proceed(fallbackRequest) + + } + } + + private val apiFallbackInterceptor = FallbackInterceptor(apiUrl, fallbackApiUrl) + private val oauthFallbackInterceptor = FallbackInterceptor(oauthUrl, fallbackOauthUrl) + override suspend fun login(form: AuthLoginResponse): AuthToken? { val username = form.email ?: return null val password = form.password ?: return null @@ -75,8 +102,10 @@ class KitsuApi: SyncAPI() { "grant_type" to grantType, "username" to username, "password" to password - ) + ), + interceptor = oauthFallbackInterceptor ).parsed() + return AuthToken( accessTokenLifetime = unixTime + token.expiresIn.toLong(), refreshToken = token.refreshToken, @@ -90,7 +119,8 @@ class KitsuApi: SyncAPI() { data = mapOf( "grant_type" to "refresh_token", "refresh_token" to token.refreshToken!! - ) + ), + interceptor = oauthFallbackInterceptor ).parsed() return AuthToken( @@ -105,7 +135,8 @@ class KitsuApi: SyncAPI() { "$apiUrl/users?filter[self]=true", headers = mapOf( "Authorization" to "Bearer ${token?.accessToken ?: return null}" - ), cacheTime = 0 + ), cacheTime = 0, + interceptor = apiFallbackInterceptor ).parsed() if (user.data.isEmpty()) { @@ -123,11 +154,14 @@ class KitsuApi: SyncAPI() { val auth = auth?.token?.accessToken ?: return null val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","episodeCount") val url = "$apiUrl/anime?filter[text]=$query&page[limit]=$KITSU_MAX_SEARCH_LIMIT&fields[anime]=${animeSelectedFields.joinToString(",")}" + val res = app.get( url, headers = mapOf( "Authorization" to "Bearer $auth", - ), cacheTime = 0 + ), cacheTime = 0, + interceptor = apiFallbackInterceptor ).parsed() + return res.data.map { val attributes = it.attributes @@ -160,7 +194,8 @@ class KitsuApi: SyncAPI() { val anime = app.get( url, headers = mapOf( "Authorization" to "Bearer $auth" - ) + ), + interceptor = apiFallbackInterceptor ).parsed().data.attributes return SyncResult( @@ -201,7 +236,8 @@ class KitsuApi: SyncAPI() { val anime = app.get( url, headers = mapOf( "Authorization" to "Bearer $accessToken" - ) + ), + interceptor = apiFallbackInterceptor ).parsed().data.firstOrNull()?.attributes if (anime == null) { @@ -224,7 +260,8 @@ class KitsuApi: SyncAPI() { val animeSelectedFields = arrayOf("titles","canonicalTitle") val url = "$apiUrl/anime?filter[text]=$title&page[limit]=$KITSU_MAX_SEARCH_LIMIT&fields[anime]=${animeSelectedFields.joinToString(",")}" - val res = app.get(url).parsed() + + val res = app.get(url, interceptor = apiFallbackInterceptor).parsed() return res.data.firstOrNull()?.id @@ -269,8 +306,10 @@ class KitsuApi: SyncAPI() { headers = mapOf( "Authorization" to "Bearer ${auth.token.accessToken}" ), + interceptor = apiFallbackInterceptor ) + return res.isSuccessful } @@ -316,7 +355,8 @@ class KitsuApi: SyncAPI() { "content-type" to "application/vnd.api+json", "Authorization" to "Bearer ${auth.token.accessToken}" ), - requestBody = data.toJson().toRequestBody() + requestBody = data.toJson().toRequestBody(), + interceptor = apiFallbackInterceptor ) return res.isSuccessful @@ -349,9 +389,11 @@ class KitsuApi: SyncAPI() { "content-type" to "application/vnd.api+json", "Authorization" to "Bearer ${auth.token.accessToken}" ), - requestBody = data.toJson().toRequestBody() + requestBody = data.toJson().toRequestBody(), + interceptor = apiFallbackInterceptor ) + return res.isSuccessful } @@ -365,6 +407,7 @@ class KitsuApi: SyncAPI() { headers = mapOf( "Authorization" to "Bearer ${auth.token.accessToken}" ), + interceptor = apiFallbackInterceptor ).parsed().data.firstOrNull() ?: return null return res.id.toInt() @@ -439,7 +482,8 @@ class KitsuApi: SyncAPI() { val res = app.get( url, headers = mapOf( "Authorization" to "Bearer ${token.accessToken}", - ) + ), + interceptor = apiFallbackInterceptor ).parsed() return res } @@ -474,7 +518,7 @@ class KitsuApi: SyncAPI() { val animeId = animeItem?.id - val description: String? = animeItem?.attributes?.synopsis + val synopsis: String? = animeItem?.attributes?.synopsis return LibraryItem( canonicalTitle ?: titles?.enJp ?: titles?.jaJp.orEmpty(), @@ -489,7 +533,7 @@ class KitsuApi: SyncAPI() { posterImage?.large ?: posterImage?.medium, null, null, - plot = description, + plot = synopsis, releaseDate = if (startDate == null) null else try { Date.from( Instant.from( @@ -770,4 +814,4 @@ query { val canonical: String? = null ) } -} \ No newline at end of file +} From 7a2222b252c98a5e422f98fd59d068eff9cca2ed Mon Sep 17 00:00:00 2001 From: Phisher98 <153359846+phisher98@users.noreply.github.com> Date: Mon, 30 Mar 2026 03:41:58 +0530 Subject: [PATCH 023/177] Adding Metdata on Player (Initial Draft) (TV) (#2461) --- .../ui/player/FullScreenPlayer.kt | 68 ++++++++++++++- .../cloudstream3/ui/player/GeneratorPlayer.kt | 50 +++++++++++ .../cloudstream3/utils/AppContextUtils.kt | 8 ++ .../bg_player_metadata_scrim_netflix.xml | 7 ++ .../res/drawable/metadata_overlay_icon.xml | 36 ++++++++ .../main/res/layout/player_custom_layout.xml | 72 ++++++++++++++++ .../res/layout/player_custom_layout_tv.xml | 83 +++++++++++++++++-- .../main/res/layout/trailer_custom_layout.xml | 73 +++++++++++++++- .../res/values/donottranslate-strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/settings_ui.xml | 5 ++ 11 files changed, 392 insertions(+), 12 deletions(-) create mode 100644 app/src/main/res/drawable/bg_player_metadata_scrim_netflix.xml create mode 100644 app/src/main/res/drawable/metadata_overlay_icon.xml 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 bc10285e3..90274c938 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 @@ -35,6 +35,7 @@ import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.animation.AnimationUtils +import android.view.animation.DecelerateInterpolator import android.widget.LinearLayout import androidx.annotation.OptIn import androidx.appcompat.app.AlertDialog @@ -97,6 +98,8 @@ import kotlin.math.max import kotlin.math.min import kotlin.math.round import kotlin.math.roundToInt +import com.lagradost.cloudstream3.utils.AppContextUtils.shouldShowPlayerMetadata + // You can zoom out more than 100%, but it will zoom back into 100% const val MINIMUM_ZOOM = 0.95f @@ -133,7 +136,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private var uiShowingBeforeGesture = false protected var isLocked = false protected var timestampShowState = false - + private var metadataVisibilityToken = 0 protected var hasEpisodes = false private set // protected val hasEpisodes @@ -235,10 +238,55 @@ open class FullScreenPlayer : AbstractPlayerFragment() { requestUpdateBrightnessOverlayOnNextLayout() } } - return root } + private fun scheduleMetadataVisibility() { + val metadataScrim = playerBinding?.playerMetadataScrim ?: return + val ctx = metadataScrim.context ?: return + + if (!ctx.shouldShowPlayerMetadata()) { + metadataScrim.isVisible = false + metadataVisibilityToken++ + return + } + + if (isLayout(PHONE)) { + metadataScrim.isVisible = false + metadataVisibilityToken++ + return + } + + val isPaused = currentPlayerStatus == CSPlayerLoading.IsPaused + val token = ++metadataVisibilityToken + + if (isPaused) { + metadataScrim.postDelayed({ + if (token != metadataVisibilityToken) return@postDelayed + metadataScrim.alpha = 0f + metadataScrim.isVisible = true + metadataScrim.animate() + .alpha(1f) + .setDuration(500L) + .setInterpolator(DecelerateInterpolator()) + .start() + hidePlayerUI() + }, 8000L) + } else { + if (metadataScrim.isVisible) { + metadataScrim.animate() + .alpha(0f) + .setDuration(300L) + .setInterpolator(AccelerateDecelerateInterpolator()) + .withEndAction { + metadataScrim.alpha = 0f // force final state + metadataScrim.isVisible = false + } + .start() + } + } + } + @SuppressLint("UnsafeOptInUsageError") override fun playerUpdated(player: Any?) { super.playerUpdated(player) @@ -456,6 +504,12 @@ open class FullScreenPlayer : AbstractPlayerFragment() { start() } } + playerBinding?.playerMetadataScrim?.let { + ObjectAnimator.ofFloat(it, "translationY", 1f).apply { + duration = 200 + start() + } + } val playerBarMove = if (isShowing) 0f else 50.toPx.toFloat() playerBinding?.bottomPlayerBar?.let { @@ -522,7 +576,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 = @@ -1013,7 +1067,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { // BOTTOM playerLockHolder.startAnimation(fadeAnimation) // player_go_back_holder?.startAnimation(fadeAnimation) - shadowOverlay.isVisible = true shadowOverlay.startAnimation(fadeAnimation) } @@ -1084,6 +1137,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { override fun playerStatusChanged() { super.playerStatusChanged() + scheduleMetadataVisibility() delayHide() } @@ -2177,6 +2231,12 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } protected fun uiReset() { + metadataVisibilityToken++ + playerBinding?.playerMetadataScrim?.let { + it.animate().cancel() + it.alpha = 0f + it.isVisible = false + } isShowing = false toggleEpisodesOverlay(false) // if nothing has loaded these buttons should not be visible 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 a2cef1122..de1b32467 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 @@ -87,6 +87,7 @@ import com.lagradost.cloudstream3.ui.result.EpisodeAdapter import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.result.ResultFragment +import com.lagradost.cloudstream3.ui.result.ResultFragment.bindLogo import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.SyncViewModel import com.lagradost.cloudstream3.ui.result.setLinearListLayout @@ -98,6 +99,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageTagIETF +import com.lagradost.cloudstream3.utils.AppContextUtils.getShortSeasonText import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -1546,6 +1548,54 @@ class GeneratorPlayer : FullScreenPlayer() { return } loadLink(links.first(), false) + showPlayerMetadata() + } + + private fun showPlayerMetadata() { + val overlay = playerBinding?.playerMetadataScrim ?: return + + val titleView = overlay.findViewById(R.id.player_movie_title) + val logoView = overlay.findViewById(R.id.player_movie_logo) + val metaView = overlay.findViewById(R.id.player_movie_meta) + val descView = overlay.findViewById(R.id.player_movie_overview) + + val load = viewModel.getLoadResponse() ?: return + val episode = currentMeta as? ResultEpisode + titleView.text = load.name + + bindLogo( + url = load.logoUrl, + headers = load.posterHeaders, + titleView = titleView, + logoView = logoView + ) + + val meta = arrayOf( + load.tags?.takeIf { it.isNotEmpty() }?.joinToString(", "), + load.year?.toString(), + if (!load.type.isMovieType()) + context?.getShortSeasonText( + episode = episode?.episode, + season = episode?.season + ) + else null, + load.score?.let { "⭐ $it" } + ).filterNotNull() + .joinToString(" • ") + + metaView.text = meta + metaView.isVisible = meta.isNotBlank() + + + val description = load.plot + + if (!description.isNullOrBlank()) { + descView.isVisible = true + descView.text = description + } else { + descView.isVisible = false + + } } override fun nextEpisode() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index 6a9ab28d8..1377ccd08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -449,6 +449,14 @@ object AppContextUtils { return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true) } + fun Context.shouldShowPlayerMetadata(): Boolean { + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + return prefs.getBoolean( + getString(R.string.show_player_metadata_key), + true + ) + } + fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List { // We are getting the weirdest crash ever done: // java.lang.ClassCastException: com.lagradost.cloudstream3.TvType cannot be cast to com.lagradost.cloudstream3.TvType diff --git a/app/src/main/res/drawable/bg_player_metadata_scrim_netflix.xml b/app/src/main/res/drawable/bg_player_metadata_scrim_netflix.xml new file mode 100644 index 000000000..b4701e42a --- /dev/null +++ b/app/src/main/res/drawable/bg_player_metadata_scrim_netflix.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/metadata_overlay_icon.xml b/app/src/main/res/drawable/metadata_overlay_icon.xml new file mode 100644 index 000000000..6d1b6510a --- /dev/null +++ b/app/src/main/res/drawable/metadata_overlay_icon.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 7974159c4..72024a918 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -7,6 +7,78 @@ android:orientation="vertical" tools:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - autoplay_next_key display_sub_key show_fillers_key + show_player_metadata_key show_trailers_key show_kitsu_posters_key show_cast_in_details_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a1a4fdc3f..e9dd2748f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -190,6 +190,7 @@ Show search suggestions while typing Clear Suggestions Show filler episode for anime + Show Player Metadata Overlay Show trailers Show posters from Kitsu Show cast panel diff --git a/app/src/main/res/xml/settings_ui.xml b/app/src/main/res/xml/settings_ui.xml index 83d0e83c0..1b516ffa3 100644 --- a/app/src/main/res/xml/settings_ui.xml +++ b/app/src/main/res/xml/settings_ui.xml @@ -84,6 +84,11 @@ android:icon="@drawable/ic_baseline_skip_next_24" android:key="@string/show_fillers_key" android:title="@string/show_fillers_settings" /> + Date: Sun, 29 Mar 2026 22:12:16 +0000 Subject: [PATCH 024/177] chore(locales): fix locale issues --- app/src/main/res/values-b+eo/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-b+eo/strings.xml b/app/src/main/res/values-b+eo/strings.xml index 6809ceb7d..ccd18eae3 100644 --- a/app/src/main/res/values-b+eo/strings.xml +++ b/app/src/main/res/values-b+eo/strings.xml @@ -159,7 +159,7 @@ Serĉi uzante tipojn Subteksta Lingvo Pli da informoj - \@string/home_play + @string/home_play Priskribo Neniu Priskribo Trovita Forigi nigrajn borderaĵojn From c26f2362027197f36e5591653ccb1ebb66ed20b7 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:29:27 +0200 Subject: [PATCH 025/177] Fix: Minor UX bugs with #2461 --- .../ui/player/FullScreenPlayer.kt | 64 +++++++++++++++++++ .../cloudstream3/ui/player/GeneratorPlayer.kt | 13 ++-- 2 files changed, 73 insertions(+), 4 deletions(-) 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 90274c938..fad4a53e1 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 @@ -241,6 +241,53 @@ open class FullScreenPlayer : AbstractPlayerFragment() { return root } + /** + * Wet code but this can not be made into a function as it is a setter. + * + * The reason for this setter is to fix a bug with the titlecard popup, as we want it to autohide + * when pressing back. + * + * Note that we move the call to autoHide after field assignment with prevField to avoid inf recursion. */ + protected var selectSourceDialog: Dialog? = null + set(value) { + val prevField = field + field = value + if (value == null && prevField != null) { + autoHide() + } + } + protected var selectTrackDialog: Dialog? = null + set(value) { + val prevField = field + field = value + if (value == null && prevField != null) { + autoHide() + } + } + protected var selectSpeedDialog: Dialog? = null + set(value) { + val prevField = field + field = value + if (value == null && prevField != null) { + autoHide() + } + } + protected var selectSubtitlesDialog: Dialog? = null + set(value) { + val prevField = field + field = value + if (value == null && prevField != null) { + autoHide() + } + } + + /** Checks if any top level dialog is open and showing */ + fun isDialogOpen() = + selectSourceDialog?.isShowing == true + || selectTrackDialog?.isShowing == true + || selectSpeedDialog?.isShowing == true + || selectSubtitlesDialog?.isShowing == true + private fun scheduleMetadataVisibility() { val metadataScrim = playerBinding?.playerMetadataScrim ?: return val ctx = metadataScrim.context ?: return @@ -262,7 +309,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() { if (isPaused) { metadataScrim.postDelayed({ + /** Make sure the user has not interacted with anything */ if (token != metadataVisibilityToken) return@postDelayed + /** If already visible, then do not rerun the animation */ + if (metadataScrim.isVisible) return@postDelayed + /** Failsafe, as this should only be shown when paused */ + if (currentPlayerStatus != CSPlayerLoading.IsPaused) return@postDelayed + /** We do not want to show the logo in the background when the user is within another screen */ + if (isDialogOpen()) return@postDelayed + metadataScrim.alpha = 0f metadataScrim.isVisible = true metadataScrim.animate() @@ -751,6 +806,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { val dialog = Dialog(ctx, R.style.DialogFullscreenPlayer).apply { setContentView(binding.root) } + this.selectSubtitlesDialog = dialog dialog.show() val isPortrait = @@ -840,20 +896,24 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } dialog.setOnDismissListener { + selectSubtitlesDialog = null if (isFullScreenPlayer) activity?.hideSystemUI() } applyBtt.setOnClickListener { + selectSubtitlesDialog = null subtitleDelay = currentOffset dialog.dismissSafe(activity) player.seekTime(1L) } resetBtt.setOnClickListener { + selectSubtitlesDialog = null subtitleDelay = 0 dialog.dismissSafe(activity) player.seekTime(1L) } cancelBtt.setOnClickListener { + selectSubtitlesDialog = null dialog.dismissSafe(activity) } } @@ -916,6 +976,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { if (isPlaying) { player.handleEvent(CSPlayerEvent.Play, PlayerEventSource.UI) } + selectSpeedDialog = null } // if (isLayout(PHONE)) { @@ -930,6 +991,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { .setView(binding.root) builder.setOnDismissListener(dismiss) val dialog = builder.create() + this.selectSpeedDialog = dialog dialog.show() //} } @@ -1124,8 +1186,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private var currentTapIndex = 0 protected fun autoHide() { + metadataVisibilityToken++ currentTapIndex++ delayHide() + scheduleMetadataVisibility() } protected fun hidePlayerUI() { 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 de1b32467..ad7c8915f 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 @@ -896,6 +896,7 @@ class GeneratorPlayer : FullScreenPlayer() { viewModel.addSubtitles(subtitleData.toSet()) selectSourceDialog?.dismissSafe() + selectSourceDialog = null showToast( String.format(ctx.getString(R.string.player_loaded_subtitles), selectedSubtitle.name), @@ -936,10 +937,6 @@ class GeneratorPlayer : FullScreenPlayer() { } } - private var selectSourceDialog: Dialog? = null - // var selectTracksDialog: AlertDialog? = null - - /** Will toast both when an error is found and when a subtitle is selected, * so only use from a user click and not a background process */ private fun addFirstSub(query: SubtitleSearch) = @@ -1072,6 +1069,7 @@ class GeneratorPlayer : FullScreenPlayer() { loadFromOpenSubsFooter.setOnClickListener { shouldDismiss = false sourceDialog.dismissSafe(activity) + selectSourceDialog = null openOnlineSubPicker(it.context, currentLoadResponse) { dismiss() } @@ -1092,6 +1090,7 @@ class GeneratorPlayer : FullScreenPlayer() { loadFromFirstSubsFooter.setOnClickListener { sourceDialog.dismissSafe(activity) + selectSourceDialog = null showToast(R.string.loading) addFirstSub( SubtitleSearch( @@ -1267,6 +1266,7 @@ class GeneratorPlayer : FullScreenPlayer() { binding.cancelBtt.setOnClickListener { sourceDialog.dismissSafe(activity) + this.selectSourceDialog = null } fun setProfileName(profile: Int) { @@ -1317,6 +1317,7 @@ class GeneratorPlayer : FullScreenPlayer() { shouldDismiss = false sourceDialog.dismissSafe(activity) + selectSourceDialog = null val index = prefValues.indexOf(currentPrefMedia) activity?.showDialog( @@ -1355,6 +1356,7 @@ class GeneratorPlayer : FullScreenPlayer() { } } sourceDialog.dismissSafe(activity) + selectSourceDialog = null } } } catch (e: Exception) { @@ -1378,6 +1380,7 @@ class GeneratorPlayer : FullScreenPlayer() { val binding: PlayerSelectTracksBinding = PlayerSelectTracksBinding.inflate(LayoutInflater.from(ctx), null, false) val trackDialog = Dialog(ctx, R.style.DialogFullscreenPlayer) + this.selectTrackDialog = trackDialog trackDialog.setContentView(binding.root) trackDialog.show() @@ -1486,6 +1489,7 @@ class GeneratorPlayer : FullScreenPlayer() { binding.cancelBtt.setOnClickListener { trackDialog.dismissSafe(activity) + this.selectTrackDialog = null } binding.applyBtt.setOnClickListener { @@ -1503,6 +1507,7 @@ class GeneratorPlayer : FullScreenPlayer() { player.setMaxVideoSize(width, height, currentVideo?.id) } trackDialog.dismissSafe(activity) + this.selectTrackDialog = null } } } catch (e: Exception) { From d23fb0ac4ca0f81d6d0de16322b3a63dec9e691e Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 30 Mar 2026 17:10:00 +0200 Subject: [PATCH 026/177] Translated using Weblate (Swedish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Italian) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Czech) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Ukrainian) Currently translated at 99.8% (725 of 726 strings) Translated using Weblate (Polish) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Korean) Currently translated at 100.0% (725 of 725 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Translated using Weblate (Korean) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Korean) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Slovak) Currently translated at 62.6% (454 of 725 strings) Translated using Weblate (Esperanto) Currently translated at 23.7% (172 of 725 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Bulgarian) Currently translated at 99.1% (719 of 725 strings) Translated using Weblate (Latvian) Currently translated at 81.2% (589 of 725 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Translated using Weblate (Esperanto) Currently translated at 17.5% (127 of 725 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Vietnamese) Currently translated at 99.8% (724 of 725 strings) Translated using Weblate (Hungarian) Currently translated at 82.0% (595 of 725 strings) Translated using Weblate (Korean) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Belarusian) Currently translated at 99.5% (722 of 725 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Translated using Weblate (Belarusian) Currently translated at 99.5% (722 of 725 strings) Translated using Weblate (Filipino) Currently translated at 21.2% (154 of 725 strings) Translated using Weblate (Hungarian) Currently translated at 77.3% (561 of 725 strings) Translated using Weblate (Dutch) Currently translated at 89.1% (646 of 725 strings) Translated using Weblate (Tamil) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (German) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Tamil) Currently translated at 100.0% (725 of 725 strings) Translated using Weblate (Hungarian) Currently translated at 75.4% (547 of 725 strings) Co-authored-by: Ardev Prisec Co-authored-by: Aron Folkerts Co-authored-by: Daniel Konstantinov Co-authored-by: David Hermann Co-authored-by: Fjuro Co-authored-by: Hosted Weblate Co-authored-by: Jen Xie Co-authored-by: Massimo Pissarello Co-authored-by: Matthaiks Co-authored-by: Nguyễn Tiến Đạt Co-authored-by: Romhányi-Kakucska Viktor Co-authored-by: Sasha Glazko Co-authored-by: Wacky Wars Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: clearstripe Co-authored-by: hollow Co-authored-by: hou1234 Co-authored-by: jpkaster 77 Co-authored-by: programutox Co-authored-by: tomas293 Co-authored-by: தமிழ்நேரம் Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) Co-authored-by: 大王叫我来巡山 Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bg/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/eo/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fil/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sv/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ar/ Translation: Cloudstream/App Translation: Cloudstream/Fastlane --- app/src/main/res/values-b+ar/strings.xml | 24 ++++ app/src/main/res/values-b+cs/strings.xml | 1 + app/src/main/res/values-b+it/strings.xml | 1 + app/src/main/res/values-b+ko/strings.xml | 126 +++++++++--------- app/src/main/res/values-b+pl/strings.xml | 1 + app/src/main/res/values-b+sv/strings.xml | 1 + app/src/main/res/values-b+uk/strings.xml | 58 ++++---- app/src/main/res/values-b+zh/strings.xml | 1 + .../metadata/android/ar/full_description.txt | 8 +- 9 files changed, 124 insertions(+), 97 deletions(-) diff --git a/app/src/main/res/values-b+ar/strings.xml b/app/src/main/res/values-b+ar/strings.xml index c68a5a649..91f8f0e64 100644 --- a/app/src/main/res/values-b+ar/strings.xml +++ b/app/src/main/res/values-b+ar/strings.xml @@ -756,4 +756,28 @@ نص الحلقة معلومات الوسائط اسم المصدر + شريط التنزيلات + لا يوجد تنزيلات قيد الانتظار حاليا. + أولوية المصدر + حَدِّد كيف يجب ترتيب مصادر الفيديو في المُشَّغِل + تنزيل الكل + إلغاء الكل + هل ترغب في تنزيل الحلقة %s ؟ + هل ترغب في إلغاء جميع التنزيلات قيد الانتظار؟ + + %d لا يوجد تنزيل نشط + %d تنزيل واحد نشط + %d تنزيلان نشطان + %d تنزيلات نشطة + %d تنزيل نشط + %d تنزيل نشط + + + %d لا يوجد تنزيلات في قائمة الانتظار + %d تنزيل واحد قيد الانتظار + %d تنزيلان قيد الانتظار + %d تنزيلات قيد الانتظار + %d تنزيل قيد الانتظار + %d تنزيل قيد الانتظار + diff --git a/app/src/main/res/values-b+cs/strings.xml b/app/src/main/res/values-b+cs/strings.xml index 3f7675534..96110d9c1 100644 --- a/app/src/main/res/values-b+cs/strings.xml +++ b/app/src/main/res/values-b+cs/strings.xml @@ -778,4 +778,5 @@ Priorita zdrojů Rozhodněte, jak mají být řazeny zdroje videí v přehrávači + Zobrazit překrytí metadat v přehrávači diff --git a/app/src/main/res/values-b+it/strings.xml b/app/src/main/res/values-b+it/strings.xml index e75b4eb8c..08a1572d6 100644 --- a/app/src/main/res/values-b+it/strings.xml +++ b/app/src/main/res/values-b+it/strings.xml @@ -783,4 +783,5 @@ Priorità sorgente Decidi come le sorgenti video devono essere ordinate nel lettore + Mostra sovrapposizione metadati lettore diff --git a/app/src/main/res/values-b+ko/strings.xml b/app/src/main/res/values-b+ko/strings.xml index 04c113b5b..868e2736e 100644 --- a/app/src/main/res/values-b+ko/strings.xml +++ b/app/src/main/res/values-b+ko/strings.xml @@ -1,11 +1,11 @@ 출연: %s - 에피소드 %d이(가) 공개됩니다 + 에피소드 %d이(가) 공개 예정 포스터 에피소드 포스터 메인 포스터 - 다음 랜덤 + 다음 추천 뒤로가기 소스 변경 미리보기 배경 @@ -33,7 +33,7 @@ 시청 보류 시청 완료 - 포기 + 시청 포기 시청 예정 다시보기 영화 재생 @@ -46,10 +46,10 @@ 에피소드 재생 다운로드 파일 재생 - 계속 다운로드 + 다운로드 재개 다운로드 일시정지 상세 정보 - 닫기 + 숨기기 재생 정보 시청 상태 설정 @@ -109,7 +109,7 @@ 플레이어 자막 설정 Chromecast 자막 Chromecast 자막 설정 - Playback 속도 + 재생 속도 스와이프하여 탐색 좌우로 스와이프하여 동영상 위치 제어하기 스와이프하여 설정 변경 @@ -150,13 +150,13 @@ 바나나 줌 앱 언어 링크를 찾을 수 없음 - 클립보드에 링크 복사됨 + 클립보드에 링크 복사함 에피소드 재생 기본값으로 재설정 에피소드 %1$d-%2$d - 진행중 - 시청 완료 + 방영 중 + 완결 상태 평점 @@ -183,7 +183,7 @@ 앱에서 재생 %s에서 재생 자동 다운로드 - 다운로드 미러 + 다운로드 가능 목록 보기 링크 새로고침 자막 다운로드 화질 탭 @@ -198,7 +198,7 @@ 이 업데이트 건너뛰기 선호하는 화질 (WiFi) 선호하는 화질 (모바일 데이터) - 본문 바로가기 + 플레이어 내 표시 정보 동영상 버퍼 크기 동영상 및 이미지 캐시 지우기 DNS over HTTPS @@ -217,10 +217,10 @@ 일반 플레이어 기능 기능 - 확장 언어 + 확장프로그램 언어 앱 레이아웃 선호하는 미디어 - 지원된 연장에 NSFW 활성화 + 확장프로그램에서 NSFW 활성화 자막 인코딩 소스 소스 테스트 @@ -277,7 +277,7 @@ 잘못된 데이터 잘못된 URL 오류 - 자막에서 선택 캡션 제거 + 자막에서 청각 장애인용 자막 요소 제거 선호하는 미디어 언어로 필터링 예고편 다음 @@ -324,7 +324,7 @@ 에피소드 %d 공개! Picture-in-picture 플레이어 크기 조정 버튼 - 다른 앱 위에 있는 미니어처 플레이어에서 재생을 계속합니다 + 미니플레이어를 통해 다른 앱 상단에서 계속 재생됩니다 검은색 테두리 제거 오른쪽 또는 왼쪽을 두 번 탭하여 앞뒤로 탐색하기 자막 @@ -353,8 +353,8 @@ 실패 평점 평점: %s - 평점 (높음에서 낮음으로) - 평점 (낮음에서 높음으로) + 평점 (높은순) + 평점 (낮은순) 19금 다큐멘터리 라이브 방송 @@ -461,8 +461,8 @@ 앱 종료시 업데이트됩니다 정렬 기준 정렬 - 업데이트됨 (새로움에서 오래된 순) - 업데이트 (오래됨에서 새로운 순) + 업데이트 (최신순) + 업데이트 (오래된순) 알파벳순 (A에서 Z) 알파벳순 (Z에서 A) 다음으로 열기 @@ -497,12 +497,12 @@ 동작 외형 랜덤 버튼 - 홈페이지 및 도서관에서 임의 버튼 표시 + 홈페이지 및 라이브러리에서 랜덤 버튼 표시 포스터 아래에 제목을 이동 내려감 올라감 다람쥐 헌 쳇바퀴에 타고파 - 자막에서 부풀림 제거 + 자막에서 불필요한 요소/코드 제거 엑스트라 https://example.com/example.mp4 트랙 @@ -515,13 +515,13 @@ 구독 %s 구독 취소 %s 보안 - 장부 + 계정 리포지토리에서 플러그인을 찾을 수 없습니다 복사됨! 레포지토리 이름 및 URL 본 테스트는 개발자만을 대상으로 하며, 확장자의 작업을 확인하거나 거부하지 않습니다. 클라우스스트림 위키 - 다시 기록된 링크 + 링크 새로고침 완료 백업 빈도 즐겨찾기 QR 이미지 @@ -608,12 +608,12 @@ \n총 비디오 우선 순위는 10입니다. \n \n참고: 합이 10 이상이면 해당 링크가 로드되면 플레이어는 자동으로 로드를 건너뜁니다! - 시즌 %1$d 에피소드 %2$d이(가) 출시됩니다 + 시즌 %1$d 에피소드 %2$d이(가) 공개 예정 다른 확장자에서 검색 새로운 에피소드 알림 - 권장 사항 표시 + 추천목록 보기 플레이어에 속도 옵션을 추가합니다 - %s로 출시 예정 + %s 후 공개 예정 %s \n남음 잠재적 중복 발견 @@ -623,8 +623,8 @@ 플러그인 삭제 경고 탐색바 미리보기 - 탐색바에서 미리보기 화면 활성화 - 처음부터 시작 + 탐색바에서 화면 미리보기 활성화 + 처음부터 재생 현재 다운로드가 없습니다. 삭제할 항목을 선택하십시오 오프라인 시청가능 @@ -645,10 +645,10 @@ 다음 시리즈의 모든 에피소드를 영구적으로 삭제 하시겠습니까?? \n \n%s - 출시일 (새로운 것부터 오래된 것) - 출시일 (오래된것부터 새로운것) - 플레이어 컨트롤 이름 숨기기 - 이 동영상은 토렌트이므로 동영상 활동이 추적될 수 있습니다.\n계속하기 전에 토렌트에 대해 충분히 이해했는지 확인하세요. + 공개일 (최신순) + 공개일 (오래된순) + 플레이어 내 버튼명 숨기기 + 이 동영상은 토렌트이므로 시청 활동이 추적될 수 있습니다.\n계속하기 전에 토렌트에 대해 충분히 이해했는지 확인하세요. 오디오 팟캐스트 시작하기 … @@ -662,7 +662,7 @@ 출시 전 버전 설치 출시 전 버전이 이미 설치되어 있습니다. 출시 전 버전 설치 실패. - 미러 재생" + 재생 가능 목록 보기" 평가 라벨 에피소드 본문 사용 가능한 자막 불러오기 @@ -671,24 +671,24 @@ 에피소드 (내림차순) 평가 (높은순) 평가 (낮은순) - 방영 날짜 (최신순) - 방영 날짜 (오래된순) + 공개일 (최신순) + 공개일 (오래된순) 에피소드 %s 평가 %s 날짜 %s 계정 없음 - 자막을 아직 불러오지 않았습니다 + 자막이 아직 로드되지 않음 백업 폴더 위치 커스텀 나가기 전에 확인 - 이 문항을 앱에서 나가기 전에 보이기 + 이 알림을 앱 종료 전에 표시 보이기 보이지 않기 테두리 크기 Settings/Providers/Preferred media 에서 토렌트 활성화 진행하려면 앱 재시작 후 토렌트 스트리밍 팝업란의 수락이 필요합니다. 소프트웨어 디코딩 - 소프트웨어 디코딩은 당신의 기기에서 지원되지 않는 영상 파일들을 재생할 수 있지만, 높은 화질에서 렉 또는 불안정한 재생을 유발할 수 있습니다. + 소프트웨어 디코딩은 해당 기기에서 지원되지 않는 영상 파일들을 재생할 수 있지만, 높은 화질에서 렉 또는 불안정한 재생을 유발할 수 있습니다. 볼륨이 100%를 초과하였습니다 100% 너머로 높이려면 한번 더 슬라이드 하십시오 플러그인 업데이트하기 @@ -697,18 +697,18 @@ 성공적으로 %d 플러그인을 업데이트 하였습니다! 업데이트 된 플러그인이 없습니다. 플레이어 알림 - 백그라운드에서 재생을 조종할 수 있는 플레이어 알림 - 내장된 + 백그라운드에서 재생을 제어하기 위한 플레이어 알림 + 내장 자막 온라인 - 모든 자막 굵게 - 모든 자막 기울기 + 자막 글꼴 굵게 표시 + 자막 글꼴 기울게 표시 병렬로 다운로드 할 수 있는 아이템의 수 병렬 다운로드 동시 연결수 다운로드 시 각 항목마다 사용할 수 있는 동시 연결의 수 다운로드로 가기 인터넷 연결 없음.\n\n인터넷에 연결 한 후 재시도 하거나, 혹은 이미 다운로드 된 항목을 재생하십시오. - 화면 경계 조정 + 화면의 잘림 현상을 방지하기 위해 경계를 조정합니다 포스터 크기 변경 포스터 크기 길게 눌러 배속 활성화 @@ -718,8 +718,8 @@ URL을 찾을 수 없습니다 잘못된 URL 혹은 이미지 입니다 이미지 업데이트 성공 - 이 에피소드 까지 봤음 표시 - 이 에피소드 까지 봤음 표시 제거 + 이 에피소드까지 시청함으로 표시 + 이 에피소드까지 시청함 표시 제거 이름 해상도 및 이름 자막 정렬 @@ -732,9 +732,9 @@ 왼쪽 위 중앙 위 오른쪽 위 - 비디오 소스가 플레이어에서 정렬되어야하는 방법 결정 - 100% 표시 광도가 초과될 때 Enable 광도 여과기 - 모든 퀴즈 다운로드를 취소하시겠습니까? + 비디오 소스가 플레이어에서 정렬되는 순서 설정 + 디스플레이 밝기가 100%를 초과하면 밝기 필터를 활성화합니다 + 모든 다운로드 큐를 취소하시겠습니까? 에피소드를 다운로드 하시겠습니까 %s? 현재 누락된 다운로드가 없습니다. @@ -743,20 +743,20 @@ %d 다운로드 - 검색 결과 표시 - 회사 소개 - 배경 반경 - Reload 공급자 - 검색 제안 - 자주 묻는 질문 + 입력하는 동안 검색어 제안 표시 + 출연진 정보 표시 + 배경 모서리 곡률 + 공급자 새로고침 + 검색어 제안 + 제안 삭제 미디어 정보 - 근원 이름 - 추가 밝기 - 다운로드 queue - 다운로드 - 모든 것 - 근원 우선권 - 구름 많음 - 관련 상품 - 추가_brightness_enabled + 소스 이름 + 최대 밝기 확장 + 다운로드 큐 + 모두 다운로드 + 모두 취소 + 소스 우선순위 + 오버스캔(화면 경계) 설정 + 새로고침 + 최대 밝기 확장 활성화 diff --git a/app/src/main/res/values-b+pl/strings.xml b/app/src/main/res/values-b+pl/strings.xml index fc167da5a..c8126f2fe 100644 --- a/app/src/main/res/values-b+pl/strings.xml +++ b/app/src/main/res/values-b+pl/strings.xml @@ -766,4 +766,5 @@ Priorytet źródła Zdecyduj, jak mają być sortowane źródła wideo w odtwarzaczu + Pokaż nakładkę metadanych odtwarzacza diff --git a/app/src/main/res/values-b+sv/strings.xml b/app/src/main/res/values-b+sv/strings.xml index ddc7636b1..e388b67e1 100644 --- a/app/src/main/res/values-b+sv/strings.xml +++ b/app/src/main/res/values-b+sv/strings.xml @@ -759,4 +759,5 @@ Gör alla undertexter fetstilta Gör alla undertexter kursivstila Bakgrundsradie + Visa spelarens metadata överlägg diff --git a/app/src/main/res/values-b+uk/strings.xml b/app/src/main/res/values-b+uk/strings.xml index b97c16a7e..cec3b6738 100644 --- a/app/src/main/res/values-b+uk/strings.xml +++ b/app/src/main/res/values-b+uk/strings.xml @@ -45,8 +45,8 @@ Завантаження Завершено Дуб. Суб. - Видалити файл - Відновити завантаження + Видалити Файл + Відновити Завантаження Приховати Переглянути Подробиці @@ -57,28 +57,28 @@ Скопіювати Закрити Зберегти - Швидкість плеєра - Колір вікна - Тип обведення + Швидкість Плеєра + Колір Вікна + Тип Межі Шрифт - Розмір шрифту + Розмір Шрифту Пошук за постачальниками Пошук за типами - Жодного банана не надано - Автовибір мови - Завантажити мови - Мова субтитрів + Жодного Benenes не надано + Авто-Вибір Мови + Завантажити Мови + Мова Субтитрів Утримуйте, щоби скинути до типових налаштувань Імпортуйте шрифти, помістивши їх до %s - Продовжити перегляд + Продовжити Перегляд Вилучити Докладніше Цей постачальник є торентом, рекомендується використовувати VPN Опис - Сюжет не знайдено - Опис не знайдено + Сюжет Не Знайдено + Опис Не Знайдено Показати Logcat 🐈 - Продовжувати відтворення в малому програвачі поверх інших застосунків + Продовжувати відтворення в мініатюрному програвачі поверх інших застосунків Прибрати чорні смуги Субтитри Субтитри Chromecast @@ -106,19 +106,19 @@ Завантаження не Вдалося Оновлення Розпочато Помилка Завантаження Посилань - Призупинити завантаження - Переглянути файл + Призупинити Завантаження + Переглянути Файл Докладніше - Фільтр закладок + Фільтрувати Закладки Очистити - Налаштування субтитрів - Колір тла - Висота субтитрів - Колір тексту - Колір обведення + Налаштування Субтитрів + Колір Тла + Висота Субтитрів + Колір Тексту + Колір Обведення Автовідтворення наступного епізоду Проведіть збоку в бік, щоби керувати часом відтворення у відео - %d бананів надано розробникам + %d Benenes надано розробникам Кнопка зміни розміру програвача @string/home_play Для коректної роботи цього постачальника може знадобитися VPN @@ -128,7 +128,7 @@ Провести, щоби перемотати Натиснути двічі, щоби перемотати Натиснути двічі, щоби призупинити - Крок перемотування (у секундах) + Крок перемотування (Секунди) Натисніть двічі посередині, щоби призупинити відтворення Використовувати системну яскравість Оновлювати прогрес перегляду @@ -138,10 +138,10 @@ Дані збережено Помилка резервного копіювання %s Пошук - Облікові записи та безпека - Оновлення та резервне копіювання + Облікові Записи та Безпека + Оновлення та Резервне Копіювання Подробиці - Розширений пошук + Розширений Пошук Показувати результати пошуку, розділені за постачальниками Показувати наповнювачі для аніме Показувати трейлери @@ -490,7 +490,7 @@ Провалено Пройдено Перезапустити - Журнал + Лог Відновити Зупинити Перевірка постачальників @@ -706,7 +706,7 @@ Попередня версія вже встановлена. Не вдалося встановити попередню версію. Текст епізоду - Пропозиції пошуку + Пропозиції Пошуку Показувати підказки пошуку під час введення тексту Очистити пропозиції Додаткова яскравість diff --git a/app/src/main/res/values-b+zh/strings.xml b/app/src/main/res/values-b+zh/strings.xml index e8e02d51f..bc7c2ca0e 100644 --- a/app/src/main/res/values-b+zh/strings.xml +++ b/app/src/main/res/values-b+zh/strings.xml @@ -771,4 +771,5 @@ 源优先级 确定在播放器中如何排列视频源的顺序 已启用额外亮度 + 显示播放器元数据遮罩层 diff --git a/fastlane/metadata/android/ar/full_description.txt b/fastlane/metadata/android/ar/full_description.txt index 9bbe01ef2..81859f665 100644 --- a/fastlane/metadata/android/ar/full_description.txt +++ b/fastlane/metadata/android/ar/full_description.txt @@ -3,10 +3,8 @@ يأتي التطبيق بدون أي إعلانات وتحليلات. و يدعم العديد من مواقع البث الاولي(التريلر) والأفلام والمزيد. وتشمل الميزات: -إشارات مرجعية +الإشارات المرجعية -قم بتنزيل ودفق الأفلام والبرامج التلفزيونية والأنيمي +تنزيل الترجمات -تنزيلات الترجمة - -دعم كروم كاست +دعم الكروم كاست (Chromecast) From 736c6374a6502f057a08866a75f189759e755457 Mon Sep 17 00:00:00 2001 From: Nguyen Van Nam Date: Tue, 31 Mar 2026 04:23:02 +0700 Subject: [PATCH 027/177] Fix: thread-safe HashMap for image bitmap cache --- .../cloudstream3/utils/downloader/DownloadUtils.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadUtils.kt index b436bb49c..9f2c31d9a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadUtils.kt @@ -20,16 +20,17 @@ import com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.getFol import com.lagradost.cloudstream3.utils.txt import kotlinx.coroutines.Job import kotlinx.coroutines.runBlocking +import java.util.concurrent.ConcurrentHashMap /** Separate object with helper functions for the downloader */ object DownloadUtils { - private val cachedBitmaps = hashMapOf() + private val cachedBitmaps = ConcurrentHashMap() internal fun Context.getImageBitmapFromUrl( url: String, headers: Map? = null ): Bitmap? = safe { - if (cachedBitmaps.containsKey(url)) { - return@safe cachedBitmaps[url] + cachedBitmaps[url]?.let { + return@safe it } val imageLoader = SingletonImageLoader.get(this) @@ -50,7 +51,7 @@ object DownloadUtils { } bitmap?.let { - cachedBitmaps[url] = it + cachedBitmaps.putIfAbsent(url, it) } return@safe bitmap From db154a8cd25aaad2f942931f33714ca2423f395c Mon Sep 17 00:00:00 2001 From: Phisher98 <153359846+phisher98@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:06:42 +0530 Subject: [PATCH 028/177] Adding IntroDB (#2599) --- .../lagradost/cloudstream3/utils/AniSkip.kt | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt index 820a01f9f..bbdadbf3f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt @@ -2,8 +2,10 @@ package com.lagradost.cloudstream3.utils import android.util.Log import androidx.annotation.StringRes +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.result.ResultEpisode @@ -86,7 +88,57 @@ object EpisodeSkip { out.addAll(list) } } + } else if (data.type == TvType.TvSeries || data.type == TvType.AsianDrama) { + val season = episode.season + val imdbId = data.getImdbId() + + if (season != null && imdbId != null) { + val result = IntroDbSkip.getResult( + imdbId, + season, + episode.episode + ) + + result?.let { res -> + listOfNotNull( + res.intro?.let { + val start = it.startMs ?: return@let null + val end = it.endMs ?: return@let null + SkipStamp( + type = SkipType.Opening, + skipToNextEpisode = hasNextEpisode && + shouldSkipToNextEpisode(end, episodeDurationMs), + startMs = start, + endMs = end + ) + }, + res.recap?.let { + val start = it.startMs ?: return@let null + val end = it.endMs ?: return@let null + SkipStamp( + type = SkipType.Recap, + skipToNextEpisode = hasNextEpisode && + shouldSkipToNextEpisode(end, episodeDurationMs), + startMs = start, + endMs = end + ) + }, + res.outro?.let { + val start = it.startMs ?: return@let null + val end = it.endMs ?: return@let null + SkipStamp( + type = SkipType.Credits, + skipToNextEpisode = hasNextEpisode && + shouldSkipToNextEpisode(end, episodeDurationMs), + startMs = start, + endMs = end + ) + } + ).let { out.addAll(it) } + } + } } + if (out.isNotEmpty()) cachedStamps[episode.id] = out return out @@ -136,4 +188,43 @@ object AniSkip { @JsonSerialize val startTime: Double, @JsonSerialize val endTime: Double ) +} + +object IntroDbSkip { + private const val TAG = "IntroDb" + + suspend fun getResult( + imdbId: String, + season: Int, + episode: Int, + ): IntroDbResponse? { + return try { + val url = + "https://api.introdb.app/segments?imdb_id=$imdbId&season=$season&episode=$episode" + app.get(url).parsed() + } catch (t: Throwable) { + Log.i(TAG, "error = ${t.message}") + logError(t) + null + } + } + + data class IntroDbResponse( + @JsonProperty("imdb_id") val imdbId: String?, + val season: Int?, + val episode: Int?, + val intro: Segment?, + val recap: Segment?, + val outro: Segment?, + ) + + data class Segment( + @JsonProperty("start_sec") val startSec: Double?, + @JsonProperty("end_sec") val endSec: Double?, + @JsonProperty("start_ms") val startMs: Long?, + @JsonProperty("end_ms") val endMs: Long?, + val confidence: Double?, + @JsonProperty("submission_count") val submissionCount: Int?, + @JsonProperty("updated_at") val updatedAt: String?, + ) } \ No newline at end of file From ba9413e972e5586bceecc91bfc74b9de2a46fec5 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sat, 4 Apr 2026 12:26:17 -0600 Subject: [PATCH 029/177] Change param name in interface to match everywhere else (#2611) --- .../main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 183f26f73..08b8ee795 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -307,7 +307,7 @@ interface IPlayer { fun setMaxVideoSize(width: Int = Int.MAX_VALUE, height: Int = Int.MAX_VALUE, id: String? = null) /** If no trackLanguage is set it'll default to first track. Specifying the id allows for track overrides as the language can be identical. */ - fun setPreferredAudioTrack(trackLanguage: String?, id: String? = null, trackIndex: Int? = null) + fun setPreferredAudioTrack(trackLanguage: String?, id: String? = null, formatIndex: Int? = null) /** Get the current subtitle cues, for use with syncing */ fun getSubtitleCues(): List From bb295ded09ee4059a1a180b9c319a162239b7ffe Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sat, 4 Apr 2026 20:26:35 +0200 Subject: [PATCH 030/177] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Korean) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Belarusian) Currently translated at 99.5% (723 of 726 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (French) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Korean) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Latvian) Currently translated at 80.8% (587 of 726 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (726 of 726 strings) Co-authored-by: Ardev Prisec Co-authored-by: Dan Co-authored-by: Hosted Weblate Co-authored-by: Man Co-authored-by: Posemartonis Co-authored-by: Sasha Glazko Co-authored-by: blueocean2308 Co-authored-by: hou1234 Co-authored-by: opakholis Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/ Translation: Cloudstream/App --- app/src/main/res/values-b+fr/strings.xml | 1 + app/src/main/res/values-b+in/strings.xml | 1 + app/src/main/res/values-b+ko/strings.xml | 125 ++++++------ app/src/main/res/values-b+lv/strings.xml | 76 ++++---- app/src/main/res/values-b+uk/strings.xml | 235 ++++++++++++----------- app/src/main/res/values-b+vi/strings.xml | 192 +++++++++--------- app/src/main/res/values-be/strings.xml | 5 +- 7 files changed, 318 insertions(+), 317 deletions(-) diff --git a/app/src/main/res/values-b+fr/strings.xml b/app/src/main/res/values-b+fr/strings.xml index f39de53f7..1cbee687f 100644 --- a/app/src/main/res/values-b+fr/strings.xml +++ b/app/src/main/res/values-b+fr/strings.xml @@ -753,4 +753,5 @@ %d téléchargements en attente %d téléchargements en attente + Afficher les métadata de l\'overlay du lecteur vidéo diff --git a/app/src/main/res/values-b+in/strings.xml b/app/src/main/res/values-b+in/strings.xml index 7fa837b23..d5bf2d4b0 100644 --- a/app/src/main/res/values-b+in/strings.xml +++ b/app/src/main/res/values-b+in/strings.xml @@ -764,4 +764,5 @@ Batalkan semua Apakah kamu ingin mengunduh episode %s? Apakah kamu ingin membatalkan semua unduhan dalam antrean? + Tampilkan overlay metadata pemutar diff --git a/app/src/main/res/values-b+ko/strings.xml b/app/src/main/res/values-b+ko/strings.xml index 868e2736e..29a0a703e 100644 --- a/app/src/main/res/values-b+ko/strings.xml +++ b/app/src/main/res/values-b+ko/strings.xml @@ -1,14 +1,14 @@ 출연: %s - 에피소드 %d이(가) 공개 예정 + 에피소드 %d 공개 예정 포스터 에피소드 포스터 메인 포스터 다음 추천 뒤로가기 소스 변경 - 미리보기 배경 + 배경 미리보기 속도 (%.2fx) 평점: %.1f 새로운 업데이트! @@ -55,14 +55,14 @@ 시청 상태 설정 저장 재생 속도 - 글자 색깔 - 외곽선 색깔 - 배경 색깔 - 창 색깔 - 가장자리 타입 + 글자 색상 + 윤곽선 색상 + 배경 색상 + 배경 색상 + 윤곽선 유형 자막 높이 폰트 - 폰트 크기 + 자막 크기 다운로드됨 다운로드중 다운로드 일시정지 @@ -119,7 +119,7 @@ 두 번 탭하여 탐색 두 번 탭하여 일시정지 플레이어 탐색 시간 (초) - 가운데를 두 번 탭하여 일시중지 + 가운데를 두 번 탭하여 일시정지 시스템 밝기 사용 어두운 오버레이 대신 앱 플레이어의 시스템 밝기를 사용합니다 시청 진행 상황 업데이트 @@ -135,14 +135,14 @@ 계정 및 보안 소스별로 구분된 검색 결과를 제공합니다 예고편 보기 - Kitsu에서 포스터 보기 + Kitsu에서 포스터 가져오기 검색 결과에서 선택한 동영상 품질 숨기기 플러그인 자동 다운로드 플러그인 자동 업데이트 추가된 저장소에서 아직 설치되지 않은 모든 플러그인을 자동으로 설치합니다. 앱 업데이트 표시 앱을 시작한 후 새 업데이트를 자동으로 검색합니다. - 일부 장치는 새 패키지 설치 프로그램을 지원하지 않습니다. 업데이트가 설치되지 않으면 레거시 옵션을 사용해보십시오. + 일부 기기에서는 최신 방식의 설치 프로그램이 작동하지 않을 수 있습니다. 업데이트가 안 된다면 \'기본 방식\' 설정을 사용해 보세요. 같은 개발자가 만든 라이트 노벨 앱 같은 개발자가 만든 애니메이션 앱 Discord에 참여하기 @@ -181,29 +181,29 @@ 토렌트 Chromecast 미러링 앱에서 재생 - %s에서 재생 + %s부터 재생 자동 다운로드 - 다운로드 가능 목록 보기 + 다운로드 소스 목록 링크 새로고침 자막 다운로드 - 화질 탭 - 더빙 탭 - 자막 탭 + 화질 라벨 + 더빙 라벨 + 자막 라벨 제목 업데이트 확인 잠금 크기 조정 소스 - 오프닝 건너뛰기 + 오프닝 스킵 이 업데이트 건너뛰기 선호하는 화질 (WiFi) 선호하는 화질 (모바일 데이터) 플레이어 내 표시 정보 동영상 버퍼 크기 - 동영상 및 이미지 캐시 지우기 + 비디오 및 이미지 캐시 삭제 DNS over HTTPS GitHub에 연결할 수 없습니다. jsDelivr 프록시를 켜는 중… - JsDelivr를 사용하여 원시 github URL 차단을 우회하십시오. 몇 일 지연 될 업데이트가 발생할 수 있습니다. + jsDelivr를 사용하여 차단된 GitHub 주소를 우회합니다. 단, 업데이트 반영이 며칠 정도 늦어질 수 있습니다. 복제 사이트 사이트 삭제 다른 URL을 사용하여 기존 사이트의 복제본을 추가합니다 @@ -251,7 +251,7 @@ 전부 최대 최소 - 윤곽선 + 윤곽선 효과 그림자 자막 동기화 1000 ms @@ -277,7 +277,7 @@ 잘못된 데이터 잘못된 URL 오류 - 자막에서 청각 장애인용 자막 요소 제거 + 청각 장애인용 자막 요소 제거 선호하는 미디어 언어로 필터링 예고편 다음 @@ -304,8 +304,8 @@ \nDiscord에 가입하거나 온라인으로 검색해 보세요. 커뮤니티 저장소 보기 공개 목록 - 모든 자막 대문자화 - 경고: CloudStream은 제3자 확장을 이용하여 어떠한 책임도 지지 않습니다! + 자막 대문자화 표시 + 경고: CloudStream은 외부 확장 프로그램 사용에 대해 어떠한 책임도 지지 않으며, 관련 기술 지원을 제공하지 않습니다! %s (사용불가) 저장소 추가 저장소 이름 (선택 사항) @@ -322,13 +322,13 @@ 충돌 정보 보기 언어 에피소드 %d 공개! - Picture-in-picture + PIP 모드 플레이어 크기 조정 버튼 - 미니플레이어를 통해 다른 앱 상단에서 계속 재생됩니다 - 검은색 테두리 제거 + 미니플레이어를 통해 다른 앱 상단에서 계속 재생합니다 + 레터박스 제거 오른쪽 또는 왼쪽을 두 번 탭하여 앞뒤로 탐색하기 자막 - 로드된 백업 파일 + 백업 파일을 성공적으로 로드하였습니다 정보 고급 검색 설정 프로세스 다시 실행 @@ -374,7 +374,7 @@ 애니 OVA 원격 오류 - 다운로드 오류, 저장 권한 확인 + 다운로드 오류, 저장소 권한을 확인하세요 Chromecast 에피소드 예기치 않은 플레이어 오류 다시 표시하지 않음 @@ -382,7 +382,7 @@ 업데이트 GitHub 프록시 동영상 버퍼 길이 - 저장소에 동영상 캐시 + 디스크 비디오 캐시 Android TV와 같이 메모리가 부족한 디바이스에서 너무 높게 설정하면 충돌이 발생할 수 있습니다. 화면 크기에 맞춤 Android TV와 같이 저장 공간이 부족한 기기에서 너무 높게 설정하면 문제가 발생할 수 있습니다. @@ -456,7 +456,7 @@ 앱 업데이트 다운로드 중… 앱 업데이트 설치 중… 새 버전의 앱을 설치할 수 없습니다 - 레거시 + 기본 방식 패키지 인스톨러 앱 종료시 업데이트됩니다 정렬 기준 @@ -489,20 +489,20 @@ 애니메이션용 필러 에피소드 표시 통과 계속 - 동영상 플레이어 제목 최대 글자 수 - 표시된 플레이어 - 빨리 감기 및 되감기 초 - 플레이어가 보일 때 사용되는 탐색량 - 플레이어 숨김 - 빨리 감기 및 되감기 초 - 플레이어가 숨겨져 있을 때 사용되는 탐색량 + 플레이어 표시 제목의 최대 글자 수 + 플레이어 표시 시 탐색 시간 + 플레이어 표시 중 탐색 간격 + 플레이어 미표시 시 탐색 시간 + 플레이어 미표시 시 탐색 간격 동작 외형 랜덤 버튼 홈페이지 및 라이브러리에서 랜덤 버튼 표시 포스터 아래에 제목을 이동 - 내려감 - 올라감 + 음각 + 양각 다람쥐 헌 쳇바퀴에 타고파 - 자막에서 불필요한 요소/코드 제거 + 불필요한 요소/코드 제거 엑스트라 https://example.com/example.mp4 트랙 @@ -531,7 +531,7 @@ 취소 저장소 열기 현재 PIN 입력 - 비디오 방향에 따라 화면 방향을 자동으로 전환합니다 + 비디오 방향에 따라 화면 방향을 자동으로 회전합니다 장치 PIN 코드를 가져올 수 없습니다, 로컬 인증을 시도하세요 PIN 코드가 만료되었습니다! 코드 만료까지 남은 시간: %1$dm %2$ds @@ -546,7 +546,7 @@ 프로필 확인 배터리 최적화 사용 안 함 - 앱 배터리 사용량이 이미 무제한으로 설정되었습니다 + 배터리 사용량이 \'제한 없음\'으로 이미 설정되어 있습니다 CloudStream의 App 정보를 열 수 없습니다. 즐겨찾기에 %s 추가 프로필 %d @@ -567,7 +567,7 @@ 모바일 데이터 사용 불가능 캐스트 장치 선택 - 복사하는 중 오류가 발생했습니다. 로그캣을 복사하고 문의하십시오. + 복사하는 중 오류가 발생했습니다. 로그캣을 복사하고 개발자에게 문의하십시오. 구독 취소 기본값 설정 구독 @@ -586,7 +586,7 @@ 계정 선택 기본 계정 사용 회전 - 화면 방향을 전환할 토글 버튼 표시 + 화면 방향 전환 버튼 표시 계정 관리 프로필 잠금 잘못된 PIN입니다. 다시 시도하세요. @@ -598,17 +598,11 @@ 여러 번 실패하면 프롬프트가 닫힙니다. 다시 시도하려면 앱을 다시 시작하세요. 재설정 플러그인 다운로드를 필터링할 모드 선택 - 데이터가 백업되었습니다. 장치에 따라 동작이 다를 수 있으며 앱 접근이 차단될 경우 앱 데이터를 완전히 지우고 백업에서 복원하세요. 이로 인해 발생하는 불편을 사과드립니다. + CloudStream 데이터 백업이 완료되었습니다. 드문 경우지만, 기기에 따라 앱 접속이 안 되는 오류가 발생할 수 있습니다. 만약 앱이 열리지 않는다면, 앱 데이터를 완전히 삭제(초기화)한 후 이 백업 파일로 복구해 주시기 바랍니다. 이용에 불편을 드려 대단히 죄송합니다. 스마트폰이나 컴퓨터에서 %s를 방문하여 위의 코드를 입력하세요 - 구독 된 TV 프로그램에 대한 특이성 다운로드 및 알림을 보려면 클라우드 스트림은 배경에서 오른쪽으로 실행할 권리가 필요합니다. 확인을 눌러 요청 대화 상자를 표시하십시오. 필요한 경우 필요에 따라 CP3에 제한되지 않고 공식을 다운로드하거나 공식화에서 확대를 누릴 필요가 있습니다. - 여기서 소스의 순서를 변경할 수 있습니다. 비디오의 우선 순위가 높은 경우에는 소스 선택에 더 높게 나타납니다. 소스 우선 순위와 품질 우선 순위의 합이 비디오 우선 순위입니다. -\n -\n참고 A: 3 -\n품질 B: 7 -\n총 비디오 우선 순위는 10입니다. -\n -\n참고: 합이 10 이상이면 해당 링크가 로드되면 플레이어는 자동으로 로드를 건너뜁니다! - 시즌 %1$d 에피소드 %2$d이(가) 공개 예정 + 구독 중인 TV 쇼의 알림을 받고 다운로드를 끊김 없이 완료하려면, CloudStream의 백그라운드 실행 권한이 필요합니다. \'확인\'을 누른 후 나타나는 요청 창에서 \'허용\'을 선택해 주세요.\n\n참고로, 이 권한을 허용한다고 해서 배터리가 계속 소모되는 것은 아닙니다. 알림을 받거나 공식 확장 프로그램에서 영상을 다운로드할 때처럼 꼭 필요한 상황에서만 백그라운드 작업을 수행합니다. + 여기서 소스의 순서를 변경할 수 있습니다. 비디오의 우선 순위가 높은 경우에는 소스 선택 화면에 더 높게 나타납니다. 소스 우선 순위와 품질 우선 순위의 합이 비디오 우선 순위입니다. \n \n참고 A: 3 \n품질 B: 7 \n총 비디오 우선 순위는 10입니다. \n \n참고: 합이 10 이상이면 해당 링크가 로드되면 플레이어는 자동으로 로드를 건너뜁니다! + 시즌 %1$d 에피소드 %2$d 공개 예정 다른 확장자에서 검색 새로운 에피소드 알림 추천목록 보기 @@ -619,7 +613,7 @@ 잠재적 중복 발견 %s의 PIN 입력 즐겨찾기에서 제거 - 캐스트미러 + Cast 소스 목록 플러그인 삭제 경고 탐색바 미리보기 @@ -627,7 +621,7 @@ 처음부터 재생 현재 다운로드가 없습니다. 삭제할 항목을 선택하십시오 - 오프라인 시청가능 + 오프라인 시청 가능 모두 선택 모두 선택해제 로컬 비디오 열기 @@ -653,7 +647,7 @@ 팟캐스트 시작하기 … 인코딩 오류 - 지원되지 않는 오류 + 미지원 오류 음성 인식 사용 불가 %1$d시간 %2$d분 %3$d초 %1$d분 %2$d초 @@ -662,7 +656,7 @@ 출시 전 버전 설치 출시 전 버전이 이미 설치되어 있습니다. 출시 전 버전 설치 실패. - 재생 가능 목록 보기" + 재생 소스 목록" 평가 라벨 에피소드 본문 사용 가능한 자막 불러오기 @@ -684,7 +678,7 @@ 이 알림을 앱 종료 전에 표시 보이기 보이지 않기 - 테두리 크기 + 윤곽선 굵기 Settings/Providers/Preferred media 에서 토렌트 활성화 진행하려면 앱 재시작 후 토렌트 스트리밍 팝업란의 수락이 필요합니다. 소프트웨어 디코딩 @@ -700,19 +694,19 @@ 백그라운드에서 재생을 제어하기 위한 플레이어 알림 내장 자막 온라인 - 자막 글꼴 굵게 표시 - 자막 글꼴 기울게 표시 + 자막 굵게 표시 + 자막 기울게 표시 병렬로 다운로드 할 수 있는 아이템의 수 병렬 다운로드 동시 연결수 다운로드 시 각 항목마다 사용할 수 있는 동시 연결의 수 - 다운로드로 가기 + 다운로드로 이동 인터넷 연결 없음.\n\n인터넷에 연결 한 후 재시도 하거나, 혹은 이미 다운로드 된 항목을 재생하십시오. 화면의 잘림 현상을 방지하기 위해 경계를 조정합니다 포스터 크기 변경 포스터 크기 길게 눌러 배속 활성화 - 길게 눌러 2배속 + 길게 눌러 2배속 재생 프로필 사진 변경 프로필 사진 URL 입력 URL을 찾을 수 없습니다 @@ -734,9 +728,9 @@ 오른쪽 위 비디오 소스가 플레이어에서 정렬되는 순서 설정 디스플레이 밝기가 100%를 초과하면 밝기 필터를 활성화합니다 - 모든 다운로드 큐를 취소하시겠습니까? + 모든 다운로드 작업을 취소하시겠습니까? 에피소드를 다운로드 하시겠습니까 %s? - 현재 누락된 다운로드가 없습니다. + 다운로드 대기열이 비어 있습니다. %d 활성 다운로드 @@ -745,18 +739,19 @@ 입력하는 동안 검색어 제안 표시 출연진 정보 표시 - 배경 모서리 곡률 + 배경 테두리 곡률 공급자 새로고침 검색어 제안 제안 삭제 미디어 정보 소스 이름 최대 밝기 확장 - 다운로드 큐 + 다운로드 작업 모두 다운로드 모두 취소 소스 우선순위 오버스캔(화면 경계) 설정 새로고침 최대 밝기 확장 활성화 + 플레이어에 메타데이터 오버레이 표시 diff --git a/app/src/main/res/values-b+lv/strings.xml b/app/src/main/res/values-b+lv/strings.xml index 055732644..89003317a 100644 --- a/app/src/main/res/values-b+lv/strings.xml +++ b/app/src/main/res/values-b+lv/strings.xml @@ -2,11 +2,11 @@ Plakāts %1$s Ep %2$d - Cast: %s + Lomās: %s Plakāts Epizodes plakāts Galvenais plakāts - Nākamais random + Nākamais nejaušais Iet atpakaļ Nomainīt dvēju Apskatīt background @@ -15,15 +15,15 @@ Jauns atjauninājums atrasts! \n%1$s -> %2$s %d galvenais - Claudstream + CloudStream Atskaņo ar cloudstream - Mājas + Sākums Meklēt Meklēt %s… Nav datu Vairāk opcijas Nākamā epizode - Internets + Pārlūks Izlaist ladešanos Lādējas… Skaties @@ -42,14 +42,14 @@ Iet atpakaļ Palaist epizodi Ieladēt - Lādēšana pauzēta + Lejupielāde iepauzēta Lādēšana sakās - Ielādēt neizdevās - Ielādēšana atcelta - Pabeidza ieladēt - Atjauninājums sakās + Lejupielāde neizdevās + Lejupielāde atcelta + Lejupielāde pabeigta + Atjaunināšana sākta Tīkla plūsma - Kļūda padejot linkus + Kļūda, ielādējot saites Iekšējā atmiņa Dub Dzēst datni @@ -78,13 +78,13 @@ Meklēt izmantojot devējus Meklēt izmantojot tipus %d Banāni iedoti veidotājiem - Episode %d būs izlaista + %d. epizode būs pieejama Filtrs - Ieladētas + Lejupielādes Meklēt… - Settingi + Iestatījumi Žanrs - Dalities + Kopīgot Atvērt pārlūkā Ieladēts Lādējas @@ -95,9 +95,9 @@ Iztīrīt Teksta krāsa Automātiski-iestādīt valodu - %1$dh %2$dm - %dm - %1$dd %2$dh %3$dm + %1$dst. %2$dmin. + %dmin. + %1$dd. %2$dst. %3$dmin. Ielādēt valodas Subtitru valoda Tūri lai restartētu uz sākumu @@ -115,7 +115,7 @@ Rādīt Logcat 🐈 Log Bilde bildē - Turpina spēlēt mazā lodziņā virs aplikācijām + Turpina atskaņošanu miniatūrā atskaņotājā virs citām lietotnēm Players izmēra poga Noņemt melnās malas Subtitri @@ -135,7 +135,7 @@ Uzpied divreiz pa labi vai kreisi lai palaistu atpakaļ vai uz priekšu Uzpied divreiz vidū lai pauzētu Lietot sistēmas gaišums - Lietot sistēmas gaišumu aplikācijas playerī nevis tumšunu + Izmanto sistēmas spilgtumu, nevis atskaņotāja tumšo pārklājumu Atjaunināt skatīšanos progresu Automātiski sync savu pašreizējo epizodes progresu Atgūt datus no backupa @@ -160,15 +160,15 @@ Automātiski lejupielādēt papildinājumus Automātiski uzstāda visus vēl neuzstādītos papildinājumus no pievienotajiem repozitorijiem. Rādīt lietotņu atjauninājumus - Automātiski meklēt jaunus atjauninājumus kad palaiž aplikāciju. + Automātiski pārbauda atjauninājumus, kad atver lietotni. Atsākt uzstādīšanas procesu Dažas ierīces neatbalsta jauno pakotnes uzstādītāju. Izmantojiet legacy (veco) uzstādītāju, ja atjauninājumus nevar uzstādīt. - Noveles aplikācija no šiem izstrādātājiem - Anime aplikāciju no tiem pašiem izstradatājiem + Viegla romānu lietotne no šiem pašiem izstrādātājiem + Anime lietotne no šiem pašiem izstrādātājiem Ienāc discordā Iedot banānu izstrādātājiem Iedotie banāni - Aplikācijas valoda + Lietotnes valoda Šim devējam nav Chromecast pieņemšana Nav linku strastu Links kopēts cliobordā @@ -206,7 +206,7 @@ Konspekts ievietots rindā Lietotie - Aplikācija + Lietotne Filmas Seriāli, raidījumi Animācija @@ -235,7 +235,7 @@ Ielādēšanas kļūda, pārbaudi atmiņas atļauju Chromecast epizode Chromecast morror - Palaist aplikācijā + Atskaņot lietotnē Atskaņot uekšā %s Automātiski ielādēt Ielādēt spoguli @@ -277,7 +277,7 @@ Atruna ISP Izlaists Links - Aplikācijas atjauninājumus + Lietotnes atjauninājumi Dublējums Papildinājumi Akcijas @@ -294,7 +294,7 @@ Randomā poga Rādīt izlases pogu Sākums un Bibliotēka sadaļās Papildinājuma valodas - Aplikācijas izskats + Lietotnes izkārtojums Izvēlētā media Iespējot nepiedienīgu, izaicinošu saturu (NSFW) atbalstītajos papildinājumos Subtitru kodējums @@ -345,7 +345,7 @@ Fogts čuhņā mīļi lenc - ģērbj, žvadz, pūkšķ Ielādēti %s Ielādēt no datnes - Aplikācijas theme + Lietotnes motīvs Lejupielādēt no interneta Lejupielādēta datne Galvenais @@ -430,7 +430,7 @@ HLS atskaņošanas saraksts Vēlamais video atskaņotājs Iekšējais atskaņotājs - Aplikācijs nav atrasta + Lietotne nav atrasta Visas valodas Beigas Kopsavilkums @@ -498,7 +498,7 @@ Lejupielādējiet to vietņu sarakstu, kuras vēlaties izmantot Vispirms uzstādīt papildinājumu Atvēršana - Sākums + Ievads Izlaist %s Noņemt no skatītajiem Atzīmēt kā skatītu @@ -509,16 +509,16 @@ Abonēto šovu atjaunināšana Abonēts Abonēts %s - %1$d. sezona un %2$d. sērija tiks izlaista pēc - %1$dh %2$dm %3$ds - %1$dm %2$ds - %1$ds + %1$d. sezonas %2$d. epizode būs pieejama + %1$dst. %2$dmin. %3$dsek. + %1$dmin. %2$dsek. + %1$dsek. Atskaņot no sākuma Runas atpazīšana nav pieejama Sāciet runāt… Šis video ir torrenta fails, kas nozīmē, ka jūsu video aktivitātes var izsekot.\nPirms turpināt, pārliecinieties, ka saprotat torrenta failu lietošanu. - Atlasiet dzēšamos vienumus - Pašlaik nav lejupielāžu. + Atlasiet vienumus, ko dzēst + Pašlaik nav pieejama neviena lejupielāde. Pieejams skatīšanai bezsaistē Bezvadu (Wi-Fi) Izmantot @@ -609,4 +609,6 @@ Vai tiešām vēlaties neatgriezeniski dzēst šīs %1$s epizodes?\n\n%2$s Jūs arī neatgriezeniski izdzēsīsiet visas šī seriāla, raidījuma epizodes:\n\n%s Vai tiešām vēlaties neatgriezeniski dzēst visas šī seriāla, raidījuma epizodes?\n\n%s + Pašlaik nav nevienas rindā ievietotas lejupielādes. + Atvērt vietējo video diff --git a/app/src/main/res/values-b+uk/strings.xml b/app/src/main/res/values-b+uk/strings.xml index cec3b6738..2eb6e2451 100644 --- a/app/src/main/res/values-b+uk/strings.xml +++ b/app/src/main/res/values-b+uk/strings.xml @@ -6,10 +6,10 @@ Змінити Постачальника Назад Рейтинг: %.1f - Актори: %s + У ролях: %s Епізод %d вийде через Плакат - %1$s Еп. %2$d + %1$s Еп %2$d %1$dд %2$dгод %3$dхв %1$dгод %2$dхв %dхв @@ -148,23 +148,23 @@ Приховати вибрану якість відео у результатах пошуку Автозавантаження розширень Показувати оновлення застосунку - Налаштувати повторно - Установлювач APK + Налаштувати повторно процес встановлення + Встановлювач APK Github Застосунок для ранобе від тих самих розробників Застосунок для аніме від тих самих розробників - Дати банан розробникам - Мова застосунку + Дати benene розробникам + Мова Застосунку Цей постачальник не має підтримування Chromecast - Посилань не знайдено - Переглянути епізод + Посилань Не Знайдено + Переглянути Епізод Скинути до типових значень - Немає сезона - Епізодів + Немає Сезону + Епізоди %1$d %2$s С Е - Видалити файл + Видалити Файл Видалити Скасувати Відновити @@ -185,15 +185,15 @@ Мультфільми Аніме OVA - Азіатські драми - Прямі трансляції + Азіатські Драми + Прямі Трансляції Інші Серіал Мультфільм Аніме - Документальний фільм - Азіатська драма - Пряма трансляція + Документальний Фільм + Азіатська Драма + Пряма Трансляція Відео Помилка джерела Віддалена помилка @@ -203,7 +203,7 @@ Переглянути в %s Автозавантаження Завантажити дзеркало - Перевірити наявність оновлень + Перевірити Наявність Оновлень Забл./Розбл. Пропустити ОП Не показувати знову @@ -211,7 +211,7 @@ Бажана якість перегляду (WiFi) Заголовок Перемкнути елементи інтерфейсу на плакаті - Оновлення не знайдено + Оновлення Не Знайдено Натисніть двічі праворуч або ліворуч, щоби перемотати вперед або назад Використовувати системну яскравість у програвачі замість темного накладання Завантажено файл резервної копії @@ -220,12 +220,12 @@ Немає дозволу на зберігання. Спробуйте ще раз. Показувати плакати від Kitsu Автооновлення розширень - Автоматично встановлювати всі розширення, які ще не встановлено, з доданих репозиторіїв. + Автоматично встановлювати всі розширення, які ще не встановлено, з доданих сховищ. Автоматично перевіряти нові оновлення після запуску застосунку. Покликання скопійовано до буфера обміну Деякі пристрої не підтримують новий інсталятор пакетів. Спробуйте старий варіант, якщо оновлення не встановлюються. Приєднуйтеся до Discord - Дано бананів + Дано benene Рік +30 %1$s %2$d%3$s @@ -239,8 +239,8 @@ Змінити розмір Стислий зміст Фільми - Перезавантажити покликання - Документальні фільми + Перезавантажити посилання + Документальні Фільми NSFW Фільм OVA @@ -249,25 +249,25 @@ NSFW Несподівана помилка програвача Помилка завантаження, перевірте дозвіл на зберігання - Дивитися через Chromecast + Chromecast епізод Мітка субтитрів Джерело Завантажити субтитри Мітка дубляжу - Пропустити це оновлення + Пропустити це Оновлення Усе На весь екран - Заповнити + Розтягнути Збільшити Доріжки Оновлення застосунку Кеш Жести - Особливості програвача + Функції програвача Субтитри Типово Вигляд - Особливості + Функції Загальні Випадкова кнопка Показувати кнопку випадкового вибору на головній сторінці та бібліотеці @@ -275,14 +275,14 @@ Макет застосунку Бажані медіа Автоматично - Макет телевізора - Макет телефону - Макет емулятора + Телевізійна Обгортка + Телефона обгортка + Емуляторна обгортка Основний колір Тема застосунку Розташування назви плаката Розмістити назву під плакатом - Пароль123 + password123 Імʼя користувача hello@world.com НоваНазваСайту @@ -296,7 +296,7 @@ %d / 10 /%d %s автентифіковано - Не вдалося ввійти в %s + Не вдалося увійти в %s Нічого Звичайний Мін. @@ -319,7 +319,7 @@ HDR SDR Web - Зображення плаката + Зображення Плаката Програвач Роздільна здатність та заголовок Недійсний ID @@ -341,21 +341,21 @@ DNS через HTTPS Шлях завантаження Додайте двійника наявного сайту, з іншою URL-адресою - Показувати мітку Дубляж/Субтитри для аніме + Показувати Дубльоване/З Субтитрами Аніме Застереження Розширення Дії 127.0.0.1 Макет Кодування субтитрів - Увімкнути NSFW вміст на підтримуваних розширеннях - Макет + Увімкнути NSFW вміст на підтримуваних Розширеннях + Обгортка Постачальники https://example.com - %2$s %1$s + %1$s %2$s Опущені обліковий запис - Створити + Створити обліковий запис Додано %s /?? Рейтинг @@ -396,7 +396,7 @@ Готово Розширення Додати репозиторій - Назва репозиторію (необов’язково) + Назва репозиторію (Опціонально) URL-адреса репозиторію або короткий код Розширення завантажено Розширення завантажено @@ -404,17 +404,17 @@ Почалося завантаження %1$d %2$s… Завантажено %1$d %2$s Усі %s вже завантажено - Завантажити пакунки + Завантажити пакунком розширення - розширень - Видалити репозиторій + розширення + Видалити сховище Завантажте список сайтів, які ви хочете використовувати Завантажено: %d Вимкнено: %d Не завантажено: %d Оновлено %d розширень - Типово у CloudStream немає жодного встановленого сайту. Вам потрібно встановити сайти з репозиторіїв.\n\nПриєднуйтеся до нашого Discord або шукайте в інтернеті. - Переглянути репозиторії спільноти + Типово у CloudStream немає жодного встановленого сайту. Вам потрібно встановити сайти зі сховищ.\n\nПриєднуйтеся до нашого Discord або шукайте в інтернеті. + Переглянути сховища спільноти Публічний список Усі субтитри великими літерами %s (вимкнено) @@ -449,10 +449,10 @@ Установлення оновлення застосунку… Не вдалося встановити нову версію застосунку Застарілий - Установлювач пакунків + Встановлювач Пакунків Застосунок буде оновлено після виходу - Це також призведе до видалення всіх розширень репозиторію - Усі мови + Це також призведе до видалення всіх розширень сховища + Усі Мови Назад Змініть вигляд застосунку відповідно до вашого пристрою Розширення видалено @@ -467,24 +467,24 @@ Застосунок не знайдено Змішаний опенінґ Вилучити з переглянутого - Оновленням (від старого до нового) - Оновленням (від нового до старого) + Оновленням (від Старого до Нового) + Оновленням (від Нового до Старого) Бібліотека Сортувати - Рейтингом (від високого до низького) + Рейтингом (від Високого до Низького) Сортувати за Алфавітом (від А до Я) - Рейтингом (від низького до високого) + Рейтингом (від Низького до Високого) Ваша бібліотека порожня :(\nУвійдіть в обліковий запис бібліотеки або додайте щось до вашої локальної бібліотеки. Алфавітом (від Я до А) - Оберіть бібліотеку - Відкрити + Оберіть Бібліотеку + Відкрити з Браузер Цей список порожній. Спробуйте перейти до іншого. Файл безпечного режиму знайдено!\nРозширення не завантажуватимуться під час запуску, доки файл не буде видалено. Android TV - Прогрвач приховано – крок перемотування - Програвач показано – крок перемотування + Прогрвач Приховано – Крок Перемотування + Програвач Показано – Крок Перемотування Крок перемотування, який використовується, коли програвач видимий Крок перемотування, який використовується, коли плеєр прихований Провалено @@ -500,15 +500,15 @@ Ви відписалися від %s Епізод %d випущено! Повернути - GitHub проксі + GitHub Проксі Не вдалось отримати доступ до GitHub. Увімкнення проксі-сервера jsDelivr… Обходи ISP - Обхід блокування GitHub за допомогою jsDelivr. Можлива затримка оновлень на кілька днів. + Обхід блокування чистих gitHub URLs за допомогою jsDelivr. Можлива затримка оновлень на кілька днів. Бажана якість перегляду (мобільні дані) - Змінити на типові + Встановити типові Профілі Довідка - Тут можна змінити порядок джерел. Відео з вищим пріоритетом з’являтиметься вище в списку джерел. Сума пріоритету джерела та пріоритету якості утворює пріоритет відео.\n\nДжерело А: 3\nЯкість Б: 7\nЗагальний пріоритет відео дорівнюватиме 10.\n\nПРИМІТКА: Якщо сума пріоритетів дорівнюватиме 10 або більше, програвач автоматично пропустить завантаження цього покликання! + Тут можна змінити порядок джерел. Відео з вищим пріоритетом з’являтиметься вище в списку джерел. Сума пріоритету джерела та пріоритету якості утворює пріоритет відео.\n\nДжерело А: 3\nЯкість Б: 7\nЗагальний пріоритет відео дорівнюватиме 10.\n\nПРИМІТКА: Якщо сума пріоритетів дорівнюватиме 10 або більше, програвач автоматично пропустить завантаження цього посилання! Профіль %d Wi-Fi Мобільні дані @@ -523,11 +523,11 @@ Не знайдено жодного розширення в репозиторії Ви вже проголосували Частота резервного копіювання - %s вилучено з обраного - Обране - %s додано до обраного + %s вилучено з вподобаних + Вподобані + %s додано до вподобаних У вашій бібліотеці виявлено можливі дублікати:\n\n%s\n\nУсе одно хочете додати цей елемент, замінити наявні чи скасувати дію? - Знайдено можливий дублікат + Знайдено Можливий Дублікат Заблокувати профіль Додати до обраного Замінити все @@ -538,16 +538,16 @@ Додати Підписатися Вилучити з обраного - Оберіть обліковий запис - Схоже, що у вашій бібліотеці вже є можливий дублікат: «%s.»\n\nУсе одно хочете додати цей елемент, замінити наявний чи скасувати дію? + Оберіть Обліковий Запис + Схоже, що у вашій бібліотеці вже є можливий дублікат: \'%s.\'\n\nУсе одно хочете додати цей елемент, замінити наявний чи скасувати дію? Уведіть PIN-код PIN-код Уведіть поточний PIN-код Увійшли як %s Уведіть PIN-код для %s - Використовувати типовий обліковий запис + Використовувати Типовий Обліковий Запис Пропускати вибір облікового запису під час запуску - Керувати обліковими записами + Керувати Обліковими Записами Редагувати обліковий запис Показувати кнопку перемикання орієнтації екрана Обернути @@ -555,28 +555,28 @@ Автообертання Увімкнути автоматичну зміну орієнтації екрана відповідно до відео Додати налаштування швидкості до програвача - Перевірити всі розширення + Перевірити всі Розширення Пошук в інших розширеннях Показати рекомендації - Ця перевірка лише для розробників і не підтверджує або заперечує роботу жодного розширення. + Ця Перевірка лише для розробників і не підтверджує або заперечує роботу жодного розширення. Сповіщення про новий епізод - Автентифікація за паролем/PIN-кодом + Автентифікація за Паролем/PIN-кодом Розблокуйте CloudStream - Біометричне блокування - Розблоковуйте застосунок за допомогою відбитка пальця, Face ID, PIN-коду, графічного ключа або пароля. + Біометричне Блокування + Розблоковуйте застосунок за допомогою відбитка пальця, Face ID, PIN-коду, Графічного Ключа або Пароля. Щойно було виконано резервне копіювання даних CloudStream. Хоча ймовірність цього вкрай мала, усі пристрої можуть поводитися по-різному. У рідкісних випадках, якщо ви втратите доступ до застосунку, повністю очистіть дані застосунку та відновіть їх із резервної копії. Просимо вибачення за будь-які незручності, що можуть виникнути. Біометрична автентифікація не підтримується на цьому пристрої Після кількох невдалих спроб вікно запиту зникне. Перезапустіть застосунок, щоби спробувати ще раз. %s\nзалишилося - Вилучити з обраного - Додати до обраного + Вилучити з вподобаного + Додати до вподобаного скопійовано! Назва репозиторію та URL - Помилка копіювання, скопіюйте logcat та зверніться до служби підтримки застосунку. - Помилка доступу до буфера обміну, спробуйте ще раз. - Гаразд + Помилка копіювання, Будь-ласка скопіюйте logcat та зверніться до служби підтримки застосунку. + Помилка доступу до буфера обміну, Будь-ласка спробуйте ще раз. + OK Вимкнути оптимізацію батареї - Щоби забезпечити безперервне завантаження та сповіщення про підписані телепередачі, CloudStream потребує дозволу на роботу у фоновому режимі. Натиснувши «Гаразд», ви побачите діалогове вікно запиту. Натисніть «Дозволити».\n\nЗверніть увагу, що цей дозвіл не означає, що CS3 розряджатиме ваш акумулятор. Він працюватиме у фоновому режимі лише за необхідності, наприклад, під час отримання сповіщень або завантаження відео з офіційних розширень. + Щоби забезпечити безперервне завантаження та сповіщення про підписані телепередачі, CloudStream потребує дозволу на роботу у фоновому режимі. Натиснувши OK, ви побачите діалогове вікно запиту. Натисніть \'Дозволити\'.\n\nЗверніть увагу, що цей дозвіл не означає, що CS3 розряджатиме ваш акумулятор. Він працюватиме у фоновому режимі лише за необхідності, наприклад, під час отримання сповіщень або завантаження відео з офіційних розширень. Споживання батареї застосунком уже змінено на необмежене Не вдається відкрити подробиці про застосунок CloudStream. Аудіокнига @@ -587,16 +587,16 @@ Сезон %1$d Епізод %2$d вийде через Оберіть пристрій для трансляції Трансляція через дзеркало - Довідник CloudStream + CloudStream Wiki Безпека Облікові записи Зображення QR-коду - Відкрити репозиторій + Відкрити сховище Відвідайте %s на своєму смартфоні або комп\'ютері та введіть вищевказаний код - Не вдається отримати PIN-код пристрою, спробуйте локальну автентифікацію - PIN-код застарів! + Не вдається отримати PIN-код пристрою, спробуйте локальну аутентифікацію + PIN-код зараз закінчився ! Термін дії коду закінчується через %1$dхв %2$dс - Локальна автентифікація + Локальна Аутентифікація Відхилити Відтворити з Початку Попередження @@ -604,25 +604,25 @@ Наразі завантажень немає. Приховати назви елементів керування в програвачі Відкрити локальне відео - Датою випуску (від нових до старих) - Датою випуску (від старих до нових) + Датою випуску (від Нових до Старих) + Датою випуску (від Старих до Нових) Оберіть Елементи для Видалення Обрати Все Зняти Вибір Всіх Видалити (%1$d | %2$s) - Ви впевнені, що хочете назавжди видалити такі епізоди «%1$s»?\n\n%2$s + Ви впевнені, що хочете назавжди видалити такі епізоди в %1$s?\n\n%2$s Ви також назавжди видалите всі епізоди в такому серіалі:\n\n%s Доступно для перегляду в оффлайн режимі - Видалити файли + Видалити Файли Ви впевнені, що хочете назавжди видалити такі елементи?\n\n%s Ви впевнені, що хочете назавжди видалити всі епізоди в такому серіалі?\n\n%s Попередній перегляд на шкалі перегляду Увімкнути мініатюру попереднього перегляду на шкалі перегляду Субтитри ще не завантажено Підтвердіть перед виходом - Показувати + Відобразити Показувати діалог перед виходом із застосунку - Не показувати + Не відображати Розташування теки для резервних копій Власний Це відео – Торрент, це означає, що ваша відео діяльність може відстежуватися.\nПереконайтеся, що розумієте, що таке Торрент, перед тим як продовжити. @@ -631,21 +631,21 @@ Подкаст Непідтримувана помилка Помилка кодування - Завантажити перші доступні - Увімкніть торент у Налаштування/Постачальники/Бажані медіа + Завантажити перший доступний + Увімкніть торент в Налаштування/Постачальники/Бажані медіа Перезапустіть застосунок та прийміть спливне вікно Stream Torrent, щоби продовжити. Програмне декодування Програмне декодування дозволяє плеєру відтворювати відеофайли, які не підтримуються вашим пристроєм, але може спричинити затримки або нестабільне відтворення у високій роздільній здатності. - Датою виходу (найновіша) - Епізодом (за зростанням) - Рейтингом (найнижчий) + Датою виходу (Найновіша) + Епізодом (За Зростанням) + Рейтингом (Найнижчий) Рейтинг %s - Епізодом (за спаданням) - Рейтингом (найвищий) - Датою виходу (найстаріша) + Епізодом (за Спаданням) + Рейтингом (Найвищий) + Датою виходу (Найстаріша) Еп. %s Дата %s - Оновити розширення + Оновити Розширення Успішно оновлено %d розширення(-ь)! Оновити розширення вручну Починається оновлення розширень! @@ -656,22 +656,22 @@ Почніть Говорити… Вбудовані Мережеві - Радіус тла - Зробити всі субтитри жирним + Радіус Тла + Зробити всі субтитри потовщеним Зробити всі субтитри курсивом Гучність перевищила 100% Ще раз проведіть угору, щоби перевищити 100% Одночасних з’єднань Змінює межі екрана Обрізання зображення - Перейти до завантажень + Перейти до Завантажень Немає підключення до Інтернету.\n\nБудь ласка, підключіться до Інтернету та спробуйте ще раз або перегляньте завантажені відео офлайн. Скільки різних елементів можна завантажити паралельно Паралельних завантажень Скільки одночасних з’єднань може використовувати кожне завантаження Зміна розміру плакатів Розмір постера - Завжди запитуйте + Завжди запитувати Змінювати швидкість при утриманні Утримуйте, щоб отримати 2-кратну швидкість %1$dгод %2$dхв %3$dс @@ -679,19 +679,19 @@ %1$dс Мітка рейтингу Немає облікового запису - Редагувати зображення профілю - Введіть URL-адресу зображення профілю - URL-адресу не знайдено - Недійсна URL-адреса або зображення - Зображення успішно оновлено + Редагувати Зображення Профілю + Введіть URL-адресу Зображення Профілю + URL-адресу Не Знайдено + Недійсна URL-адреса або Зображення + Зображення Успішно Оновлено Позначити як переглянуте до цього епізоду Вилучити переглянуті до цього епізоду Перезавантажено - Постачальник послуг поповнення рахунку - Грати в дзеркало" - Ім\'я + Перезавантажити Постачальника Послуг + Переглянути в дзеркалі" + Назва Роздільна здатність та назва - Вирівнювання субтитрів + Вирівнювання Субтитрів Внизу ліворуч Внизу по центру Внизу праворуч @@ -702,18 +702,18 @@ Верхній центр Угорі праворуч Відтворити Повні Серії - Встановити передрелізну версію - Попередня версія вже встановлена. - Не вдалося встановити попередню версію. - Текст епізоду + Встановити перед-релізну версію + Перед-релізна версія вже встановлена. + Не вдалося встановити перед-релізну версію. + Текст Епізоду Пропозиції Пошуку Показувати підказки пошуку під час введення тексту - Очистити пропозиції + Очистити Пропозиції Додаткова яскравість Увімкнути фільтр яскравості при перевищенні 100% яскравості дисплея extra_brightness_enabled Показати панель трансляції - Інформація про медіа + Інформація Про Медіа Назва джерела Черга завантаження Наразі немає завантажень у черзі. @@ -735,4 +735,5 @@ Пріоритетне джерело Виберіть спосіб сортування джерел відео у програвачі + Показувати Накладання Метаданих Програвача diff --git a/app/src/main/res/values-b+vi/strings.xml b/app/src/main/res/values-b+vi/strings.xml index 7999feb99..895184652 100644 --- a/app/src/main/res/values-b+vi/strings.xml +++ b/app/src/main/res/values-b+vi/strings.xml @@ -6,17 +6,17 @@ Diễn viên: %s Tập %d sẽ ra mắt sau %1$dng %2$dg %3$dph - %1$dgi %2$dph + %1$dh %2$dm %dm Poster - Ảnh bìa - Episode Poster - Main Poster - Next Random - Quay trở lại - Change Provider - Preview Background + Poster + Poster tập phim + Poster chính + Tập tiếp theo ngẫu nhiên + Quay lại + Thay đổi nguồn phát + Xem trước hình nền Tốc độ (%.2fx) Đánh giá: %.1f @@ -38,12 +38,12 @@ Thể loại Chia sẻ Mở bằng trình duyệt - Bỏ qua + Bỏ qua quá trình tải Đang tải… Đang xem Đang chờ Đã xem - Bỏ qua + Bỏ xem Xem sau Xem lại Xem Ngay @@ -58,11 +58,11 @@ Tải xuống Đã tải Đang tải - Tạm dừng + Đã tạm dừng tải xuống Đã bắt đầu tải Tải lỗi - Đã hủy - Tải thành công + Đã hủy tải xuống + Tải xuống thành công Luồng mạng Lỗi khi tải liên kết Bộ nhớ trong @@ -76,17 +76,17 @@ Ẩn Xem ngay Thông tin - Lọc theo danh sách đã lưu - Danh sách đã lưu + Lọc danh sách của tôi + Danh sách của tôi Xóa Đặt trạng thái xem Áp dụng Sao chép Đóng - Huỷ bỏ + Xóa Lưu Tốc độ phát - Cài đặt hiển thị phụ đề + Cài đặt phụ đề Màu chữ Màu viền chữ Màu nền @@ -97,45 +97,45 @@ Kích thước chữ Tìm kiếm theo nguồn phim Tìm kiếm theo thể loại - %d lời cảm ơn đã được gửi tặng nhà phát triển - Hãy tặng cho nhà phát triển một lời cảm ơn + %d lượt ủng hộ đã gửi đến nhà phát triển + Không có lượt ủng hộ đã nhận Tự động chọn ngôn ngữ Ngôn ngữ khi tải xuống Ngôn ngữ phụ đề - Giữ để làm mới toàn bộ + Nhấn giữ để đặt lại về mặc định Thêm phông chữ tại %s Tiếp tục xem Loại bỏ Thông tin thêm @string/home_play - Bạn có thể sẽ cần sử dụng VPN để xem phim này - Phim này được chiếu dưới dạng Torrent. Hãy sử dụng VPN để xem + Có thể cần dùng VPN để nguồn này hoạt động đúng + Nguồn này là một torrent, khuyến nghị dùng VPN Thông tin phim - Đang cập nhật - Không tìm thấy thông tin + Không tìm thấy nội dung + Không tìm thấy thông tin chi tiết Hiển thị Logcat 🐈 - Chế độ cửa sổ nhỏ - Tiếp tục xem phim khi thoát ứng dụng hoặc khi tìm kiếm - Bật nút thu phóng khi xem - Xóa khoảng đen của phim + Hình trong hình + Tiếp tục phát trong trình phát thu nhỏ trên các ứng dụng khác + Nút thay đổi kích cỡ trình phát + Xóa bỏ các viền đen Phụ đề Cài đặt phụ đề Phụ đề Chromecast Cài đặt phụ đề Chromecast Tốc độ phát Vuốt để tua nhanh - Vuốt sang trái hoặc phải để tua video - Vuốt để chỉnh độ sáng và âm lượng - Vuốt lên hoặc vuốt xuống ở hai bên để điều chỉnh độ sáng và âm lượng + Vuốt ngang qua lại để tua video + Vuốt để thay đổi cài đặt + Vuốt lên hoặc xuống cạnh trái hoặc phải để điều chỉnh độ sáng hoặc âm lượng Tự động phát tập tiếp theo Phát tập tiếp theo sau khi hết tập hiện tại Nhấn 2 lần để tua Nhấn 2 lần để tạm dừng Thời lượng tua (Giây) - Nhấn 2 lần vào bên trái hoặc bên phải màn hình để tua trước hoặc sau + Nhấn 2 lần vào cạnh trái hoặc phải để tua về trước hoặc sau Nhấn vào giữa hai lần để tạm dừng Sử dụng độ sáng hệ thống - Sử dụng độ sáng hệ thống trong trình phát ứng dụng + Dùng độ sáng hệ thống thay cho lớp phủ tối trong trình phát ứng dụng Cập nhật tiến trình xem Tự động đồng bộ tiến trình hiện tại của bạn Khôi phục dữ liệu từ bản sao lưu @@ -182,28 +182,27 @@ Xóa Tệp Xóa Hủy bỏ - Tạm Dừng - Tiếp Tục + Tạm dừng + Tiếp tục -30 +30 %s sẽ bị xoá vĩnh viễn \nBạn có chắc chắn muốn xóa? - %dm -\ncòn lại + %d phút\ncòn lại Đang chiếu - Hoàn Thành + Hoàn thành Trạng Thái Năm - Đánh Giá - Thời Lượng + Đánh giá + Thời lượng Nguồn Thông tin Hàng chờ Không có phụ đề - Mặc Định + Mặc định Còn trống Đã sử dụng - App + Ứng dụng Phim Lẻ Phim Bộ @@ -229,14 +228,14 @@ NSFW Video Lỗi nguồn phim - Lỗi kết nối tới máy chủ - Không thể render + Lỗi nguồn từ xa + Lỗi kết xuất Đã có lỗi xảy ra. Vui lòng thử lại sau Lỗi tải xuống. Hãy kiểm tra quyền truy cập bộ nhớ của ứng dụng Tập Chromecast Chiếu Chromecast - Xem với trình phát mặc định - Xem với trình phát %s + Xem trong ứng dụng + Xem trong %s Tự động tải xuống Nguồn tải xuống Lấy link mới nhất @@ -244,45 +243,45 @@ Nhãn chất lượng phim Nhãn lồng tiếng Nhãn phụ đề - Tiêu đề - Thay đổi giao diện trên poster + Tên + Thành phần giao diện trên poster Bạn đang dùng phiên bản mới nhất Kiểm tra cập nhật Khóa - Thu Phóng - Nguồn & Phụ đề - Bỏ qua OP + Thu phóng + Nguồn + Bỏ qua giới thiệu Không hiện lại Bỏ qua bản cập nhật này Cập nhật Chất lượng xem ưu tiên (WiFi) - Kí tự tối đa trên tiêu đề + Số ký tự tối đa trên tiêu đề trình phát video Hiện thông tin trình phát Kích thước bộ nhớ đệm video Thời lượng bộ nhớ đệm Lưu bộ nhớ đệm video trên ổ cứng Xoá bộ nhớ đệm hình ảnh và video - Sẽ gây lỗi nếu đặt quá cao trên máy có dung lượng ram thấp như Android TV. + Sẽ gây lỗi nếu đặt quá cao trên thiết bị có bộ nhớ thấp như Android TV. Sẽ gây lỗi nếu đặt quá cao trên máy có dung lượng lưu trữ thấp như Android TV. DNS over HTTPS Rất hữu ích để bỏ chặn ISP Sao chép trang web Xoá trang web - Thêm bản sao của một trang web, với một địa chỉ khác + Thêm bản sao của trang hiện có bằng một URL khác Đường dẫn tải xuống Địa chỉ máy chủ Nginx - Hiển thị nhãn Phụ đề hoặc Thuyết minh + Hiển thị Anime Lồng tiếng/Phụ đề Vừa màn hình Kéo dãn Phóng to - Disclaimer + Tuyên bố miễn trừ trách nhiệm Tổng quan Nút ngẫu nhiên Hiện nút ngẫu nhiên trên Trang chủ và Thư viện Ngôn ngữ mở rộng - Giao diện App + Bố cục ứng dụng Thể loại ưu tiên - Kích hoạt NSFW trên các tiện ích mở rộng được hỗ trợ + Kích hoạt NSFW trên các Tiện ích mở rộng được hỗ trợ Mã hoá phụ đề Nguồn phim Giao diện @@ -291,7 +290,7 @@ Giao diện điện thoại Giao diện giả lập Màu chính - Chủ đề App + Chủ đề ứng dụng Vị trí tiêu đề Đặt tiêu đề dưới poster @@ -299,7 +298,7 @@ Tài khoản Email 127.0.0.1 - Tên mới + Tên trang mới https://example.com Mã ngôn ngữ (vi) %1$s %2$s @@ -329,10 +328,10 @@ Đổ bóng Nâng Chỉnh phụ đề - 1000ms + 1000 mili giây Độ trễ phụ đề - Dùng nếu phụ đề bị nhanh %dms - Dùng nếu phụ đề bị trễ %dms + Dùng nếu phụ đề bị nhanh %d mili giây + Dùng nếu phụ đề bị trễ %d mili giây Không chỉnh Poster Poster @@ -15,7 +15,7 @@ Poster chính Tập tiếp theo ngẫu nhiên Quay lại - Thay đổi nguồn phát + Thay đổi Nguồn phim Xem trước hình nền Tốc độ (%.2fx) @@ -25,10 +25,10 @@ Bộ lọc %d phút CloudStream - Mở với CloudStream + Phát bằng CloudStream Trang Chủ Tìm Kiếm - Tải Về + Tải xuống Cài Đặt Tìm kiếm… Tìm kiếm %s… @@ -38,7 +38,7 @@ Thể loại Chia sẻ Mở bằng trình duyệt - Bỏ qua quá trình tải + Bỏ tải Đang tải… Đang xem Đang chờ @@ -46,35 +46,35 @@ Bỏ xem Xem sau Xem lại - Xem Ngay - Phát trực tiếp - Xem Torrent - Nguồn Phim + Phát + Phát Livestream + Phát Torrent + Nguồn phim Phụ đề Thử kết nối lại… Quay lại - Xem Tập Phim + Phát Tập phim Tải xuống - Đã tải - Đang tải + Đã tải xuống + Đang tải xuống Đã tạm dừng tải xuống - Đã bắt đầu tải - Tải lỗi - Đã hủy tải xuống + Tải xuống đã bắt đầu + Tải xuống thất bại + Tải xuống đã hủy Tải xuống thành công Luồng mạng - Lỗi khi tải liên kết + Lỗi tải liên kết Bộ nhớ trong Lồng tiếng Phụ đề Xóa Tệp - Xem Tệp - Tiếp tục tải - Tạm dừng tải + Phát Tệp + Tiếp tục tải xuống + Tạm dừng tải xuống Thông tin thêm Ẩn - Xem ngay + Phát Thông tin Lọc danh sách của tôi Danh sách của tôi @@ -92,7 +92,7 @@ Màu nền Màu cửa sổ Kiểu viền - Độ nâng + Độ nâng phụ đề Kiểu chữ Kích thước chữ Tìm kiếm theo nguồn phim @@ -100,12 +100,12 @@ %d lượt ủng hộ đã gửi đến nhà phát triển Không có lượt ủng hộ đã nhận Tự động chọn ngôn ngữ - Ngôn ngữ khi tải xuống + Ngôn ngữ tải xuống Ngôn ngữ phụ đề Nhấn giữ để đặt lại về mặc định Thêm phông chữ tại %s Tiếp tục xem - Loại bỏ + Xóa Thông tin thêm @string/home_play Có thể cần dùng VPN để nguồn này hoạt động đúng @@ -116,14 +116,14 @@ Hiển thị Logcat 🐈 Hình trong hình Tiếp tục phát trong trình phát thu nhỏ trên các ứng dụng khác - Nút thay đổi kích cỡ trình phát + Nút thay đổi kích thước trình phát Xóa bỏ các viền đen Phụ đề Cài đặt phụ đề Phụ đề Chromecast Cài đặt phụ đề Chromecast Tốc độ phát - Vuốt để tua nhanh + Vuốt để tua Vuốt ngang qua lại để tua video Vuốt để thay đổi cài đặt Vuốt lên hoặc xuống cạnh trái hoặc phải để điều chỉnh độ sáng hoặc âm lượng @@ -140,44 +140,44 @@ Tự động đồng bộ tiến trình hiện tại của bạn Khôi phục dữ liệu từ bản sao lưu Sao lưu dữ liệu - Đã tải dữ liệu sao lưu + Đã tải tệp sao lưu Không thể khôi phục dữ liệu từ %s - Sao lưu dữ liệu thành công - Thiếu quyền truy cập bộ nhớ, hãy thử lại. + Dữ liệu đã lưu + Thiếu quyền truy cập bộ nhớ. Vui lòng thử lại. Lỗi khi sao lưu %s Tìm kiếm Tài khoản và Bảo mật Cập nhật và Sao lưu Thông tin Tìm kiếm nâng cao - Cho phép tìm kiếm theo bộ lọc từng nhà cung cấp + Cung cấp cho bạn kết quả tìm kiếm được phân loại theo từng nguồn phim Hiển thị tập phụ cho anime Hiển thị trailer Hiển thị poster từ Kitsu Ẩn chất lượng video trong kết quả tìm kiếm - Tự động cập nhật plugin + Tự động cập nhật tiện ích mở rộng Hiển thị thông báo cập nhật ứng dụng Tự động tìm kiếm bản cập nhật mới sau khi khởi động ứng dụng. Github Ứng dụng đọc tiểu thuyết của cùng nhà phát triển Ứng dụng xem Anime của cùng nhà phát triển Tham gia cộng đồng trên Discord - Gửi lời cảm ơn tới nhà phát triển - Gửi lời cảm ơn + Gửi ủng hộ tới nhà phát triển + Gửi ủng hộ Ngôn ngữ ứng dụng Nguồn phim này chưa hỗ trợ Chromecast Không tìm thấy liên kết Đã sao chép liên kết vào bộ nhớ tạm - Xem Phim - Thiết lập lại giá trị mặc định + Phát Tập phim + Đặt lại giá trị mặc định Mùa Không có mùa nào Tập Tập %1$d-%2$d %1$d %2$s - M - T + Mùa + Tập Không có tập nào Xóa Tệp Xóa @@ -196,7 +196,7 @@ Đánh giá Thời lượng Nguồn - Thông tin + Tóm tắt phim Hàng chờ Không có phụ đề Mặc định @@ -231,20 +231,20 @@ Lỗi nguồn từ xa Lỗi kết xuất Đã có lỗi xảy ra. Vui lòng thử lại sau - Lỗi tải xuống. Hãy kiểm tra quyền truy cập bộ nhớ của ứng dụng - Tập Chromecast - Chiếu Chromecast - Xem trong ứng dụng - Xem trong %s + Lỗi tải xuống. Hãy kiểm tra quyền truy cập bộ nhớ + Phát tập phim bằng Chromecast + Phản chiếu màn hình bằng Chromecast + Phát bằng ứng dụng + Phát bằng %s Tự động tải xuống - Nguồn tải xuống - Lấy link mới nhất - Tải phụ đề + Nguồn tải dự phòng + Tải lại các liên kết + Tải xuống phụ đề Nhãn chất lượng phim Nhãn lồng tiếng Nhãn phụ đề - Tên - Thành phần giao diện trên poster + Tên phim + Hiển thị các thông tin trên poster Bạn đang dùng phiên bản mới nhất Kiểm tra cập nhật Khóa @@ -255,17 +255,17 @@ Bỏ qua bản cập nhật này Cập nhật Chất lượng xem ưu tiên (WiFi) - Số ký tự tối đa trên tiêu đề trình phát video + Số ký tự tối đa tiêu đề trình phát Hiện thông tin trình phát Kích thước bộ nhớ đệm video Thời lượng bộ nhớ đệm - Lưu bộ nhớ đệm video trên ổ cứng + Bộ nhớ đệm video trên thiết bị Xoá bộ nhớ đệm hình ảnh và video Sẽ gây lỗi nếu đặt quá cao trên thiết bị có bộ nhớ thấp như Android TV. Sẽ gây lỗi nếu đặt quá cao trên máy có dung lượng lưu trữ thấp như Android TV. DNS over HTTPS Rất hữu ích để bỏ chặn ISP - Sao chép trang web + Bản sao trang web Xoá trang web Thêm bản sao của trang hiện có bằng một URL khác Đường dẫn tải xuống @@ -291,8 +291,8 @@ Giao diện giả lập Màu chính Chủ đề ứng dụng - Vị trí tiêu đề - Đặt tiêu đề dưới poster + Vị trí tên phim + Đặt tên phim dưới poster Mật khẩu Tài khoản @@ -328,23 +328,23 @@ Đổ bóng Nâng Chỉnh phụ đề - 1000 mili giây + 1000 ms Độ trễ phụ đề - Dùng nếu phụ đề bị nhanh %d mili giây - Dùng nếu phụ đề bị trễ %d mili giây - Không chỉnh + Dùng nếu phụ đề bị nhanh %d ms + Dùng nếu phụ đề bị trễ %d ms + Không điều chỉnh - Xem trước mẫu phụ đề + Bạch kim rất quý nên sẽ dùng để lắp vô xương Được đề xuất Đã tải %s - Chọn từ máy + Chọn từ tệp Chọn từ Internet - Tệp đã tải + Tệp đã tải xuống Vai chính Vai phụ Lý lịch @@ -376,9 +376,9 @@ Lỗi dữ liệu Lỗi đường dẫn Lỗi - Xoá phụ đề đã dùng - Loại bỏ mã hoá phụ đề - Lọc theo ngôn ngữ media được chuộng hơn + Xóa bỏ chú thích chi tiết trong phụ đề + Xóa bỏ nội dung thừa trong phụ đề + Lọc theo ngôn ngữ ưu tiên Thêm Trailer https://example.com/example.mp4 @@ -394,22 +394,22 @@ Thêm kho tiện ích Tên kho tiện ích (Tùy chọn) URL kho tiện ích hoặc Mã ngắn - Đã tải plugin - Plugin đã xoá + Tiện ích mở rộng đã tải + Tiện ích mở rộng đã xoá Không tải được %s 18+ Đã bắt đầu tải xuống %1$d %2$s… Đã tải xuống %1$d %2$s Toàn bộ %s đã được tải xuống - Tải hàng loạt - plugin - plugin + Tải xuống hàng loạt + tiện ích mở rộng + tiện ích mở rộng Việc này sẽ xóa tất cả tiện ích mở rộng trong kho tiện ích Xoá kho tiện ích - Tải nguồn phim bạn muốn dùng - Đã tải: %d + Tải xuống danh sách các trang web bạn muốn sử dụng + Đã tải xuống: %d Đã vô hiệu: %d - Không tải: %d + Chưa tải xuống: %d CloudStream không có sẵn trang web nào. Bạn cần cài đặt các trang web từ kho lưu trữ. \n \nHãy tham gia Discord của chúng tôi hoặc tìm kiếm trực tuyến. @@ -418,27 +418,27 @@ In hoa toàn bộ phụ đề Cảnh báo: CloudStream không chịu trách nhiệm về các tiện ích mở rộng bên thứ ba và không cung cấp bất kỳ sự hỗ trợ nào! %s (Đã vô hiệu hoá) - Âm thanh & Chất lượng + Âm thanh & Độ phân giải Âm thanh - Chất lượng Video - Áp dụng khi khởi động lại ứng dụng. + Độ phân giải video + Khởi động lại ứng dụng để thấy câc thay đổi. Chế độ an toàn được bật - Đã xảy ra sự cố và chúng tôi đã tự động tắt tất cả các tiện ích mở rộng, hãy tìm và xóa tiện ích mở rộng đang gây ra sự cố. + Tất cả tiện ích mở rộng đã được tắt do ứng dụng bị ngừng bất thường để giúp bạn tìm ra vấn đề gây lỗi. Xem thông tin sự cố Lịch sử Đánh dấu là đã xem - Tự động tải xuống plugin - Làm lại tiến trình cài đặt - Bộ cài APK - Một số thiết bị không hỗ trợ trình cài đặt gói mới. Hãy thử tùy chọn cũ nếu các bản cập nhật không cài đặt. + Tự động tải xuống tiện ích mở rộng + Làm lại tiến trình thiết lập + Trình cài đặt APK + Một số thiết bị không hỗ trợ trình cài đặt gói mới. Hãy thử chọn chế độ tương thích cũ nếu các bản cập nhật không cài đặt. %1$s %2$d%3$s - Xem Trailer - Tự động cài đặt tất cả plugin chưa được cài đặt từ những kho lưu trữ đã thêm vào. + Phát Trailer + Tự động cài đặt tất cả tiện ích mở rộng chưa được cài đặt từ những kho tiện ích đã thêm. Bắt đầu cập nhật Liên kết Danh sách HLS Trình phát ưu tiên - Trình phát mặc định + Trình phát tích hợp Đánh giá: %s Không Phiên bản @@ -447,7 +447,7 @@ Sao lưu Tiện ích mở rộng Hành động - Cache + Bộ nhớ đệm Cử chỉ Tính năng trình phát Phụ đề @@ -464,41 +464,41 @@ Cài đặt tiện ích mở rộng trước Không thấy ứng dụng Tất cả ngôn ngữ - Tua %s + Bỏ qua %s Mở đầu Kết thúc - Tóm tắt - Các kết thúc hỗn hợp - Các mở đầu hỗn hợp + Điểm lại nội dung + Kết thúc hỗn hợp + Mở đầu hỗn hợp Danh đề Giới thiệu Xoá lịch sử Hiện các popup bỏ qua cho mở đầu/kết thúc - Văn bản quá dài. Không thể lưu vào khay nhớ tạm. + Văn bản quá dài. Không thể lưu vào bộ nhớ tạm. Xoá khỏi đã xem Bạn có chắc muốn thoát? - Đang tải bản cập nhật… + Đang tải xuống bản cập nhật… Đang cài bản cập nhật… Không thể cài đặt phiên bản mới - Ứng dụng sẽ được cập nhật khi thoát + Ứng dụng sẽ được cập nhật sau khi thoát Thư viện Trình duyệt - Plugin đã tải - Legacy + Tiện ích mở rộng đã tải xuống + Chế độ tương thích cũ Đã cập nhật (Mới đến Cũ) Đã cập nhật (Cũ đến Mới) Thư viện của bạn đang trống :( \nĐăng nhập vào tài khoản thư viện hoặc thêm phim vào thư viện cục bộ. - Mở với + Mở bằng Siêu dữ liệu không được cung cấp bởi trang web, video sẽ không tải được nếu nó không tồn tại trên trang web. - PackageInstaller + Trình cài đặt gói Sắp xếp Xếp hạng (Cao đến Thấp) Xếp hạng (Thấp đến Cao) Chữ cái (Z đến A) Sắp xếp theo - Danh sách này trống, hãy thử chuyển sang danh sách khác. + Danh sách này trống. Vui lòng thử chuyển sang danh sách khác. Chữ cái (A đến Z) Chọn Thư viện Nhật ký @@ -514,18 +514,18 @@ Đã đăng kí %s Tập %d đã ra mắt! Đã đăng kí - Dừng lại + Dừng Bỏ chặn nhà mạng Đã bỏ đăng ký %s Tìm thấy tệp Safe mode! \nKhông tải bất cứ tiện ích mở rộng nào khi khởi động cho đến khi loại bỏ tệp. - Đảo ngược lại + Hoàn tác Đang cập nhật các phim đã đăng kí Bỏ chặn các URL gốc của GitHub bằng jsDelivr. Có thể làm cập nhật bị trễ vài ngày. - Thời lượng tua khi trình phát bị ẩn - Thời lượng tua - Lượng tua thêm được sử dụng khi trình phát hiện lên - Thời lượng tua + Lượng thời gian tua được dùng khi trình phát đang bị ẩn + Thời lượng tua khi trình phát đang ẩn + Lượng thời gian tua được dùng khi trình phát đang hiển thị + Thời lượng tua khi trình phát đang hiện Hồ sơ %d Dữ liệu di động Đặt mặc định @@ -542,14 +542,14 @@ \nSẽ có mức độ ưu tiên video kết hợp là 10. \n \nLƯU Ý: Nếu tổng là 10 hoặc nhiều hơn, trình phát sẽ tự động bỏ tải khi liên kết đó được tải! - Các chất lượng + Chất lượng Bạn đã bình chọn Vô hiệu hoá Không tìm thấy kho tiện ích, hãy kiểm tra URL và thử lại với VPN Không tìm thấy tiện ích mở rộng Không thể khởi tạo UI, đây là một LỖI LỚN và cần được báo cáo ngay lập tức tới %s - Chọn chế độ để lọc plugin tải xuống - %s đã loại bỏ khỏi mục yêu thích + Chọn chế độ lọc tiện ích mở rộng tải xuống + %s đã xóa khỏi mục yêu thích Yêu thích %s đã thêm vào mục yêu thích Các mục có thể trùng lặp đã được tìm thấy trong thư viện của bạn: @@ -562,60 +562,60 @@ Khóa hồ sơ Thêm vào mục yêu thích Thay thế tất cả - Mã PIN không chính xác. Vui lòng thử lại. + Mã PIN không đúng. Vui lòng thử lại. Hủy đăng ký Mã PIN phải có 4 ký tự Thay thế Thêm vào Đăng ký - Loại bỏ khỏi mục yêu thích - Chọn một tài khoản + Xóa khỏi mục yêu thích + Ai đang xem Có vẻ như một mục có khả năng trùng lặp đã tồn tại trong thư viện của bạn: \'%s.\' \n \nBạn vẫn muốn thêm mục này, thay thế mục hiện có hay hủy hành động? - Nhập PIN + Nhập mã PIN PIN Nhập mã PIN hiện tại Đã đăng nhập với tư cách %s Nhập mã PIN cho %s - Sử dụng tài khoản mặc định - Bỏ qua lựa chọn tài khoản khi khởi động - Quản lý tài khoản - Chỉnh sửa tài khoản + Sử dụng Hồ sơ mặc định + Bỏ qua lựa chọn hồ sơ lúc khởi động + Quản lý hồ sơ + Chỉnh sửa hồ sơ Tải lại liên kết Tìm kiếm tiện ích mở rộng khác Hiển thị đề xuất Kiểm tra tất cả Tiện ích mở rộng Xoay Thông báo tập mới - Chỉnh tốc độ trong trình phát + Thêm tùy chọn tốc độ phát trong trình phát Hiển thị nút xoay màn hình - Kích hoạt chế độ xoay màn hình tự động + Bật tự động xoay màn hình theo hướng của video Tự động xoay đã sao chép! - Vấn đề truy cập Bảng ghi tạm, Hãy thử lại. - Lỗi sao chép, Hãy sao chép logcat và liên hệ hỗ trợ ứng dụng. + Lỗi truy cập Bộ nhớ tạm, Vui lòng thử lại. + Lỗi sao chép, Vui lòng sao chép logcat và liên hệ hỗ trợ ứng dụng. Yêu thích OK - Vô hiệu Tối ưu pin - Không thể mở thông tin ứng dụng của CloudStream. - Không thích + Tắt tối ưu hóa pin + Không thể mở thông tin ứng dụng CloudStream. + Bỏ yêu thích Mở khóa Cloudstream Nhạc Sách nói - Khóa với sinh trắc học + Khóa bằng sinh trắc học %s\ncòn lại Xác thực bằng sinh trắc học không được hỗ trợ trên thiết bị này - Mật khẩu/PIN Xác thực + Xác thực bằng Mật khẩu/PIN Dữ liệu CloudStream của bạn hiện đã được sao lưu. Mặc dù khả năng xảy ra điều này là rất thấp nhưng tất cả các thiết bị đều có thể hoạt động khác nhau. Trong trường hợp hiếm gặp là bạn bị khóa truy cập ứng dụng, hãy xóa hoàn toàn dữ liệu ứng dụng và khôi phục từ bản sao lưu. Chúng tôi rất xin lỗi vì bất kỳ sự bất tiện nào phát sinh từ việc này. Mở khóa ứng dụng bằng Vân tay, Khuôn mặt, PIN, Hình vẽ và Mật khẩu. - Màn hình bị đóng sau nhiều lần thử thất bại. Hãy khởi động lại ứng dụng. + Sau vài lần thử thất bại, hộp thoại sẽ tự đóng. Chỉ cần khởi động lại ứng dụng để thử lại. Bài kiểm tra này chỉ dành cho các nhà phát triển và không xác nhận hay phủ nhận việc hoạt động của bất kỳ tiện ích mở rộng nào. Chế độ tiêu thụ pin của ứng dụng đã được đặt ở mức không giới hạn Phương tiện Tên và URL kho tiện ích Đặt lại - Để đảm bảo quá trình tải xuống và thông báo cho các chương trình truyền hình đã đăng ký không bị gián đoạn, CloudStream cần có quyền chạy ở chế độ nền. Bằng cách nhấn OK, một hộp thoại yêu cầu sẽ hiển thị. Hãy nhấn \"Cho phép\".\n\nXin lưu ý, quyền này không có nghĩa là CS3 sẽ làm hao pin của bạn. Nó sẽ chỉ hoạt động ở chế độ nền khi cần thiết, chẳng hạn như khi nhận được thông báo hoặc tải xuống video từ các tiện ích mở rộng chính thức. + Để đảm bảo quá trình tải xuống và thông báo cho các chương trình truyền hình đã đăng ký không bị gián đoạn, CloudStream cần có quyền chạy ở chế độ nền. Bằng cách nhấn OK, một hộp thoại yêu cầu sẽ hiển thị. Vui lòng nhấn \"Cho phép\".\n\nXin lưu ý, quyền này không có nghĩa là CS3 sẽ làm hao pin của bạn. Nó sẽ chỉ hoạt động ở chế độ nền khi cần thiết, chẳng hạn như khi nhận được thông báo hoặc tải xuống video từ các tiện ích mở rộng chính thức. Mùa %1$d Tập %2$d sẽ được phát hành vào Sắp tới sau %s Chọn thiết bị truyền @@ -627,12 +627,12 @@ CloudStream Wiki Truy cập %s trên điện thoại hoặc máy tính và nhập mã bên trên Mã PIN đã hết hạn! - Mã sẽ hết hạn trong %1$dm %2$ds - Không lấy được mã PIN, hãy thử xác thực cục bộ - Hiện không có bản tải xuống nào. + Mã sẽ hết hạn trong %1$d phút %2$d giây + Không lấy được mã PIN, vui lòng thử xác thực cục bộ + Không có tải xuống nào. Xác thực cục bộ Phản chiếu màn hình - Xem từ đầu + Phát từ đầu Mở video có sẵn Cảnh báo Ngày phát hành (Mới đến Cũ) @@ -648,20 +648,18 @@ Bạn có chắc chắn muốn xóa vĩnh viễn các tập trong %1$s? \n \n%2$s - Bạn cũng sẽ xóa vĩnh viễn tất cả các tập trong loạt phim sau: -\n -\n%s + Bạn cũng sẽ xóa vĩnh viễn tất cả các tập trong loạt phim: \n \n%s Bạn có chắc chắn muốn xóa vĩnh viễn tất cả các tập trong loạt phim này không? \n \n%s - Xóa plugin + Xóa tiện ích mở rộng Ngày phát hành (Cũ đến mới) - Ẩn tên các nút điều khiển - Bật chế độ xem trước hình thu nhỏ trên seekbar - Xem trước Seekbar - Chưa tải phụ đề + Ẩn tên các nút điều khiển trình phát + Bật xem trước hình thu nhỏ trên thanh tua + Xem trước trên thanh tua + Chưa tải phụ đề nào Xác nhận trước khi thoát - Hiện hộp thoại xác nhận thoát ứng dụng + Hiện hộp thoại xác nhận trước khi thoát ứng dụng Không hiển thị Hiển thị Vị trí thư mục sao lưu @@ -683,16 +681,16 @@ Xếp hạng %s Ngày %s Tập %s - Cập nhật plugin - Không có plugin nào được cập nhật. - Ngày phát hành (Cũ nhất) - Đã cập nhật thành công %d plugin! + Cập nhật tiện ích mở rộng + Không có tiện ích mở rộng nào được cập nhật. + Ngày phát sóng (Cũ nhất) + Đã cập nhật thành công %d tiện ích mở rộng! Xếp hạng (Cao nhất) - Cập nhật plugin thủ công + Cập nhật tiện ích mở rộng thủ công Tập (Giảm dần) - Bắt đầu quá trình cập nhật plugin! + Bắt đầu quá trình cập nhật tiện ích mở rộng! Thông báo trình phát - Thông báo trình phát để điều khiển phát lại từ nền + Điều khiển phát lại trong nền bằng thông báo của trình phát Bắt đầu nói… Tìm kiếm giọng nói không khả dụng Trực tuyến @@ -705,33 +703,33 @@ Luôn hỏi Nhãn đánh giá Số lượng mục khác nhau có thể tải xuống cùng lúc - Tải xuống song song + Tải xuống đồng thời Kết nối đồng thời - Số lượng kết nối đồng thời có thể sử dụng cho mỗi lượt tải - Đến mục tải xuống + Số lượng kết nối đồng thời mà mỗi lần tải xuống có thể sử dụng + Đến mục Tải xuống Không có kết nối Internet. \n\nVui lòng kết nối Internet rồi thử lại, hoặc xem các nội dung đã tải xuống khi đang ngoại tuyến. Thay đổi khung hiển thị màn hình Vượt khung - Thay đổi kích thước của hình poster + Thay đổi kích thước poster Kích thước poster Tăng tốc độ phát khi nhấn giữ Nhấn giữ để tăng tốc độ phát 2x - %1$dh %2$dm %3$ds - %1$dm %2$ds - %1$ds + %1$d giờ %2$d phút %3$d giây + %1$d phút %2$d giây + %1$d giây Không có tài khoản - Đổi hình đại điện - Nhập url hình đại diện - Không tìm thấy url - URL hoặc hình không hợp lệ - Tải hình lên thành công + Chỉnh sửa ảnh Hồ sơ + Nhập URL ảnh Hồ sơ + Không tìm thấy URL + URL hoặc ảnh không hợp lệ + Đã cập nhật ảnh thành công Đánh dấu là đã xem đến tập này Xóa những tập đã xem đến tập này Đã tải lại - Tải lại nguồn phát + Tải lại nguồn phim Tên Độ phân giải và tên - Xem phản chiếu" + Phản chiếu màn hình" Căn chỉnh phụ đề Dưới trái Dưới giữa @@ -749,25 +747,25 @@ Cài đặt phiên bản phát hành trước Bản phát hành trước đã được cài đặt. Cài đặt bản phát hành trước thất bại. - Thông tin tập + Tập Bật bộ lọc độ sáng khi độ sáng màn hình vượt quá 100% - Hiển thị bảng điều khiển + Hiển thị bảng diễn viên Thông tin video Độ sáng bổ sung Tên nguồn Hàng đợi tải xuống - Hiện tại không có tệp nào đang chờ tải xuống. - Hãy quyết định cách sắp xếp các nguồn video trong trình phát. + Không có tải xuống đang chờ nào. + Quyết định cách sắp xếp các nguồn video trong trình phát. Ưu tiên nguồn Tải xuống tất cả Hủy tất cả Bạn có muốn tải xuống tập %s không? - Bạn có muốn hủy tất cả các lượt tải xuống đang chờ xử lý không? + Bạn có muốn hủy tất cả tải xuống đang chờ không? %d đang tải xuống - %d lượt tải xuống đang chờ xử lý + %d tải xuống đang chờ Đã bật độ sáng bổ sung Hiển thị lớp phủ siêu dữ liệu trình phát 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 c8f9df9a2..78ba57310 100644 --- a/app/src/main/res/values-b+zh+TW/strings.xml +++ b/app/src/main/res/values-b+zh+TW/strings.xml @@ -763,4 +763,5 @@ 在「設定/影片來源/首選媒體」中啟用 Torrent下載 軟體解碼使程式可以播放裝置不支援的影片,但可能導致播放高解析的影片時的延遲或不穩定。 音量已超過 100% + 顯示播放器元資料遮罩層 diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml new file mode 100644 index 000000000..1aa5f0a92 --- /dev/null +++ b/app/src/main/res/values-sq/strings.xml @@ -0,0 +1,417 @@ + + + %1$s Ep %2$d + Kasti: %s + Episodi %d do të publikohet në + Sezoni %1$d Episodi %2$d do të publikohet në + %1$dd %2$do %3$dm + %1$do %2$dm + %dm + %1$do %2$dm %3$ds + %1$dm %2$ds + %1$ds + Posteri + Posteri + Posteri i episodit + Posteri kryesor + Tjetër rastësore + Kthehu prapa + Luaj nga fillimi + Ndrysho ofruesin + Parashikim sfondi + Shpejtësia (%.2fx) + Vlerësuar: %.1f + U gjet update i ri!\n%1$s -> %2$s + Filler + %d min + CloudStream + Luaj me CloudStream + Kryefaqja + Kërko + Shkarkimet + Radhë shkarkimesh + Cilësimet + Kërko… + Kërko %s… + Njohja e të folurit nuk është e disponueshme + Fillo të flasësh… + Nuk ka të dhëna + Më shumë opsione + Episodi i radhës + Zhanri + Shpërndaje + Hap në Browser + Browser + Anashkalo ngarkimin + Duke u ngarkuar… + Duke parë + Në pritje + Përfunduar + Braktisur + Planifikoni të shikoni + Duke rishikuar + Luaj Filmin + Luaj Trailerin + Luaj Transmetimin Live + Transmeto Torrent + Luaj serinë e plotë + Kjo video është një Torrent, kjo do të thotë që aktiviteti juaj i videos mund të gjurmohet.\nSigurohuni që kuptoni Torrent-in para se të vazhdoni. + Burime + Titrat + Ripovo lidhjen… + Kthehu mbrapa + Luaj Episodin + Shkarko + U shkarkua + Duke u shkarkuar + Shkarkimi u ndërpre + Shkarkimi filloi + Shkarkimi dështoi + Shkarkimi u anulua + Shkarkimi u krye + Zgjidhni artikuj për të fshirë + Momentalisht nuk ka shkarkime. + Momentalisht nuk ka shkarkime në radhë. + E disponueshme për shikim offline + Zgjidh të gjitha + Çzgjidh të gjitha + Update-i filloi + Transmetim në rrjet + Hap video lokale + Gabim gjatë ngarkimit të lidhjeve + Lidhjet u ringarkuan + Memorie e brendshme + Dublim + Titra + Fshi Skedarin + Luaj skedarin + Rifillo shkarkimin + Ndalo shkarkimin + Më shumë info + Fsheh + Luaj + Informacion + Filtro të ruajturat + Të ruajturat + Hiq + Vendos statusin e shikueshmërisë + Apliko + Kopjo + Mbyll + Fshi + Ruaj + Emri i repository-t dhe URL + U kopjua! + Njoftimi i episodeve të reja + Kërko në shtesat e tjera + Shfaq rekomandime + Shpejtësia e luajtësit + Cilësimet e titrave + Ngjyra e tekstit + Ngjyra e konturit + Ngjyra e sfondit + Ngjyra e faqes + Lloji i konturit + Lartësia e titrave + Stili i shkrimit + Madhësia e shkrimit + Kërko duke përdorur ofruesit + Kërko duke përdorur tipet + %d Banane të dhëna per developer-at + Asnjë Banane e dhënë + Zgjedhje automatike e gjuhës + Shkarko gjuhët + Gjuha e titrave + Mbaje shtupur për ti rikthyer në gjendjen fillestare + Importo stilin e shkrimit duke i vendosur në %s + Vazhdo shikimin + Hiq + Më shumë informacion + \@string/home_play + Një VPN mund të nevojitet që ky ofrues të funksionojë në rregull + Ky ofrues është Torrent, rekomandohet një VPN + Metadata nuk ofrohet nga kjo faqe, ngarkimi i videos do të dështojë nëse nuk ekziston në këtë faqe. + Përshkrimi + Skenari nuk u gjet + Përshkrimi nuk u gjet + Shfaq regjistrin Logcat 🐈 + Rregjistër + Imazh-brenda-imazhit + Vazhdo shikimin në një luajtës të vogël mbi aplikacionet e tjera + Butoni për ndryshimin e madhësisë së luajtësit + Hiq kufijtë e errët + Titrat + Cilësimet e titrave të luajtësit + Titrat e Chromecast + Cilësimet e titrave të Chromecast + Shpejtësia e rikthimit + Shto një opsion shpejtësie në luajtës + Rrëshqit për të kaluar + Rrëshqit nga njëra anë në tjetrën për të lëvizur pozicionin e shikimit në video + Rrëshqit per te ndryshuar cilësimet + Rrëshqit lart ose poshtë në të djathtë ose të majtë për të ndryshuar nivelin e ndriçimit dhe volumin + Luaj episodin tjetër automatikisht + Luaj episodin tjetër kur episodi aktual mbaron + Shtyp dy herë për të kaluar + Shtyp dy herë për të ndaluar + Sasia e kalimit të luajtësit (sekonda) + Shtyp dy herë në të djathtë ose të majtë për ta kaluar para ose mbrapa + Shtyp dy herë në mes për të ndaluar + Përdor nivelin e ndriçimit të sitemit + Përdor ndriçimin e sistemit në luajtësin e aplikacionit në vend të një mbivendosjeje të errët + Ndriçim ekstra + Aktivizo filtrin e ndriçimit kur ndriçimi i ekranit kalon 100% + extra_brightness_enabled + Përditëso progresin e shikimit + Sinkronizo automatikisht progresin e episodit aktual + Rikthe të dhënat nga kopja rezervë + Të dhënat e kopjes rezervë + Shpeshtësia e kopjimit rezervë + Kopja rezervë u ngarkua + Dështoi rikthimi i të dhënave nga skedari %s + Të dhënat u ruajtën + Lejet për ruajtje mungojnë. Ju lutem provoni përsëri. + Gabim gjatë kopjimit rezervë të %s + Kërko + Bilblioteka + Llogaritë dhe Siguria + Përditësime dhe Kopje Rezervë + Informacion + Kërkim i avancuar + Tregon rezultatet e kërkimit të ndara sipas ofruesit + Kërkime të sugjeruara + Trego sugjerimet ndërkohë që shkruani + Pastro sugjerimet + Trego episodin Filler për anime + Shfaq informacionin e Metadata-s mbi video + Shfaq trailerat + Shfaq posterat nga Kitsu + Shfaq panelin e aktorëve + Mos shfaq cilësinë e videos së përzgjedhur në rezultatet e kërkimit + Përditësim automatik i shtesave + Shkarkim automatik i shtesave + Zgjidh modalitetin për të filtruar shtesat që shkarkohen + Instalo automatikisht të gjitha shtesat që janë shtuar dhe ende nuk janë instaluar nga repository-t. + Shfaq përditësimet e aplikacionit + Kërko automatikisht për përditësime të reja kur hapet aplikacioni. + Ribëj procesin e konfigurimit + Instalo versione beta + Versioni beta është i instaluar. + Dështoi instalimi i versionit beta. + Instaluesi i APK-ve + Disa pajisje nuk mbështesin instaluesin e ri të paketës. Provoni opsionin e vjetër nëse përditësimet nuk instalohen. + Github + Aplikacioni Light Novel nga të njëjtit developer-a + Aplikacioni i Anime-ve nga të njëjtit developer-a + Na u bashko në Discord + Jep një banane për developer-at + Banane të dhëna + Gjuha e aplikacionit + Ky ofrues nuk suporton Chromecast + Nuk u gjetën lidhje + Lidhja u kopjua në kujtesën e përkohshme + Luaj Episodin + Rikthe në vlerën fillestare + Sezoni + %1$s %2$d%3$s + Asnje sezon + Episodi + Episodet + %1$d-%2$d + %1$d %2$s + Vazhdon në %s + S + E + Nuk u gjet asnjë episod + Fshij + Fshij skedarin + Fshij skedarët + Fshij (%1$d | %2$s) + Anulo + Ndalo + Fillo + Dështoi + Kaloi + Paralajmërim + Vazhdo + -30 + +30 + Kjo do të fshijë përgjithmonë %s\nJeni të sigurt? + Jeni të sigurt që dëshironi të fshini përgjithmonë artikujt e mëposhtëm?\n\n%s + Jeni të sigurt që dëshironi të fshini përgjithmonë episodet e mëposhtme në %1$s?\n\n%2$s + Do të fshini gjithashtu përgjithmonë të gjitha episodet në seritë e mëposhtme:\n\n%s + Jeni të sigurt që dëshironi të fshini përfundimisht të gjitha episodet në serinë e mëposhtme?\n\n%s + %dm\ntë mbetura + %s\ntë mbetura + Në vazhdim + Përfunduar + Statusi + Viti + Vlerësimi + Kohëzgjatja + Faqja + Sinopsisi + në rradhë + Pa titra + Parazgjedhur + Falas + Përdorur + Aplikacion + Filmat + Seriale televizive + Vizatimore + Anime + Torrents + Dokumentarë + OVA + Drama aziatike + Transmetime live + NSFW + Tjera + Film + Seri + Vizatimorë + Anime + OVA + Torrent + Dokumentar + Dramë aziatike + Transmetim live + NSFW + Video + Muzikë + Libër audio + Media + Audio + Podkast + Gabim në burim + Gabim në distancë + Gabim në renderer + Gabim në enkodim + Gabim i pasuportueshëm + Gabim i papritur i luajtësit + Gabim në shkarkim, kontrolloni lejet e memories + Episod Chromecast-i + Pasqyrim Chromecast + Pasqyrim Cast + Luaj në aplikacion + Luaj pasqyrimin" + Luaj në %s + Shkarko automatikisht + Pasqyrim shkarkimi + Ringarko lidhjet + Shkarko titrat + Etiketa e cilësisë + Etiketa e dublimit + Etiketa e titrave + Etiketa e vlerësimit + Titulli + Teksti i episodit + Zgjidh elementët e ndërfaqes mbi poster + Nuk u gjet asnjë përditësim + Kontrollo për përditësim + Çelës + Ndrysho madhësinë + Burimi + Kalo Intron + Mos e shfaq përsëri + Anashkalo këtë përditësim + Përditëso + Cilësia e preferuar e shikimit (WiFi) + Cilësia e preferuar e shikimit (Mobile Data) + Numri maksimal i karaktereve të titullit + Shfaq informacionin e luajtësit + Madhësia e bufferit të videos + Gjatësia e bufferit të videos + Video cache në disk + Pastro cache-në e videove dhe imazheve + Sasia e kalimit - Luajtësi i dukshëm + Sasia e kalimit kur luajtësi është i dukshëm + Sasia e kalimit - Luajtësi i fshehur + Sasia e kalimit kur luajtësi është i fshehur + Shkakton keqfunksionim nëse vendoset shumë lartë në pajisje me memorie të ulët, si Android TV. + Shkakton probleme nëse vendoset shumë lartë në pajisje me memorie të ulët, si Android TV. + DNS në vend të HTTPS + E dobishme për të anashkaluar bllokimet e ISP-së + GitHub Proxy + Nuk mund të arrihej GitHub. Po aktivizohet proxy jsDelivr… + Anashkalon bllokimin e URL-ve të GitHub duke përdorur jsDelivr. Mund të shkaktojë vonesa të disa ditëve në përditësime. + Klono faqen + Fshi faqen + Shto një klonim të një faqeje ekzistuese, me një URL tjetër + Destinacioni i shkarkimit + URL e serverit NGINX + Shfaq Anime të Dubluar/me Titra + Përshtat me ekranin + Shtrij + Zmadho + Mohim përgjegjësie + Tejkalues ISP-je + Lidhjet + Përditësimet e aplikacionit + Kopja rezervë + Shtesat + Veprimet + Cache + Android TV + Gjeste + Siguria + Llogaritë + Veçoritë e luajtësit + Titrat + Paraqitja + Parazgjedhjet + Pamja + Veçoritë + Të përgjithshme + Buton i rastësishëm + Shfaq butonin e rastësishëm në Faqen kryesore dhe Bibliotekë + Zgjidh Bibliotekën + Biblioteka juaj është bosh :(\nHyni në një llogari biblioteke ose shtoni shfaqje në bibliotekën tuaj lokale. + Gjuha e shtesave + Paraqitja e aplikacionit + Media e preferuar + Aktivizo NSFW për shtesat që e suportojnë + Enkodimi i titrave + Ofruesit + Testim i ofruesve + Testo të gjitha shtesat + Ky test është vetëm për developer-a dhe nuk verifikon apo mohon funksionimin e asnjë shtese. + Paraqitja + Automatikisht + Paraqitja për TV + Paraqitja për Celular + Paraqitja për emulator + Ngjyra kryesore + Pamja e aplikacionit + Vendodhja e titullit te posterit + Vendos titullin poshte posterit + Fjalëkalimi123 + Emri i përdoruesit + përshëndetje@shqipëri.com + 127.0.0.1 + EmriRiFaqes + https://shembull.com + Kodi i gjuhës (al) + %1$s %2$s + llogaria + Çkycu + Hyr + Autentifikohu lokalisht + Ndrysho llogari + Shto llogari + Krijo një llogari + Shto gjurmim + U shtua %s + Rifresko + Vlerësuar + %d / 10 + /?? + /%d + %s u autentifikua + Nuk mund të hysh në %s + Çaktivizuar + Asnjë + Normal + Të gjitha + diff --git a/fastlane/metadata/android/sq/changelogs/2.txt b/fastlane/metadata/android/sq/changelogs/2.txt new file mode 100644 index 000000000..209d5f4c7 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/2.txt @@ -0,0 +1 @@ +- U shtua regjistri i ndryshimeve! diff --git a/fastlane/metadata/android/sq/full_description.txt b/fastlane/metadata/android/sq/full_description.txt new file mode 100644 index 000000000..f9d6e6f9e --- /dev/null +++ b/fastlane/metadata/android/sq/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 ju lejon të shikoni dhe shkarkoni filma, seriale televizive dhe anime. + +Aplikacioni vjen pa asnjë reklamë dhe analitikë +dhe suporton faqe të shumta trailerash dhe filmash dhe më shumë, p.sh. + +Të ruajturat + +Shkarkime titrash + +Chromecast diff --git a/fastlane/metadata/android/sq/short_description.txt b/fastlane/metadata/android/sq/short_description.txt new file mode 100644 index 000000000..a2a07e19c --- /dev/null +++ b/fastlane/metadata/android/sq/short_description.txt @@ -0,0 +1 @@ +Shiko dhe shkarko filma, seriale televizive dhe anime. diff --git a/fastlane/metadata/android/sq/title.txt b/fastlane/metadata/android/sq/title.txt new file mode 100644 index 000000000..dde89d58f --- /dev/null +++ b/fastlane/metadata/android/sq/title.txt @@ -0,0 +1 @@ +CloudStream From f51885fb6ed19c606bad21f94341bb048534406a Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:00:53 -0600 Subject: [PATCH 038/177] Fix MotionEvent gestures getting stuck in player (#2629) --- .../lagradost/cloudstream3/ui/player/FullScreenPlayer.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 fad4a53e1..eb5ac8f36 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 @@ -1660,7 +1660,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { playerBinding?.playerIntroPlay?.isGone = true // Handle pan with two fingers - if (event.pointerCount == 2 && !isLocked && isFullScreenPlayer && !hasTriggeredSpeedUp && currentTouchAction == null) { + if ((event.pointerCount == 2 || lastPan != null) && !isLocked && isFullScreenPlayer && !hasTriggeredSpeedUp && currentTouchAction == null) { holdhandler.removeCallbacks(holdRunnable) // remove 2x speed // Gesture detectors for zoom & pan @@ -1695,7 +1695,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { lastPan = newPan } - MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_UP -> { + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_UP -> { // Reset touch lastPan = null currentTouchStart = null @@ -1777,7 +1777,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } - MotionEvent.ACTION_UP -> { + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { holdhandler.removeCallbacks(holdRunnable) if (hasTriggeredSpeedUp) { player.setPlaybackSpeed(DataStoreHelper.playBackSpeed) From 1d03b05a7cceeb9656d28f27227f957f8097251d Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:04:39 +0000 Subject: [PATCH 039/177] Refactor: New SkipAPI for SkipStamp (#2601) --- .../ui/player/AbstractPlayerFragment.kt | 6 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 15 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 6 +- .../cloudstream3/ui/player/IPlayer.kt | 8 +- .../ui/player/PlayerGeneratorViewModel.kt | 9 +- .../lagradost/cloudstream3/utils/AniSkip.kt | 230 ------------------ .../cloudstream3/utils/videoskip/AniSkip.kt | 68 ++++++ .../utils/videoskip/IntroDbSkip.kt | 77 ++++++ .../cloudstream3/utils/videoskip/SkipAPI.kt | 104 ++++++++ 9 files changed, 271 insertions(+), 252 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/SkipAPI.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index ea6babb20..1e6d827e6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -54,10 +54,10 @@ import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.AppContextUtils.requestLocalAudioFocus import com.lagradost.cloudstream3.utils.DataStoreHelper -import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage +import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp import java.net.SocketTimeoutException enum class PlayerResize(@StringRes val nameRes: Int) { @@ -127,11 +127,11 @@ abstract class AbstractPlayerFragment( throw NotImplementedError() } - open fun onTimestamp(timestamp: EpisodeSkip.SkipStamp?) { + open fun onTimestamp(timestamp: VideoSkipStamp?) { } - open fun onTimestampSkipped(timestamp: EpisodeSkip.SkipStamp) { + open fun onTimestampSkipped(timestamp: VideoSkipStamp) { } 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 a134ae911..43b281a28 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 @@ -95,14 +95,13 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.DrmExtractorLink -import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.PLAYREADY_UUID import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToLanguageName import com.lagradost.cloudstream3.utils.WIDEVINE_UUID -import io.github.anilbeesetti.nextlib.media3ext.ffdecoder.NextRenderersFactory +import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp import kotlinx.coroutines.delay import okhttp3.Interceptor import org.chromium.net.CronetEngine @@ -884,10 +883,10 @@ class CS3IPlayer : IPlayer { private var currentTextRenderer: TextRenderer? = null } - private fun getCurrentTimestamp(writePosition: Long? = null): EpisodeSkip.SkipStamp? { + private fun getCurrentTimestamp(writePosition: Long? = null): VideoSkipStamp? { val position = writePosition ?: this@CS3IPlayer.getPosition() ?: return null for (lastTimeStamp in lastTimeStamps) { - if (lastTimeStamp.startMs <= position && (position + (toleranceBeforeUs / 1000L) + 1) < lastTimeStamp.endMs) { + if (lastTimeStamp.timestamp.startMs <= position && (position + (toleranceBeforeUs / 1000L) + 1) < lastTimeStamp.timestamp.endMs) { return lastTimeStamp } } @@ -999,7 +998,7 @@ class CS3IPlayer : IPlayer { if (lastTimeStamp.skipToNextEpisode) { handleEvent(CSPlayerEvent.NextEpisode, source) } else { - seekTo(lastTimeStamp.endMs + 1L) + seekTo(lastTimeStamp.timestamp.endMs + 1L) } event(TimestampSkippedEvent(timestamp = lastTimeStamp, source = source)) } @@ -1578,9 +1577,9 @@ class CS3IPlayer : IPlayer { } } - private var lastTimeStamps: List = emptyList() + private var lastTimeStamps: List = emptyList() - override fun addTimeStamps(timeStamps: List) { + override fun addTimeStamps(timeStamps: List) { lastTimeStamps = timeStamps timeStamps.forEach { timestamp -> exoPlayer?.createMessage { _, _ -> @@ -1589,7 +1588,7 @@ class CS3IPlayer : IPlayer { // onTimestampInvoked?.invoke(payload) } ?.setLooper(Looper.getMainLooper()) - ?.setPosition(timestamp.startMs) + ?.setPosition(timestamp.timestamp.startMs) //?.setPayload(timestamp) ?.setDeleteAfterDelivery(false) ?.send() 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 ad7c8915f..16b03e4f6 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 @@ -106,7 +106,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.Qualities @@ -124,6 +123,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.downloader.DownloadUtils.getImageBitmapFromUrl import com.lagradost.cloudstream3.utils.setText import com.lagradost.cloudstream3.utils.txt +import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp import com.lagradost.safefile.SafeFile import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -2052,11 +2052,11 @@ class GeneratorPlayer : FullScreenPlayer() { } } - override fun onTimestampSkipped(timestamp: EpisodeSkip.SkipStamp) { + override fun onTimestampSkipped(timestamp: VideoSkipStamp) { displayTimeStamp(false) } - override fun onTimestamp(timestamp: EpisodeSkip.SkipStamp?) { + override fun onTimestamp(timestamp: VideoSkipStamp?) { if (timestamp != null) { playerBinding?.skipChapterButton?.setText(timestamp.uiText) displayTimeStamp(true) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index 08b8ee795..43ec756ed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -4,8 +4,8 @@ import android.content.Context import android.graphics.Bitmap import android.util.Rational import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle -import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp enum class PlayerEventType(val value: Int) { Pause(0), @@ -86,13 +86,13 @@ data class ErrorEvent( /** Event when timestamps appear, null when it should disappear */ data class TimestampInvokedEvent( - val timestamp: EpisodeSkip.SkipStamp, + val timestamp: VideoSkipStamp, override val source: PlayerEventSource = PlayerEventSource.Player, ) : PlayerEvent() /** Event for when a chapter is skipped, aka when event is handled (or for future use when skip automatically ads/sponsor) */ data class TimestampSkippedEvent( - val timestamp: EpisodeSkip.SkipStamp, + val timestamp: VideoSkipStamp, override val source: PlayerEventSource = PlayerEventSource.Player, ) : PlayerEvent() @@ -254,7 +254,7 @@ interface IPlayer { fun updateSubtitleStyle(style: SaveCaptionStyle) fun saveData() - fun addTimeStamps(timeStamps: List) + fun addTimeStamps(timeStamps: List) fun loadPlayer( context: Context, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index d8c5e777c..96468490a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -13,9 +13,10 @@ import com.lagradost.cloudstream3.mvvm.safe import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType +import com.lagradost.cloudstream3.utils.videoskip.SkipAPI +import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -35,8 +36,8 @@ class PlayerGeneratorViewModel : ViewModel() { private val _loadingLinks = MutableLiveData>() val loadingLinks: LiveData> = _loadingLinks - private val _currentStamps = MutableLiveData>(emptyList()) - val currentStamps: LiveData> = _currentStamps + private val _currentStamps = MutableLiveData>(emptyList()) + val currentStamps: LiveData> = _currentStamps private val _currentSubtitleYear = MutableLiveData(null) val currentSubtitleYear: LiveData = _currentSubtitleYear @@ -181,7 +182,7 @@ class PlayerGeneratorViewModel : ViewModel() { if (page != null && meta is ResultEpisode) { _currentStamps.postValue(listOf()) _currentStamps.postValue( - EpisodeSkip.getStamps( + SkipAPI.videoStamps( page, meta, duration, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt deleted file mode 100644 index bbdadbf3f..000000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt +++ /dev/null @@ -1,230 +0,0 @@ -package com.lagradost.cloudstream3.utils - -import android.util.Log -import androidx.annotation.StringRes -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.databind.annotation.JsonSerialize -import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId -import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.ui.result.ResultEpisode -import java.lang.Long.min - -object EpisodeSkip { - private const val TAG = "EpisodeSkip" - - enum class SkipType(@StringRes name: Int) { - Opening(R.string.skip_type_op), - Ending(R.string.skip_type_ed), - 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), - Intro(R.string.skip_type_creddits), - } - - data class SkipStamp( - val type: SkipType, - val skipToNextEpisode: Boolean, - val startMs: Long, - val endMs: Long, - ) { - val uiText = if (skipToNextEpisode) txt(R.string.next_episode) else txt( - R.string.skip_type_format, - txt(type.name) - ) - } - - private val cachedStamps = HashMap>() - - private fun shouldSkipToNextEpisode(endMs: Long, episodeDurationMs: Long): Boolean { - return episodeDurationMs - endMs < 20_000L // some might have outro that we don't care about tbh - } - - suspend fun getStamps( - data: LoadResponse, - episode: ResultEpisode, - episodeDurationMs: Long, - hasNextEpisode: Boolean, - ): List { - cachedStamps[episode.id]?.let { list -> - return list - } - - val out = mutableListOf() - Log.i(TAG, "Requesting SkipStamp from ${data.syncData}") - - if (data is AnimeLoadResponse && (data.type == TvType.Anime || data.type == TvType.OVA)) { - data.getMalId()?.toIntOrNull()?.let { malId -> - val (resultLength, stamps) = AniSkip.getResult( - malId, - episode.episode, - episodeDurationMs - ) ?: return@let null - // because it also returns an expected episode length we use that just in case it is mismatched with like 2s next episode will still work - val dur = min(episodeDurationMs, resultLength) - stamps.mapNotNull { stamp -> - val skipType = when (stamp.skipType) { - "op" -> SkipType.Opening - "ed" -> SkipType.Ending - "recap" -> SkipType.Recap - "mixed-ed" -> SkipType.MixedEnding - "mixed-op" -> SkipType.MixedOpening - else -> null - } ?: return@mapNotNull null - val end = (stamp.interval.endTime * 1000.0).toLong() - val start = (stamp.interval.startTime * 1000.0).toLong() - SkipStamp( - type = skipType, - skipToNextEpisode = hasNextEpisode && shouldSkipToNextEpisode( - end, - dur - ), - startMs = start, - endMs = end - ) - }.let { list -> - out.addAll(list) - } - } - } else if (data.type == TvType.TvSeries || data.type == TvType.AsianDrama) { - val season = episode.season - val imdbId = data.getImdbId() - - if (season != null && imdbId != null) { - val result = IntroDbSkip.getResult( - imdbId, - season, - episode.episode - ) - - result?.let { res -> - listOfNotNull( - res.intro?.let { - val start = it.startMs ?: return@let null - val end = it.endMs ?: return@let null - SkipStamp( - type = SkipType.Opening, - skipToNextEpisode = hasNextEpisode && - shouldSkipToNextEpisode(end, episodeDurationMs), - startMs = start, - endMs = end - ) - }, - res.recap?.let { - val start = it.startMs ?: return@let null - val end = it.endMs ?: return@let null - SkipStamp( - type = SkipType.Recap, - skipToNextEpisode = hasNextEpisode && - shouldSkipToNextEpisode(end, episodeDurationMs), - startMs = start, - endMs = end - ) - }, - res.outro?.let { - val start = it.startMs ?: return@let null - val end = it.endMs ?: return@let null - SkipStamp( - type = SkipType.Credits, - skipToNextEpisode = hasNextEpisode && - shouldSkipToNextEpisode(end, episodeDurationMs), - startMs = start, - endMs = end - ) - } - ).let { out.addAll(it) } - } - } - } - - if (out.isNotEmpty()) - cachedStamps[episode.id] = out - return out - } -} - -// taken from https://github.com/saikou-app/saikou/blob/3803f8a7a59b826ca193664d46af3a22bbc989f7/app/src/main/java/ani/saikou/others/AniSkip.kt -// the following is GPLv3 code https://github.com/saikou-app/saikou/blob/main/LICENSE.md -object AniSkip { - private const val TAG = "AniSkip" - suspend fun getResult( - malId: Int, - episodeNumber: Int, - episodeLength: Long - ): Pair>? { - return try { - val url = - "https://api.aniskip.com/v2/skip-times/$malId/$episodeNumber?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=${episodeLength / 1000L}" - Log.i(TAG, "Requesting $url") - - val a = app.get(url) - val res = a.parsed() - Log.i(TAG, "Found ${res.found} with ${res.results?.size} results") - if (res.found && !res.results.isNullOrEmpty()) (res.results[0].episodeLength * 1000).toLong() to res.results else null - } catch (t: Throwable) { - Log.i(TAG, "error = ${t.message}") - logError(t) - null - } - } - - data class AniSkipResponse( - @JsonSerialize val found: Boolean, - @JsonSerialize val results: List?, - @JsonSerialize val message: String?, - @JsonSerialize val statusCode: Int - ) - - data class Stamp( - @JsonSerialize val interval: AniSkipInterval, - @JsonSerialize val skipType: String, - @JsonSerialize val skipId: String, - @JsonSerialize val episodeLength: Double - ) - - data class AniSkipInterval( - @JsonSerialize val startTime: Double, - @JsonSerialize val endTime: Double - ) -} - -object IntroDbSkip { - private const val TAG = "IntroDb" - - suspend fun getResult( - imdbId: String, - season: Int, - episode: Int, - ): IntroDbResponse? { - return try { - val url = - "https://api.introdb.app/segments?imdb_id=$imdbId&season=$season&episode=$episode" - app.get(url).parsed() - } catch (t: Throwable) { - Log.i(TAG, "error = ${t.message}") - logError(t) - null - } - } - - data class IntroDbResponse( - @JsonProperty("imdb_id") val imdbId: String?, - val season: Int?, - val episode: Int?, - val intro: Segment?, - val recap: Segment?, - val outro: Segment?, - ) - - data class Segment( - @JsonProperty("start_sec") val startSec: Double?, - @JsonProperty("end_sec") val endSec: Double?, - @JsonProperty("start_ms") val startMs: Long?, - @JsonProperty("end_ms") val endMs: Long?, - val confidence: Double?, - @JsonProperty("submission_count") val submissionCount: Int?, - @JsonProperty("updated_at") val updatedAt: String?, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt new file mode 100644 index 000000000..0db90afea --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/AniSkip.kt @@ -0,0 +1,68 @@ +package com.lagradost.cloudstream3.utils.videoskip + +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.lagradost.cloudstream3.AnimeLoadResponse +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.ui.result.ResultEpisode + +// taken from https://github.com/saikou-app/saikou/blob/3803f8a7a59b826ca193664d46af3a22bbc989f7/app/src/main/java/ani/saikou/others/AniSkip.kt +// the following is GPLv3 code https://github.com/saikou-app/saikou/blob/main/LICENSE.md +class AniSkip : SkipAPI() { + override val name: String = "AniSkip" + override val supportedTypes: Set = setOf(TvType.Anime, TvType.OVA) + + override suspend fun stamps( + data: LoadResponse, + episode: ResultEpisode, + episodeDurationMs: Long + ): List? { + if (data !is AnimeLoadResponse) return null // Filter actual anime + + val malId = data.getMalId()?.toIntOrNull() ?: return null + val url = + "https://api.aniskip.com/v2/skip-times/$malId/${episode.episode}?types[]=ed&types[]=mixed-ed&types[]=mixed-op&types[]=op&types[]=recap&episodeLength=${episodeDurationMs / 1000L}" + + val response = app.get(url).parsed() + + // because it also returns an expected episode length we use that just in case it is mismatched with like 2s next episode will still work + return response.results?.mapNotNull { stamp -> + val skipType = when (stamp.skipType) { + "op" -> SkipType.Opening + "ed" -> SkipType.Ending + "recap" -> SkipType.Recap + "mixed-ed" -> SkipType.MixedEnding + "mixed-op" -> SkipType.MixedOpening + else -> null + } ?: return@mapNotNull null + val end = (stamp.interval.endTime * 1000.0).toLong() + val start = (stamp.interval.startTime * 1000.0).toLong() + SkipStamp( + type = skipType, + startMs = start, + endMs = end, + ) + } + } + + data class AniSkipResponse( + @JsonSerialize val found: Boolean, + @JsonSerialize val results: List?, + @JsonSerialize val message: String?, + @JsonSerialize val statusCode: Int + ) + + data class Stamp( + @JsonSerialize val interval: AniSkipInterval, + @JsonSerialize val skipType: String, + @JsonSerialize val skipId: String, + @JsonSerialize val episodeLength: Double + ) + + data class AniSkipInterval( + @JsonSerialize val startTime: Double, + @JsonSerialize val endTime: Double + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt new file mode 100644 index 000000000..ce284f3fe --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt @@ -0,0 +1,77 @@ +package com.lagradost.cloudstream3.utils.videoskip + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.ui.result.ResultEpisode + +class IntroDbSkip : SkipAPI() { + override val name = "IntroDb" + + override val supportedTypes = setOf(TvType.TvSeries, TvType.AsianDrama) + + override suspend fun stamps( + data: LoadResponse, + episode: ResultEpisode, + episodeDurationMs: Long + ): List? { + val season = episode.season ?: return null + val imdbId = data.getImdbId() ?: return null + + val url = + "https://api.introdb.app/segments?imdb_id=$imdbId&season=$season&episode=${episode.episode}" + val response = app.get(url).parsed() + + return listOfNotNull( + response.intro?.let { + val start = it.startMs ?: return@let null + val end = it.endMs ?: return@let null + SkipStamp( + type = SkipType.Opening, + startMs = start, + endMs = end + ) + }, + response.recap?.let { + val start = it.startMs ?: return@let null + val end = it.endMs ?: return@let null + SkipStamp( + type = SkipType.Recap, + startMs = start, + endMs = end + ) + }, + response.outro?.let { + val start = it.startMs ?: return@let null + val end = it.endMs ?: return@let null + SkipStamp( + type = SkipType.Credits, + startMs = start, + endMs = end + ) + } + ) + } + + + data class IntroDbResponse( + @JsonProperty("imdb_id") val imdbId: String?, + val season: Int?, + val episode: Int?, + val intro: Segment?, + val recap: Segment?, + val outro: Segment?, + ) + + data class Segment( + @JsonProperty("start_sec") val startSec: Double?, + @JsonProperty("end_sec") val endSec: Double?, + @JsonProperty("start_ms") val startMs: Long?, + @JsonProperty("end_ms") val endMs: Long?, + val confidence: Double?, + @JsonProperty("submission_count") val submissionCount: Int?, + @JsonProperty("updated_at") val updatedAt: String?, + ) +} \ No newline at end of file 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 new file mode 100644 index 000000000..df16d77ca --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/SkipAPI.kt @@ -0,0 +1,104 @@ +package com.lagradost.cloudstream3.utils.videoskip + +import androidx.annotation.StringRes +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.mvvm.safeAsync +import com.lagradost.cloudstream3.ui.result.ResultEpisode +import com.lagradost.cloudstream3.utils.txt +import java.util.concurrent.ConcurrentHashMap + + +enum class SkipType(@StringRes val res: Int) { + Opening(R.string.skip_type_op), + Ending(R.string.skip_type_ed), + 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), + Intro(R.string.skip_type_creddits), +} + +data class SkipStamp( + val type: SkipType, + /** Start position in milliseconds of the skip, where it should start showing up */ + val startMs: Long, + /** End position in milliseconds of the skip, where it will skip to */ + val endMs: Long, + /** Custom visual label instead of using the type. Only use this for content not covered by SkipType */ + val label: String? = null, +) + +data class VideoSkipStamp( + val timestamp: SkipStamp, + val skipToNextEpisode: Boolean, + val source: String, +) { + val uiText = + if (skipToNextEpisode) txt(R.string.next_episode) else + txt( + R.string.skip_type_format, + timestamp.label?.let { txt(it) } ?: txt(timestamp.type.res) + ) +} + +abstract class SkipAPI { + open val name: String = "NONE" + + /** On what types SkipAPI should trigger on */ + abstract val supportedTypes: Set + + /** Get all video skip stamps of the associated episode */ + @Throws + open suspend fun stamps( + data: LoadResponse, + episode: ResultEpisode, + episodeDurationMs: Long, + ): List? { + throw NotImplementedError() + } + + companion object { + private val skipApis: List = listOf(AniSkip(), IntroDbSkip()) + private val cachedStamps = ConcurrentHashMap>() + + /** Get all video timestamps from an episode */ + suspend fun videoStamps( + data: LoadResponse, + episode: ResultEpisode, + episodeDurationMs: Long, + hasNextEpisode: Boolean, + ): List { + cachedStamps[episode.id]?.let { list -> + return list + } + + for (api in skipApis) { + /** Unsupported type, so we do not waste a get call */ + if (!api.supportedTypes.contains(data.type)) { + continue + } + + /** Find first non-empty stamps */ + val stamps = safeAsync { api.stamps(data, episode, episodeDurationMs) } + if (stamps.isNullOrEmpty()) { + continue + } + + return stamps.map { stamp -> + VideoSkipStamp( + timestamp = stamp, + skipToNextEpisode = hasNextEpisode && episodeDurationMs - stamp.endMs < 20_000L, + source = api.name + ) + }.also { stamps -> + /** Put in cache, this is such small data, it should be fine to never clear it */ + cachedStamps[episode.id] = stamps + } + } + return emptyList() + } + } +} + From b5109420274c6cdce5c643cd6d67fb2e33f07553 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:11:40 -0600 Subject: [PATCH 040/177] Bump newpipeextractor to v0.26.0 (#2624) --- 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 482f776b6..3b060a53c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ lifecycleKtx = "2.10.0" material = "1.14.0-alpha09" media3 = "1.9.2" navigationKtx = "2.9.7" -newpipeextractor = "v0.25.2" +newpipeextractor = "v0.26.0" nextlibMedia3 = "1.9.1-0.11.0" nicehttp = "0.4.17" overlappingpanels = "0.1.5" From f28924f704c99fe210e6110b6d7d826ded61d54f Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:17:21 +0000 Subject: [PATCH 041/177] Fix intent launches (#2554) --- app/src/main/AndroidManifest.xml | 38 ++++++++++--------- .../ui/account/AccountSelectActivity.kt | 18 ++++++++- .../cloudstream3/ui/search/SearchFragment.kt | 13 +++++-- .../lagradost/cloudstream3/utils/UIHelper.kt | 6 ++- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 56622aab9..b2c7091b0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -108,14 +108,31 @@ android:launchMode="singleTask" is a bit experimental, it makes loading repositories from browser still stay on the same page no idea about side effects + + Not exported to prevent bypassing the AccountSelectActivity --> + android:supportsPictureInPicture="true" /> + + + + + + + + + + + + @@ -173,7 +190,7 @@ - + @@ -186,21 +203,6 @@ - - - - - - - - - - - - ( binding.searchFilter.isFocusable = true binding.searchFilter.isFocusableInTouchMode = true } - + // Hide suggestions when search view loses focus (phone only) if (isLayout(PHONE)) { binding.mainSearch.setOnQueryTextFocusChangeListener { _, hasFocus -> @@ -572,7 +573,7 @@ class SearchFragment : BaseFragment( removeKey("$currentAccount/$SEARCH_HISTORY_KEY", searchItem.key) searchViewModel.updateHistory() } - + SEARCH_HISTORY_CLEAR -> { // Show confirmation dialog (from footer button) activity?.let { ctx -> @@ -653,7 +654,11 @@ class SearchFragment : BaseFragment( sq?.let { query -> if (query.isBlank()) return@let - mainSearch.setQuery(query, true) + + // Queries are dropped if you are submitted before layout finishes + mainSearch.doOnLayout { + mainSearch.setQuery(query, true) + } // Clear the query as to not make it request the same query every time the page is opened arguments?.remove(SEARCH_QUERY) savedInstanceState?.remove(SEARCH_QUERY) @@ -674,7 +679,7 @@ class SearchFragment : BaseFragment( val hasSuggestions = suggestions.isNotEmpty() binding.searchSuggestionsRecycler.isVisible = hasSuggestions (binding.searchSuggestionsRecycler.adapter as? SearchSuggestionAdapter?)?.submitList(suggestions) - + // On non-phone layouts, redirect focus and handle back button if (!isLayout(PHONE)) { if (hasSuggestions) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index ebd7b2988..c12674816 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -259,10 +259,12 @@ object UIHelper { } // Open activities from an activity outside the nav graph - fun Context.openActivity(activity: Class<*>, args: Bundle? = null) { + fun Context.openActivity(activity: Class<*>, args: Bundle? = null, baseIntent: Intent? = null) { val tag = "NavComponent" try { - val intent = Intent(this, activity) + val intent = baseIntent ?: Intent() + intent.setClass(this, activity) + if (args != null) { intent.putExtras(args) } From f6920fb05d2de026a43c9f28463b46849fe294c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20Civano=C4=9Flu?= Date: Thu, 9 Apr 2026 02:22:57 +0300 Subject: [PATCH 042/177] feat: Force landscape orientation and pillarbox portrait videos on TV and emulator devices. (#2560) --- .../lagradost/cloudstream3/ui/player/FullScreenPlayer.kt | 9 +++++++++ 1 file changed, 9 insertions(+) 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 eb5ac8f36..4bec57f9c 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 @@ -2707,6 +2707,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } override fun playerDimensionsLoaded(width: Int, height: Int) { + // On TV, don't rotate for portrait videos; display with pillarbox (black bars on sides) + if (isLayout(TV or EMULATOR)) { + isVerticalOrientation = false + return + } isVerticalOrientation = height > width updateOrientation() } @@ -2730,6 +2735,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } private fun dynamicOrientation(): Int { + // TV should always remain in landscape mode + if (isLayout(TV or EMULATOR)) { + return ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + } return if (autoPlayerRotateEnabled) { if (isVerticalOrientation) { ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT From e22a596d0c8f203c456c3a2ac1a4806c933239f4 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 9 Apr 2026 09:09:56 +0200 Subject: [PATCH 043/177] Translated using Weblate (Arabic) Currently translated at 100.0% (726 of 726 strings) Translated using Weblate (Albanian) Currently translated at 68.0% (494 of 726 strings) Co-authored-by: 007 Co-authored-by: Hosted Weblate Co-authored-by: hollow04 Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sq/ Translation: Cloudstream/App --- app/src/main/res/values-b+ar/strings.xml | 1 + app/src/main/res/values-sq/strings.xml | 80 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/app/src/main/res/values-b+ar/strings.xml b/app/src/main/res/values-b+ar/strings.xml index 91f8f0e64..17e809d8d 100644 --- a/app/src/main/res/values-b+ar/strings.xml +++ b/app/src/main/res/values-b+ar/strings.xml @@ -780,4 +780,5 @@ %d تنزيل قيد الانتظار %d تنزيل قيد الانتظار + عرض واجهة منبثقة للبيانات الوصفية للمشغِّل diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index 1aa5f0a92..977fd597a 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -414,4 +414,84 @@ Asnjë Normal Të gjitha + Maksimumi + Minimumi + Kontur + Të zhytura + Hije + Të ngritura + Sinkronizo titrat + 1000 ms + Vonesa e titrave + Përdore nëse titrat shfaqen %d ms më herët + Përdore nëse titrat shfaqen %d ms më vonë + Pa vonesë titrash + Dhelpra e shpejtë ngjyra kafe kërcen mbi qenin dembel + E rekomanduar + U ngarkua %s + Ngarko nga skedari + Ngarko nga interneti + Ngarko të parën e disponueshme + Skedar i shkarkuar + Kryesor + Mbështetës + Figurant + Burimi + Rastësor + Së shpejti… + Kamera + Kamera + Kamera + HQ + HD + TS + TC + Blu-ray + WP + DVD + 4K + SD + UHD + HDR + SDR + Web + Imazhi i posterit + Imazhi i kodit QR + Luajtësi + Rezolucioni dhe titulli + Titulli + Rezolucioni + Informacion i medias + ID e pavlefshme + Të dhëna të pavlefshme + URL e pavlefshme + Gabim + Hiq titrat e mbyllura + Hiq të tepërtat nga titrat + Filtro sipas gjuhës së preferuar të medias + Ekstra + Traileri + https://shembull.com/shembull.mp4 + Referues (opsional) + Tjetër + Shiko videot në këto gjuhë + E mëparshme + Kalo konfigurimin + Ndrysho pamjen e aplikacionit për tu përshtatur me pajisjen tuaj + Çfarë dëshironi të shihni + U krye + Shtesat + Shto repository + Emri i repository-t (Opsional) + URL-ja ose kodi i shkurtër i repository-t + Shtesa u ngarkua + Shtesa u shkarkua + Shtesa u fshi + Nuk mund të ngarkohej %s + 18+ + Filloi shkarkimi i %1$d %2$s… + U shkarkuan %1$d %2$s + Të gjitha %s janë shkarkuar tashmë + Asnjë shtesë nuk u gjet në repository + Repository nuk u gjet, verifiko URL-në ose provoje me VPN From 04b22ba4dfe5cadc02534a3768868dd2fdaf2fd6 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:11:15 +0000 Subject: [PATCH 044/177] Small backup fix --- .../java/com/lagradost/cloudstream3/utils/BackupUtils.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 29410ab4d..88cb7481c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -62,6 +62,7 @@ object BackupUtils { AccountManager.ACCOUNT_TOKEN, AccountManager.ACCOUNT_IDS, + // TODO proper getter for string res keys to ensure that they are updated "biometric_key", // can lock down users if backup is shared on a incompatible device "nginx_user", // Nginx user key @@ -103,7 +104,10 @@ object BackupUtils { // Prevent backups from automatically starting downloads KEY_RESUME_IN_QUEUE, KEY_RESUME_PACKAGES, - QUEUE_KEY + QUEUE_KEY, + + // Prevent automatic plugin download after restoring backup + "auto_download_plugins_key2" ) /** false if key should not be contained in backup */ From 8bdc1a83d706713317a17f0e5d01ca3f3186a8eb Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 9 Apr 2026 04:12:53 -0600 Subject: [PATCH 045/177] Use InternalAPI rather than permanent deprecations in PluginManager (#2615) --- .../lagradost/cloudstream3/MainActivity.kt | 2 - .../cloudstream3/plugins/PluginManager.kt | 49 +++++-------------- .../services/SubscriptionWorkManager.kt | 2 +- .../ui/settings/SettingsUpdates.kt | 1 - 4 files changed, 14 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a7c0a8a27..709e92a41 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -274,7 +274,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa * @return true if the str has launched an app task (be it successful or not) * @param isWebview does not handle providers and opening download page if true. Can still add repos and login. * */ - @Suppress("DEPRECATION_ERROR") fun handleAppIntentUrl( activity: FragmentActivity?, str: String?, @@ -1169,7 +1168,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } } - @Suppress("DEPRECATION_ERROR") override fun onCreate(savedInstanceState: Bundle?) { app.initClient(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) 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 ba3357102..feb0ba6d4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -26,6 +26,7 @@ import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.InternalAPI import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent @@ -259,12 +260,8 @@ object PluginManager { * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices. * If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT! */ - @Suppress("FunctionName", "DEPRECATION_ERROR") - @Deprecated( - "Calling this function from a plugin will lead to crashes, use loadPlugin and unloadPlugin", - replaceWith = ReplaceWith("loadPlugin"), - level = DeprecationLevel.ERROR - ) + @Suppress("FunctionName") + @InternalAPI @Throws suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_updateAllOnlinePluginsAndLoadThem(activity: Activity) { assertNonRecursiveCallstack() @@ -340,12 +337,8 @@ object PluginManager { * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices. * If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT! */ - @Suppress("FunctionName", "DEPRECATION_ERROR") - @Deprecated( - "Calling this function from a plugin will lead to crashes, use loadPlugin and unloadPlugin", - replaceWith = ReplaceWith("loadPlugin"), - level = DeprecationLevel.ERROR - ) + @Suppress("FunctionName") + @InternalAPI @Throws suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_downloadNotExistingPluginsAndLoad( activity: Activity, @@ -454,12 +447,8 @@ object PluginManager { * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices. * If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT! */ - @Suppress("FunctionName", "DEPRECATION_ERROR") - @Deprecated( - "Calling this function from a plugin will lead to crashes, use loadPlugin and unloadPlugin", - replaceWith = ReplaceWith("loadPlugin"), - level = DeprecationLevel.ERROR - ) + @Suppress("FunctionName") + @InternalAPI @Throws suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins(context: Context) { assertNonRecursiveCallstack() @@ -480,13 +469,9 @@ object PluginManager { * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices. * If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT! */ - @Suppress("FunctionName", "DEPRECATION_ERROR") + @Suppress("FunctionName") + @InternalAPI @Throws - @Deprecated( - "Calling this function from a plugin will lead to crashes, use loadPlugin and unloadPlugin", - replaceWith = ReplaceWith("loadPlugin"), - level = DeprecationLevel.ERROR - ) suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_hotReloadAllLocalPlugins(activity: FragmentActivity?) { assertNonRecursiveCallstack() @@ -505,12 +490,8 @@ object PluginManager { * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices. * If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT! */ - @Suppress("FunctionName", "DEPRECATION_ERROR") - @Deprecated( - "Calling this function from a plugin will lead to crashes, use loadPlugin and unloadPlugin", - replaceWith = ReplaceWith("loadPlugin"), - level = DeprecationLevel.ERROR - ) + @Suppress("FunctionName") + @InternalAPI @Throws suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_loadAllLocalPlugins(context: Context, forceReload: Boolean) { assertNonRecursiveCallstack() @@ -814,13 +795,9 @@ object PluginManager { * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices. * If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT! */ - @Suppress("FunctionName", "DEPRECATION_ERROR") + @Suppress("FunctionName") + @InternalAPI @Throws - @Deprecated( - "Calling this function from a plugin will lead to crashes, use loadPlugin and unloadPlugin", - replaceWith = ReplaceWith("loadPlugin"), - level = DeprecationLevel.ERROR - ) suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_manuallyReloadAndUpdatePlugins(activity: Activity) { assertNonRecursiveCallstack() diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt index 242f08129..7134650ed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt @@ -97,7 +97,7 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete .build() ) } - @Suppress("DEPRECATION_ERROR") + override suspend fun doWork(): Result { try { // println("Update subscriptions!") 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 2b74eab4c..118d89ac4 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 @@ -67,7 +67,6 @@ class SettingsUpdates : BasePreferenceFragmentCompat() { } } - @Suppress("DEPRECATION_ERROR") override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { hideKeyboard() setPreferencesFromResource(R.xml.settings_updates, rootKey) From ca96aa68916891caf4ba0324bebd52a48079f47d Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 9 Apr 2026 04:16:24 -0600 Subject: [PATCH 046/177] Bump actions (#2588) * Keep gradle/actions/setup-gradle@v5 for now --- .github/workflows/generate_dokka.yml | 2 +- .github/workflows/pull_request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/generate_dokka.yml b/.github/workflows/generate_dokka.yml index e3dac3857..91f03a434 100644 --- a/.github/workflows/generate_dokka.yml +++ b/.github/workflows/generate_dokka.yml @@ -52,7 +52,7 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Set up Android SDK - uses: android-actions/setup-android@v3 + uses: android-actions/setup-android@v4 - name: Generate Dokka run: | diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index a9d480c02..a5a7d56e3 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -27,7 +27,7 @@ jobs: run: ./gradlew assemblePrereleaseDebug lint - name: Upload Artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: pull-request-build path: "app/build/outputs/apk/prerelease/debug/*.apk" From c9a24e198c71202a1d738c5f8d7957d94cc16260 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 9 Apr 2026 04:32:41 -0600 Subject: [PATCH 047/177] Add true configuration cache support for git commit hash (#2285) Co-authored-by: firelight <147925818+fire-light42@users.noreply.github.com> --- app/build.gradle.kts | 78 +++++++++++++------ .../ui/settings/SettingsFragment.kt | 6 +- .../lagradost/cloudstream3/utils/GitInfo.kt | 20 +++++ .../cloudstream3/utils/InAppUpdater.kt | 3 +- app/src/main/res/layout/main_settings.xml | 5 +- 5 files changed, 83 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/GitInfo.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c1e0e1960..0b201d1cb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,26 +13,52 @@ plugins { val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) -fun getGitCommitHash(): String { - return try { - val headFile = file("${project.rootDir}/.git/HEAD") +abstract class GenerateGitHashTask : DefaultTask() { - // Read the commit hash from .git/HEAD - if (headFile.exists()) { - val headContent = headFile.readText().trim() - if (headContent.startsWith("ref:")) { - val refPath = headContent.substring(5) // e.g., refs/heads/main - val commitFile = file("${project.rootDir}/.git/$refPath") - if (commitFile.exists()) commitFile.readText().trim() else "" - } else headContent // If it's a detached HEAD (commit hash directly) - } else { - "" // If .git/HEAD doesn't exist - }.take(7) // Return the short commit hash - } catch (_: Throwable) { - "" // Just return an empty string if any exception occurs + @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val headFile: RegularFileProperty + + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val headsDir: DirectoryProperty + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun generate() { + val head = headFile.get().asFile + + val hash = try { + if (head.exists()) { + // Read the commit hash from .git/HEAD + val headContent = head.readText().trim() + if (headContent.startsWith("ref:")) { + val refPath = headContent.substring(5) // e.g., refs/heads/main + val commitFile = File(head.parentFile, refPath) + if (commitFile.exists()) commitFile.readText().trim() else "" + } else headContent // If it's a detached HEAD (commit hash directly) + } else "" // If .git/HEAD doesn't exist + } catch (_: Throwable) { + "" // Just set to an empty string if any exception occurs + }.take(7) // Get the short commit hash + + val outFile = outputDir.file("git-hash.txt").get().asFile + outFile.parentFile.mkdirs() + outFile.writeText(hash) } } +val generateGitHash = tasks.register("generateGitHash") { + val gitDir = layout.projectDirectory.dir("../.git") + + headFile.set(gitDir.file("HEAD")) + headsDir.set(gitDir.dir("refs/heads")) + + outputDir.set(layout.buildDirectory.dir("generated/git")) +} + android { @Suppress("UnstableApiUsage") testOptions { @@ -47,6 +73,15 @@ android { includeInBundle = false } + androidComponents { + onVariants { variant -> + variant.sources.assets?.addGeneratedSourceDirectory( + generateGitHash, + GenerateGitHashTask::outputDir + ) + } + } + signingConfigs { // We just use SIGNING_KEY_ALIAS here since it won't change // so won't kill the configuration cache. @@ -72,8 +107,6 @@ android { versionCode = 68 versionName = "4.7.0" - resValue("string", "commit_hash", getGitCommitHash()) - manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get() // Reads local.properties @@ -142,11 +175,11 @@ android { } java { - // Use Java 17 toolchain even if a higher JDK runs the build. + // Use Java 17 toolchain even if a higher JDK runs the build. // We still use Java 8 for now which higher JDKs have deprecated. - toolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.jdkToolchain.get())) - } + toolchain { + languageVersion.set(JavaLanguageVersion.of(libs.versions.jdkToolchain.get())) + } } lint { @@ -156,7 +189,6 @@ android { buildFeatures { buildConfig = true - resValues = true viewBinding = true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 097eb2c60..e41109b59 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLandscape import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.GitInfo.currentCommitHash import com.lagradost.cloudstream3.utils.ImageLoader.loadImage import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding @@ -247,7 +248,7 @@ class SettingsFragment : BaseFragment( } val appVersion = BuildConfig.VERSION_NAME - val commitInfo = getString(R.string.commit_hash) + val commitHash = activity?.currentCommitHash() ?: "" val buildTimestamp = SimpleDateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, Locale.getDefault() ).apply { timeZone = TimeZone.getTimeZone("UTC") @@ -255,8 +256,9 @@ class SettingsFragment : BaseFragment( binding.appVersion.text = appVersion binding.buildDate.text = buildTimestamp + binding.commitHash.text = commitHash binding.appVersionInfo.setOnLongClickListener { - clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo $buildTimestamp") + clipboardHelper(txt(R.string.extension_version), "$appVersion $commitHash $buildTimestamp") true } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/GitInfo.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/GitInfo.kt new file mode 100644 index 000000000..58ff44bb2 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/GitInfo.kt @@ -0,0 +1,20 @@ +package com.lagradost.cloudstream3.utils + +import android.content.Context + +/** + * Simple helper to get the short commit hash from assets. + * The hash is generated at build and stored as an asset + * that can be accessed at runtime for Gradle + * configuration cache support. + */ +object GitInfo { + fun Context.currentCommitHash(): String = try { + assets.open("git-hash.txt") + .bufferedReader() + .readText() + .trim() + } catch (_: Exception) { + "" + } +} 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 057923eb0..9380285ca 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -24,6 +24,7 @@ import com.lagradost.cloudstream3.services.PackageInstallerService import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.GitInfo.currentCommitHash import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import okio.BufferedSink @@ -170,7 +171,7 @@ object InAppUpdater { Log.d(LOG_TAG, "Fetched GitHub tag: $updateCommitHash") return Update( - getString(R.string.commit_hash) != updateCommitHash, + currentCommitHash() != updateCommitHash, foundAsset.browserDownloadUrl, updateCommitHash, found.body, diff --git a/app/src/main/res/layout/main_settings.xml b/app/src/main/res/layout/main_settings.xml index ba3774554..5c05599e8 100644 --- a/app/src/main/res/layout/main_settings.xml +++ b/app/src/main/res/layout/main_settings.xml @@ -134,8 +134,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="10dp" - android:text="@string/commit_hash" - android:textColor="?attr/textColor" /> + android:textColor="?attr/textColor" + tools:text="1234567" /> - \ No newline at end of file From bb4e5da5c9c3433968fb91961e7db13f52d3bc7a Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:07:10 -0600 Subject: [PATCH 048/177] Bump androidx libraries (#2607) --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3b060a53c..576ccd782 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,16 @@ # https://docs.gradle.org/current/userguide/plugins.html#sec:version_catalog_plugin_application # https://docs.gradle.org/current/userguide/dependency_versions.html#sec:strict-version [versions] -activityKtx = "1.12.4" +activityKtx = "1.13.0" androidGradlePlugin = "8.13.2" appcompat = "1.7.1" -biometric = "1.4.0-alpha05" +biometric = "1.4.0-alpha06" buildkonfigGradlePlugin = "0.17.1" coil = "3.3.0" colorpicker = "6b46b49" conscryptAndroid = { strictly = "2.5.2" } # 2.5.3 crashes everything constraintlayout = "2.2.1" -coreKtx = "1.17.0" +coreKtx = "1.18.0" desugar_jdk_libs_nio = "2.1.5" dokkaGradlePlugin = "2.1.0" espressoCore = "3.7.0" @@ -44,7 +44,7 @@ tmdbJava = "2.13.0" torrentserver = "7861970" tvprovider = "1.1.0" video = "1.0.0" -workRuntimeKtx = "2.11.1" +workRuntimeKtx = "2.11.2" zipline = "1.24.0" jvmTarget = "1.8" From fe0829ff64fa52dba63d02d5462a900ecc719d4e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:11:17 -0600 Subject: [PATCH 049/177] Bump material (#2609) --- 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 576ccd782..c1048028b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ juniversalchardet = "2.5.0" kotlinGradlePlugin = "2.3.0" kotlinxCoroutinesCore = "1.10.2" lifecycleKtx = "2.10.0" -material = "1.14.0-alpha09" +material = "1.14.0-alpha10" media3 = "1.9.2" navigationKtx = "2.9.7" newpipeextractor = "v0.26.0" From d7b030e7ef446155619eebe3ad5d820a779fcbb1 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:19:57 -0600 Subject: [PATCH 050/177] Update gradle to 9.4.1 (#2610) --- gradle/wrapper/gradle-wrapper.jar | Bin 46175 -> 48966 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 61285a659d17295f1de7c53e24fdf13ad755c379..d997cfc60f4cff0e7451d19d49a82fa986695d07 100644 GIT binary patch delta 39855 zcmXVXQ+TCK*K{%yXUE#HZQHhO+vc9wwrx+$i8*m5wl%T&&+~r&Ngv!-pWN44UDd0q zdi&(t$mh2PCnV6y+L8_uoB`iaN$a}!Vy7BP$w_57W_S6jHBPo!x>*~H3E@!NHJR5n zxF3}>CVFmQ;Faa4z^^SqupNL0u)AhC`5XDvqE|eW zxDYB9iI_{E3$_gIvlD|{AHj^enK;3z&B%)#(R@Fow?F81U63)Bn1oKuO$0f29&ygL zJVL(^sX6+&1hl4Dgs%DC0U0Cgo0V#?m&-9$knN2@%cv6E$i_opz66&ZXFVUQSt_o% zAt3X+x+`1B(&?H=gM?$C(o3aNMEAX%6UbKAyfDlj{4scw@2;a}sZX%!SpcbPZzYl~ z>@NoDW1zM}tqD?2l4%jOLgJtT#~Iz^TnYGaUaW8s`irY13k|dLDknw)4hH6w+!%zP zoWo3z>|22WGFM$!KvPE74{rt7hs(l?Uk7m+SjozYJG7AZA~TYS$B-k(FqX51pZ2+x zWoDwrCVtHlUaQAS%?>?Zcs`@`M)*S6$a-E5SkXYjm`9L>8EtTzxP%`iXPCgUJhF)LmcO8N zeCq?6sCOM!>?In*g-Nf^!FLX_tD>tdP}Qu&LbWx+5!Z5l7?X!!hk3jRFlKDb!=Jb4 z7y6)re6Y!QE1a;yXoZC*S$_|pT`pA*(6Wwg%;_Q+d*jw;i=|e$DQU=EcB-K+hg9=O z{1{BQsH*V!6t5tw;`ONRF!yo~+cF4p}|xHPE&)@e@Lv4qTL%3}vh4G|Gb$6%Eu zF`@mf2gOj$jYquFnvFCfb9%(9@mOC4N7VWF#;_-4Hr`(ikV(L)V=*hH^P3I<8RXOBnd0%J)*S^v*+L=*srT zh$IKKg?&n5H(Rho@`U^AyL=sN%WY)ZC9U)pfGVfaJpz+_n0|qnri_sF-g>-w^_4A;{;3 z2zTOH6bxZt8k`rB(XAAo>wufzcNZRTJSseFF{MmVV&4XVmKoPC0qRQJG-r9i z#yqN9hrZoA&Zp?DMIJLUtN3A!LZ89wr@`lge7butX>Q;1Yyi18b3#kDs|o$Q-f=a? zS;F_#_D1zk={}uf4ziZ+zjshKO^HC9-@G@n%RhXcLA%&TP#874IHEe;@#u!C3X@nY zaHpT0mAZ-N7)vR8Z|0maGSnM=QxJ8gamH0hLc#sW`>p;KU>wz515s9BDjB0eaqI1( z-&+*wV~o4?ha@KJ;U1zi`2(eKXkxc`NMkKxnz>GSlA0~7IHQ4KQWUPKD<}r@FOC_{ zQIDL`U!eq4@;?!9qWmvk%A6XHbxRY5BPh%#HKP`2>-jhY*TfF#gwLOR~f=$-qCq2V;*bz#LtA+nS@}dcA9S9exiGl z^t`RA_OgVRSg5O!GyJTc)4w-v(m~t)U{2ti*am#Q9`)B^wNC!pE9&ktf6^Cgs(3X9 znK~S~S}nNMh1+T6K>hr}(e9VlKKdt<1`D@~mE;aSB-I=?S;M$lD9`O$<99XzLG2F4 zg8`M+SrA_Cb-Bfo#>)U*nB@lBkUE&<;vN{rnAmuX<|-}ae2*aJG4k@$v%Rc;IM}_v z)wgICOxg ze%Zi6xg$romfi!Wy}i| zT8L+Xa*7}ZVYkJGkOKG>+S57jEDu7AiCi}B5m-HgeIInYmDQX8g6_Liajf_Dx@k^H zg*_C0VY^d-Ta|p6or>0LP}E$ZB{BKT?Up&p1Y|j7746nM)xXv!Tbpbo+eiB_F>?By zkhP*}9ZfjtUYuZUHP^ z>k3^hW#o2WXM~+rrPq9-S8e7APJzY^smW%tJr+s9W{Vi(i`b0pOOfxG`?0-rvo|Fu z#?Do52Z*#pPec0jqtd!y(#T zT|aPAx4<9ST0a)9E5r8l8Y4V0L4;bA_y?{VLNbAme_|R39vQ}m8Ix2Ay0~v%g}07A z86rGJYvG6Be5-4ml(;u`uZMOHPvEiySJ7Jm+^Hu3@33Ko4X$4i= z`nC#q;)J6=<0x<*q_BM)Def2(Xf%!7=adUcN5IX)Yw?1f*V=O+4!h3b)2;N{b>uUxh6KU zFO)rh!~d~HK-z83C*6m5@*(L@qJC@#9TY`${f#|l=ZoRMp7&rBx+gM))6PcXsA0v! z5eQ5U2zyP2%erLHmg=vZbWV&{KE@|FET}xun4QZ+j8GfNg+mtsW-R6kjeuGyVnU=K zBiAQ(?wz7!cz3VX?;-Xic;#aO&xN z-%mu;`sXgYc3{cqb|L1|aGf5UQDzrp1yHOB(HMD^+cpK9SIuM4E5cl5UM~-mybU^`JdHZ6$#~n_V)iQ+PAHacfSa#|SN;k`n%p(7#uf)Q> zlHE8+)PczLFiHEnu~aXa{g_hI94R&V(ZF;Wxh%tFIgmzT8f&bA)>us* zNA*!XoNoV-UPx|T<+mz&aZktvj-_f#meX&88P?CcuJY<%Iz z9~lFd)ITw&2kg3C!vE$_NDd!s8Mn5lu-na9mcBg$=B^ioWX6p8iLP&hule^!6j67i0mYIxNfR>X!CfH?G;y9Tl5)Q+4#bAL!BH~e%- zPkNQrOZIc5s*qXJ;9&h7_s5AJYt*oo2A?tQ*WAM`iaFre%Av|~a>uh&Pzl}s%(oCEd$G1=Km=P=^Tf==pM>*RcAANEI6hw9Vl<3&v zSEdp|TFrt)z!kqdUdibz_*TSj9WEbzlm+6Oym9gQk~vz@*OmO2cWHk$mMEtd*b*r7 z)drx#>)3)0d`ZeHYcf+1exTAWv9*UhjwA1*)%MKl5*IH}epmne{i8njH@p|m(oyy( zD{I8)8qH_SnUA6WFkaH2e4`UtYtt5I_@a_w%%E(o8bb0;@{8i`s?+C zGTz{xBP2eyi~$TfW3N(-R|c))j)dk$yggJDLo-Ur;A@or+w#Fuaqk zx#9j&Vv2ob(sZQpA{>3KU?H*Hf87&w!P(9lj3uA8s_0vlDtUVyIOvgPV@#~%%rVt@ zw6BW$7zKDvf#*ftc& z`H~cLVIoq;Ffl<@kX=47^^aG^#9GFmQE6-w$GApb zd5u1D4@*oJ9mk=`1HaHs?x`)mSd1G??$5*?JEn_`4Ckr-e%Lv8 zcB#IIsb5(CF>u-E29hB(7#I%{7?_gmcZlQ@Vk=OvyPfz5I?DDe+*)JmOOPpev2s!5 zIK)0cqIa_;UB%ily_J+%A|T>dKT_6--1`pFwIsG;*K~n)&@9E%hVLui3^)JrM*gqf zFR%tc@a|xLfAk1%?bH-MF}=Myt7mhS#jC-nv-iRC{I#EKf*^9;PGLcO7a!YiedEhe zeMZothG#o&RMk==LcAw{a;bg2&b7K%WTk+4=gLh#9dDO`(_v0oYCTZ|BCdJ7i!ms{ zB=J|Hn`Nc3mWiQn{&&-{ws!}kD9Sim;8}pt^2HC`x{Ay?Roy54c-d-cnHg{7D5K9z zv@o)c)kswkaHTdvQly_s^g+sDyCjBAbP1%W229JAba?|uqOL*t$|KD^5g3dLKn=Xb z9IW_k?k*)kVn>2Rqj3QejshvLqXQ*1NVJuhKbcUhCA`nKZE_RACNfT&L* zI$YUQJO#8X!-yd3ATPe6yf7LIrHOsIX=b_STgI2a#J8f~@@ll&;%8Kx5|0McAwYlI zNs3D#p)W1q4pJN-#V@~&`C6yx!RKxhy`Cpk?OS$q4dS1IV;hOu-vH(l)%`YjbxgI-26N1|9c;#^ zv+fX)nq-IF#F{VG3bBNiglftne*B||U<63~qoRGb*J2JI7MaAxT6Pdd&(djcek2<= zsBapXlGbq_5`*;^l;cX+-Yulze+duS0ywRjUgkT)#(DTchjKp+>*L;RCt;mZ0$n-k z8u*%CMZ{sj|raK-MZ8XXWWlW)mEyE%K ztogoO4IMeUy1H89tZs(Vig2oUO8UKwC9>3rBxqq_g|@NvW(7NtqQTVfAn$BnHFI4O zZ}Lgk1PBRc%zl^=?B=SeX?x|xi9m0-pMZ}xi`&b{XcL+s=~>u6(+ldBR)}&hKUL9P zVzKOnJ?rBrkSm1gfFcFtn7^rsiJ5L4iyp}T`Y6l7WI}Urs8CuV<`%O12R%B%pvcko(+GnA~)yiUirPXJc=q1P_Rh-`zw_0r9tn*fwW6^V^o z)sML@p8m+~EowB=h?CjA+cr9xRfa$NmNxAalqixbE_s7ZUI!@;K82(r`=l&XyUwfq z!`lnA7>3ylx!48Wlgz>P-lb~w$b6a5+oec>)-d-M;nIHp7nFy0n24)&YO=>S0Z(Yp zO+c<;-(@g9FLsB2vu7RO!0A0{9UTU@frfuP7NgNzHlBvJ+!4@JygLpm{!|eyBtPp4 z3ymxmEb*`x(!{EU%z)C~WOHhb@J zfye(U_Ml~XTl7!d_W$<3ishk^C-c#ef)Ds^SywIDI{mDc9%P1WrBo{1tAiAHb$ zy&0#M4f-qfza8F84nQaWL~S&xNQzG|P>PQy{7o@?vfOk|$I}L{<>eEhVJ~=lJjGym zaWU54Hl1|b@B!8q_oTS?5{Gk{K&8em|M=<&KRlvg^r6cQJO zAu8~Z0eU3i>e=5qqP&$9=w_%xFYB^^LO7LLiRHA^|;S4F6ANMoL=;hZq->= zcSZ^2L)TMD99%?aFwzkZ2$=wMj1ihM{noHe=8-z}K}`R$`FI!B97|x@V}UbVRgO1y z5V37pra5X%7**FZt$6qSDskj3OMr8Dr{wqUpW?%Gj+WaI7IGC{QiQ_?6;BUws?iy9 zr?uCbV7fBv7#rQ!;fPu!Qv?;xMp~V;dS54b?$6MVY(Ljrd4$RVQ^uG=kJ!W`a>&%8 z{N;cW{8i2M^VZ4>D@LN0doB%ye<{pMpKn(ja8DnCG4Kjm?9foo%>}4B#jq zqVJ5aYS;aOeS$JPxW(!)UQWD%y-oS6x&B_=UC=)Wuf_ZRPE9$VPrx&G65;!18!SF# z8JNxYs%6L)e=H6SdCNvIkz)F0yeP*PMcXA6ZE&C~|S^US~Pw2fuW)yo8&XHYgy&QKWjlOsY|OFcq}iu28r z#83E>BRjZsGq~O-)*9))zhWJIa`hY?aJ)2j4|v$nY39=H+-39&s0#Ldiy?@So(>2a zR{k?D8-7N01QN4s>pMqB|38Z$v%);7COMHI81xK@5d)h9j70z{1BQk+E)CK`H@l`b z>1|^8B4&1w`%ov;oh^(Z^jTxcA;Af+EMfV9qa=RBm`SstuEtDq=!)Y%g~~VWxT;-_Q6;X z_oe!AJ3ptQr}_)qdK#%}cRtT*3%K zE>9)EnWh)2ol4C@>6=M89Wntx8XnICocs*JfbX5Y`^LX36EK&NUMp1dkspMN`wbHR&eKLgSS?2O;0?>XODKO444mdhRf z4lUz}Wk$%=Dbhd}WWZ;M!Aq@^tg~dG9u`#FVA5G+iaqaX55onBmg`B8VttXe%0v9! z)2!wlh{C+f#(~QiCyFPbH_hBa85E*3DNR0Nq6T>-KgacFeg|M7G1=f5z2nXf>GusU z{SEjTW2bp5OX~@XR;$;VDvN>Wd}vF{A6jjHT95|&jUMh6r5KbbNfCQ8!vAKi~a{NIp-4h91Q0|o|0oZLW$ z@Xsk_2kB~}X#zJ#At;Bm$P3so&9iJ^0~2Trkh_N?Qoq5XE=n}tGr3AhP_Q~%43ugR z>iJ*l2%MQ3`q@`Q>S)^Mzs(cQZO_d+TC`&XRcq6-9{XA5`}a2entZ>RVRQt~8TmFC zO{qBYMlf97!9ojQ-y+ns*xPg-u2Eyp<;}7#0nwDvj5)ySJL%4vWUf<}(xqs3X*BMC zuVa1ZGCpTAk!bSgk~{Z^&4rin?ifHAg~h^%oP_<2hA z^XcLK@xD}z84HB>%@hXfcUEb{c@_iEY=Nd!7E{wbQNxWsmz@^Fp@MXXZG>J|3pEG; z4I;ee&RgnGmN_mbgc(k3NH63T71RG0PflRE{`iTpJLKlGdx$2cs~ z#8YxgR93!?Pa_MMS#63_z!EY`1#~L?P>D>GPxrHj;_*!73POA4irGJjAPSLK24yNF zjbf$m>Y4l`Sij`np_S{rQk5Ir%`!%c77r8E&Anwc=~E{OCD7bp8)m~882=)R17(F6 zObD&-rkQTf<=k@Axu-{*1E#|&3#Jo+7?(=!T7Vwi##NR!xIJTeU{nR^c*UTl{I`83?m6Z#KF(`VcUkH02b)Y)4W%iXpCZe8&hQ%M_lTq3z3t~J&{mi=D-jX*b}n-W`RIpVQMDh z@!aALf&*Y#s!Ucb!7OQ(|JcqI!&O5v?qFBIfoQtNH(62KRLU$};@N$4wJCH+acP-o zZs3E@s(_cicL$IhaggsA{r;O`X6=&A)PucscLa{3d{<@}Ycbl*4MLX3Oh@q#PTRX? zK_mx>oFh4bh`WCU+K&<-t>f8i4K(g7XeJcjV2~LQp9bd_!fy&>438B;{iOHo=>fL8 zHUH)HOTFOnsSDZ$&-hPcTYIv>=V?%%BV|hoGD%R}-kh{wrM`o>N{)}Jl zdZ1P13p<^gUJY^wDb`)}x$+D9p?1SZ6qB5ZKSBI%SI zHb+Y1-B@PDFQ!I+*?GP@Hh|YfAn1Q4`~gZZo`_87mM9sM6AP&b z*s=0$xQNUsHdW%(JSmxvlMke+Y~=NLf7hFU4ew8I@JXm1Qjk zUp67_=$uQ-Q68@wg+JwRa}lRcv(lfLQ?$;9N_SKYSql6k7Gs-fEuPz}(5lhBn@@Yn zLw!L{&LdsFF=h*OoMv$#-8D&{?UE=Uz|4*kU**U7oC+NytdL1gI|*{M=COpy&=5## zLsvg;tf?Emq)D6lL*AsM1Yj4wA#2B0u%qpgk<*Ovv*T}?YKjXn1&mG=QH>h-CAo-c zge6B-8IRB1uSA(RlBe#`iGt?#I5=}2vb?*rqj(2???JkzS4&!ayf>Os!)x@a5jm;= z*k0(h(r(ELR|oD^azGYV)AC^pruZcBf<{iUv4YooTz)KM&)9zUT;w@P%wWH;2=4C- za4pwrs4_yDSf*iVv3my2=o!1&PwlI!zw^O@V`GI#6269RibKU8ImtT9$r2Gb2KjZ> zGm+LxJ8rVfO*3jTW(W6*`-ui~|w(Bq3D6>lIas>>v|P_BfK!>$rw&JI4Uk zbzAuareUX-UsUrAJrt%odUZL+jz0XeDn`YW21CxGW!{hMoQtEmmF?jP};#B*Pv*R!Z zxW%{;y$)-|J7&}p{gLIy8<6ij4$sJV-}~?hD=MsV*W@~!2_O4HUKhj9>r?>_2vkDz+5pwx|${|ob208d2 zxTyRewhZx#fEE{ZwmaPuL#?aM2QqLKX|i;i#? z%_<@1c$5G+c3(hEYS+BOe`J(aOWT^X0d8FrlZXz5sZNtX-2U}6qyQritVN{(o6MhbCh8Uo{X6V*; zCI+H%>Z8OjPDIkwlLI0f>t{!!{olryPV=7_|HvmpID}GqEU0Ul526k**RV*BhVHA- zC4rtOpUB?O#F+^?>VlXdTs=1DhNTD50kG@Twho=Ex9K};$f)HG_ zo;HdwX};3TWz{*5o71j>mBxT56XUMM$jp&oDKpG^54F4>cN_;a2sO5+9XR+CY+1T& zaf_o~I4A1QI;b!nLleQ|)=@Nqf4LeLBOP{%oHzK0Xg7%H6Gdu6u}n>QUUcdf4Z;gS z9%jHM9cg$^Fvi|W{3>*12;o8%9*|F}w48L4UEx-WmZD!wGRhxyuzveCXk%#j1YmVv zbbdBla;l8+#U4=Pr8y~RBi#xETz|&VQWvEmGdYf#y?aaAJs^|G@7;Xn5>#DX36ILjY`xqFFiDBSK!_ zSmrO)O?FnBtaWU<5)SF0%-@N95E(JkOS}-3HQw0_((7^3pcCz7Db#aH{Ztv}3c{F3 z9`wC};pA~_{8Nv%u8NQ)EV~Zn!|3B1S<9#=Hhz0=pi$PH6;ZSW1w{kSLFw~+8l1n2 z@c5=1c5B!zR?*TZWQ*zVSALXonhlVp=<@*W=WUf%JHU)yNGW5*(%xpj-C2&oI~JClY8V^7KfP>nN+>ti0V+ zaPvJbvYfidk?RUsBie4JyIZz@XzL!k#5pRJ&df8wTc)2yO!#{J`hK&*P+pUvdu3f{!mwdcnK{`y_r%EBVWa}+`47qTjA2|D3teK0ElsnzK2CN+rPqq z9%eLs7SjMK^wSB*F##!MXzvC!C!I7S?FT=JLUg*_2&Eyv8}F;-k6WnaW&a(w{92c; zyE2eo^_d!T>kPz~)8Bf*fAO2}lAtFTqw!Kr@q16OXJb`4uRAoS>1J_n0ViR;L{%XF z%LU-^5ZagUhsGmY9Eh)vIgC!<(4svy*7?;Zc31KO^g|VZa3FEXK{$-d)nwGxzBxrX$%|GWfsvxnAtX8#)L&Fe3H2f)4LMepvhiG7#&o?gx@u~Gf< zcvX1N6sW~u_p}wxi*Qw#pTc;8CqCKVAMRX6L#xWVjc zE4f~S`3&zbKj9!mk;{hL=Lg{@{cFlhaY50yE7rpZZ1CV2BlQG}W{`BgvclA_m2Gw` z47q{A??Iq$doUbf0|1h6f5EK&1^!+H<#!qQ_0I%_hJiw`vm${61Jn3F>M@f34;m4Z z73!El=F0sJ3qr{L>tyc9Bh7`S8~!%MotQ-k%F#51a0+TLQ4`)hd0gu?%W2DT704gR z0Y6+7VG!}Sua)~&X!iODEIhY-?=0Bf?v~rGzz}bgb{3|lvQNW_(rkn|VB@~C!#{pc zwG8F>Ip2ZM#78_L%R+|F%$?4l=Bfg(Y01C^%9Gx=5~P}EN*1rcjW6~hNghXAN?Z8# z(6k1G+RzJ&=OWLxkyW$FX6Y=McV-+ZhmJ=oGZvZL*~ba#+aal!6=!TF4ovQrD{fAS zERD$3@aH2GmE$02=lWoH^<3GH;k9AzXi7GY*VT-NpmkWgamq zxBv6<{lD_9mQ5b!{v$Su|I_+ukdTsT#4$jkF6L(D4sO=QcCHMjcE+x*>S~Z+|F(gF z#j0<*qN$^QZBm?4SpV=-q9Ig|ky?w_7>=eDz$iuQjt-g1)wsFylMJfBZiElIuG2d2_}13!Do&dKc9H z@wOaxB@rFfIS{MjMpl(p99dzbVVhOAl4VU+Z4sHgvB#r%mV=m{;-jL!cP7)LTq`L# z5oK^3X;qt4L(@`1;g`c`pd^FEkW|OsZEEOn!UKCID{~95?@*otOw&(QB)FyOx(|@N zT+gl+?wUo`OI&&P1K+)yj4SgIkoy$H5Bmy+697LVbv#u`;N zVAC|KaCIN>z47DhjXZc6Td%SI9Q=Og2O%mV)K2IOG*S@wvu-uhpzyj*7ii#bb(*yC zx-H<&@t~L7*@cl4ppH((zG)DH=rKXru1T>A6Kr;qRaY@|nz(Xc20aM2HJ~i`>SQ+> z`aO$XUHlkTfvLUz(8ZNe%I`GAZhM4R;C`P>G~V7~idPN$3_on4@na3Yzt~IhN509) zx-ZY%>^*ARzsM(>&J@#uI4GvD?R#*o$XEb?NTCH?-XsN>l&kg>xh93KfGRp59U0z&mBmzI?36&Oxw zhgbj?xh5uxdXCV|@^vhJIG}(NC=X4l>XE_G-i$jy5K}+YE&Pcey zExBLQ5&itH3SngF0tjFF17{oNLA?L)oDIED*(|}cvXhRFwu--aQQ@$~M*jHJrp1_6 zJXaB$O@u6ED?{{{Cgo$NK!~&pIN-USDZyTzWbwSVRp&paO*`w`5JQ79N7EnJEsuoc z!a`YO!j)3mFR)&L*>Na^Tog$;cUKmz!3JlIff}6f$zK2-2m<@aYUV}6>IoEeDZB=T z@5Lj_@QEByMx-N!&#h~)jVn=2kLdzs$NCF*OwdL_BVF>{`QBlHLES(CzZfwzLWuAz zF5Gf)G_3qR6|B7C`h?XW$t}4M=+m9sIJaaxmc5n85i9hDza1(%q%kCv2TPS5C+fjP+^*LHjt|vjQfB z*`RBRAhu&aR&Sm*wC51(E+f8k3DX;Icg%rhQhy=^sFx<@tKp+uD7yVMyPcfqZL=*) z$ud6>OJc+2mN_l1lU2-1DFDvL1J%^*(l|3@!-NwJD|&~2FWVzqp+`IpKH(FE57CbF z!ih(S&?tM)UG}>9ai|%Yd^f4jQ$462$mG1%*7TL_bIS38lw3@edk9l6^@{m7bAdqL z=>u8`;U6-}zzQU<|C_1K{*Tyj#f?CJDpr*CgMnyhFkw+;@e6`?23hR(e)e2%~Xk=5DYaZ}`sSzP$cjump=ohVk3j-md$Fw8pYUx&XTr)Q-Ct z#P!!wMz&l9?QsE-*+Dw_cO;T83(`Kpuw7Ksm@kW8A91D_Hc7SIz)6DLbPKS)o=>kb93KaYu#6aDV#>|P)TfdSc2PB3 zEHV{eey)!ipL%}`r?S{n!vcF1i^fx<1zLQcSEIf>jFoj*RN5#&6Vbe+RJy44kzsgx zFr`n0k0Lh-Zlm4-4_*xi;}0$f_t&Ak=KZD?foPasbJIr^@y-{vFBQBTzq&++<+s!` z!Fxyl=L~vNDA#Y6XfE=3w)wFP8tGqUZyBR6L4La>^D|3)bS{C0w-yqOXI0NF&C{dv zTCU1F(_aYqoNgU4aCId&Y_b zqBo6j1L>*9xS<^&!#Ye6A&&i4p-5EId%sY3*qIJ-wng%gxK!1wnXE_y{dMa`$Zd zU8az`#zNr^UbR7_&BZ&5cLGjfo43l=J;R#j4mueY~^Wdyr9a#Vj4H>+79(ew9F^8y)U zfVzm9)Q|CBdB!bP zHJ+OvP6<^mr?H}ndMAbak1>lO5i+x?v=90Bg!f`^)8EKz!Q3^oo^mboGN1M{Up`j% zDZ!?VLwCEnJeO?^vGE-oU}sp;5Snc1fMwf+TnzDe+q6&qvd9E5nxJc?S(Es1^CrsQ zwM>`cBQEJ(g<4Ed9vw5#=8}2Ny{d;A?vd@ne-A$$E;=DX_zeU^Rd-k8D8+WXI0{8k zLeQhH*Y;M2byiVD_s^A?plT0C1F7qH>WnJh0`(ieJ9HHN#J}zrf=H$PY(0M6;Bgjr z^S+Q^JkE#g#gAaJ;{h3y@u5^mv6^wdBxveguBNt3mobrIkOD~S9M?&VGVFUPgjls} zSYvb+zhz6Nj14cNd^u9ME$#{vg~btue>p*5oQeZ#gkSWW_$Xf^cD;7#VKF#?DxrH} zan5G!6&Z`nQF2glWo}kpl0Mw{JR>EZ8N`-75lc~C=;5^dXQ1E)V9LOmjkD>23hwwQ z(`S|ZviG8@bBxHt3%;~HTNDDmcX#zJ*AdyJ7tfZjfZ$C%W*Z50eN-~wETOAW>s$pj zRHE_4P(fc3TpZ!5c*yA>mc3f5;8JR+xLFbFF;{dLg8s&wj!$**3A#O}!Fv<~-3$c- z!91soC^WUL0VI%6(*#h39lW89ZBe|+Fd-rgiMj(w8rti}_l%uJ`=84KSl?W`R^i|O z9$XyT_*WE$na}$;qhq<@^()6hkn}9j-fI9yqzGNlc?dUBvVjy?_i7G9A8|0K5XoYi z(v|4mWZd4#D%WDXN!b_Rl_V5a-C|9A^C4iWrH{w)AgAj^#IjXH#8MBYJElZG6^fgn zcW8+d=-zS5OHe$cjNtC9qm^Y#4Z9~JXeNK;VyUfi-IwW+DgV#LdXI;?_Ya&K3zrF` ziWC>Pmj!Nfq;d~u3SL9?0AcR(i@gncxM$Llx{ny0u6vk=@|TV`BqoYeXhzhhG{92t zBP~m*{QCxjK!B9{^d8w-g^V(4S4efF{;-dUE}M)mSUUA7cF9*z_o$rs12zjyikr`# z;@L1IM4akqoO0&f&=y&~gX4Vl;{P*$P%Wlf_crFD{pm0*x*B@47dR<6 zJBPr(1kY@pgXj4LCfUEVDw4o!jfCvt&~r(opbX#SaC4|wmYe5M&Q;D`F6;Kim7w9T z@9h!RVVskbO&yv(iPoHzOX(X6e#HebSGXF;XPL}+vaD~cp!*J3l-$>T z3x5R7DD_~Cmol0FNe7E1;1=o2p$1^s~UgDkj$b3M(I$)vBt?c-{$CbkmJ6+}fhH z20e!9LZ`g3GKESCpRA=CF#1JG3b}0cGccXem79Uw(8P)pRq+;Q#94Hh>XvQXe&mkq zSKWE`zfi4;D3Z@$aF_h9cjxTly`IoE;Oq&UktgUK{{RYDdxAJy6}v>!dFq`G^6+nV zEN;u9t1(*Mu^bX4dVdJXUFGF?Kv;%XGa(Ug*S$)nZNCeMeL?3(DzwK? zL{YY4+a;`y2&7)rkBF#wz<7a2{EuD^;G;oM{~l8b|6eFERf!R#3G0RX2jw%L)Ye>F z+KwBR3oB~ecrtAmMWmqvHF>awUc`(tqC|dqeho9xvuNi-AuPPk|5}*2W%+n*w5$1{rq+`IFX5 zjr#Uly#-xuhX5z?cvXj#&KXy^V{Mj>FT--yxy(SWm%tek;)~r60K|D|dVulS(vG`M_4MTb6oNSE0 z&xn#L9N)J;npM7ktR((G7o|VySCZR98h|^F0D-e|6Q1(L1(TU}#ZJ>~P;yg0JLl7C zPgQn;P9bD?>)OT6HSe&y#2jk? zZkP5h48Vt~e=1aBLjVEHkzbbxwEZ7YSFlN7*-YlRDBI%4W^@GL$85Q4X8?0CPkwa^ zEFt3i(*t=^qxStn>+|*?5tmLnRVaWey!I`J3Bh3WCBHdw{?{KRU!of z<+OqxfhtBS&gzwAsJ6@a^;Muj?+TZ~{Yfn+-K-!Zu;_$>ZFxo@tCh{`OrlLHt8pr18=;(PT3U#De8>reXFgWXplR$= z`!ZV5e<0Hj11xBB2W>mol9NI2wKUU*{Dd0fl&pP>!hkG2tENeuY13o~SI@?NT*Hbh z^;_i|Tqn>n6WS*OP}ZMUur4)Bs@?86Ug^gTcoi$#xML@YzJ}MBrP;+CVg$-yJ7KA# z@O5~-AFst5SZ38!YGN7)G){tiIn~u}=sHi&e}&XEq4v9OVIhAD{cUPj<z@DOvY;`Ik^O)sjO<;EKq-fo!0jnd$eemn(a%e-I}fTt4W@U74{b9 zLiPkh;F0njigJ_~G*VksoiVXibQ#8;d~RlZPY~=G%4sid(%o`q*~Y1}?P?|y=fy^_ zf4v*G`tdH@HqVRO1u6-r3=i2d1utcEe_nSY72Q<)pqlsMeL*&6?oghY0e$>6A=|kFrn}bD)O@(|tI=Hlr*-9D~z3 z?_yoeM0dDL+f6Mck;(Q?!6yhS-ldyae;AAE1$zI7Dt8i>OndEq5})$pPJCKm^$Xg; z&C<_GnS-VBH~oGJ?jlf&u5e4mVaB4!*s59<`?Qn~1@>o?x7m zNarmOc|qA!l;`BsSpu8kaf2a-$ zzT{p`rNsd}BGZ30t*GhE3ja?s>=@S5q!;$HayBpVaNJyv5wg0P_IQB zLtA=!wuXH8#w5`R5&4$1``g^mmY`#Koi5nl#rLWhxbG998#L9_%uo@cKNP4tX}h7| z$JDz)`oo8x2xLPO>uAVeZyi$ge^6Stv?N=OP;%Tk@?J|7Z-NkoLYti(Lgg9R658s# zhNPG!lPHuQKX$yuhoAAf;-e#gpUYD|hF>r`(gMRwU+oy+!!OxK6i?*ClL0*79`rZ# zx??xFzbo~S4qD08)~-?T2i_(O-9|mhhm|QoQeIZvRV#|Kbl{)xXFvXkf4>MUcfpW0 zqRBydZ`<@TE1znn+FhD?{1n~R+p}pm+t)>1Q`Q&PQS0CFbQS)Ff4Gg$h9O(NOvc-> zX+#=#vf2C>o{?~QR^Zf=S*+kVONr(XJ>w1d!iJq2rmY3fW6Y1|_+&!(gvRxKj1+Gg z+2Y63*<42J$Y%4lY(3nLe_vEgsvRfqz$H?J$1i4yO8($X`9tRfd8Td54$T@bcmYu* zi_9_MFCEWOwBEAhBg)V>nkJh85nw^+D3;QYCV8!)UOr!P+>T9E@DPIm0`i4dc3hEMSQws@r#U1^0HR$6V& ze`DFFPw*kLTVNy3^ z7G;2VcoemX&S9KVz|s+%F3{C9f<}Sca2`J*0{0`DNOX_jEP(>n#zt_SV6pXy?gN<9 z>`-KPha=4eT(slB*n{DNR4YUie_P-gLl6}TY8Ad;@f^Ymf1(Q7#%PPj<&xq*m|9g# zg88_(Xy6$%SQ@w@oY=K%80(vkpuPDBHjZL*qO)ljF9{z(*U}@16>!-h$iFIVL%b+` z3n}TAi$>9#kQxfOyi;@)u(P{>-4_4r9;3&QTbN z;8o#a*!MX~e`fQcoTV3QoH2+6&bSbD&bS!MoH2ycopB}3az@t$0f;e@^oT-UjeG?b zO^h=Ff@4$oFg6DFj^Nq~`nATPu6L+os2Rl#3CS78tB>N1@|+cpS}!V=Jc~J^ncsd? zU`IIfipbF_NgO+&zrD3%IwswSX@~ z_))+YV^UA6ClY*+d)!Z$bIqYTPwW6f)cKV}thiOHM?~aSV^4}!&w;VWBM-rIh$}7+ zesy;Ne_y{HYa_J2y;E+~75wHfzH=BqI0k?4M_dji_|sNTxT%h@yf^r`yK@0gM1sHS zbe1iaVv*g!U%PVdg02GyM-Jn+$8fQn4*s5#NAXw5x(oj-;NJxyiYuE(#Vmq9+%zn_ z1)=a9%?07(P!O{Zjfy#mS}|`}1n(P**vGioI4OUyAWm+RWf7^|Fh&i^r)HcK23T*w>`5(E)~;Cv!$ zC$;1WfSU+`TPb}PtHYyAiYEw{r-%sb$BaDR(T973m7 ze=KnD$a8l(ZTv{SqJq~@^I9*xoy9Y{wo9t@!&Z-s5?`5#bA z2M9B)4G&NY0012p002-+0|XQR2nYxOldU8Wl7SbK-C8YwyZu11qM-Q2s!$TP8>3=_ z!~~_lLk*<0CO$Q{yVLE`{mR|l8e-&!_%DnJ8cqBG{wU+LXpG{6FZa%znKN@{?)~=t z^H%^5uq^QI__$enV|1lGpwKZk47+En8Fm!Jo-b1`3e6yLh;cS-^+F=$g)XB*QVI8B zyjHzmt(guDjkh|4K%o_7%BCI9CxMknxt6P>h7 zFncJ6((+~KTKnBYvQrJy0t?&qovn7`MQ69UwcV(HciOFbv$MDVye?2~{ARS$k+R1E z`ljuBp_e`p$W>Nf3e5kV^fdE)hm?kr!1U%gw}f*j7BGYJ0{M)kRr{<>$Av#swT_aM z0u2`hiY}!GD&l$4BZ1}0StYAyp%O0PashLg=fuC zf-PY23uaz@#B90z2@5BbBX^v`X57gxG`dC>(eI9tz=t@WJx`*}v_t?~hLaxPYmE_wDvReU%yN z4Y^z{r7q-5>ZWdu#m+QN)lE*!Jz2s)+^jGtU6Fs@guV`PS)dIxlWnPLY?T>zTxJW* z7gs#%(|>=_TgxC+sLoiDD~%)a#+6J5@_}zLPv__JROK|tw+RRV(}$+_nr@6G0jG^G zlhR{uDS7tTw&au5uYCGbw`knawI2VDVOPN68V5`)x-z-T)}*@__65ZBLb~sGVRU@* z$Y320Vi-fPWda9d1rg^Rh<*T2O9u!+{qJ}90000ild*ywlLK8hf6ZEXd{ouF|NYJ^ zcXBg8NC+@2GD47SlL#te5HVp5BmoIahef=Zxk*N5iL(UaLe*-mt=nsDD{A|!wM}d7 zW^oct743rB+EriezP#>>-B+vTeb2dfl9^-z`rbc}Pr|+ToZs(ve%tvi=j2PTJ@y0< zoh#nUbobGtJ62t_f4IvC9WvwL#Z8Mt-HYoNhZ3>ANYqG267fJR5jHWNG^3`GGBMd} zqynK{Gju4GiKP}dbsN!?S--fiClE9G0uf2$ysni-c;)$kO|Ht}cW0te45WIEz;b+= z@t#QBG?S5d4@UdVWD09xd{x6a4XXlSvw!h59%3fFGm%M#f6R@MsL527NcJ@LB#m&? zY&@Ja`ufad<0kdF$NFkFB5{qJOl6lF{YGQdi1##Z>$=Fvox8brY2`h-PeadnMFBV~p%$w+#jaU#rWFL`O2 zPNg)R>5Nmue`-|5Gz|-_gR(4%nHEf1Vtf|F%c(-AnKX-O?o?13&1NbE*|tPT854@h z5sjPa#$7wwKxi)cbeco+n7sKj8ZBUQr4ze$v`#{61=<<3NT-G5FGOqAXfaa>*6f6j z#30739BRI{y;Ma@by`Aa!7AM_u7|1%tY*P!RLkTxf3L{E$CxUs+a{WIbHr{#1mQ~Bh1jaGuCbi(q; zF}(mpjsSZVT~JErQxmu;;$|9MnDYiT+>ub8w%+XCn8?J#8;)%+spg67)gJ3G7w^1O7y}KizBkf4A&z z_g9+@Jq`ZA`q+S+T@xGVH=-G{2HWA?SRrhtLdl4&pYmdE@Lsx0@_8&5wbkm)$)quW zh&=V!-e&R?QdRshMtvh zUxL5JjDao_D<#w0Y!5G*Jwg0A`if3Z(N~#7AmE{|GX+j7NOL#Xwd0XS-;^8R_3Hcu zot~%vf{cN{zDw5}sPoW^fA~ONLMfH<(sv{`b@W{%g;b_1WxID}b!*W${eAj@g#IC7 zZX#YF?cUcJ{7);YMKI5DSoX*C6REQQW?J#a@iqDxqM6OEv~qJ25}sZCI(RAM;urKw zoqkTg0=4S3sTy0KYZ_`j^c$!&5)Ye4wspg2puAQu{f=Iey86BJf92Mx)cHpV@+Y(; ziFmUe#+h1*dCnW<_Am5T$?e~eAQZQfS;gx=5WT997i1!bJFSnT0efgdl{kH z#t0mc2(RS20mV;q4%03xU(;z+rq0q(0@X+)p4w^-c+q5`e14Dx)0~N-v}7XDFfuQr zrQ(2x-8#EuY2%g^e^opT%%b8?L1wj=OIQa9E=BxEC#*>?PeTcVL9|KJQ5_&G=G5!u zGWsGk!!woEp~k)_iaak@DDyIUA9oa;WV%;HgH|uk<~gtu&xMSMct^sn3%oo}YWOLh zkKM26A&c5s z@nd{e2`}Ykx!$G_K;s&nYh{4tH6E^?B9KW3=LV^lMkey`a%ihBGqDP^Bju@U-CQ{3 zbNF28H0L3GS`y|LoP0jhlIp@%Vv53$W%BdG&-zi=7rJm29k_pyp`Q%l6R5u`04bR*?;=isa2O za9~qLF0B=)v0p^Rj7G+8>(#X;O&Us1#D`(!)oPH*dJq6@5B;E zmK9#!$-7G6iMz4cavR>uZ<4$H0S?M2nA#BQlZ)-ce=g%%MoZ#MMXtpDx)j?80|zH% zmpo|<34vB*QC@+7vZu$0s<1ZR>M-KOe2Y~-lD9vWiKZji$bPH9YVdHk&ZZ12i)^TH z!c6&POV?}kn|>ocV1WV>oy@W+JIh@#%x2i7Es;2sfu;^27_Q&2v3Xb9&V!qFG_P;l zaBx@We})|gH*ag-;N=(!SdMbsIw8qveu6yT zf8HS@elw%1SzJU4`|x0cIx9d*<99)18MK!b4L1{YXo>wEo$uuLVogg5rlQ9j_EPI? ze@P81yz?=>y9DUyaOM|5T8~~dnlQo|zpuEb7Ne>$nx5%#GkrLbJhU?sGZQj6Gt$`y z`2G^UkI~l50k8d#Vsg-{tDZvEVr>t9h(E0J`x$M|it1ugTW+$t2yUyTypKxs2g?YN zX-?FLb%l+p!h@x%vzcxyN_&FwRu?;de>w$Ar%?CmV#XiK0=vEZasGr(F8<^UH=_+( zJicxu-k&&RHnu5A+Re1lZG^zvfW{9aFvP|On4ZfI3^pDxdJ|zQGo`Amz*8jEO@%0r z0seQB){>{jt(iQ#&WJ`kBeLk^l8pJzI7%8hqQot=&sdnIJ^6O4}78;-~>u`6TsebXl#;qx>6tPC&ciMi3k&%xoN zMk?KEHAi0ls#P?84b#xoH&8L8e~fN(R}xA1j44ji$4EcVFUUZFW_DUS(cHPNwKZ4m zzo-tc`P;|=?d#9;@ON`3rDGQu?Pe-v^qA`-J*F&izi(w|Wt6zQ7+F4bhAvJ6{QQuA zr1KB>$4stWJ2wVac^Dn42V`3Y(lUz9E=F@-iM7-A)hxdjh1DYG1V=UjyWokv@ej zNR0`$#uS`zSYv4L=9x!Af6+`T(ywmYnnNL|u-%A5izso{Ch#Q)LgYm}`yu zn9g38$V9^`4uz5?Jj&mv4#fT89JIQ&kZQGJmq(y)^~8;MLKX+A)!pJ13&k0z2E`&5 z$$v9iE_M*V@MNz4hqiVgMI>UDA=D+3K%cpA>_#ipYsBMbG^Mn<&ic^AS-Ja`Ng!?D zM-$adB6-*&YIU(xf3|J9RF(zCbY^wljao7KP+mYZ09Bw9)zZlUNmNFYsqo}Hkd})T zx>zR8VOsrva6?VVc2%AJt&1j7<|XoAJvuPH`LVj1$X&yT^TjG%tP~d%^lUqOVYRR( zRwELmqNdp=H}@6^zD8W6iwnitT(e$yv7?D*K!)I%Ua^jzf0f?09$K(3*1cjQZP!JO z*d&YNNS8;nqA)Gu!7YhI8k^ndlQ~cwl%eLr#@VWiHW@WaqKE}jcKB~i;ZBMhF{zcb zOceVj+*^tcu}wPY_S`X$eGROfz75$&>Tid5$G^0Cv1}(#+wiv z$9na=8F^wpe`#-7Q{ZK<*r$u2*zYC7db?E0vaj&wdJ1f7Ghe2QPGKPXARoxhWf^Va z>99451w$e%Er-ojnUc5g@T?>00(R$BPraV#5xo*!CPrAS!9FO68ku;g*Gx88rHize zM;wwC0;U~dmY$~D%*C9Th)X>rJmj(N1g$!c>EhE|e^^=s@<}GmZh1RlSBjvW6e*ob zMY`bZun;6NM^1G+dY(D}MTa<6&C)za@f#WhSD#v`NZ zG);9|Wp|f3ZThz~@5pO9^E01)p)1~u;A{6$^2*C2u9JUFQRG}V?_g5A1zA_zz|`o6 zPhg?2fB&!%Ndrhlu;J!C?GU zMBD8ad*Pi;Qe!``(xI?F%0QD;Rg+87g;WX-1YRvot?TX9nA{ zw5+@)OO3~XK+v~Eleuy^Lx5>%2M`;Jsr$=aK(D^uN!L5$E z&hp*0!?bsZ_MO-&$7_e^vJ-?#g{D)G4$yq6qH0=8Lfk3;WQm-k_!Jtg(P#;=Mr%g_ ze`tL-6OED%Tsei;*+2lq0r74{O)?MH#e56ib@{gnmS~y}Lh3}$hidC`JcsbxUEW)M zd6wcsbVZiZ)=%3A^#}Lw?--&Z&PV8K*W*+d3_8k>b~?+i?aa~*<#mtH+jFD0VDvUQ zx+gbs2S(m0M}p;d0!2bl(Wwe;;gej?e?az;XIWmOe2=pB|#)Ba{s`xdJ}t z5Iy=RonUHm``nMx(@e+sS)WV3f0^k?kZ#hl^tEIB5uaB64P}a%BlJ9QCF-{ZN1wy^ zx3l!UW8?#x1_S=cryb1FPqXyvCfDHTLzw@qns1QvWoxqZhm{hr5}<#!Kr3C&f6LU{ zkFxZ4iF6o9|5QkRiR2sy^=a;Lu^MfVBrUv;@m3bFX*ZQfs1gNrqt7+MuAr~vU8Q7r)Al9|7*v6f38Z8^D-%FrANuy)4=Kn9G@(*z2Gqffw6 zR~N7=i4VSJOwE}Mu~wpFd4YUC$LEx6EgGQ*gB?TcFTW$pOOA7Omg`_Vmt||(B;RtD zc2{s9%V!5yYWEU!gU=ONUb$y*^m%+#YCgB4Qj>zXotH^7yAN8kk4Vq1f2-hCL%e#J zo10v6$zb51&o#vBv%IN-TeI9|t#FdO`1HAl`I0?8XR!Pz#=zH}uY!<1*(5X^zjWz8qN&fil9tAekd<1}nH{hp0)Lb%fs^e{2ub9_I(J)-ZqM z;1GYT-si4+j7Nw*l@~1QJ1h9{T(m?qQ!$Zmrv;;QKWSDBR6qS1-LKJ88hxJV6)khH?Jw;&wCc&%l9HmV~fPS6>8b!b?nTiI>`SqkvHE;b$pgB_jAtYM> zXP%1FQ7R?(*fd#_e{y(!-mpdwstM41l^P{?|D=UdCEPhm+oe8qnKLFKa3|5304&AO zt5jo6T+E{s%2zbsAX!y;=OUS3)VoSICuunn4T@a+zXVeaU^R+=Slf2K^A$}U}9Be<%Uk-L4?W%1%ER) zr~BMZ+8|9E3tn1%5LAZwTUq{2lc$2eH_Sg#8?}NFMt_;*-;VH02(r$V*lK^O^kB>U zwX7=3f46tx5dQ=FPp$4fXzj!%O-3xwaef(u5KL5>f7E@>rV`XDK8(B~N5maIS5ry7 zj0locy`*%UN5_cC$StXO=+qk3GF zjEGW13gCGHwe>?{dRADqRB)?IBWXLmND--9k`S}TurVEM&x$#B({d~9Osmg|d5ST= z3?ve_fA(O7Sdbs1WHjM+?id#SS>nuCg;;W-h%HhWDL{=Sf577U5z!WG9}?~Oz9iUwlFI6zaNb9H zy<sdh|b{tt$^5>6?@tdH5UdEG>653tN^=R!=k%3D=x1P(X8mhY$;-D z`I^oOaRr7mV-+dm>#99jadf;;ZFAHD?Akgz_Dy=fOWW|jY;wEX{ zf06=S*VfyL8pHDGu!)s7fcTDa&@q6LDF9T>Tp@0+9TM+6fdJn}{f>8tTWNr9QqNoI z9{J=K`G?{HB#W2$uj=_Szbc=CMTvTr2(PHYbGn$Rp0mXw^;{xq)U!owav-paP2v&- z-zj#>r-L1(>N(9(rk>@FD)n6ESSz1)e~S7k%^gMM?a}yz44!-+D)d~qmHFaj(qExj zEYnJH7!~kerMQQNRCbz<%rOO=0#PA(HKKPO5RHN0#lvnpiA*L%`A`z%X4yt4k_%+U z0+lt0^`ca!4|`&k%!hJ96H7I*43nCuapq<(M%0%W%kaAt#6-&|M#eB|au`d;e=t1A z7i7`0;bqG*f&TdNyJQPw@%4&g_FvQ~^Q(J|hy=F?&6K^4J(?#$9XT;QHX&`H1SA_s zDa$W$Cl1LjOWdl`UOz3Qc}RPUkoKy;@GEB2C497Ec3A?>vy?cIVk zh3f9`zjzOxUSb}GZ$8AI=7;_VP)i30OlKHPf*1e*?J|?0Bpj3Db1{Dld|PD||9?r_ zdz)sjmTt=!qm&K0u4%_$Wdsq$DA9Is~PQvmWHx*90ahvODJ7HTHo0|hxCL9~E zV;5wy$xMBu&q`$MruxDDaMBtKJHlgiZ>tq=J(jfTHO2FN*+ha1nE@+&6j3|X@1$%y z?WFp-y4_A^D2wZBnvZT?6OP;4>)&faDFnLQY&vFda1yq{VmE)?-_oD9;t9KDN7@=3 zw9_r^sf=eO5=)OVP^K_1?z?G1SjQq zYZcNB6ZM`7E2=jW%eSoK@^gZihw4g{qc(^Ds^n`y5W)OcD2Q2@Enf!*F$Z(y>ktKh zgPg0up#d1EQz)bB>A!;-mUm2!A*~CR8ew3m!mNJVJKKMfK<1-0w|KB@dnv-T1k7d26=Ka3!_<>wb0YzgH&80-0()iH=Zqs zB8#K2N~9f4Xv}4Z= z;zX>K-IIT)u9FciL9EL!ouV*@#;)tlxQVQ1pKW;qL9EYPcdEjo=~KeMX}pkDEM{kz zkt>;#{S7l_(3@E?!{Ma`*d~RBzH7(Z0yrIKC>;3~4;eU<+U5yQcawC$S(1>QID0~w z=(;H5*+~N%={Y;idtG}#?X#(+M_p|zNewn(b0vSea1QTypXDU7Y5Pq2!RlwqR8N&K z??6AT`mWT73h9 zbbo)JE5+OPVgm|?PMOxlQY6-LK10j7MV zogDNo>fi~+qUZ@tDQk4ZyYZd?-i7y)G{F@SPp8dmSiWU)&3GT)FY-RXOEPKCzz2(= z)U4N~)0UQL;6njiCPl<=#p9D=S*T!gC9i+LhlTD+CeTC$4SbZrbUd3eaG8PgCz#M) zSf_Fy!^f*|6|Sb0Z`?Os%DQ?+YDYf6i9iq**nb6tPyPUxenG>c<=mTc(;2z}U;4sVT zc)-Y@g+s=vJ7e}>{?6T*??3rcJeq&E<8H1sXY}PWaW9dy&4Rt1)uw*>wo|-JL3{`I z3zzTG8%3>7$@cZxX*<5rwsht82J7aVbeY7p#UDl z4;0EbZ`u%EW8y~&jpKwRJf`hxj|8wEKbDeq;8+rQcwNf%>iVQ|)$vXZ z)UlE==YPXXGexEsQ_a9{8L5obXKzlkkS=MMRO2Q`=^6Y!fZyTSNwY+;Xv{cEJSR8r zj|!^U#GmO7Iw|9(B2@A(()WLCuh5=?_^Y_**Z3P%b2H5;PB|w2&apvKF6~l(k2Um& zw=~R9@;~r$fPL_v#hRZlV{#+tzJDwDHg_H9h$VYG`5(MmiC6GniuT+NcL#e9Ulik_ zOR1+6{Xe`Oz=as2Av>H@+})8e72gOZ$7|1WQY`5Qms-&_V5Ph43$uTADyFN7@~bkQ zSLO6iuahbS(Nu=Q!tqmdi3~W!2~kx_Rt@kKW2!0^vSU}THq|T|FU{9VxhaSG>YJ

SdO`o?U^bCPz6Ehh!k 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


Xdyd*p!l1Y@nCA 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0e68c05b4..17cc7e431 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,8 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists # Binary-only ZIP Checksum: https://gradle.org/release-checksums/ -distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06 -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index adff685a0..739907dfd 100755 --- a/gradlew +++ b/gradlew @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. From a7f5f9a35a12f0ae3c53c2327c8b0ecc0bbbc75c Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:58:50 +0000 Subject: [PATCH 051/177] Feat: TheIntroDBSkip + Bugfix (#2631) --- .../cloudstream3/utils/videoskip/SkipAPI.kt | 5 +- .../utils/videoskip/TheIntroDBSkip.kt | 76 +++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt 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 df16d77ca..cd6727a24 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 @@ -17,7 +17,8 @@ enum class SkipType(@StringRes val res: Int) { MixedOpening(R.string.skip_type_mixed_op), MixedEnding(R.string.skip_type_mixed_ed), Credits(R.string.skip_type_creddits), - Intro(R.string.skip_type_creddits), + Intro(R.string.skip_type_intro), + Preview(R.string.skip_type_preview), } data class SkipStamp( @@ -60,7 +61,7 @@ abstract class SkipAPI { } companion object { - private val skipApis: List = listOf(AniSkip(), IntroDbSkip()) + private val skipApis: List = listOf(AniSkip(), TheIntroDBSkip(), IntroDbSkip()) private val cachedStamps = ConcurrentHashMap>() /** Get all video timestamps from an episode */ diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt new file mode 100644 index 000000000..cc2661cb0 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/TheIntroDBSkip.kt @@ -0,0 +1,76 @@ +package com.lagradost.cloudstream3.utils.videoskip + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.getTMDbId +import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.ui.result.ResultEpisode +import com.lagradost.cloudstream3.app + +/** https://theintrodb.org/docs */ +class TheIntroDBSkip : SkipAPI() { + override val name = "TheIntroDB" + override val supportedTypes = setOf( + TvType.TvSeries, TvType.Cartoon, TvType.Anime, TvType.Movie, + TvType.AsianDrama + ) + + val mainUrl = "https://api.theintrodb.org" + + override suspend fun stamps( + data: LoadResponse, + episode: ResultEpisode, + episodeDurationMs: Long + ): List? { + val idSuffix = + data.getTMDbId()?.let { tmdbId -> "tmdb_id=$tmdbId" } + ?: data.getImdbId()?.let { imdbId -> "imdb_id=$imdbId" } + ?: return null + + val url = if (data.isMovie()) { + "$mainUrl/v2/media?$idSuffix" + } else { + val season = episode.season ?: return null + "$mainUrl/v2/media?$idSuffix&season=$season&episode=${episode.episode}" + } + val root = app.get(url).parsed() + return arrayOf( + root.intro to SkipType.Intro, + root.credits to SkipType.Credits, + root.recap to SkipType.Recap, + root.preview to SkipType.Preview + ).map { (list, type) -> + list.map { stamp -> + SkipStamp( + type, + stamp.startMs ?: 0L, + stamp.endMs ?: episodeDurationMs + ) + } + }.flatten() + } + + data class Root( + @JsonProperty("tmdb_id") + val tmdbId: Long, + @JsonProperty("type") + val type: String, + @JsonProperty("intro") + val intro: List = emptyList(), + @JsonProperty("recap") + val recap: List = emptyList(), + @JsonProperty("credits") + val credits: List = emptyList(), + @JsonProperty("preview") + val preview: List = emptyList(), + ) + + data class Stamp( + @JsonProperty("start_ms") + val startMs: Long?, + @JsonProperty("end_ms") + val endMs: Long?, + ) +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e9dd2748f..b7956e9d8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -560,6 +560,7 @@ Mixed ending Mixed opening Credits + Preview Intro Clear history History From c304e8556e073cd19cf3b89232cbcf12ff4dad14 Mon Sep 17 00:00:00 2001 From: Phisher98 <153359846+phisher98@users.noreply.github.com> Date: Fri, 10 Apr 2026 01:29:31 +0530 Subject: [PATCH 052/177] Minor Fix IntroDbSkip (#2634) --- .../com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt index ce284f3fe..869515f43 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/videoskip/IntroDbSkip.kt @@ -47,7 +47,7 @@ class IntroDbSkip : SkipAPI() { val start = it.startMs ?: return@let null val end = it.endMs ?: return@let null SkipStamp( - type = SkipType.Credits, + type = SkipType.Ending, startMs = start, endMs = end ) From 0f1cb3a7738441ace6bc35004fc756b56ab8ba24 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:59:53 -0600 Subject: [PATCH 053/177] Add strictly for coil lib (#2635) --- 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 c1048028b..435cda8a7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ androidGradlePlugin = "8.13.2" appcompat = "1.7.1" biometric = "1.4.0-alpha06" buildkonfigGradlePlugin = "0.17.1" -coil = "3.3.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 constraintlayout = "2.2.1" From b89f36c9bc04d4a07e0c2ff437d256a897be31f9 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 9 Apr 2026 14:01:20 -0600 Subject: [PATCH 054/177] Bump material to 1.14.0-beta01 (#2636) --- 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 435cda8a7..c92a937bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ juniversalchardet = "2.5.0" kotlinGradlePlugin = "2.3.0" kotlinxCoroutinesCore = "1.10.2" lifecycleKtx = "2.10.0" -material = "1.14.0-alpha10" +material = "1.14.0-beta01" media3 = "1.9.2" navigationKtx = "2.9.7" newpipeextractor = "v0.26.0" 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 055/177] 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 43b281a28..60c87532b 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 4bec57f9c..8699202b9 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 000000000..52cd4361b --- /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 000000000..8d848d46a --- /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 000000000..3001281fd --- /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 72024a918..407de4a3f 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 056/177] 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 41d45b6bb..2c536c825 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 057/177] 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 c92a937bd..5a46edf3d 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 058/177] 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 118d89ac4..9250f6f6f 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 9380285ca..8bcd1b88e 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 059/177] 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 f72dd10c6..b5960d5d9 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 91f03a434..8ca1f9688 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 4286e6b68..a410fcfff 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 03cb68cbc..d9a20a04b 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 a5a7d56e3..675ce3b2f 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 5b170d540..0a538d5d4 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 060/177] 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 a410fcfff..e354d657d 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 061/177] 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 709e92a41..071ce6c89 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 ec486d61d..6234297d0 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 81eabbe77..aeab8ef9f 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 6502cc831..4b163867d 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 062/177] 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 5a46edf3d..fc9926c93 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 063/177] 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 a99d0a16a..60a4d0453 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 064/177] 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 0168ae437..b6d502f9a 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 065/177] 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 29c35dea6..ad323c7d1 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 066/177] 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 fc9926c93..f997e4f6e 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 067/177] 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 8ca1f9688..d67b8a519 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 068/177] 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 60c87532b..8a643cc69 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 069/177] 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 8a643cc69..887777934 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 f997e4f6e..a4f921952 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 070/177] 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 37b9a1002..028356e76 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 11c35e9ec..94962de13 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 071/177] 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 a4f921952..c02e79398 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 072/177] 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 feb0ba6d4..0dc65358a 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 c917a55ae..61f87b8ba 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 073/177] 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 860f9b540..67eb49c9a 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 4f3f05df6..1fd39943c 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 074/177] 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 16b03e4f6..c3d8306e6 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 075/177] 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 887777934..29a77883b 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 076/177] 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 0dc65358a..eae14a6c0 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 45ed65611..07d6aaa37 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 e0fd906b4..dfc61eba5 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 077/177] 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 0b201d1cb..0ea37a025 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 cca263dd4..e35c1f611 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 c02e79398..e304fd57f 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 078/177] 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 48cdec04a..b2f5e8f2b 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 079/177] 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 e304fd57f..6aaa0c43f 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 080/177] 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 6aaa0c43f..19be8d6ad 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 081/177] 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 cd6727a24..6c7126049 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 9bc697acf..365a317e3 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 17e809d8d..158748bbf 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 eb6ad4aa4..f5338a9e5 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 096e9f66b..6c8e38722 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 96110d9c1..71ddc8697 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 9a67f9d20..dfcf97ce2 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 4b671644b..6839d9944 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 5e59477ce..e692ecc4a 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 1cbee687f..10b8cf9ef 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 8b3a6fbf3..c629c492f 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 8bd2ac7ac..bffc0a86a 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 d5bf2d4b0..830972586 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 08a1572d6..c96299c72 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 ef4cb9202..0b0479679 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 0b66ca8b2..b489db37d 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 256fc26e9..efe034258 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 89003317a..101498b83 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 6998c49db..4af4995ea 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 83492a5ff..9ec0192cf 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 4a7a50aa7..0938e4f98 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 30b8b2def..3cdea9d8f 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 374b033c6..55b5303eb 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 8c9379f5b..40a2915fd 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 c8126f2fe..0536e6807 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 58d598708..72ecbf3d9 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 a1abfa338..88eccbeac 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 d60a4e32c..8a43e97d7 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 dbd607666..bb49563ec 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 9f6b53aa7..1a5db40a1 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 09499af00..9e4e9f9f1 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 e388b67e1..695050336 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 626554c18..5cdbeaa37 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 e84d5271e..47db0e97e 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 2eb6e2451..74acb48ae 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 5f6d8aa14..acd6e4097 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 a51a3551d..d0d6059aa 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 78ba57310..cb58e96f5 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 bc7c2ca0e..496afe81c 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 ee2c24972..374de33d2 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 d59065a64..e41c01fda 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 082/177] 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 29a77883b..4323c98fd 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 083/177] 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 0ea37a025..368445097 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 6eab987fc..cc48f6549 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 09d4683bc..8456094d1 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" - - var list: HashMap? = null - var cache: HashMap> = hashMapOf() - - private fun fixName(name: String): String { - return name.lowercase(Locale.ROOT)/*.replace(" ", "")*/.replace("-", " ") - .replace("[^a-zA-Z0-9 ]".toRegex(), "") - } - - 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 - } - - 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() - } - return false - } - fun String?.toClassDir(): String { val q = this ?: "null" val z = (6..10).random().calc() return q + "cache" + z } - suspend fun getFillerEpisodes(query: String): HashMap? { - try { - cache[query]?.let { - return it - } - if (!getFillerList()) return null - val localList = list ?: return null + 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, + ) - // 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(")", "\\)") - })""" - ) + 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?, + ) - 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() + data class Season( + @JsonProperty("tvdb") + val tvdb: Long?, + @JsonProperty("tmdb") + val tmdb: Long?, + ) + + 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 + } + + /** The entire "database" is stored as a json file we can parse */ + val stream: InputStream = com.lagradost.AnimeDB.getDatabaseStream()!! + val text = stream.reader().readText() + + 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 + } + + 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 19be8d6ad..f0b24c8e7 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 084/177] 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 50a68c62f..000000000 --- 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 e8f8c49ac..94ddaf61e 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 000000000..7756f7290 --- /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 6c10a92d9..ad4def1de 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 8c0cbec32..51e127e3f 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 7e00dbf95..85212e6bb 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 8a56783b1..8f8a0c0ce 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 e744fdb39..324640355 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 f64863a9f..896228b51 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 c7b658606..dec679594 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 702501a1e..000000000 --- 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 802d9ea3a..000000000 --- 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 e2588feb6..000000000 --- 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 db883d6af..c721db6b9 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 7fafe05be..b7f618e95 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 e70cae6bd..5e47dd2de 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 91150992b..b72213e66 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 469efc5ec..849b2b6d9 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 f9d45ebb8..000000000 --- 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 bd259b175..11927c507 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 ab228ee3c..000000000 --- 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 37b8ecb23..2fdd7082a 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 000000000..43ceb2314 --- /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 + *