From a14f23063dd2685c138da2d238e32c38c5d71eef Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Fri, 14 Jul 2023 20:16:23 +0200 Subject: [PATCH 001/441] Migrated to Media3 --- app/build.gradle.kts | 26 +++-- .../lagradost/cloudstream3/CommonActivity.kt | 24 ++++ .../lagradost/cloudstream3/MainActivity.kt | 46 ++++++-- .../ui/player/AbstractPlayerFragment.kt | 45 ++++---- .../cloudstream3/ui/player/CS3IPlayer.kt | 107 +++++++++++------- .../ui/player/CustomSubtitleDecoderFactory.kt | 30 ++--- .../ui/player/CustomTextRenderer.kt | 4 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 4 +- .../ui/player/NonFinalTextRenderer.java | 47 ++++---- .../cloudstream3/ui/player/PlayerPipHelper.kt | 12 +- .../ui/player/PlayerSubtitleHelper.kt | 4 +- .../subtitles/ChromecastSubtitlesFragment.kt | 2 +- .../ui/subtitles/SubtitlesFragment.kt | 4 +- .../cloudstream3/utils/CastHelper.kt | 2 +- .../layout/chromecast_subtitle_settings.xml | 2 +- app/src/main/res/layout/fragment_player.xml | 2 +- .../main/res/layout/fragment_player_tv.xml | 2 +- app/src/main/res/layout/fragment_trailer.xml | 2 +- .../main/res/layout/player_custom_layout.xml | 2 +- .../res/layout/player_custom_layout_tv.xml | 2 +- app/src/main/res/layout/subtitle_settings.xml | 3 +- .../main/res/layout/trailer_custom_layout.xml | 2 +- gradle.properties | 2 +- 23 files changed, 241 insertions(+), 135 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ebde6187..94dd7c83 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,7 +19,7 @@ fun String.execute() = ByteArrayOutputStream().use { baot -> workingDir = projectDir commandLine = this@execute.split(Regex("\\s")) standardOutput = baot - }.exitValue == 0) + }.exitValue == 0) String(baot.toByteArray()).trim() else null } @@ -74,12 +74,18 @@ android { isDebuggable = false isMinifyEnabled = false isShrinkResources = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } debug { isDebuggable = true applicationIdSuffix = ".debug" - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } } flavorDimensions.add("state") @@ -155,10 +161,16 @@ dependencies { // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09") // Exoplayer - implementation("com.google.android.exoplayer:exoplayer:2.18.2") - implementation("com.google.android.exoplayer:extension-cast:2.18.2") - implementation("com.google.android.exoplayer:extension-mediasession:2.18.2") - implementation("com.google.android.exoplayer:extension-okhttp:2.18.2") + implementation("androidx.media3:media3-common:1.1.0") + implementation("androidx.media3:media3-exoplayer:1.1.0") + implementation("androidx.media3:media3-datasource-okhttp:1.1.0") + implementation("androidx.media3:media3-ui:1.1.0") + implementation("androidx.media3:media3-session:1.1.0") + implementation("androidx.media3:media3-cast:1.1.0") + implementation("androidx.media3:media3-exoplayer-hls:1.1.0") + implementation("androidx.media3:media3-exoplayer-dash:1.1.0") + + // Use the Jellyfin ffmpeg extension for easy ffmpeg audio decoding in exoplayer. Thank you Jellyfin <3 // implementation("org.jellyfin.exoplayer:exoplayer-ffmpeg-extension:2.18.2+1") diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 89f0ae51..0a42da72 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -222,6 +222,7 @@ object CommonActivity { "AmoledLight" -> R.style.AmoledModeLight "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) R.style.MonetMode else R.style.AppTheme + else -> R.style.AppTheme } @@ -244,8 +245,10 @@ object CommonActivity { "Pink" -> R.style.OverlayPrimaryColorPink "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal + "Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal + else -> R.style.OverlayPrimaryColorNormal } act.theme.applyStyle(currentTheme, true) @@ -271,12 +274,15 @@ object CommonActivity { FocusDirection.Left -> { view.nextFocusLeftId } + FocusDirection.Up -> { view.nextFocusUpId } + FocusDirection.Right -> { view.nextFocusRightId } + FocusDirection.Down -> { view.nextFocusDownId } @@ -328,30 +334,39 @@ object CommonActivity { KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> { PlayerEventType.SeekForward } + KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> { PlayerEventType.SeekBack } + KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> { PlayerEventType.NextEpisode } + KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> { PlayerEventType.PrevEpisode } + KeyEvent.KEYCODE_MEDIA_PAUSE -> { PlayerEventType.Pause } + KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> { PlayerEventType.Play } + KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> { PlayerEventType.Lock } + KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> { PlayerEventType.ToggleHide } + KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> { PlayerEventType.ToggleMute } + KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> { PlayerEventType.ShowMirrors } @@ -359,21 +374,27 @@ object CommonActivity { KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> { PlayerEventType.SearchSubtitlesOnline } + KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> { PlayerEventType.ShowSpeed } + KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> { PlayerEventType.Resize } + KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> { PlayerEventType.SkipOp } + KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> { PlayerEventType.SkipCurrentChapter } + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation PlayerEventType.PlayPauseToggle } + else -> null }?.let { playerEvent -> playerEventListener?.invoke(playerEvent) @@ -398,16 +419,19 @@ object CommonActivity { act.currentFocus, FocusDirection.Left ) + KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus( act, act.currentFocus, FocusDirection.Right ) + KeyEvent.KEYCODE_DPAD_UP -> getNextFocus( act, act.currentFocus, FocusDirection.Up ) + KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus( act, act.currentFocus, diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index d054f504..23bd7001 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -10,7 +10,11 @@ import android.os.Build import android.os.Bundle import android.util.AttributeSet import android.util.Log -import android.view.* +import android.view.KeyEvent +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.WindowManager import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.annotation.IdRes @@ -31,7 +35,11 @@ import androidx.preference.PreferenceManager import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.google.android.gms.cast.framework.* +import com.google.android.gms.cast.framework.CastButtonFactory +import com.google.android.gms.cast.framework.CastContext +import com.google.android.gms.cast.framework.Session +import com.google.android.gms.cast.framework.SessionManager +import com.google.android.gms.cast.framework.SessionManagerListener import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.snackbar.Snackbar @@ -49,7 +57,10 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale -import com.lagradost.cloudstream3.mvvm.* +import com.lagradost.cloudstream3.mvvm.Resource +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins @@ -83,7 +94,7 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateT import com.lagradost.cloudstream3.ui.settings.SettingsGeneral import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions -import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.ApkInstaller import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable @@ -98,6 +109,8 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching +import com.lagradost.cloudstream3.utils.Event +import com.lagradost.cloudstream3.utils.IOnBackPressed import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState @@ -108,11 +121,26 @@ import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.requestRW +import com.lagradost.cloudstream3.utils.USER_PROVIDER_API +import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser -import kotlinx.android.synthetic.main.activity_main.* -import kotlinx.android.synthetic.main.bottom_resultview_preview.* -import kotlinx.android.synthetic.main.fragment_result_swipe.* +import kotlinx.android.synthetic.main.activity_main.cast_mini_controller_holder +import kotlinx.android.synthetic.main.activity_main.nav_host_fragment +import kotlinx.android.synthetic.main.activity_main.nav_rail_view +import kotlinx.android.synthetic.main.activity_main.nav_view +import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_description +import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_loading +import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_loading_shimmer +import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_meta_duration +import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_meta_rating +import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_meta_type +import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_meta_year +import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_more_info +import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_poster +import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_result +import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_title +import kotlinx.android.synthetic.main.fragment_result_swipe.media_route_button import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.io.File @@ -464,9 +492,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { Configuration.ORIENTATION_LANDSCAPE -> { true } + Configuration.ORIENTATION_PORTRAIT -> { false } + else -> { false } @@ -839,6 +869,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { showToast(this, R.string.error) hidePreviewPopupDialog() } + is Resource.Loading -> { showPreviewPopupDialog().apply { resultview_preview_loading?.isVisible = true @@ -846,6 +877,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { resultview_preview_loading_shimmer?.startShimmer() } } + is Resource.Success -> { val d = resource.value showPreviewPopupDialog().apply { 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 52f0b760..17e64b67 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 @@ -7,7 +7,6 @@ import android.graphics.drawable.AnimatedVectorDrawable import android.media.metrics.PlaybackErrorEvent import android.os.Build import android.os.Bundle -import android.support.v4.media.session.MediaSessionCompat import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -17,14 +16,13 @@ import androidx.annotation.LayoutRes import androidx.annotation.StringRes import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.media.session.MediaButtonReceiver import androidx.preference.PreferenceManager import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat -import com.google.android.exoplayer2.ExoPlayer -import com.google.android.exoplayer2.PlaybackException -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout -import com.google.android.exoplayer2.ui.SubtitleView +import androidx.media3.common.PlaybackException +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.session.MediaSession +import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode @@ -34,6 +32,7 @@ import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.AppUtils @@ -171,7 +170,7 @@ abstract class AbstractPlayerFragment( } canEnterPipMode = isPlayingRightNow && hasPipModeSupport - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isInPIPMode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { activity?.let { act -> PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow) } @@ -180,6 +179,7 @@ abstract class AbstractPlayerFragment( private var pipReceiver: BroadcastReceiver? = null override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode) try { isInPIPMode = isInPictureInPictureMode if (isInPictureInPictureMode) { @@ -202,9 +202,7 @@ abstract class AbstractPlayerFragment( } } val filter = IntentFilter() - filter.addAction( - ACTION_MEDIA_CONTROL - ) + filter.addAction(ACTION_MEDIA_CONTROL) activity?.registerReceiver(pipReceiver, filter) val isPlaying = player.getIsPlaying() val isPlayingValue = @@ -215,7 +213,10 @@ abstract class AbstractPlayerFragment( piphide?.isVisible = true exitedPipMode() pipReceiver?.let { - activity?.unregisterReceiver(it) + // Prevents java.lang.IllegalArgumentException: Receiver not registered + normalSafeApiCall { + activity?.unregisterReceiver(it) + } } activity?.hideSystemUI() this.view?.let { UIHelper.hideKeyboard(it) } @@ -270,18 +271,21 @@ abstract class AbstractPlayerFragment( gotoNext = true ) } + PlaybackException.ERROR_CODE_REMOTE_ERROR, PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, PlaybackException.ERROR_CODE_TIMEOUT, PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE -> { showToast( "${ctx.getString(R.string.remote_error)}\n$errorName ($code)\n$msg", gotoNext = true ) } + PlaybackException.ERROR_CODE_DECODING_FAILED, PlaybackErrorEvent.ERROR_AUDIO_TRACK_INIT_FAILED, PlaybackErrorEvent.ERROR_AUDIO_TRACK_OTHER, PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED -> { showToast( "${ctx.getString(R.string.render_error)}\n$errorName ($code)\n$msg", gotoNext = true ) } + else -> { showToast( "${ctx.getString(R.string.unexpected_error)}\n$errorName ($code)\n$msg", @@ -290,12 +294,14 @@ abstract class AbstractPlayerFragment( } } } + is InvalidFileException -> { showToast( "${ctx.getString(R.string.source_error)}\n${exception.message}", gotoNext = true ) } + else -> { exception.message?.let { showToast( @@ -316,15 +322,8 @@ abstract class AbstractPlayerFragment( private fun playerUpdated(player: Any?) { if (player is ExoPlayer) { context?.let { ctx -> - val mediaButtonReceiver = ComponentName(ctx, MediaButtonReceiver::class.java) - MediaSessionCompat(ctx, "Player", mediaButtonReceiver, null).let { media -> - //media.setCallback(mMediaSessionCallback) - //media.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS) - val mediaSessionConnector = MediaSessionConnector(media) - mediaSessionConnector.setPlayer(player) - media.isActive = true - mMediaSessionCompat = media - } + mMediaSession?.release() + mMediaSession = MediaSession.Builder(ctx, player).build() } // Necessary for multiple combined videos @@ -334,8 +333,7 @@ abstract class AbstractPlayerFragment( } } - private var mediaSessionConnector: MediaSessionConnector? = null - private var mMediaSessionCompat: MediaSessionCompat? = null + private var mMediaSession: MediaSession? = null // this can be used in the future for players other than exoplayer //private val mMediaSessionCallback: MediaSessionCompat.Callback = object : MediaSessionCompat.Callback() { @@ -436,6 +434,7 @@ abstract class AbstractPlayerFragment( playerEventListener = null keyEventListener = null canEnterPipMode = false + mMediaSession?.release() SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged keepScreenOn(false) 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 9ec18b9c..aa664b34 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 @@ -6,29 +6,40 @@ import android.os.Handler import android.os.Looper import android.util.Log import android.widget.FrameLayout +import androidx.media3.common.C import androidx.preference.PreferenceManager -import com.google.android.exoplayer2.* -import com.google.android.exoplayer2.C.* -import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON -import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER -import com.google.android.exoplayer2.database.StandaloneDatabaseProvider -import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource -import com.google.android.exoplayer2.mediacodec.MediaCodecSelector -import com.google.android.exoplayer2.source.* -import com.google.android.exoplayer2.text.TextRenderer -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector -import com.google.android.exoplayer2.trackselection.TrackSelectionOverride -import com.google.android.exoplayer2.trackselection.TrackSelector -import com.google.android.exoplayer2.ui.SubtitleView -import com.google.android.exoplayer2.upstream.DataSource -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory -import com.google.android.exoplayer2.upstream.DefaultHttpDataSource -import com.google.android.exoplayer2.upstream.HttpDataSource -import com.google.android.exoplayer2.upstream.cache.CacheDataSource -import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor -import com.google.android.exoplayer2.upstream.cache.SimpleCache -import com.google.android.exoplayer2.util.MimeTypes -import com.google.android.exoplayer2.video.VideoSize +import androidx.media3.common.C.* +import androidx.media3.common.Format +import androidx.media3.common.MediaItem +import androidx.media3.common.TrackSelectionOverride +import androidx.media3.common.MimeTypes +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.common.TrackGroup +import androidx.media3.common.Tracks +import androidx.media3.common.VideoSize +import androidx.media3.database.StandaloneDatabaseProvider +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DefaultDataSourceFactory +import androidx.media3.datasource.DefaultHttpDataSource +import androidx.media3.datasource.HttpDataSource +import androidx.media3.datasource.cache.CacheDataSource +import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor +import androidx.media3.datasource.cache.SimpleCache +import androidx.media3.datasource.okhttp.OkHttpDataSource +import androidx.media3.exoplayer.DefaultLoadControl +import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.SeekParameters +import androidx.media3.exoplayer.source.ClippingMediaSource +import androidx.media3.exoplayer.source.ConcatenatingMediaSource +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.exoplayer.source.MergingMediaSource +import androidx.media3.exoplayer.source.SingleSampleMediaSource +import androidx.media3.exoplayer.text.TextRenderer +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector +import androidx.media3.exoplayer.trackselection.TrackSelector +import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -44,7 +55,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File -import java.time.Duration import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession @@ -387,6 +397,7 @@ class CS3IPlayer : IPlayer { Log.i(TAG, "setPreferredSubtitles REQUIRES_RELOAD") return@let true } + SubtitleStatus.IS_ACTIVE -> { Log.i(TAG, "setPreferredSubtitles IS_ACTIVE") @@ -412,6 +423,7 @@ class CS3IPlayer : IPlayer { // }, 1) //} } + SubtitleStatus.NOT_FOUND -> { Log.i(TAG, "setPreferredSubtitles NOT_FOUND") return@let true @@ -678,22 +690,22 @@ class CS3IPlayer : IPlayer { // Enable Ffmpeg extension // setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON) }.createRenderers( - eventHandler, - videoRendererEventListener, - audioRendererEventListener, - textRendererOutput, - metadataRendererOutput - ).map { - if (it is TextRenderer) { - currentTextRenderer = CustomTextRenderer( - subtitleOffset, - textRendererOutput, - eventHandler.looper, - CustomSubtitleDecoderFactory() - ) - currentTextRenderer!! - } else it - }.toTypedArray() + eventHandler, + videoRendererEventListener, + audioRendererEventListener, + textRendererOutput, + metadataRendererOutput + ).map { + if (it is TextRenderer) { + currentTextRenderer = CustomTextRenderer( + subtitleOffset, + textRendererOutput, + eventHandler.looper, + CustomSubtitleDecoderFactory() + ) + currentTextRenderer!! + } else it + }.toTypedArray() } .setTrackSelector( trackSelector ?: getTrackSelector( @@ -810,9 +822,11 @@ class CS3IPlayer : IPlayer { CSPlayerEvent.Play -> { play() } + CSPlayerEvent.Pause -> { pause() } + CSPlayerEvent.ToggleMute -> { if (volume <= 0) { //is muted @@ -823,6 +837,7 @@ class CS3IPlayer : IPlayer { volume = 0f } } + CSPlayerEvent.PlayPauseToggle -> { if (isPlaying) { pause() @@ -830,6 +845,7 @@ class CS3IPlayer : IPlayer { play() } } + CSPlayerEvent.SeekForward -> seekTime(seekActionTime) CSPlayerEvent.SeekBack -> seekTime(-seekActionTime) CSPlayerEvent.NextEpisode -> nextEpisode?.invoke() @@ -954,6 +970,7 @@ class CS3IPlayer : IPlayer { Player.STATE_READY -> { onRenderFirst() } + else -> {} } @@ -963,6 +980,7 @@ class CS3IPlayer : IPlayer { Player.STATE_READY -> { } + Player.STATE_ENDED -> { // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(context) @@ -974,12 +992,15 @@ class CS3IPlayer : IPlayer { handleEvent(CSPlayerEvent.NextEpisode) } } + Player.STATE_BUFFERING -> { updatedTime() } + Player.STATE_IDLE -> { // IDLE } + else -> Unit } } @@ -994,11 +1015,13 @@ class CS3IPlayer : IPlayer { && exoPlayer?.duration != TIME_UNSET -> { exoPlayer?.prepare() } + error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> { // Re-initialize player at the current live window default position. exoPlayer?.seekToDefaultPosition() exoPlayer?.prepare() } + else -> { playerError?.invoke(error) } @@ -1025,6 +1048,7 @@ class CS3IPlayer : IPlayer { Player.STATE_READY -> { } + Player.STATE_ENDED -> { // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(context) @@ -1036,12 +1060,15 @@ class CS3IPlayer : IPlayer { handleEvent(CSPlayerEvent.NextEpisode) } } + Player.STATE_BUFFERING -> { updatedTime() } + Player.STATE_IDLE -> { // IDLE } + else -> Unit } } @@ -1169,6 +1196,7 @@ class CS3IPlayer : IPlayer { null } } + SubtitleOrigin.URL -> { if (onlineSourceFactory != null) { activeSubtitles.add(sub) @@ -1181,6 +1209,7 @@ class CS3IPlayer : IPlayer { null } } + SubtitleOrigin.EMBEDDED_IN_VIDEO -> { if (offlineSourceFactory != null) { activeSubtitles.add(sub) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt index 974a5d26..b830d4e0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt @@ -3,19 +3,23 @@ package com.lagradost.cloudstream3.ui.player import android.content.Context import android.util.Log import androidx.preference.PreferenceManager -import com.google.android.exoplayer2.Format -import com.google.android.exoplayer2.text.* -import com.google.android.exoplayer2.text.cea.Cea608Decoder -import com.google.android.exoplayer2.text.cea.Cea708Decoder -import com.google.android.exoplayer2.text.dvb.DvbDecoder -import com.google.android.exoplayer2.text.pgs.PgsDecoder -import com.google.android.exoplayer2.text.ssa.SsaDecoder -import com.google.android.exoplayer2.text.subrip.SubripDecoder -import com.google.android.exoplayer2.text.ttml.TtmlDecoder -import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder -import com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder -import com.google.android.exoplayer2.text.webvtt.WebvttDecoder -import com.google.android.exoplayer2.util.MimeTypes +import androidx.media3.common.Format +import androidx.media3.common.MimeTypes +import androidx.media3.exoplayer.text.ExoplayerCuesDecoder +import androidx.media3.exoplayer.text.SubtitleDecoderFactory +import androidx.media3.extractor.text.SubtitleDecoder +import androidx.media3.extractor.text.SubtitleInputBuffer +import androidx.media3.extractor.text.SubtitleOutputBuffer +import androidx.media3.extractor.text.cea.Cea608Decoder +import androidx.media3.extractor.text.cea.Cea708Decoder +import androidx.media3.extractor.text.dvb.DvbDecoder +import androidx.media3.extractor.text.pgs.PgsDecoder +import androidx.media3.extractor.text.ssa.SsaDecoder +import androidx.media3.extractor.text.subrip.SubripDecoder +import androidx.media3.extractor.text.ttml.TtmlDecoder +import androidx.media3.extractor.text.tx3g.Tx3gDecoder +import androidx.media3.extractor.text.webvtt.Mp4WebvttDecoder +import androidx.media3.extractor.text.webvtt.WebvttDecoder import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import org.mozilla.universalchardet.UniversalDetector diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt index d3f4171a..514aaeab 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt @@ -1,8 +1,8 @@ package com.lagradost.cloudstream3.ui.player import android.os.Looper -import com.google.android.exoplayer2.text.SubtitleDecoderFactory -import com.google.android.exoplayer2.text.TextOutput +import androidx.media3.exoplayer.text.SubtitleDecoderFactory +import androidx.media3.exoplayer.text.TextOutput class CustomTextRenderer( offset: Long, 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 fd29d998..fdb19d7a 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 @@ -19,8 +19,8 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager -import com.google.android.exoplayer2.Format.NO_VALUE -import com.google.android.exoplayer2.util.MimeTypes +import androidx.media3.common.Format.NO_VALUE +import androidx.media3.common.MimeTypes import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java index 3b47b27a..b5ecfe8f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java @@ -15,10 +15,10 @@ */ package com.lagradost.cloudstream3.ui.player; -import static com.google.android.exoplayer2.text.Cue.DIMEN_UNSET; -import static com.google.android.exoplayer2.text.Cue.LINE_TYPE_NUMBER; -import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; +import static androidx.media3.common.text.Cue.DIMEN_UNSET; +import static androidx.media3.common.text.Cue.LINE_TYPE_NUMBER; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Handler; @@ -28,25 +28,24 @@ import android.os.Message; import androidx.annotation.IntDef; import androidx.annotation.Nullable; - -import com.google.android.exoplayer2.BaseRenderer; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.CueGroup; -import com.google.android.exoplayer2.text.Subtitle; -import com.google.android.exoplayer2.text.SubtitleDecoder; -import com.google.android.exoplayer2.text.SubtitleDecoderException; -import com.google.android.exoplayer2.text.SubtitleDecoderFactory; -import com.google.android.exoplayer2.text.SubtitleInputBuffer; -import com.google.android.exoplayer2.text.SubtitleOutputBuffer; -import com.google.android.exoplayer2.text.TextOutput; -import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.text.Cue; +import androidx.media3.common.text.CueGroup; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.BaseRenderer; +import androidx.media3.exoplayer.FormatHolder; +import androidx.media3.exoplayer.RendererCapabilities; +import androidx.media3.exoplayer.source.SampleStream; +import androidx.media3.exoplayer.text.SubtitleDecoderFactory; +import androidx.media3.exoplayer.text.TextOutput; +import androidx.media3.extractor.text.Subtitle; +import androidx.media3.extractor.text.SubtitleDecoder; +import androidx.media3.extractor.text.SubtitleDecoderException; +import androidx.media3.extractor.text.SubtitleInputBuffer; +import androidx.media3.extractor.text.SubtitleOutputBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -310,7 +309,7 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { return; } // Try and read the next subtitle from the source. - @ReadDataResult int result = readSource(formatHolder, nextInputBuffer, /* readFlags= */ 0); + @SampleStream.ReadDataResult int result = readSource(formatHolder, nextInputBuffer, /* readFlags= */ 0); if (result == C.RESULT_BUFFER_READ) { if (nextInputBuffer.isEndOfStream()) { inputStreamEnded = true; diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt index 0fbc22f6..8757aae0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt @@ -26,7 +26,7 @@ class PlayerPipHelper { activity, code, Intent("media_control").putExtra("control_type", code), - 0 + PendingIntent.FLAG_IMMUTABLE ) } } @@ -88,7 +88,15 @@ class PlayerPipHelper { ) ) activity.setPictureInPictureParams( - PictureInPictureParams.Builder().setActions(actions).build() + PictureInPictureParams.Builder() + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + setSeamlessResizeEnabled(true) + setAutoEnterEnabled(isPlaying) + } + } + .setActions(actions) + .build() ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index 8d85f176..e532d1a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -4,8 +4,8 @@ import android.util.Log import android.util.TypedValue import android.view.ViewGroup import android.widget.FrameLayout -import com.google.android.exoplayer2.ui.SubtitleView -import com.google.android.exoplayer2.util.MimeTypes +import androidx.media3.common.MimeTypes +import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveCaptions diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index 83d134cb..bbb5e833 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -14,7 +14,7 @@ import android.widget.TextView import android.widget.Toast import androidx.fragment.app.Fragment import com.fasterxml.jackson.annotation.JsonProperty -import com.google.android.exoplayer2.text.Cue +import androidx.media3.common.text.Cue import com.google.android.gms.cast.TextTrackStyle import com.google.android.gms.cast.TextTrackStyle.* import com.jaredrummler.android.colorpicker.ColorPickerDialog diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index ff0e0e82..01995af1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -18,8 +18,8 @@ import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty -import com.google.android.exoplayer2.text.Cue -import com.google.android.exoplayer2.ui.CaptionStyleCompat +import androidx.media3.common.text.Cue +import androidx.media3.ui.CaptionStyleCompat import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt index 9e8cc1d4..6b5e9ec2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt @@ -1,7 +1,7 @@ package com.lagradost.cloudstream3.utils import android.net.Uri -import com.google.android.exoplayer2.util.MimeTypes +import androidx.media3.common.MimeTypes import com.google.android.gms.cast.* import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.cast.framework.media.RemoteMediaClient diff --git a/app/src/main/res/layout/chromecast_subtitle_settings.xml b/app/src/main/res/layout/chromecast_subtitle_settings.xml index 624c2201..4d3b50df 100644 --- a/app/src/main/res/layout/chromecast_subtitle_settings.xml +++ b/app/src/main/res/layout/chromecast_subtitle_settings.xml @@ -35,7 +35,7 @@ android:layout_height="match_parent" android:contentDescription="@string/preview_background_img_des" /> - - - - - - - - Date: Tue, 18 Jul 2023 21:46:34 +0200 Subject: [PATCH 002/441] Fixed PiP aspect ratio and removed duplicated code. --- .../ui/player/AbstractPlayerFragment.kt | 2 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 7 ++++ .../cloudstream3/ui/player/IPlayer.kt | 14 +++++++ .../cloudstream3/ui/player/PlayerPipHelper.kt | 38 +++++++++++-------- 4 files changed, 44 insertions(+), 17 deletions(-) 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 17e64b67..27b37be8 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 @@ -172,7 +172,7 @@ abstract class AbstractPlayerFragment( canEnterPipMode = isPlayingRightNow && hasPipModeSupport if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { activity?.let { act -> - PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow) + PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow, player.getAspectRatio()) } } } 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 aa664b34..0e8e5e6e 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 @@ -5,6 +5,7 @@ import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log +import android.util.Rational import android.widget.FrameLayout import androidx.media3.common.C import androidx.preference.PreferenceManager @@ -453,6 +454,12 @@ class CS3IPlayer : IPlayer { } } + override fun getAspectRatio(): Rational? { + return exoPlayer?.videoFormat?.let { format -> + Rational(format.width, format.height) + } + } + override fun updateSubtitleStyle(style: SaveCaptionStyle) { subtitleHelper.setSubStyle(style) } 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 ba5a4a85..3038cb8d 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 @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.player import android.content.Context +import android.util.Rational import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink @@ -167,6 +168,19 @@ interface IPlayer { fun getVideoTracks(): CurrentTracks + /** + * Original video aspect ratio used for PiP mode + * + * Set using: Width, Height. + * Example: Rational(16, 9) + * + * If null will default to set no aspect ratio. + * + * PiP functions calling this needs to coerce this value between 0.418410 and 2.390000 + * to prevent crashes. + */ + fun getAspectRatio(): Rational? + /** If no parameters are set it'll default to no set size, Specifying the id allows for track overrides to force the player to pick the quality. */ fun setMaxVideoSize(width: Int = Int.MAX_VALUE, height: Int = Int.MAX_VALUE, id: String? = null) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt index 8757aae0..4bed0c9d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt @@ -7,28 +7,22 @@ import android.app.RemoteAction import android.content.Intent import android.graphics.drawable.Icon import android.os.Build +import android.util.Rational import androidx.annotation.RequiresApi import androidx.annotation.StringRes import com.lagradost.cloudstream3.R +import kotlin.math.roundToInt class PlayerPipHelper { companion object { + @RequiresApi(Build.VERSION_CODES.O) private fun getPen(activity: Activity, code: Int): PendingIntent { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.getBroadcast( - activity, - code, - Intent("media_control").putExtra("control_type", code), - PendingIntent.FLAG_IMMUTABLE - ) - } else { - PendingIntent.getBroadcast( - activity, - code, - Intent("media_control").putExtra("control_type", code), - PendingIntent.FLAG_IMMUTABLE - ) - } + return PendingIntent.getBroadcast( + activity, + code, + Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, code), + PendingIntent.FLAG_IMMUTABLE + ) } @RequiresApi(Build.VERSION_CODES.O) @@ -48,7 +42,7 @@ class PlayerPipHelper { } @RequiresApi(Build.VERSION_CODES.O) - fun updatePIPModeActions(activity: Activity, isPlaying: Boolean) { + fun updatePIPModeActions(activity: Activity, isPlaying: Boolean, aspectRatio: Rational?) { val actions: ArrayList = ArrayList() actions.add( getRemoteAction( @@ -87,6 +81,17 @@ class PlayerPipHelper { CSPlayerEvent.SeekForward ) ) + + // Nessecary to prevent crashing. + val mixAspectRatio = 0.41841f // ~1/2.39 + val maxAspectRatio = 2.39f // widescreen standard + val ratioAccuracy = 100000 // To convert the float to int + + // java.lang.IllegalArgumentException: setPictureInPictureParams: Aspect ratio is too extreme (must be between 0.418410 and 2.390000) + val fixedRational = aspectRatio?.toFloat()?.coerceIn(mixAspectRatio, maxAspectRatio)?.let { + Rational((it * ratioAccuracy).roundToInt(), ratioAccuracy) + } + activity.setPictureInPictureParams( PictureInPictureParams.Builder() .apply { @@ -95,6 +100,7 @@ class PlayerPipHelper { setAutoEnterEnabled(isPlaying) } } + .setAspectRatio(fixedRational) .setActions(actions) .build() ) From c1e87b3fd02eab7d3a779bd64679a7c055ee329e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 1 Aug 2023 00:55:51 +0200 Subject: [PATCH 003/441] fixed merge errors --- app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt | 3 +-- .../lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 5b33c251..0728fe61 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -67,7 +67,6 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.databinding.ActivityMainBinding import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding -import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall @@ -80,7 +79,7 @@ import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch 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 706d7631..e6f93377 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 @@ -25,6 +25,7 @@ import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.PlayerView import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey From 8dae4c2b0f3ddc62414c5e7f8016093ca7b9fb45 Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Tue, 1 Aug 2023 01:25:28 +0200 Subject: [PATCH 004/441] Self similarity/master (#525) * Migrated to Media3 --------- Co-authored-by: self-similarity <137652432+self-similarity@users.noreply.github.com> --- app/build.gradle.kts | 26 +++++--- .../lagradost/cloudstream3/MainActivity.kt | 3 +- .../ui/player/AbstractPlayerFragment.kt | 49 +++++++-------- .../cloudstream3/ui/player/CS3IPlayer.kt | 63 ++++++++++++------- .../ui/player/CustomSubtitleDecoderFactory.kt | 30 +++++---- .../ui/player/CustomTextRenderer.kt | 4 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 4 +- .../cloudstream3/ui/player/IPlayer.kt | 14 +++++ .../ui/player/NonFinalTextRenderer.java | 47 +++++++------- .../cloudstream3/ui/player/PlayerPipHelper.kt | 48 +++++++++----- .../ui/player/PlayerSubtitleHelper.kt | 4 +- .../subtitles/ChromecastSubtitlesFragment.kt | 2 +- .../ui/subtitles/SubtitlesFragment.kt | 4 +- .../cloudstream3/utils/CastHelper.kt | 2 +- .../layout/chromecast_subtitle_settings.xml | 2 +- app/src/main/res/layout/fragment_player.xml | 2 +- .../main/res/layout/fragment_player_tv.xml | 2 +- app/src/main/res/layout/fragment_trailer.xml | 2 +- .../main/res/layout/player_custom_layout.xml | 2 +- .../res/layout/player_custom_layout_tv.xml | 2 +- app/src/main/res/layout/subtitle_settings.xml | 3 +- .../main/res/layout/trailer_custom_layout.xml | 2 +- gradle.properties | 2 +- 23 files changed, 189 insertions(+), 130 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 27bd1e48..6fd4ae21 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,7 +18,7 @@ fun String.execute() = ByteArrayOutputStream().use { baot -> workingDir = projectDir commandLine = this@execute.split(Regex("\\s")) standardOutput = baot - }.exitValue == 0) + }.exitValue == 0) String(baot.toByteArray()).trim() else null } @@ -78,12 +78,18 @@ android { isDebuggable = false isMinifyEnabled = false isShrinkResources = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } debug { isDebuggable = true applicationIdSuffix = ".debug" - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) } } flavorDimensions.add("state") @@ -160,10 +166,16 @@ dependencies { // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09") // Exoplayer - implementation("com.google.android.exoplayer:exoplayer:2.18.2") - implementation("com.google.android.exoplayer:extension-cast:2.18.2") - implementation("com.google.android.exoplayer:extension-mediasession:2.18.2") - implementation("com.google.android.exoplayer:extension-okhttp:2.18.2") + implementation("androidx.media3:media3-common:1.1.0") + implementation("androidx.media3:media3-exoplayer:1.1.0") + implementation("androidx.media3:media3-datasource-okhttp:1.1.0") + implementation("androidx.media3:media3-ui:1.1.0") + implementation("androidx.media3:media3-session:1.1.0") + implementation("androidx.media3:media3-cast:1.1.0") + implementation("androidx.media3:media3-exoplayer-hls:1.1.0") + implementation("androidx.media3:media3-exoplayer-dash:1.1.0") + + // Use the Jellyfin ffmpeg extension for easy ffmpeg audio decoding in exoplayer. Thank you Jellyfin <3 // implementation("org.jellyfin.exoplayer:exoplayer-ffmpeg-extension:2.18.2+1") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index b7add6ff..0728fe61 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -40,6 +40,7 @@ import androidx.preference.PreferenceManager import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.Session import com.google.android.gms.cast.framework.SessionManager @@ -62,10 +63,10 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale +import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.databinding.ActivityMainBinding import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding -import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall 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 a00127fd..e6f93377 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 @@ -7,7 +7,6 @@ import android.graphics.drawable.AnimatedVectorDrawable import android.media.metrics.PlaybackErrorEvent import android.os.Build import android.os.Bundle -import android.support.v4.media.session.MediaSessionCompat import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -20,15 +19,14 @@ import androidx.annotation.LayoutRes import androidx.annotation.StringRes import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.media.session.MediaButtonReceiver import androidx.preference.PreferenceManager import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat -import com.google.android.exoplayer2.ExoPlayer -import com.google.android.exoplayer2.PlaybackException -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout -import com.google.android.exoplayer2.ui.PlayerView -import com.google.android.exoplayer2.ui.SubtitleView +import androidx.media3.common.PlaybackException +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.session.MediaSession +import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.PlayerView +import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode @@ -38,6 +36,7 @@ import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.AppUtils @@ -179,15 +178,16 @@ abstract class AbstractPlayerFragment( } canEnterPipMode = isPlayingRightNow && hasPipModeSupport - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isInPIPMode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { activity?.let { act -> - PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow) + PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow, player.getAspectRatio()) } } } private var pipReceiver: BroadcastReceiver? = null override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode) try { isInPIPMode = isInPictureInPictureMode if (isInPictureInPictureMode) { @@ -210,9 +210,7 @@ abstract class AbstractPlayerFragment( } } val filter = IntentFilter() - filter.addAction( - ACTION_MEDIA_CONTROL - ) + filter.addAction(ACTION_MEDIA_CONTROL) activity?.registerReceiver(pipReceiver, filter) val isPlaying = player.getIsPlaying() val isPlayingValue = @@ -223,7 +221,10 @@ abstract class AbstractPlayerFragment( piphide?.isVisible = true exitedPipMode() pipReceiver?.let { - activity?.unregisterReceiver(it) + // Prevents java.lang.IllegalArgumentException: Receiver not registered + normalSafeApiCall { + activity?.unregisterReceiver(it) + } } activity?.hideSystemUI() this.view?.let { UIHelper.hideKeyboard(it) } @@ -276,18 +277,21 @@ abstract class AbstractPlayerFragment( gotoNext = true ) } + PlaybackException.ERROR_CODE_REMOTE_ERROR, PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS, PlaybackException.ERROR_CODE_TIMEOUT, PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE -> { showToast( "${ctx.getString(R.string.remote_error)}\n$errorName ($code)\n$msg", gotoNext = true ) } + PlaybackException.ERROR_CODE_DECODING_FAILED, PlaybackErrorEvent.ERROR_AUDIO_TRACK_INIT_FAILED, PlaybackErrorEvent.ERROR_AUDIO_TRACK_OTHER, PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED, PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED -> { showToast( "${ctx.getString(R.string.render_error)}\n$errorName ($code)\n$msg", gotoNext = true ) } + else -> { showToast( "${ctx.getString(R.string.unexpected_error)}\n$errorName ($code)\n$msg", @@ -296,12 +300,14 @@ abstract class AbstractPlayerFragment( } } } + is InvalidFileException -> { showToast( "${ctx.getString(R.string.source_error)}\n${exception.message}", gotoNext = true ) } + else -> { exception.message?.let { showToast( @@ -322,15 +328,8 @@ abstract class AbstractPlayerFragment( private fun playerUpdated(player: Any?) { if (player is ExoPlayer) { context?.let { ctx -> - val mediaButtonReceiver = ComponentName(ctx, MediaButtonReceiver::class.java) - MediaSessionCompat(ctx, "Player", mediaButtonReceiver, null).let { media -> - //media.setCallback(mMediaSessionCallback) - //media.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS) - val mediaSessionConnector = MediaSessionConnector(media) - mediaSessionConnector.setPlayer(player) - media.isActive = true - mMediaSessionCompat = media - } + mMediaSession?.release() + mMediaSession = MediaSession.Builder(ctx, player).build() } // Necessary for multiple combined videos @@ -340,8 +339,7 @@ abstract class AbstractPlayerFragment( } } - private var mediaSessionConnector: MediaSessionConnector? = null - private var mMediaSessionCompat: MediaSessionCompat? = null + private var mMediaSession: MediaSession? = null // this can be used in the future for players other than exoplayer //private val mMediaSessionCallback: MediaSessionCompat.Callback = object : MediaSessionCompat.Callback() { @@ -442,6 +440,7 @@ abstract class AbstractPlayerFragment( playerEventListener = null keyEventListener = null canEnterPipMode = false + mMediaSession?.release() SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged keepScreenOn(false) 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 f491f995..70e12a47 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 @@ -5,30 +5,42 @@ import android.net.Uri import android.os.Handler import android.os.Looper import android.util.Log +import android.util.Rational import android.widget.FrameLayout +import androidx.media3.common.C import androidx.preference.PreferenceManager -import com.google.android.exoplayer2.* -import com.google.android.exoplayer2.C.* -import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON -import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER -import com.google.android.exoplayer2.database.StandaloneDatabaseProvider -import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource -import com.google.android.exoplayer2.mediacodec.MediaCodecSelector -import com.google.android.exoplayer2.source.* -import com.google.android.exoplayer2.text.TextRenderer -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector -import com.google.android.exoplayer2.trackselection.TrackSelectionOverride -import com.google.android.exoplayer2.trackselection.TrackSelector -import com.google.android.exoplayer2.ui.SubtitleView -import com.google.android.exoplayer2.upstream.DataSource -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory -import com.google.android.exoplayer2.upstream.DefaultHttpDataSource -import com.google.android.exoplayer2.upstream.HttpDataSource -import com.google.android.exoplayer2.upstream.cache.CacheDataSource -import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor -import com.google.android.exoplayer2.upstream.cache.SimpleCache -import com.google.android.exoplayer2.util.MimeTypes -import com.google.android.exoplayer2.video.VideoSize +import androidx.media3.common.C.* +import androidx.media3.common.Format +import androidx.media3.common.MediaItem +import androidx.media3.common.TrackSelectionOverride +import androidx.media3.common.MimeTypes +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.common.TrackGroup +import androidx.media3.common.Tracks +import androidx.media3.common.VideoSize +import androidx.media3.database.StandaloneDatabaseProvider +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DefaultDataSourceFactory +import androidx.media3.datasource.DefaultHttpDataSource +import androidx.media3.datasource.HttpDataSource +import androidx.media3.datasource.cache.CacheDataSource +import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor +import androidx.media3.datasource.cache.SimpleCache +import androidx.media3.datasource.okhttp.OkHttpDataSource +import androidx.media3.exoplayer.DefaultLoadControl +import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.SeekParameters +import androidx.media3.exoplayer.source.ClippingMediaSource +import androidx.media3.exoplayer.source.ConcatenatingMediaSource +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.exoplayer.source.MergingMediaSource +import androidx.media3.exoplayer.source.SingleSampleMediaSource +import androidx.media3.exoplayer.text.TextRenderer +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector +import androidx.media3.exoplayer.trackselection.TrackSelector +import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -44,7 +56,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File -import java.time.Duration import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession @@ -451,6 +462,12 @@ class CS3IPlayer : IPlayer { } } + override fun getAspectRatio(): Rational? { + return exoPlayer?.videoFormat?.let { format -> + Rational(format.width, format.height) + } + } + override fun updateSubtitleStyle(style: SaveCaptionStyle) { subtitleHelper.setSubStyle(style) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt index 974a5d26..b830d4e0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt @@ -3,19 +3,23 @@ package com.lagradost.cloudstream3.ui.player import android.content.Context import android.util.Log import androidx.preference.PreferenceManager -import com.google.android.exoplayer2.Format -import com.google.android.exoplayer2.text.* -import com.google.android.exoplayer2.text.cea.Cea608Decoder -import com.google.android.exoplayer2.text.cea.Cea708Decoder -import com.google.android.exoplayer2.text.dvb.DvbDecoder -import com.google.android.exoplayer2.text.pgs.PgsDecoder -import com.google.android.exoplayer2.text.ssa.SsaDecoder -import com.google.android.exoplayer2.text.subrip.SubripDecoder -import com.google.android.exoplayer2.text.ttml.TtmlDecoder -import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder -import com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder -import com.google.android.exoplayer2.text.webvtt.WebvttDecoder -import com.google.android.exoplayer2.util.MimeTypes +import androidx.media3.common.Format +import androidx.media3.common.MimeTypes +import androidx.media3.exoplayer.text.ExoplayerCuesDecoder +import androidx.media3.exoplayer.text.SubtitleDecoderFactory +import androidx.media3.extractor.text.SubtitleDecoder +import androidx.media3.extractor.text.SubtitleInputBuffer +import androidx.media3.extractor.text.SubtitleOutputBuffer +import androidx.media3.extractor.text.cea.Cea608Decoder +import androidx.media3.extractor.text.cea.Cea708Decoder +import androidx.media3.extractor.text.dvb.DvbDecoder +import androidx.media3.extractor.text.pgs.PgsDecoder +import androidx.media3.extractor.text.ssa.SsaDecoder +import androidx.media3.extractor.text.subrip.SubripDecoder +import androidx.media3.extractor.text.ttml.TtmlDecoder +import androidx.media3.extractor.text.tx3g.Tx3gDecoder +import androidx.media3.extractor.text.webvtt.Mp4WebvttDecoder +import androidx.media3.extractor.text.webvtt.WebvttDecoder import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import org.mozilla.universalchardet.UniversalDetector diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt index d3f4171a..514aaeab 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt @@ -1,8 +1,8 @@ package com.lagradost.cloudstream3.ui.player import android.os.Looper -import com.google.android.exoplayer2.text.SubtitleDecoderFactory -import com.google.android.exoplayer2.text.TextOutput +import androidx.media3.exoplayer.text.SubtitleDecoderFactory +import androidx.media3.exoplayer.text.TextOutput class CustomTextRenderer( offset: Long, 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 46bf8568..c280af59 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 @@ -19,8 +19,8 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager -import com.google.android.exoplayer2.Format.NO_VALUE -import com.google.android.exoplayer2.util.MimeTypes +import androidx.media3.common.Format.NO_VALUE +import androidx.media3.common.MimeTypes import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull 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 ba5a4a85..3038cb8d 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 @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.player import android.content.Context +import android.util.Rational import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink @@ -167,6 +168,19 @@ interface IPlayer { fun getVideoTracks(): CurrentTracks + /** + * Original video aspect ratio used for PiP mode + * + * Set using: Width, Height. + * Example: Rational(16, 9) + * + * If null will default to set no aspect ratio. + * + * PiP functions calling this needs to coerce this value between 0.418410 and 2.390000 + * to prevent crashes. + */ + fun getAspectRatio(): Rational? + /** If no parameters are set it'll default to no set size, Specifying the id allows for track overrides to force the player to pick the quality. */ fun setMaxVideoSize(width: Int = Int.MAX_VALUE, height: Int = Int.MAX_VALUE, id: String? = null) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java index 3b47b27a..b5ecfe8f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java @@ -15,10 +15,10 @@ */ package com.lagradost.cloudstream3.ui.player; -import static com.google.android.exoplayer2.text.Cue.DIMEN_UNSET; -import static com.google.android.exoplayer2.text.Cue.LINE_TYPE_NUMBER; -import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; +import static androidx.media3.common.text.Cue.DIMEN_UNSET; +import static androidx.media3.common.text.Cue.LINE_TYPE_NUMBER; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; import static java.lang.annotation.ElementType.TYPE_USE; import android.os.Handler; @@ -28,25 +28,24 @@ import android.os.Message; import androidx.annotation.IntDef; import androidx.annotation.Nullable; - -import com.google.android.exoplayer2.BaseRenderer; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.source.SampleStream.ReadDataResult; -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.CueGroup; -import com.google.android.exoplayer2.text.Subtitle; -import com.google.android.exoplayer2.text.SubtitleDecoder; -import com.google.android.exoplayer2.text.SubtitleDecoderException; -import com.google.android.exoplayer2.text.SubtitleDecoderFactory; -import com.google.android.exoplayer2.text.SubtitleInputBuffer; -import com.google.android.exoplayer2.text.SubtitleOutputBuffer; -import com.google.android.exoplayer2.text.TextOutput; -import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.text.Cue; +import androidx.media3.common.text.CueGroup; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.BaseRenderer; +import androidx.media3.exoplayer.FormatHolder; +import androidx.media3.exoplayer.RendererCapabilities; +import androidx.media3.exoplayer.source.SampleStream; +import androidx.media3.exoplayer.text.SubtitleDecoderFactory; +import androidx.media3.exoplayer.text.TextOutput; +import androidx.media3.extractor.text.Subtitle; +import androidx.media3.extractor.text.SubtitleDecoder; +import androidx.media3.extractor.text.SubtitleDecoderException; +import androidx.media3.extractor.text.SubtitleInputBuffer; +import androidx.media3.extractor.text.SubtitleOutputBuffer; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -310,7 +309,7 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { return; } // Try and read the next subtitle from the source. - @ReadDataResult int result = readSource(formatHolder, nextInputBuffer, /* readFlags= */ 0); + @SampleStream.ReadDataResult int result = readSource(formatHolder, nextInputBuffer, /* readFlags= */ 0); if (result == C.RESULT_BUFFER_READ) { if (nextInputBuffer.isEndOfStream()) { inputStreamEnded = true; diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt index 0fbc22f6..4bed0c9d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt @@ -7,28 +7,22 @@ import android.app.RemoteAction import android.content.Intent import android.graphics.drawable.Icon import android.os.Build +import android.util.Rational import androidx.annotation.RequiresApi import androidx.annotation.StringRes import com.lagradost.cloudstream3.R +import kotlin.math.roundToInt class PlayerPipHelper { companion object { + @RequiresApi(Build.VERSION_CODES.O) private fun getPen(activity: Activity, code: Int): PendingIntent { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.getBroadcast( - activity, - code, - Intent("media_control").putExtra("control_type", code), - PendingIntent.FLAG_IMMUTABLE - ) - } else { - PendingIntent.getBroadcast( - activity, - code, - Intent("media_control").putExtra("control_type", code), - 0 - ) - } + return PendingIntent.getBroadcast( + activity, + code, + Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, code), + PendingIntent.FLAG_IMMUTABLE + ) } @RequiresApi(Build.VERSION_CODES.O) @@ -48,7 +42,7 @@ class PlayerPipHelper { } @RequiresApi(Build.VERSION_CODES.O) - fun updatePIPModeActions(activity: Activity, isPlaying: Boolean) { + fun updatePIPModeActions(activity: Activity, isPlaying: Boolean, aspectRatio: Rational?) { val actions: ArrayList = ArrayList() actions.add( getRemoteAction( @@ -87,8 +81,28 @@ class PlayerPipHelper { CSPlayerEvent.SeekForward ) ) + + // Nessecary to prevent crashing. + val mixAspectRatio = 0.41841f // ~1/2.39 + val maxAspectRatio = 2.39f // widescreen standard + val ratioAccuracy = 100000 // To convert the float to int + + // java.lang.IllegalArgumentException: setPictureInPictureParams: Aspect ratio is too extreme (must be between 0.418410 and 2.390000) + val fixedRational = aspectRatio?.toFloat()?.coerceIn(mixAspectRatio, maxAspectRatio)?.let { + Rational((it * ratioAccuracy).roundToInt(), ratioAccuracy) + } + activity.setPictureInPictureParams( - PictureInPictureParams.Builder().setActions(actions).build() + PictureInPictureParams.Builder() + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + setSeamlessResizeEnabled(true) + setAutoEnterEnabled(isPlaying) + } + } + .setAspectRatio(fixedRational) + .setActions(actions) + .build() ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index 8d85f176..e532d1a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -4,8 +4,8 @@ import android.util.Log import android.util.TypedValue import android.view.ViewGroup import android.widget.FrameLayout -import com.google.android.exoplayer2.ui.SubtitleView -import com.google.android.exoplayer2.util.MimeTypes +import androidx.media3.common.MimeTypes +import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveCaptions diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index 07d00b07..ffb593a0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -14,7 +14,7 @@ import android.widget.TextView import android.widget.Toast import androidx.fragment.app.Fragment import com.fasterxml.jackson.annotation.JsonProperty -import com.google.android.exoplayer2.text.Cue +import androidx.media3.common.text.Cue import com.google.android.gms.cast.TextTrackStyle import com.google.android.gms.cast.TextTrackStyle.* import com.jaredrummler.android.colorpicker.ColorPickerDialog diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index ea8524e3..f053023d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -18,8 +18,8 @@ import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty -import com.google.android.exoplayer2.text.Cue -import com.google.android.exoplayer2.ui.CaptionStyleCompat +import androidx.media3.common.text.Cue +import androidx.media3.ui.CaptionStyleCompat import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt index 9e8cc1d4..6b5e9ec2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt @@ -1,7 +1,7 @@ package com.lagradost.cloudstream3.utils import android.net.Uri -import com.google.android.exoplayer2.util.MimeTypes +import androidx.media3.common.MimeTypes import com.google.android.gms.cast.* import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.cast.framework.media.RemoteMediaClient diff --git a/app/src/main/res/layout/chromecast_subtitle_settings.xml b/app/src/main/res/layout/chromecast_subtitle_settings.xml index 624c2201..4d3b50df 100644 --- a/app/src/main/res/layout/chromecast_subtitle_settings.xml +++ b/app/src/main/res/layout/chromecast_subtitle_settings.xml @@ -35,7 +35,7 @@ android:layout_height="match_parent" android:contentDescription="@string/preview_background_img_des" /> - - - - - - - - Date: Tue, 1 Aug 2023 03:12:32 +0200 Subject: [PATCH 005/441] fixed #524 --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1feb7d88..d69c18ef 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 @@ -624,7 +624,7 @@ class ResultFragmentTv : Fragment() { resultPlaySeries.setOnClickListener { viewModel.handleAction( EpisodeClickEvent( - ACTION_PLAY_EPISODE_IN_PLAYER, + ACTION_CLICK_DEFAULT, first ) ) From 7c60ccdef274ef5cc377d3d376d4909642cf6d9b Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 1 Aug 2023 04:03:43 +0200 Subject: [PATCH 006/441] fixed #521 --- .../lagradost/cloudstream3/CommonActivity.kt | 6 ++- .../lagradost/cloudstream3/MainActivity.kt | 41 ++++++++++++------- .../ui/home/HomeParentItemAdapterPreview.kt | 27 +++++------- 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 9c7c319e..3cfde983 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -372,7 +372,11 @@ object CommonActivity { } // if cant focus but visible then break and let android decide - if (!next.isFocusable && next.isShown) return null + // the exception if is the view is a parent and has children that wants focus + val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent -> + parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0 + } ?: false + if (!next.isFocusable && next.isShown && !hasChildrenThatWantsFocus) return null // if not shown then continue because we will "skip" over views to get to a replacement if (!next.isShown) return getNextFocus(act, next, direction, depth + 1) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 0728fe61..f25bef7e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -37,10 +37,10 @@ import androidx.navigation.NavOptions import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.RecyclerView import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.Session import com.google.android.gms.cast.framework.SessionManager @@ -63,10 +63,10 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale -import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.databinding.ActivityMainBinding import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding +import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall @@ -780,7 +780,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { return ret } - private var binding: ActivityMainBinding? = null + var binding: ActivityMainBinding? = null object TvFocus { data class FocusTarget( @@ -808,10 +808,18 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { var focusOutline: WeakReference = WeakReference(null) var lastFocus: WeakReference = WeakReference(null) private val layoutListener: View.OnLayoutChangeListener = - View.OnLayoutChangeListener { v, _, _, _, _, _, _, _, _ -> - updateFocusView( - v, same = true - ) + View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + // shitty fix for layouts + lastFocus.get()?.apply { + updateFocusView( + this, same = true + ) + postDelayed({ + updateFocusView( + lastFocus.get(), same = false + ) + }, 300) + } } private val attachListener: View.OnAttachStateChangeListener = object : View.OnAttachStateChangeListener { @@ -843,9 +851,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { @MainThread fun updateFocusView(newFocus: View?, same: Boolean = false) { val focusOutline = focusOutline.get() ?: return - lastFocus.get()?.apply { - removeOnLayoutChangeListener(layoutListener) - removeOnAttachStateChangeListener(attachListener) + val lastView = lastFocus.get() + val exactlyTheSame = lastView == newFocus && newFocus != null + if (!exactlyTheSame) { + lastView?.removeOnLayoutChangeListener(layoutListener) + lastView?.removeOnAttachStateChangeListener(attachListener) + (lastView?.parent as? RecyclerView)?.removeOnLayoutChangeListener(layoutListener) } val wasGone = focusOutline.isGone @@ -857,6 +868,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (newFocus != null) { lastFocus = WeakReference(newFocus) + val out = IntArray(2) newFocus.getLocationInWindow(out) val (screenX, screenY) = out @@ -871,10 +883,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (screenX == 0 && screenY == 0) { focusOutline.isVisible = false } - - newFocus.addOnLayoutChangeListener(layoutListener) - newFocus.addOnAttachStateChangeListener(attachListener) - + if (!exactlyTheSame) { + (newFocus.parent as? RecyclerView)?.addOnLayoutChangeListener(layoutListener) + newFocus.addOnLayoutChangeListener(layoutListener) + newFocus.addOnAttachStateChangeListener(attachListener) + } val start = FocusTarget( x = currentX, y = currentY, 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 fd2412da..84964950 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 @@ -14,11 +14,12 @@ import androidx.viewbinding.ViewBinding import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable -import com.google.android.material.navigationrail.NavigationRailView import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity +import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.databinding.FragmentHomeHeadBinding @@ -467,24 +468,18 @@ class HomeParentItemAdapterPreview( } homePreviewHiddenNextFocus.setOnFocusChangeListener { _, hasFocus -> - if (hasFocus) { - previewViewpager.setCurrentItem(previewViewpager.currentItem + 1, true) - homePreviewInfoBtt.requestFocus() - } + if (!hasFocus) return@setOnFocusChangeListener + previewViewpager.setCurrentItem(previewViewpager.currentItem + 1, true) + homePreviewInfoBtt.requestFocus() } homePreviewHiddenPrevFocus.setOnFocusChangeListener { _, hasFocus -> - if (hasFocus) { - previewViewpager.apply { - if (currentItem <= 0) { - findViewById(R.id.nav_rail_view)?.menu?.getItem( - 0 - )?.actionView?.requestFocus() - } else { - setCurrentItem(currentItem - 1, true) - binding.homePreviewPlayBtt.requestFocus() - } - } + if (!hasFocus) return@setOnFocusChangeListener + if (previewViewpager.currentItem <= 0) { + (activity as? MainActivity)?.binding?.navRailView?.requestFocus() + } else { + previewViewpager.setCurrentItem(previewViewpager.currentItem - 1, true) + binding.homePreviewPlayBtt.requestFocus() } } } From 363ffa26de27dbc4dfccf840e06ada0fce8c1f6f Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Tue, 1 Aug 2023 04:11:20 +0200 Subject: [PATCH 007/441] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e3d033ba..8949304e 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ + **AdFree**, No ads whatsoever + No tracking/analytics + Bookmarks -+ Download and stream movies, tv-shows and anime ++ Phone and TV support + Chromecast ++ Extension system for personal customization ### Supported languages: From a8ed8773deb901ec5c5aaae5fb7c0d8fa8b5e9c0 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Tue, 1 Aug 2023 20:50:02 +0700 Subject: [PATCH 008/441] Extractor: fix Gofile and added Userscloud (#523) * Extractor: added Pixeldrain, Wibufile and fix some extractors * Extractor: added Moviesapi and fix some extractors * Extractor: fix Gofile and added Userscloud --------- Co-authored-by: Sofie99 --- .../cloudstream3/extractors/Gofile.kt | 5 ++- .../cloudstream3/extractors/Userscloud.kt | 45 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 1 + 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt index 2ec185e0..d76b0e11 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt @@ -21,7 +21,10 @@ open class Gofile : ExtractorApi() { ) { val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z]+)").find(url)?.groupValues?.get(1) val token = app.get("$mainApi/createAccount").parsedSafe()?.data?.get("token") - app.get("$mainApi/getContent?contentId=$id&token=$token&websiteToken=12345") + val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let { + Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1) + } + app.get("$mainApi/getContent?contentId=$id&token=$token&websiteToken=$websiteToken") .parsedSafe()?.data?.contents?.forEach { callback.invoke( ExtractorLink( diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt new file mode 100644 index 00000000..e8bd6c57 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt @@ -0,0 +1,45 @@ +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 + +open class Userscloud : ExtractorApi() { + override val name = "Userscloud" + override val mainUrl = "https://userscloud.com" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url).document + val video = res.selectFirst("video#vjsplayer source")?.attr("src") + val quality = res.selectFirst("div.innerTB h2 b")?.text() + callback.invoke( + ExtractorLink( + this.name, + this.name, + video ?: return, + "$mainUrl/", + getQuality(quality), + headers = mapOf( + "Accept" to "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5", + "Range" to "bytes=0-", + "Sec-Fetch-Dest" to "video", + "Sec-Fetch-Mode" to "no-cors", + ) + ) + ) + } + + private fun getQuality(str: String?): Int { + return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull() + ?: Qualities.Unknown.value + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 6a5a665a..ed190bcc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -370,6 +370,7 @@ val extractorApis: MutableList = arrayListOf( Gofile(), Vicloud(), Uservideo(), + Userscloud(), Movhide(), StreamhideCom(), From 827cbbb0b569df9dfd176db0c8f2c23e7e680bdc Mon Sep 17 00:00:00 2001 From: Jace <54625750+Jacekun@users.noreply.github.com> Date: Tue, 1 Aug 2023 21:54:15 +0800 Subject: [PATCH 009/441] Feature: Refactor autodownload plugin to have multiple modes. (#518) --- .../com/lagradost/cloudstream3/MainAPI.kt | 12 ++++++ .../lagradost/cloudstream3/MainActivity.kt | 11 ++--- .../cloudstream3/plugins/PluginManager.kt | 41 +++++++++++-------- .../ui/settings/SettingsUpdates.kt | 24 +++++++++++ app/src/main/res/values/array.xml | 9 +++- app/src/main/res/values/strings.xml | 3 ++ app/src/main/res/xml/settings_updates.xml | 5 +-- 7 files changed, 78 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 51d218bf..76abda97 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -813,6 +813,18 @@ enum class TvType(value: Int?) { Others(12) } +public enum class AutoDownloadMode(val value: Int) { + Disable(0), + FilterByLang(1), + All(2), + NsfwOnly(3) + ; + + companion object { + infix fun getEnum(value: Int): AutoDownloadMode? = AutoDownloadMode.values().firstOrNull { it.value == value } + } +} + // IN CASE OF FUTURE ANIME MOVIE OR SMTH fun TvType.isMovieType(): Boolean { return this == TvType.Movie || this == TvType.AnimeMovie || this == TvType.Torrent || this == TvType.Live diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index f25bef7e..b1f60ad7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1092,13 +1092,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { loadAllOnlinePlugins(this@MainActivity) } - //Automatically download not existing plugins - if (settingsManager.getBoolean( - getString(R.string.auto_download_plugins_key), - false - ) - ) { - PluginManager.downloadNotExistingPluginsAndLoad(this@MainActivity) + //Automatically download not existing plugins, using mode specified. + val auto_download_plugin = AutoDownloadMode.getEnum(settingsManager.getInt(getString(R.string.auto_download_plugins_key), 0)) ?: AutoDownloadMode.Disable + if (auto_download_plugin != AutoDownloadMode.Disable) { + PluginManager.downloadNotExistingPluginsAndLoad(this@MainActivity, auto_download_plugin) } } 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 49b5a752..4c32088a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -290,7 +290,7 @@ object PluginManager { * 2. Fetch all not downloaded plugins * 3. Download them and reload plugins **/ - fun downloadNotExistingPluginsAndLoad(activity: Activity) { + fun downloadNotExistingPluginsAndLoad(activity: Activity, mode: AutoDownloadMode) { val newDownloadPlugins = mutableListOf() val urls = (getKey>(REPOSITORIES_KEY) ?: emptyArray()) + PREBUILT_REPOSITORIES @@ -304,6 +304,8 @@ object PluginManager { // Iterate online repos and returns not downloaded plugins val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData -> val sitePlugin = onlineData.second + val tvtypes = sitePlugin.tvTypes ?: listOf() + //Don't include empty urls if (sitePlugin.url.isBlank()) { return@mapNotNull null @@ -318,22 +320,29 @@ object PluginManager { return@mapNotNull null } - //Omit lang not selected on language setting - val lang = sitePlugin.language ?: return@mapNotNull null - //If set to 'universal', don't skip any language - if (!providerLang.contains(AllLanguagesName) && !providerLang.contains(lang)) { - return@mapNotNull null - } - //Log.i(TAG, "sitePlugin lang => $lang") - - //Omit NSFW, if disabled - sitePlugin.tvTypes?.let { tvtypes -> - if (!settingsForProvider.enableAdult) { - if (tvtypes.contains(TvType.NSFW.name)) { - return@mapNotNull null - } + //Omit non-NSFW if mode is set to NSFW only + if (mode == AutoDownloadMode.NsfwOnly) { + if (tvtypes.contains(TvType.NSFW.name) == false) { + return@mapNotNull null } } + //Omit NSFW, if disabled + if (!settingsForProvider.enableAdult) { + if (tvtypes.contains(TvType.NSFW.name)) { + return@mapNotNull null + } + } + + //Omit lang not selected on language setting + if (mode == AutoDownloadMode.FilterByLang) { + val lang = sitePlugin.language ?: return@mapNotNull null + //If set to 'universal', don't skip any language + if (!providerLang.contains(AllLanguagesName) && !providerLang.contains(lang)) { + return@mapNotNull null + } + //Log.i(TAG, "sitePlugin lang => $lang") + } + val savedData = PluginData( url = sitePlugin.url, internalName = sitePlugin.internalName, @@ -697,4 +706,4 @@ object PluginManager { return null } } -} \ No newline at end of file +} 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 915ef15f..9227409d 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 @@ -11,10 +11,14 @@ import androidx.appcompat.app.AlertDialog import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.AcraApplication +import com.lagradost.cloudstream3.AutoDownloadMode import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.databinding.LogcatBinding import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -166,5 +170,25 @@ class SettingsUpdates : PreferenceFragmentCompat() { } return@setOnPreferenceClickListener true } + + getPref(R.string.auto_download_plugins_key)?.setOnPreferenceClickListener { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context) + + val prefNames = resources.getStringArray(R.array.auto_download_plugin) + val prefValues = enumValues().sortedBy { x -> x.value }.map { x -> x.value } + + val current = settingsManager.getInt(getString(R.string.auto_download_plugins_pref), 0) + + activity?.showBottomDialog( + prefNames.toList(), + prefValues.indexOf(current), + getString(R.string.automatic_plugin_download_mode_title), + true, + {}) { + settingsManager.edit().putInt(getString(R.string.auto_download_plugins_pref), prefValues[it]).apply() + (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } + } + return@setOnPreferenceClickListener true + } } } diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 6aff7a59..1df7b9d6 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -41,7 +41,14 @@ 0 1 - + + + + @string/disable + @string/subtitles_filter_lang + @string/all + @string/nsfw + @string/player_settings_play_in_app diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c101b0b1..6a1e9eac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,7 @@ auto_update auto_update_plugins auto_download_plugins_key + auto_download_plugins_pref skip_update_key prerelease_update manual_check_update @@ -249,6 +250,7 @@ Hide selected video quality in search results Automatic plugin updates Automatically download plugins + Select mode to filter plugins download Automatically install all not yet installed plugins from added repositories. Show app updates Automatically search for new updates after starting the app. @@ -482,6 +484,7 @@ %s authenticated Could not log in at %s + Disable None Normal All diff --git a/app/src/main/res/xml/settings_updates.xml b/app/src/main/res/xml/settings_updates.xml index f2ec6747..9989e47b 100644 --- a/app/src/main/res/xml/settings_updates.xml +++ b/app/src/main/res/xml/settings_updates.xml @@ -58,8 +58,7 @@ android:key="@string/auto_update_plugins_key" android:title="@string/automatic_plugin_updates" /> - - \ No newline at end of file + From 0afc9f15d272e7ddc606e1f04eef95044c969fe0 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 1 Aug 2023 16:03:58 +0200 Subject: [PATCH 010/441] Translated using Weblate (Swedish) (#522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 83.1% (518 of 623 strings) Translated using Weblate (Swedish) Currently translated at 72.8% (454 of 623 strings) Translated using Weblate (Croatian) Currently translated at 99.8% (622 of 623 strings) Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Merge remote-tracking branch 'origin/master' Translated using Weblate (Galician) Currently translated at 16.6% (104 of 623 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (623 of 623 strings) Added translation using Weblate (Galician) Translated using Weblate (Czech) Currently translated at 100.0% (623 of 623 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (623 of 623 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (623 of 623 strings) Translated using Weblate (Polish) Currently translated at 100.0% (623 of 623 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Bulgarian) Currently translated at 92.5% (575 of 621 strings) Translated using Weblate (German) Currently translated at 25.0% (1 of 4 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 50.0% (2 of 4 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 50.0% (2 of 4 strings) Translated using Weblate (Ukrainian) Currently translated at 99.3% (617 of 621 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (English) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Polish) Currently translated at 50.0% (2 of 4 strings) Translated using Weblate (Greek) Currently translated at 94.5% (587 of 621 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (621 of 621 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Japanese) Currently translated at 46.3% (288 of 621 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (621 of 621 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Indonesian) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Japanese) Currently translated at 46.2% (287 of 621 strings) Translated using Weblate (Bulgarian) Currently translated at 90.6% (563 of 621 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (621 of 621 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (621 of 621 strings) Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ 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/el/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/gl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sv/ 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/fastlane/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/en/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/pl/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/zh_Hans/ Translation: Cloudstream/App Translation: Cloudstream/Fastlane Co-authored-by: Allan Nordhøy Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> Co-authored-by: Eryk Michalak Co-authored-by: Fjuro Co-authored-by: Hristo Hristov Co-authored-by: Julian Co-authored-by: Milo Ivir Co-authored-by: Osten <11805592+LagradOst@users.noreply.github.com> Co-authored-by: PiterDev Co-authored-by: Red Star Over Earth Co-authored-by: Rex_sa Co-authored-by: Reza Almanda Co-authored-by: Rudy Tantono Co-authored-by: Salif Mehmed Co-authored-by: Skrripy Co-authored-by: Thanasis Co-authored-by: dabao1955 Co-authored-by: gallegonovato Co-authored-by: ngocanhtve Co-authored-by: tictactoe --- app/src/main/res/values-hr/strings.xml | 19 +++++-- app/src/main/res/values-sv/strings.xml | 79 +++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 11 deletions(-) diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 754b7a3a..c606ab5e 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -48,7 +48,7 @@ Podijeli Otvori u pregledniku Preskoči učitavanje - Učitavanje... + Učitavanje … Gledam Na čekanju Dovršeno @@ -306,7 +306,7 @@ Omogući NSFW na podržanim pružateljima usluga Kodiranje titlova Pružatelji usluga - Izgled + Respored Auto TV izgled Izgled za telefone @@ -495,7 +495,7 @@ Geste Značajke playera Titlovi - Izgled + Respored Zadane postavke Izgled Značajke @@ -554,4 +554,15 @@ Neuspješno dohvaćanje GitHuba. Uključuje se jsdelivr proxy … Zaobilazi blokiranje GitHuba koristeći jsdelivr. Može odgoditi ažuriranja za nekoliko dana. Preferirana kvaliteta gledanja (podatkovna mobilna mreža) - + Profil %d + Wi-Fi + Mobilni podaci + Postavi standardnu vrijednost + Koristi + Uredi + Profili + Pomoć + Kvalitete + Pozadina profila + Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s + \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 168e23fa..a2b4a6d9 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -10,7 +10,7 @@ Nerladdningar Instälningar Sök… - Poster + Affisch Ingen Data Mer Instälningar Gå tillbacka @@ -100,7 +100,7 @@ Skickar endast data när appen kraschar Skickar ingen data Visa appuppdateringar - Sök automatiskt efter nya uppdateringar vid start + Sök automatiskt efter nya uppdateringar vid start. Uppdatera till beta-version Sök efter beta-version istället för fullständiga utgåvor av appen Github @@ -175,7 +175,7 @@ Pausa Återuppta Ett nerladdningsfel uppstod, kolla om appen har lagringsbehörigheter - Föredragen videokvalitet + Föredragen videokvalitet (WiFi) Allmänna Inställningar Textstorlek Använd system ljusstyrka @@ -216,7 +216,7 @@ %dm Spela med CloudStream Chromecast-undertexter - Tryck i mitten för att pausa + Dubbeltryck i mitten för att pausa Återställ data från backup Konton Uppdateringar och backup @@ -244,7 +244,7 @@ %dm \nåterstår NSFW - @string/ova + OVA Torrent NSFW +30 @@ -273,7 +273,7 @@ Asiatiska draman Andra Tecknade serier - @string/anime + Anime Dokumentär Asiatisk drama Video @@ -368,4 +368,69 @@ Titta på videor på dessa språk Föregående Spår - + Uppdaterar + Logg + Videospelarens hoppsträcka (Sekunder) + Ändra status + Webbläsare + NGINX server URL + Emulator-layout + Är du säker på att du vill avsluta\? + Laddar ner uppdatering till appen… + Slumpknapp + Visa sub + Kunde inte öppna appen + raw.githubusercontent.com Proxy + Webbläsarens videospelare + Installerar uppdatering till appen… + Kunde inte nå GitHub, sätter på jsDelivr proxy… + Leverantörer + MinTrevligaWebbsida + Ta bort reklam från undertexter + VLC + Alla språk + Rensa historik + PackageInstaller + Bibliotek + Standardvärden + Appen kommer uppdateras efter avslut + Sortera efter + Betyg (Låg till Hög) + Senast uppdaterad (Ny till Gammal) + Senast uppdaterad (Gammal till Ny) + Alfabetisk (A till Z) + Android TV + Lyckades + Starta om + Testa leverantörer + Föredragen videokvalitet (Mobildata) + Visa en slumpknapp på förstasidan + 18+ + Ljudspår + Direktsändningar + Videospelare + Öppna med + Synkronisera undertexter + MPV + Web Video Cast + Misslyckades + Ja + Videospår + Visa dub + Gammal version + Undertexter skrivs med versaler + Nej + Profil %d + Wi-Fi + Mobildata + Använd + Ändra + Rekommenderade + Sortera + Kunde inte installera den nya uppdateringen + Betyg (Hög till Låg) + Alfabetisk (Z till A) + Profiler + Hjälp + Kvalitet + \ No newline at end of file From 6ff4f4c1ce8ea8287ea8c2430cb50f700035c99c Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 14:04:17 +0000 Subject: [PATCH 011/441] chore(locales): fix locale issues --- app/src/main/res/values-hr/strings.xml | 2 +- app/src/main/res/values-sv/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index c606ab5e..92920ab4 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -565,4 +565,4 @@ Kvalitete Pozadina profila Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s - \ No newline at end of file + diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index a2b4a6d9..397faa48 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -433,4 +433,4 @@ Profiler Hjälp Kvalitet - \ No newline at end of file + From b06f098447d9a36063952950e99d1fcba38ca343 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 2 Aug 2023 02:13:30 +0200 Subject: [PATCH 012/441] fixed android tv trailer bug --- .../cloudstream3/ui/result/ResultFragmentTv.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 d69c18ef..698a0ab5 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 @@ -195,7 +195,7 @@ class ResultFragmentTv : Fragment() { } }) } - this.animate().translationX(if (turnVisible) 0f else if(isRtl()) -100.0f else 100f).apply { + this.animate().translationX(if (turnVisible) 0f else if (isRtl()) -100.0f else 100f).apply { duration = 200 interpolator = DecelerateInterpolator() } @@ -205,7 +205,7 @@ class ResultFragmentTv : Fragment() { binding?.apply { episodesShadow.fade(show) episodeHolderTv.fade(show) - if(episodesShadow.isRtl()) { + if (episodesShadow.isRtl()) { episodesShadow.scaleX = -1.0f episodesShadow.scaleY = -1.0f } else { @@ -263,7 +263,7 @@ class ResultFragmentTv : Fragment() { toggleEpisodes(!episodeHolderTv.isVisible) } - // resultEpisodes.onFocusChangeListener = leftListener + // resultEpisodes.onFocusChangeListener = leftListener redirectToPlay.setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) return@setOnFocusChangeListener @@ -297,8 +297,8 @@ class ResultFragmentTv : Fragment() { resultSeasonSelection, resultRangeSelection, resultDubSelection, + resultEpisodes, resultPlayTrailer, - resultEpisodes ) for (requestView in views) { if (!requestView.isShown) continue @@ -349,7 +349,7 @@ class ResultFragmentTv : Fragment() { ArrayList(), resultRecommendationsList, ) { callback -> - if(callback.action == SEARCH_ACTION_FOCUSED) + if (callback.action == SEARCH_ACTION_FOCUSED) toggleEpisodes(false) else SearchHelper.handleSearchClickCallback(callback) From 180987e2d0787e0515f632c10f1cb57d29bd147b Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 2 Aug 2023 05:30:50 +0200 Subject: [PATCH 013/441] added local accounts --- .../lagradost/cloudstream3/AcraApplication.kt | 8 + .../cloudstream3/ui/WhoIsWatchingAdapter.kt | 106 +++++++++ .../ui/home/HomeParentItemAdapterPreview.kt | 14 +- .../ui/player/FullScreenPlayer.kt | 4 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 2 - .../lagradost/cloudstream3/utils/DataStore.kt | 45 +++- .../cloudstream3/utils/DataStoreHelper.kt | 210 +++++++++++++++++- .../lagradost/cloudstream3/utils/UIHelper.kt | 28 ++- .../drawable/ic_outline_account_circle_24.xml | 17 +- app/src/main/res/drawable/outline_card.xml | 11 +- .../main/res/layout/fragment_home_head.xml | 28 ++- .../layout/player_quality_profile_item.xml | 2 +- app/src/main/res/layout/who_is_watching.xml | 35 +++ .../res/layout/who_is_watching_account.xml | 47 ++++ .../layout/who_is_watching_account_add.xml | 29 +++ .../layout/who_is_watching_account_edit.xml | 135 +++++++++++ app/src/main/res/values/strings.xml | 1 + 17 files changed, 693 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt create mode 100644 app/src/main/res/layout/who_is_watching.xml create mode 100644 app/src/main/res/layout/who_is_watching_account.xml create mode 100644 app/src/main/res/layout/who_is_watching_account_add.xml create mode 100644 app/src/main/res/layout/who_is_watching_account_edit.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 76b2321f..069287b0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -148,6 +148,14 @@ class AcraApplication : Application() { _context = WeakReference(value) } + fun getKeyClass(path: String, valueType: Class): T? { + return context?.getKey(path, valueType) + } + + fun setKeyClass(path: String, value: T) { + context?.setKey(path, value) + } + fun removeKeys(folder: String): Int? { return context?.removeKeys(folder) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt new file mode 100644 index 00000000..6b3090a9 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WhoIsWatchingAdapter.kt @@ -0,0 +1,106 @@ +package com.lagradost.cloudstream3.ui + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.lagradost.cloudstream3.databinding.WhoIsWatchingAccountAddBinding +import com.lagradost.cloudstream3.databinding.WhoIsWatchingAccountBinding +import com.lagradost.cloudstream3.ui.result.setImage +import com.lagradost.cloudstream3.utils.DataStoreHelper + +class WhoIsWatchingAdapter( + private val selectCallBack: (DataStoreHelper.Account) -> Unit = { }, + private val editCallBack: (DataStoreHelper.Account) -> Unit = { }, + private val addAccountCallback: () -> Unit = {} +) : + ListAdapter(DiffCallback()) { + + companion object { + const val FOOTER = 1 + const val NORMAL = 0 + } + + override fun getItemCount(): Int { + return currentList.size + 1 + } + + override fun getItemViewType(position: Int): Int = when (position) { + currentList.size -> FOOTER + else -> NORMAL + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WhoIsWatchingHolder = + WhoIsWatchingHolder( + binding = when (viewType) { + NORMAL -> WhoIsWatchingAccountBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + + FOOTER -> WhoIsWatchingAccountAddBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + + else -> throw NotImplementedError() + }, + selectCallBack = selectCallBack, + addAccountCallback = addAccountCallback, + editCallBack = editCallBack, + ) + + + override fun onBindViewHolder(holder: WhoIsWatchingHolder, position: Int) = + holder.bind(currentList.getOrNull(position)) + + class WhoIsWatchingHolder( + val binding: ViewBinding, + val selectCallBack: (DataStoreHelper.Account) -> Unit, + val addAccountCallback: () -> Unit, + val editCallBack: (DataStoreHelper.Account) -> Unit + ) : + RecyclerView.ViewHolder(binding.root) { + + fun bind(card: DataStoreHelper.Account?) { + when (binding) { + is WhoIsWatchingAccountBinding -> binding.apply { + if(card == null) return@apply + outline.isVisible = card.keyIndex == DataStoreHelper.selectedKeyIndex + profileText.text = card.name + profileImageBackground.setImage(card.image) + root.setOnClickListener { + selectCallBack(card) + } + root.setOnLongClickListener { + editCallBack(card) + return@setOnLongClickListener true + } + } + + is WhoIsWatchingAccountAddBinding -> binding.apply { + root.setOnClickListener { + addAccountCallback() + } + } + } + } + } + + class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: DataStoreHelper.Account, + newItem: DataStoreHelper.Account + ): Boolean = oldItem.keyIndex == newItem.keyIndex + + override fun areContentsTheSame( + oldItem: DataStoreHelper.Account, + newItem: DataStoreHelper.Account + ): Boolean = oldItem == newItem + } +} \ No newline at end of file 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 84964950..eb7b6f74 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 @@ -40,6 +40,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarMargin import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -250,6 +251,11 @@ class HomeParentItemAdapterPreview( private var bookmarkRecyclerView: RecyclerView = itemView.findViewById(R.id.home_bookmarked_child_recyclerview) + private var homeAccount: View? = + itemView.findViewById(R.id.home_switch_account) + + private var topPadding : View? = itemView.findViewById(R.id.home_padding) + private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding) private val previewCallback: ViewPager2.OnPageChangeCallback = @@ -432,6 +438,8 @@ class HomeParentItemAdapterPreview( resumeRecyclerView.setLinearListLayout() bookmarkRecyclerView.setLinearListLayout() + fixPaddingStatusbarMargin(topPadding) + for ((chip, watch) in toggleList) { chip.isChecked = false chip.setOnCheckedChangeListener { _, isChecked -> @@ -445,6 +453,10 @@ class HomeParentItemAdapterPreview( } } + homeAccount?.setOnClickListener { v -> + DataStoreHelper.showWhoIsWatching(v?.context ?: return@setOnClickListener) + } + (binding as? FragmentHomeHeadTvBinding)?.apply { homePreviewChangeApi.setOnClickListener { view -> view.context.selectHomepage(viewModel.repo?.name) { api -> @@ -485,8 +497,6 @@ class HomeParentItemAdapterPreview( } (binding as? FragmentHomeHeadBinding)?.apply { - fixPaddingStatusbar(binding.homeSearch) - homeSearch.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { viewModel.queryTextSubmit(query) 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 9b72f6c9..9739b627 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 @@ -96,8 +96,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { // protected var currentPrefQuality = // Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell protected var fastForwardTime = 10000L - protected var androidTVInterfaceOffSeekTime = 10000L; - protected var androidTVInterfaceOnSeekTime = 30000L; + protected var androidTVInterfaceOffSeekTime = 10000L + protected var androidTVInterfaceOnSeekTime = 30000L protected var swipeHorizontalEnabled = false protected var swipeVerticalEnabled = false protected var playBackSpeedEnabled = false 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 c280af59..4a807544 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 @@ -1252,7 +1252,6 @@ class GeneratorPlayer : FullScreenPlayer() { private fun displayTimeStamp(show: Boolean) { if (timestampShowState == show) return skipIndex++ - println("displayTimeStamp = $show") timestampShowState = show playerBinding?.skipChapterButton?.apply { val showWidth = 170.toPx @@ -1294,7 +1293,6 @@ class GeneratorPlayer : FullScreenPlayer() { override fun onTimestamp(timestamp: EpisodeSkip.SkipStamp?) { if (timestamp != null) { - println("timestamp: $timestamp") playerBinding?.skipChapterButton?.setText(timestamp.uiText) displayTimeStamp(true) val currentIndex = skipIndex diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index dd2b40a3..abcef753 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -6,7 +6,12 @@ import androidx.preference.PreferenceManager import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.lagradost.cloudstream3.AcraApplication.Companion.getKeyClass +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKeyClass import com.lagradost.cloudstream3.mvvm.logError +import kotlin.reflect.KClass +import kotlin.reflect.KProperty const val DOWNLOAD_HEADER_CACHE = "download_header_cache" @@ -20,6 +25,31 @@ const val PREFERENCES_NAME = "rebuild_preference" // TODO degelgate by value for get & set +class PreferenceDelegate( + val key: String, val default: T //, private val klass: KClass +) { + private val klass: KClass = default::class + // simple cache to make it not get the key every time it is accessed, however this requires + // that ONLY this changes the key + private var cache: T? = null + + operator fun getValue(self: Any?, property: KProperty<*>) = + cache ?: getKeyClass(key, klass.java).also { newCache -> cache = newCache } ?: default + + operator fun setValue( + self: Any?, + property: KProperty<*>, + t: T? + ) { + cache = t + if (t == null) { + removeKey(key) + } else { + setKeyClass(key, t) + } + } +} + object DataStore { val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() @@ -89,7 +119,7 @@ object DataStore { } fun Context.removeKeys(folder: String): Int { - val keys = getKeys(folder) + val keys = getKeys("$folder/") keys.forEach { value -> removeKey(value) } @@ -106,6 +136,15 @@ object DataStore { } } + fun Context.getKey(path: String, valueType: Class): T? { + try { + val json: String = getSharedPrefs().getString(path, null) ?: return null + return json.toKotlinObject(valueType) + } catch (e: Exception) { + return null + } + } + fun Context.setKey(folder: String, path: String, value: T) { setKey(getFolderName(folder, path), value) } @@ -114,6 +153,10 @@ object DataStore { return mapper.readValue(this, T::class.java) } + fun String.toKotlinObject(valueType: Class): T { + return mapper.readValue(this, valueType) + } + // GET KEY GIVEN PATH AND DEFAULT VALUE, NULL IF ERROR inline fun Context.getKey(path: String, defVal: T?): T? { try { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 3bdb64e1..137e1457 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -1,6 +1,14 @@ package com.lagradost.cloudstream3.utils +import android.content.Context +import android.content.DialogInterface +import android.text.Editable +import android.view.LayoutInflater +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isGone +import androidx.core.widget.doOnTextChanged import com.fasterxml.jackson.annotation.JsonProperty +import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -8,10 +16,20 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.databinding.WhoIsWatchingAccountEditBinding +import com.lagradost.cloudstream3.databinding.WhoIsWatchingBinding +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType +import com.lagradost.cloudstream3.ui.WhoIsWatchingAdapter +import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState +import com.lagradost.cloudstream3.ui.result.setImage +import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe const val VIDEO_POS_DUR = "video_pos_dur" const val VIDEO_WATCH_STATE = "video_watch_state" @@ -26,6 +44,197 @@ const val RESULT_SEASON = "result_season" const val RESULT_DUB = "result_dub" object DataStoreHelper { + // be aware, don't change the index of these as Account uses the index for the art + private val profileImages = arrayOf( + R.drawable.profile_bg_dark_blue, + R.drawable.profile_bg_blue, + R.drawable.profile_bg_orange, + R.drawable.profile_bg_pink, + R.drawable.profile_bg_purple, + R.drawable.profile_bg_red, + R.drawable.profile_bg_teal + ) + + data class Account( + @JsonProperty("keyIndex") + val keyIndex: Int, + @JsonProperty("name") + val name: String, + @JsonProperty("customImage") + val customImage: String? = null, + @JsonProperty("defaultImageIndex") + val defaultImageIndex: Int, + ) { + val image: UiImage + get() = customImage?.let { UiImage.Image(it) } ?: UiImage.Drawable( + profileImages.getOrNull(defaultImageIndex) ?: profileImages.first() + ) + } + + const val TAG = "data_store_helper" + private var accounts by PreferenceDelegate("$TAG/account", arrayOf()) + var selectedKeyIndex by PreferenceDelegate("$TAG/account_key_index", 0) + val currentAccount: String get() = selectedKeyIndex.toString() + + private fun setAccount(account: Account) { + selectedKeyIndex = account.keyIndex + showToast(account.name) + MainActivity.bookmarksUpdatedEvent(true) + } + + private fun editAccount(context: Context, account: Account, isNewAccount: Boolean) { + val binding = + WhoIsWatchingAccountEditBinding.inflate(LayoutInflater.from(context), null, false) + val builder = + AlertDialog.Builder(context, R.style.AlertDialogCustom) + .setView(binding.root) + + var currentEditAccount = account + val dialog = builder.show() + binding.accountName.text = Editable.Factory.getInstance()?.newEditable(account.name) + binding.accountName.doOnTextChanged { text, _, _, _ -> + currentEditAccount = currentEditAccount.copy(name = text?.toString() ?: "") + } + + binding.deleteBtt.isGone = isNewAccount + binding.deleteBtt.setOnClickListener { + val dialogClickListener = + DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + // remove all keys as well as the account, note that default wont get + // deleted from currentAccounts, as it is not part of "accounts", + // but the watch keys will + removeKeys(account.keyIndex.toString()) + val currentAccounts = accounts.toMutableList() + currentAccounts.removeIf { it.keyIndex == account.keyIndex } + accounts = currentAccounts.toTypedArray() + + // update UI + setAccount(getDefaultAccount(context)) + MainActivity.bookmarksUpdatedEvent(true) + dialog?.dismissSafe() + } + + DialogInterface.BUTTON_NEGATIVE -> {} + } + } + + try { + AlertDialog.Builder(context).setTitle(R.string.delete).setMessage( + context.getString(R.string.delete_message).format( + currentEditAccount.name + ) + ) + .setPositiveButton(R.string.delete, dialogClickListener) + .setNegativeButton(R.string.cancel, dialogClickListener) + .show().setDefaultFocus() + } catch (t: Throwable) { + logError(t) + // ye you somehow fucked up formatting did you? + } + } + + binding.cancelBtt.setOnClickListener { + dialog?.dismissSafe() + } + + binding.profilePic.setImage(account.image) + binding.profilePic.setOnClickListener { + // rolls the image forwards once + currentEditAccount = + currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % profileImages.size) + binding.profilePic.setImage(currentEditAccount.image) + } + + binding.applyBtt.setOnClickListener { + val currentAccounts = accounts.toMutableList() + + val overrideIndex = + currentAccounts.indexOfFirst { it.keyIndex == currentEditAccount.keyIndex } + + // if an account is found that has the same keyIndex then override that one, if not then append it + if (overrideIndex != -1) { + currentAccounts[overrideIndex] = currentEditAccount + } else { + currentAccounts.add(currentEditAccount) + } + + // set the new default account as well as add the key for the new account + setAccount(currentEditAccount) + accounts = currentAccounts.toTypedArray() + + dialog.dismissSafe() + } + } + + private fun getDefaultAccount(context: Context): Account { + return accounts.let { currentAccounts -> + currentAccounts.getOrNull(currentAccounts.indexOfFirst { it.keyIndex == 0 }) ?: Account( + keyIndex = 0, + name = context.getString(R.string.default_account), + defaultImageIndex = 0 + ) + } + } + + fun showWhoIsWatching(context: Context) { + val binding: WhoIsWatchingBinding = WhoIsWatchingBinding.inflate( + LayoutInflater.from(context) + ) + + val showAccount = accounts.toMutableList().apply { + val item = getDefaultAccount(context) + remove(item) + add(0, item) + } + + val builder = + BottomSheetDialog(context) + builder.setContentView(binding.root) + val accountName = context.getString(R.string.account) + + binding.profilesRecyclerview.setLinearListLayout(isHorizontal = true) + binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter( + selectCallBack = { account -> + setAccount(account) + builder.dismissSafe() + }, + addAccountCallback = { + val currentAccounts = accounts + val remainingImages = + profileImages.toSet() - currentAccounts.filter { it.customImage == null } + .mapNotNull { profileImages.getOrNull(it.defaultImageIndex) }.toSet() + val image = + profileImages.indexOf(remainingImages.randomOrNull() ?: profileImages.random()) + val keyIndex = (currentAccounts.maxOfOrNull { it.keyIndex } ?: 0) + 1 + + // create a new dummy account + editAccount( + context, + Account( + keyIndex = keyIndex, + name = "$accountName $keyIndex", + customImage = null, + defaultImageIndex = image + ), isNewAccount = true + ) + builder.dismissSafe() + }, + editCallBack = { account -> + editAccount( + context, account, isNewAccount = false + ) + builder.dismissSafe() + } + ).apply { + submitList(showAccount) + } + + builder.show() + } + + data class PosDur( @JsonProperty("position") val position: Long, @JsonProperty("duration") val duration: Long @@ -117,7 +326,6 @@ object DataStoreHelper { /** * A datastore wide account for future implementations of a multiple account system **/ - var currentAccount: String = "0" //TODO ACCOUNT IMPLEMENTATION fun getAllWatchStateIds(): List? { val folder = "$currentAccount/$RESULT_WATCH_STATE" 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 eb2067d6..5a393ed5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -15,6 +15,7 @@ import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle import android.view.* +import android.view.ViewGroup.MarginLayoutParams import android.view.inputmethod.InputMethodManager import android.widget.ImageView import android.widget.ListAdapter @@ -33,6 +34,10 @@ import androidx.core.graphics.blue import androidx.core.graphics.drawable.toBitmapOrNull import androidx.core.graphics.green import androidx.core.graphics.red +import androidx.core.view.marginBottom +import androidx.core.view.marginLeft +import androidx.core.view.marginRight +import androidx.core.view.marginTop import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.NavHostFragment @@ -55,7 +60,6 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings -import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import jp.wasabeef.glide.transformations.BlurTransformation import kotlin.math.roundToInt @@ -77,8 +81,8 @@ object UIHelper { || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) } - fun populateChips(view: ChipGroup?, tags : List) { - if(view == null) return + fun populateChips(view: ChipGroup?, tags: List) { + if (view == null) return view.removeAllViews() val context = view.context ?: return @@ -304,7 +308,7 @@ object UIHelper { else req } - if(radius > 0) { + if (radius > 0) { builder = builder.apply(bitmapTransform(BlurTransformation(radius, sample))) } @@ -496,6 +500,22 @@ object UIHelper { ) } + fun fixPaddingStatusbarMargin(v: View?) { + if (v == null) return + val ctx = v.context ?: return + + v.layoutParams = v.layoutParams.apply { + if (this is MarginLayoutParams) { + setMargins( + v.marginLeft, + v.marginTop + ctx.getStatusBarHeight(), + v.marginRight, + v.marginBottom + ) + } + } + } + fun fixPaddingStatusbarView(v: View?) { if (v == null) return val ctx = v.context ?: return diff --git a/app/src/main/res/drawable/ic_outline_account_circle_24.xml b/app/src/main/res/drawable/ic_outline_account_circle_24.xml index cc564471..27c2d574 100644 --- a/app/src/main/res/drawable/ic_outline_account_circle_24.xml +++ b/app/src/main/res/drawable/ic_outline_account_circle_24.xml @@ -1,6 +1,13 @@ - - - + + + diff --git a/app/src/main/res/drawable/outline_card.xml b/app/src/main/res/drawable/outline_card.xml index 02116bb8..5716de45 100644 --- a/app/src/main/res/drawable/outline_card.xml +++ b/app/src/main/res/drawable/outline_card.xml @@ -1,21 +1,20 @@ + android:color="@android:color/white"> - + android:width="2dp" + android:color="@android:color/white" /> + - + - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home_head.xml b/app/src/main/res/layout/fragment_home_head.xml index 603621f7..d8b5dfe5 100644 --- a/app/src/main/res/layout/fragment_home_head.xml +++ b/app/src/main/res/layout/fragment_home_head.xml @@ -38,16 +38,21 @@ + android:layout_height="50dp" + android:gravity="center" + android:orientation="horizontal"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 6a1e9eac..4dd6eadc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -307,6 +307,7 @@ queued No Subtitles Default + @string/default_subtitles Free Used App From 32e243ce94857546bf579706d85a651c72138957 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Wed, 2 Aug 2023 03:31:35 +0000 Subject: [PATCH 014/441] Use ffmpeg library (#528) --- app/build.gradle.kts | 8 +++----- .../com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 5 +++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6fd4ae21..c669c870 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -165,7 +165,7 @@ dependencies { // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09") - // Exoplayer + // Media 3 implementation("androidx.media3:media3-common:1.1.0") implementation("androidx.media3:media3-exoplayer:1.1.0") implementation("androidx.media3:media3-datasource-okhttp:1.1.0") @@ -174,10 +174,8 @@ dependencies { implementation("androidx.media3:media3-cast:1.1.0") implementation("androidx.media3:media3-exoplayer-hls:1.1.0") implementation("androidx.media3:media3-exoplayer-dash:1.1.0") - - - // Use the Jellyfin ffmpeg extension for easy ffmpeg audio decoding in exoplayer. Thank you Jellyfin <3 -// implementation("org.jellyfin.exoplayer:exoplayer-ffmpeg-extension:2.18.2+1") + // Custom ffmpeg extension for audio codecs + implementation("com.github.recloudstream:media-ffmpeg:1.1.0") //implementation("com.google.android.exoplayer:extension-leanback:2.14.0") 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 70e12a47..4efe3aec 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 @@ -30,6 +30,7 @@ import androidx.media3.datasource.cache.SimpleCache import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.SeekParameters import androidx.media3.exoplayer.source.ClippingMediaSource @@ -701,9 +702,9 @@ class CS3IPlayer : IPlayer { ExoPlayer.Builder(context) .setRenderersFactory { eventHandler, videoRendererEventListener, audioRendererEventListener, textRendererOutput, metadataRendererOutput -> DefaultRenderersFactory(context).apply { -// setEnableDecoderFallback(true) + setEnableDecoderFallback(true) // Enable Ffmpeg extension -// setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON) + setExtensionRendererMode(EXTENSION_RENDERER_MODE_PREFER) }.createRenderers( eventHandler, videoRendererEventListener, From 87d85429f86e89bce239ec586d3cce1f32ae27ca Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Wed, 2 Aug 2023 17:13:50 +0200 Subject: [PATCH 015/441] Revert "Use ffmpeg library (#528)" (#532) This reverts commit 32e243ce94857546bf579706d85a651c72138957. --- app/build.gradle.kts | 8 +++++--- .../com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 5 ++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c669c870..6fd4ae21 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -165,7 +165,7 @@ dependencies { // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09") - // Media 3 + // Exoplayer implementation("androidx.media3:media3-common:1.1.0") implementation("androidx.media3:media3-exoplayer:1.1.0") implementation("androidx.media3:media3-datasource-okhttp:1.1.0") @@ -174,8 +174,10 @@ dependencies { implementation("androidx.media3:media3-cast:1.1.0") implementation("androidx.media3:media3-exoplayer-hls:1.1.0") implementation("androidx.media3:media3-exoplayer-dash:1.1.0") - // Custom ffmpeg extension for audio codecs - implementation("com.github.recloudstream:media-ffmpeg:1.1.0") + + + // Use the Jellyfin ffmpeg extension for easy ffmpeg audio decoding in exoplayer. Thank you Jellyfin <3 +// implementation("org.jellyfin.exoplayer:exoplayer-ffmpeg-extension:2.18.2+1") //implementation("com.google.android.exoplayer:extension-leanback:2.14.0") 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 4efe3aec..70e12a47 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 @@ -30,7 +30,6 @@ import androidx.media3.datasource.cache.SimpleCache import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultRenderersFactory -import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.SeekParameters import androidx.media3.exoplayer.source.ClippingMediaSource @@ -702,9 +701,9 @@ class CS3IPlayer : IPlayer { ExoPlayer.Builder(context) .setRenderersFactory { eventHandler, videoRendererEventListener, audioRendererEventListener, textRendererOutput, metadataRendererOutput -> DefaultRenderersFactory(context).apply { - setEnableDecoderFallback(true) +// setEnableDecoderFallback(true) // Enable Ffmpeg extension - setExtensionRendererMode(EXTENSION_RENDERER_MODE_PREFER) +// setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON) }.createRenderers( eventHandler, videoRendererEventListener, From 2475088f7629121f8824a09b29e2404bc2784c3a Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 2 Aug 2023 17:31:55 +0200 Subject: [PATCH 016/441] added local accounts to TV layout --- .../outline_drawable_forced_round.xml | 5 +++++ .../main/res/layout/fragment_home_head_tv.xml | 14 ++++++++++++ app/src/main/res/layout/who_is_watching.xml | 22 ++++++++++++++++++- .../layout/who_is_watching_account_edit.xml | 1 + 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/outline_drawable_forced_round.xml diff --git a/app/src/main/res/drawable/outline_drawable_forced_round.xml b/app/src/main/res/drawable/outline_drawable_forced_round.xml new file mode 100644 index 00000000..7736f088 --- /dev/null +++ b/app/src/main/res/drawable/outline_drawable_forced_round.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home_head_tv.xml b/app/src/main/res/layout/fragment_home_head_tv.xml index 8592daea..d7fbb9e9 100644 --- a/app/src/main/res/layout/fragment_home_head_tv.xml +++ b/app/src/main/res/layout/fragment_home_head_tv.xml @@ -36,12 +36,26 @@ + + + + + + + Date: Wed, 2 Aug 2023 17:35:41 +0000 Subject: [PATCH 017/441] fix player session id (#534) --- .../cloudstream3/ui/player/AbstractPlayerFragment.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 e6f93377..53ee5e12 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 @@ -37,6 +37,7 @@ import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.unixTimeMs import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.AppUtils @@ -329,7 +330,10 @@ abstract class AbstractPlayerFragment( if (player is ExoPlayer) { context?.let { ctx -> mMediaSession?.release() - mMediaSession = MediaSession.Builder(ctx, player).build() + mMediaSession = MediaSession.Builder(ctx, player) + // Ensure unique ID for concurrent players + .setId(unixTimeMs.toString()) + .build() } // Necessary for multiple combined videos @@ -441,6 +445,7 @@ abstract class AbstractPlayerFragment( keyEventListener = null canEnterPipMode = false mMediaSession?.release() + mMediaSession = null SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged keepScreenOn(false) From c5f6f36fc768b4a0d37cd6283906f4245b915841 Mon Sep 17 00:00:00 2001 From: Vu Hoan Huy <43563783+d4rkk3y@users.noreply.github.com> Date: Thu, 3 Aug 2023 00:36:05 +0700 Subject: [PATCH 018/441] fix: can not switch subtitle after integrate ffmpeg decoder. (#533) * Revert "Revert "Use ffmpeg library (#528)" (#532)" This reverts commit 87d85429f86e89bce239ec586d3cce1f32ae27ca. * fix: can not select subtitle --- app/build.gradle.kts | 8 +++---- .../cloudstream3/ui/player/CS3IPlayer.kt | 22 ++++++++----------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6fd4ae21..c669c870 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -165,7 +165,7 @@ dependencies { // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09") - // Exoplayer + // Media 3 implementation("androidx.media3:media3-common:1.1.0") implementation("androidx.media3:media3-exoplayer:1.1.0") implementation("androidx.media3:media3-datasource-okhttp:1.1.0") @@ -174,10 +174,8 @@ dependencies { implementation("androidx.media3:media3-cast:1.1.0") implementation("androidx.media3:media3-exoplayer-hls:1.1.0") implementation("androidx.media3:media3-exoplayer-dash:1.1.0") - - - // Use the Jellyfin ffmpeg extension for easy ffmpeg audio decoding in exoplayer. Thank you Jellyfin <3 -// implementation("org.jellyfin.exoplayer:exoplayer-ffmpeg-extension:2.18.2+1") + // Custom ffmpeg extension for audio codecs + implementation("com.github.recloudstream:media-ffmpeg:1.1.0") //implementation("com.google.android.exoplayer:extension-leanback:2.14.0") 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 70e12a47..2067eb04 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 @@ -7,16 +7,14 @@ import android.os.Looper import android.util.Log import android.util.Rational import android.widget.FrameLayout -import androidx.media3.common.C -import androidx.preference.PreferenceManager import androidx.media3.common.C.* import androidx.media3.common.Format import androidx.media3.common.MediaItem -import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.MimeTypes import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.TrackGroup +import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.Tracks import androidx.media3.common.VideoSize import androidx.media3.database.StandaloneDatabaseProvider @@ -30,6 +28,7 @@ import androidx.media3.datasource.cache.SimpleCache import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.SeekParameters import androidx.media3.exoplayer.source.ClippingMediaSource @@ -41,6 +40,7 @@ import androidx.media3.exoplayer.text.TextRenderer import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.trackselection.TrackSelector import androidx.media3.ui.SubtitleView +import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -49,8 +49,8 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle -import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorUri @@ -397,7 +397,7 @@ class CS3IPlayer : IPlayer { if (subtitle == null) { trackSelector.setParameters( trackSelector.buildUponParameters() - .setPreferredTextLanguage(null) + .setTrackTypeDisabled(TRACK_TYPE_TEXT, true) .clearOverridesOfType(TRACK_TYPE_TEXT) ) } else { @@ -415,6 +415,7 @@ class CS3IPlayer : IPlayer { .apply { val track = getTextTrack(subtitle.getId()) if (track != null) { + setTrackTypeDisabled(TRACK_TYPE_TEXT, false) setOverrideForType( TrackSelectionOverride( track.first, @@ -662,12 +663,7 @@ class CS3IPlayer : IPlayer { private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector { val trackSelector = DefaultTrackSelector(context) - trackSelector.parameters = DefaultTrackSelector.ParametersBuilder(context) - // .setRendererDisabled(C.TRACK_TYPE_VIDEO, true) - .setRendererDisabled(C.TRACK_TYPE_TEXT, true) - // Experimental, I think this causes issues with audio track init 5001 -// .setTunnelingEnabled(true) - .setDisabledTextTrackSelectionFlags(C.TRACK_TYPE_TEXT) + trackSelector.parameters = trackSelector.buildUponParameters() // This will not force higher quality videos to fail // but will make the m3u8 pick the correct preferred .setMaxVideoSize(Int.MAX_VALUE, maxVideoHeight ?: Int.MAX_VALUE) @@ -701,9 +697,9 @@ class CS3IPlayer : IPlayer { ExoPlayer.Builder(context) .setRenderersFactory { eventHandler, videoRendererEventListener, audioRendererEventListener, textRendererOutput, metadataRendererOutput -> DefaultRenderersFactory(context).apply { -// setEnableDecoderFallback(true) + setEnableDecoderFallback(true) // Enable Ffmpeg extension -// setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON) + setExtensionRendererMode(EXTENSION_RENDERER_MODE_ON) }.createRenderers( eventHandler, videoRendererEventListener, From 7e6a28bb996eec749c15a509d62f2516ab334733 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 2 Aug 2023 21:00:04 +0200 Subject: [PATCH 019/441] fixed tv focus issue --- .../lagradost/cloudstream3/CommonActivity.kt | 13 ++- .../lagradost/cloudstream3/MainActivity.kt | 88 ++++++++++++++++++- .../ui/home/HomeParentItemAdapterPreview.kt | 4 + .../main/res/layout/fragment_home_head_tv.xml | 26 +++++- app/src/main/res/layout/fragment_home_tv.xml | 1 + 5 files changed, 118 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 3cfde983..4d7afaba 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -301,7 +301,8 @@ object CommonActivity { private fun localLook(from: View, id: Int): View? { if (id == NO_ID) return null var currentLook: View = from - while (true) { + // limit to 15 look depth + for (i in 0..15) { currentLook.findViewById(id)?.let { return it } currentLook = (currentLook.parent as? View) ?: break } @@ -359,18 +360,14 @@ object CommonActivity { // if not specified then use forward id nextId = view.nextFocusForwardId // if view is still not found to next focus then return and let android decide - if (nextId == NO_ID) return null + if (nextId == NO_ID) + return null } var next = act.findViewById(nextId) ?: return null next = localLook(view, nextId) ?: next - var currentLook: View = view - while (currentLook.findViewById(nextId)?.also { next = it } == null) { - currentLook = (currentLook.parent as? View) ?: break - } - // if cant focus but visible then break and let android decide // the exception if is the view is a parent and has children that wants focus val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent -> @@ -520,7 +517,7 @@ object CommonActivity { else -> null } - + // println("NEXT FOCUS : $nextView") if (nextView != null) { nextView.requestFocus() keyEventListener?.invoke(Pair(event, true)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index b1f60ad7..d6e275ed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -27,6 +27,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.core.view.marginStart import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ViewModelProvider import androidx.navigation.NavController @@ -37,6 +38,8 @@ import androidx.navigation.NavOptions import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSnapHelper import androidx.recyclerview.widget.RecyclerView import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper @@ -91,6 +94,7 @@ import com.lagradost.cloudstream3.ui.home.HomeViewModel import com.lagradost.cloudstream3.ui.player.BasicLink import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.LinkGenerator +import com.lagradost.cloudstream3.ui.result.LinearListLayout import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST import com.lagradost.cloudstream3.ui.result.setImage @@ -110,6 +114,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.isLtr import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable +import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.loadRepository import com.lagradost.cloudstream3.utils.AppUtils.loadResult @@ -146,6 +151,7 @@ import java.lang.ref.WeakReference import java.net.URI import java.net.URLDecoder import java.nio.charset.Charset +import kotlin.math.abs import kotlin.math.absoluteValue import kotlin.reflect.KClass import kotlin.system.exitProcess @@ -848,6 +854,24 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { private var animator: ValueAnimator? = null + /** if this is enabled it will keep the focus unmoving + * during listview move */ + private const val NO_MOVE_LIST: Boolean = false + + /** If this is enabled then it will try to move the + * listview focus to the left instead of center */ + private const val LEFTMOST_MOVE_LIST: Boolean = true + + private val reflectedScroll by lazy { + try { + RecyclerView::class.java.declaredMethods.firstOrNull { + it.name == "scrollStep" + }?.also { it.isAccessible = true } + } catch (t : Throwable) { + null + } + } + @MainThread fun updateFocusView(newFocus: View?, same: Boolean = false) { val focusOutline = focusOutline.get() ?: return @@ -867,17 +891,67 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (newFocus != null) { lastFocus = WeakReference(newFocus) + val parent = newFocus.parent + var targetDx = 0 + if (parent is RecyclerView) { + val layoutManager = parent.layoutManager + if (layoutManager is LinearListLayout && layoutManager.orientation == LinearLayoutManager.HORIZONTAL) { + val dx = + LinearSnapHelper().calculateDistanceToFinalSnap(layoutManager, newFocus) + ?.get(0) + if (dx != null) { + val rdx = if (LEFTMOST_MOVE_LIST) { + // this makes the item the leftmost in ltr, instead of center + val diff = + ((layoutManager.width - layoutManager.paddingStart - newFocus.measuredWidth) / 2) - newFocus.marginStart + dx + if (parent.isRtl()) { + -diff + } else { + diff + } + } else { + if (dx > 0) dx else 0 + } + + if(!NO_MOVE_LIST) { + parent.smoothScrollBy(rdx, 0) + }else { + val smoothScroll = reflectedScroll + if(smoothScroll == null) { + parent.smoothScrollBy(rdx, 0) + } else { + try { + // this is very fucked but because it is a protected method to + // be able to compute the scroll I use reflection, scroll, then + // scroll back, then smooth scroll and set the no move + val out = IntArray(2) + smoothScroll.invoke(parent, rdx, 0, out) + val scrolledX = out[0] + if(abs(scrolledX) <= 0) { // newFocus.measuredWidth*2 + smoothScroll.invoke(parent, -rdx, 0, out) + parent.smoothScrollBy(scrolledX, 0) + if (NO_MOVE_LIST) targetDx = scrolledX + } + } catch (t : Throwable) { + parent.smoothScrollBy(rdx, 0) + } + } + } + } + } + } val out = IntArray(2) newFocus.getLocationInWindow(out) val (screenX, screenY) = out var (x, y) = screenX.toFloat() to screenY.toFloat() val (currentX, currentY) = focusOutline.translationX to focusOutline.translationY - // println(">><<< $x $y $currentX $currentY") + if (!newFocus.isLtr()) { x = x - focusOutline.rootView.width + newFocus.measuredWidth } + x -= targetDx // out of bounds = 0,0 if (screenX == 0 && screenY == 0) { @@ -1093,9 +1167,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } //Automatically download not existing plugins, using mode specified. - val auto_download_plugin = AutoDownloadMode.getEnum(settingsManager.getInt(getString(R.string.auto_download_plugins_key), 0)) ?: AutoDownloadMode.Disable + val auto_download_plugin = AutoDownloadMode.getEnum( + settingsManager.getInt( + getString(R.string.auto_download_plugins_key), + 0 + ) + ) ?: AutoDownloadMode.Disable if (auto_download_plugin != AutoDownloadMode.Disable) { - PluginManager.downloadNotExistingPluginsAndLoad(this@MainActivity, auto_download_plugin) + PluginManager.downloadNotExistingPluginsAndLoad( + this@MainActivity, + auto_download_plugin + ) } } 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 eb7b6f74..8557d26f 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 @@ -14,6 +14,7 @@ import androidx.viewbinding.ViewBinding import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable +import com.google.android.material.chip.ChipGroup import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.CommonActivity.activity @@ -416,6 +417,7 @@ class HomeParentItemAdapterPreview( isChecked = checked.contains(watch) } } + toggleListHolder?.isGone = visible.isEmpty() } } ?: debugException { "Expected findViewTreeLifecycleOwner" } } @@ -428,6 +430,8 @@ class HomeParentItemAdapterPreview( Pair(itemView.findViewById(R.id.home_plan_to_watch_btt), WatchType.PLANTOWATCH), ) + private val toggleListHolder : ChipGroup? = itemView.findViewById(R.id.home_type_holder) + init { previewViewpager.setPageTransformer(HomeScrollTransformer()) diff --git a/app/src/main/res/layout/fragment_home_head_tv.xml b/app/src/main/res/layout/fragment_home_head_tv.xml index d7fbb9e9..d2c20bc4 100644 --- a/app/src/main/res/layout/fragment_home_head_tv.xml +++ b/app/src/main/res/layout/fragment_home_head_tv.xml @@ -153,6 +153,7 @@ android:layout_marginStart="@dimen/navbar_width" android:backgroundTint="@color/semiWhite" android:minWidth="150dp" + android:nextFocusUp="@id/home_preview_play_btt" android:nextFocusLeft="@id/nav_rail_view" android:nextFocusDown="@id/home_watch_child_recyclerview" /> @@ -178,6 +179,8 @@ From 22c0022684ec2097c5fb9c4fdc7b2a753d20c119 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 2 Aug 2023 21:14:37 +0200 Subject: [PATCH 020/441] Translations update from Hosted Weblate (#527) Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> Co-authored-by: Eryk Michalak Co-authored-by: PiterDev Co-authored-by: Rex_sa Co-authored-by: Reza Almanda Co-authored-by: dabao1955 Co-authored-by: gallegonovato Co-authored-by: infoekcz --- app/src/main/res/values-ar/strings.xml | 4 +- app/src/main/res/values-cs/strings.xml | 4 +- app/src/main/res/values-es/strings.xml | 5 +- app/src/main/res/values-gl/strings.xml | 156 ++++++++++++++++++++++++- app/src/main/res/values-in/strings.xml | 4 +- app/src/main/res/values-ja/strings.xml | 9 +- app/src/main/res/values-pl/strings.xml | 5 +- 7 files changed, 180 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 40d445ae..3e80da12 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -579,4 +579,6 @@ النوعيات خلفية الملف الشخصي تعذر إنشاء واجهة المستخدم بشكل صحيح ، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور %s - + حدد الوضع لتصفية تنزيل المكونات الإضافية + تعطيل + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index db9398ca..e38ab3d5 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -571,4 +571,6 @@ \n \nPOZNÁMKA: Pokud je součet 10 nebo vyšší, přehrávač automaticky přeskočí načítání při načtení daného odkazu! Nepodařilo se správně vytvořit rozhraní. Toto je VÁŽNÁ CHYBA, kterou je potřeba ihned nahlásit %s - + Zakázat + Výběr režimu pro filtrování stahování doplňků + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ac757b15..99c9bf61 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -547,4 +547,7 @@ Calidades Perfil del fondo La interfaz de usuario no se ha podido crear correctamente, se trata de un GRAN BUG y debe ser reportado inmediatamente %s - + Selecciona el modo para filtrar la descarga de los plugins + Desactivar + \@string/default_subtitles + \ No newline at end of file diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 8386188c..c2963895 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -13,4 +13,158 @@ Póster do Episodio Regresar Cambiar provedor - + Nova actualización atopada! +\n%s -> %s + Recheo + %d min + Configuración + Procurar… + Procurar en %s… + Sen datos + Máis opcións + Seguinte episodio + Xéneros + Mirando + En espera + Completado + Descartado + Planeando ver + Ningún + Remirando + Marcadores + Borrar + Seleccionar estado de visualización + Aplicar + Cancelar + Copiar + Cerrar + Limpar + Gardar + Velocidade do reproductor + Configurar Subtítulos + Cor de Fondo + Cor de Texto + Cor de Contorno + Cor de Ventá + Continuar Vendo + Nota: %.1f + CloudStream + Inicio + Descarga + Compartir + Abrir no Navegador + Navegador + Omitir carga + Cargando… + Reproducir o filme + Reproducir Trailer + Reproducir transmisión en vivo + Transmitir Torrent + Fontes + Tentar de novo a conexión… + Volver + Reproducir Episodio + Descargar + Descargado + Descargando + Descarga Pausada + Descarga comezada + Descarga Fallida + Descarga Cancelada + Descarga rematada + Actualización Comezada + Subtitulado + Borrar Arquivo + Reproducir Arquivo + Continuar Descarga + Pausar Descarga + Desactivar reporte automático de bugs + Máis información + Agochar + Reproducir + Tipo de Borde + Fonte + Non se deron bananas + Seleccionar idioma automáticamente + Descargar Idiomas + Idioma do Subtítulo + Manteña premido para restablecer os valores predeterminados + Importar fontes colocandoas en %s + Borrar + Máis Info + \@string/home_play + Unha VPN pode ser necesaria para o correcto funcionamento deste provedor + Este proveedor é un torrent, recomendase o uso dunha VPN + Os metadatos non son proporcionados polo sitio, a carga do video fallará se non existe no sitio + Descripción + Trama non atopada + Descripción non atopada + Amosar Logcat 🐈 + Rexistro + Continúa a reprodución nun reproductor miniatura enriba doutras aplicacións + Reproducir con CloudStream + Procurar + Vista previa do fondo + Velocidade (%.2fx) + Subtítulos + Transmitir + Error Cargando Ligazón + Dobrado + Filtrar Marcadores + Almacenamento Interno + Tamaño de Fonte + Info + Elevación de Subtítulo + Procurar usando proveedores + %d Bananas dadas aos desenvolvedores + Procurar por tipos + Imaxe en Imaxe + Configuración de subtítulos do reprodutor + Subtítulos de Chromecast + Configuración de subtítulos de Chromecast + Modo Eigengravy + Engadir a opción de velocidade no reprodutor + Deslice para avanzar/retroceder + Deslice o dedo de lado a lado para controlar a posición nun video + Deslice para cambiar a configuración + Deslice cara arriba ou cara abaixo no lado esquerdo ou dereito para cambiar o brillo ou o volumen + Reproducir automáticamente episodio seguinte + Comezar o seguinte episodio cando o actual remate + Toca dúas veces para procurar + Tocar dúas veces para pausar + Tempo de rebobinado do reprodutor (segundos) + Toque dúas veces no lado dereito ou esquerdo para rebobinar cara adiante ou cara atrás + Toque dúas veces no medio para pausar + Usar brillo do sistema + Actualizar progreso do visto + Restaurar datos dende o respaldo + Crear respaldo de datos + Arquivo de respaldo cargado + Fallou a restauración dos datos dende o arquivo %s + Datos gardados + Faltan permisos de almacenamento. Por favor tenteo de novo. + Error respaldando de %s + Procurar + Biblioteca + Contas + Actualizacións e copias de seguridade + Info + Procura Avanzada + Da os resultados da procura separados por proveedor + Botón de cambio de tamaño do reprodutor + Eliminar bordes negros + Subtítulos + Sincronizar automáticamente o progreso do episodio actual + Use o brillo do sistema no reprodutor da aplicación en lugar dunha superposición oscura + Só envía datos se a aplicacción falla inesperadamente + Non enviar datos + Mostrar episodio de recheo para Anime + Mostrar Trailers + Mostrar pósters de Kitsu + Ocultar calidade de video nos resultados da procura + Actualicación automática de complementos + Descarga automática de complementos + Selecciona o modo para filtrar a descarga dos complementos + Instala automáticamente todos os complementos aínda non instalados dos repositorios engadidos. + Mostrar actualizacións da aplicación + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index f27b12fe..e3393d2a 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -570,4 +570,6 @@ Kualitas Latar belakang profil UI tidak dapat dibuat dengan benar, ini adalah BUG UTAMA dan harus segera dilaporkan %s - + Nonaktif + Pilih mode untuk memfilter unduhan plugin + \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 5e415dc6..e54eb582 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -204,4 +204,11 @@ エピソード %d は にリリースされます 再視聴 ストリームトレント - + 文字の色 + ファイルを削除 + 再生ファイル + 背景の色 + 窓の色 + エッジタイプ + ダウンロードを一時停止する + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 60b776b1..0d238ca7 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -550,4 +550,7 @@ Jakości Tło profilu Nie można było poprawnie utworzyć interfejsu użytkownika, jest to POWAŻNY BŁĄD i należy go natychmiast zgłosić %s - + Wybierz tryb filtrowania pobieranych rozszerzeń + Wyłączać + \@string/default_subtitles + \ No newline at end of file From 653982a6bdb0f20eb040dc214d45160287d2ddf5 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 19:14:54 +0000 Subject: [PATCH 021/441] chore(locales): fix locale issues --- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 4 ++-- app/src/main/res/values-gl/strings.xml | 4 ++-- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 3e80da12..41ee5ed0 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -581,4 +581,4 @@ تعذر إنشاء واجهة المستخدم بشكل صحيح ، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور %s حدد الوضع لتصفية تنزيل المكونات الإضافية تعطيل - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index e38ab3d5..a2c3df3e 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -573,4 +573,4 @@ Nepodařilo se správně vytvořit rozhraní. Toto je VÁŽNÁ CHYBA, kterou je potřeba ihned nahlásit %s Zakázat Výběr režimu pro filtrování stahování doplňků - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 99c9bf61..1ac81c20 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -549,5 +549,5 @@ La interfaz de usuario no se ha podido crear correctamente, se trata de un GRAN BUG y debe ser reportado inmediatamente %s Selecciona el modo para filtrar la descarga de los plugins Desactivar - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index c2963895..ad7916be 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -92,7 +92,7 @@ Importar fontes colocandoas en %s Borrar Máis Info - \@string/home_play + @string/home_play Unha VPN pode ser necesaria para o correcto funcionamento deste provedor Este proveedor é un torrent, recomendase o uso dunha VPN Os metadatos non son proporcionados polo sitio, a carga do video fallará se non existe no sitio @@ -167,4 +167,4 @@ Selecciona o modo para filtrar a descarga dos complementos Instala automáticamente todos os complementos aínda non instalados dos repositorios engadidos. Mostrar actualizacións da aplicación - \ No newline at end of file + diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index e3393d2a..c413ba60 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -572,4 +572,4 @@ UI tidak dapat dibuat dengan benar, ini adalah BUG UTAMA dan harus segera dilaporkan %s Nonaktif Pilih mode untuk memfilter unduhan plugin - \ No newline at end of file + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e54eb582..7131ee25 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -211,4 +211,4 @@ 窓の色 エッジタイプ ダウンロードを一時停止する - \ No newline at end of file + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 0d238ca7..6db36065 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -552,5 +552,5 @@ Nie można było poprawnie utworzyć interfejsu użytkownika, jest to POWAŻNY BŁĄD i należy go natychmiast zgłosić %s Wybierz tryb filtrowania pobieranych rozszerzeń Wyłączać - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + From f5c4864a3cf88dce6ebc903b5efad3fba65266db Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 4 Aug 2023 05:37:41 +0200 Subject: [PATCH 022/441] tv focus changes + gradle bump + pip crash fix --- app/build.gradle.kts | 36 ++-- .../lagradost/cloudstream3/CommonActivity.kt | 95 ++++++++--- .../ui/download/DownloadChildFragment.kt | 9 +- .../ui/download/DownloadFragment.kt | 13 +- .../ui/home/HomeChildItemAdapter.kt | 8 +- .../cloudstream3/ui/home/HomeFragment.kt | 24 ++- .../ui/home/HomeParentItemAdapter.kt | 19 ++- .../ui/home/HomeParentItemAdapterPreview.kt | 5 +- .../cloudstream3/ui/player/PlayerPipHelper.kt | 32 ++-- .../ui/result/LinearListLayout.kt | 104 ++++++++--- .../ui/result/ResultFragmentPhone.kt | 19 ++- .../ui/result/ResultFragmentTv.kt | 32 +++- .../cloudstream3/ui/search/SearchFragment.kt | 7 +- .../settings/extensions/ExtensionsFragment.kt | 14 +- .../extensions/PluginDetailsFragment.kt | 161 +++++++++--------- .../ui/settings/extensions/PluginsFragment.kt | 102 ++++++----- .../cloudstream3/utils/DataStoreHelper.kt | 9 +- .../res/layout/fragment_child_downloads.xml | 57 ++++--- .../main/res/layout/fragment_extensions.xml | 11 +- app/src/main/res/layout/fragment_plugins.xml | 10 +- .../main/res/layout/fragment_result_tv.xml | 20 +-- app/src/main/res/layout/fragment_search.xml | 33 ++-- .../main/res/layout/fragment_search_tv.xml | 41 +++-- app/src/main/res/layout/standard_toolbar.xml | 31 ++-- app/src/main/res/layout/tvtypes_chips.xml | 1 + .../main/res/layout/tvtypes_chips_scroll.xml | 11 +- build.gradle.kts | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 28 files changed, 590 insertions(+), 320 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c669c870..5c864117 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,3 @@ -import com.android.build.gradle.api.BaseVariantOutput import org.jetbrains.dokka.gradle.DokkaTask import java.io.ByteArrayOutputStream import java.net.URL @@ -52,7 +51,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.1" + versionName = "4.1.2" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") @@ -108,14 +107,19 @@ android { versionCode = (System.currentTimeMillis() / 60000).toInt() } } + //toolchain { + // languageVersion.set(JavaLanguageVersion.of(17)) + // } + // jvmToolchain(17) + compileOptions { isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "17" freeCompilerArgs = listOf("-Xjvm-default=compatibility") } lint { @@ -131,22 +135,22 @@ repositories { dependencies { implementation("com.google.android.mediahome:video:1.0.0") - implementation("androidx.test.ext:junit-ktx:1.1.3") + implementation("androidx.test.ext:junit-ktx:1.1.5") testImplementation("org.json:json:20180813") - implementation("androidx.core:core-ktx:1.8.0") - implementation("androidx.appcompat:appcompat:1.4.2") // need target 32 for 1.5.0 + implementation("androidx.core:core-ktx:1.10.1") + implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0 // dont change this to 1.6.0 it looks ugly af implementation("com.google.android.material:material:1.5.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.navigation:navigation-fragment-ktx:2.5.1") - implementation("androidx.navigation:navigation-ui-ktx:2.5.1") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1") + implementation("androidx.navigation:navigation-fragment-ktx:2.6.0") + implementation("androidx.navigation:navigation-ui-ktx:2.6.0") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.3") - androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test:core") //implementation("io.karn:khttp-android:0.1.2") //okhttp instead @@ -201,8 +205,8 @@ dependencies { //implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0") // Downloading - implementation("androidx.work:work-runtime:2.8.0") - implementation("androidx.work:work-runtime-ktx:2.8.0") + implementation("androidx.work:work-runtime:2.8.1") + implementation("androidx.work:work-runtime-ktx:2.8.1") // Networking // implementation("com.squareup.okhttp3:okhttp:4.9.2") diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 4d7afaba..0bcd4152 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -19,8 +19,11 @@ import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat +import androidx.core.view.children import androidx.preference.PreferenceManager import com.google.android.gms.cast.framework.CastSession +import com.google.android.material.chip.ChipGroup +import com.google.android.material.navigationrail.NavigationRailView import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.mvvm.logError @@ -39,6 +42,13 @@ import org.schabi.newpipe.extractor.NewPipe import java.lang.ref.WeakReference import java.util.* +enum class FocusDirection { + Start, + End, + Up, + Down, +} + object CommonActivity { private var _activity: WeakReference? = null @@ -318,17 +328,70 @@ object CommonActivity { currentLook = currentLook.parent as? View ?: break }*/ + /** skips the initial stage of searching for an id using the view, see getNextFocus for specification */ + fun continueGetNextFocus( + root: Any?, + view: View, + direction: FocusDirection, + nextId: Int, + depth: Int = 0 + ): View? { + if (nextId == NO_ID) return null + + // do an initial search for the view, in case the localLook is too deep we can use this as + // an early break and backup view + var next = + when (root) { + is Activity -> root.findViewById(nextId) + is View -> root.rootView.findViewById(nextId) + else -> null + } ?: return null + + next = localLook(view, nextId) ?: next + + // if cant focus but visible then break and let android decide + // the exception if is the view is a parent and has children that wants focus + val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent -> + parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0 + } ?: false + if (!next.isFocusable && next.isShown && !hasChildrenThatWantsFocus) return null + + // if not shown then continue because we will "skip" over views to get to a replacement + if (!next.isShown) { + // we don't want a while true loop, so we let android decide if we find a recursive view + if (next == view) return null + return getNextFocus(root, next, direction, depth + 1) + } + + (when (next) { + is ChipGroup -> { + next.children.firstOrNull { it.isFocusable && it.isShown } + } + + is NavigationRailView -> { + next.findViewById(next.selectedItemId) ?: next.findViewById(R.id.navigation_home) + } + + else -> null + })?.let { + return it + } + + // nothing wrong with the view found, return it + return next + } + /** recursively looks for a next focus up to a depth of 10, * this is used to override the normal shit focus system * because this application has a lot of invisible views that messes with some tv devices*/ - private fun getNextFocus( - act: Activity?, + fun getNextFocus( + root: Any?, view: View?, direction: FocusDirection, depth: Int = 0 ): View? { // if input is invalid let android decide + depth test to not crash if loop is found - if (view == null || depth >= 10 || act == null) { + if (view == null || depth >= 10 || root == null) { return null } @@ -363,31 +426,9 @@ object CommonActivity { if (nextId == NO_ID) return null } - - var next = act.findViewById(nextId) ?: return null - - next = localLook(view, nextId) ?: next - - // if cant focus but visible then break and let android decide - // the exception if is the view is a parent and has children that wants focus - val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent -> - parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0 - } ?: false - if (!next.isFocusable && next.isShown && !hasChildrenThatWantsFocus) return null - - // if not shown then continue because we will "skip" over views to get to a replacement - if (!next.isShown) return getNextFocus(act, next, direction, depth + 1) - - // nothing wrong with the view found, return it - return next + return continueGetNextFocus(root, view, direction, nextId, depth) } - private enum class FocusDirection { - Start, - End, - Up, - Down, - } fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) { //println("Keycode: $keyCode") @@ -517,7 +558,7 @@ object CommonActivity { else -> null } - // println("NEXT FOCUS : $nextView") + // println("NEXT FOCUS : $nextView") if (nextView != null) { nextView.requestFocus() keyEventListener?.invoke(Pair(event, true)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index 1d813ef1..f62482ed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -5,11 +5,12 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick +import com.lagradost.cloudstream3.ui.result.FOCUS_SELF +import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys @@ -110,7 +111,11 @@ class DownloadChildFragment : Fragment() { downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } binding?.downloadChildList?.adapter = adapter - binding?.downloadChildList?.layoutManager = GridLayoutManager(context, 1) + binding?.downloadChildList?.setLinearListLayout( + isHorizontal = false, + nextDown = FOCUS_SELF, + nextRight = FOCUS_SELF + )//layoutManager = GridLayoutManager(context, 1) updateList(folder) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index c8b381a6..27c2e1a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -40,6 +40,8 @@ import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding import com.lagradost.cloudstream3.databinding.StreamInputBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.player.BasicLink +import com.lagradost.cloudstream3.ui.result.FOCUS_SELF +import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import java.net.URI @@ -74,7 +76,7 @@ class DownloadFragment : Fragment() { super.onDestroyView() } - var binding : FragmentDownloadsBinding? = null + var binding: FragmentDownloadsBinding? = null override fun onCreateView( inflater: LayoutInflater, @@ -151,6 +153,7 @@ class DownloadFragment : Fragment() { ) } } + 1 -> { (activity as AppCompatActivity?)?.loadResult( click.data.url, @@ -187,7 +190,13 @@ class DownloadFragment : Fragment() { binding?.downloadList?.apply { this.adapter = adapter - layoutManager = GridLayoutManager(context, 1) + setLinearListLayout( + isHorizontal = false, + nextRight = FOCUS_SELF, + nextUp = FOCUS_SELF, + nextDown = FOCUS_SELF + ) + //layoutManager = GridLayoutManager(context, 1) } // Should be visible in emulator layout 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 607cda01..f84966eb 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 @@ -88,14 +88,14 @@ class HomeChildItemAdapter( private val nextFocusUp: Int? = null, private val nextFocusDown: Int? = null, private val isHorizontal: Boolean = false, - private val isRtl : Boolean + private val isRtl: Boolean ) : RecyclerView.ViewHolder(binding.root) { fun bind(card: SearchResponse, position: Int) { // TV focus fixing - val nextFocusBehavior = when (position) { + /*val nextFocusBehavior = when (position) { 0 -> true itemCount - 1 -> false else -> null @@ -113,7 +113,7 @@ class HomeChildItemAdapter( } else { itemView.nextFocusRightId = -1 itemView.nextFocusLeftId = -1 - } + }*/ when (binding) { @@ -171,7 +171,7 @@ class HomeChildItemAdapter( card, position, itemView, - nextFocusBehavior, + null, // nextFocusBehavior, nextFocusUp, nextFocusDown ) 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 a6e1b5e6..6f9a1654 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 @@ -310,6 +310,17 @@ class HomeFragment : Fragment() { selectedTypes: List, validTypes: List, callback: (List) -> Unit + ) { + bindChips(header, selectedTypes, validTypes, callback, null, null) + } + + fun bindChips( + header: TvtypesChipsBinding?, + selectedTypes: List, + validTypes: List, + callback: (List) -> Unit, + nextFocusDown: Int?, + nextFocusUp: Int? ) { if (header == null) return val pairList = getPairList(header) @@ -317,6 +328,17 @@ class HomeFragment : Fragment() { val isValid = validTypes.any { types.contains(it) } button?.isVisible = isValid button?.isChecked = isValid && selectedTypes.any { types.contains(it) } + button?.isFocusable = true + if (isTrueTvSettings()) { + button?.isFocusableInTouchMode = true + } + + if (nextFocusDown != null) + button?.nextFocusDownId = nextFocusDown + + if (nextFocusUp != null) + button?.nextFocusUpId = nextFocusUp + button?.setOnCheckedChangeListener { _, _ -> val list = ArrayList() for ((sbutton, vvalidTypes) in pairList) { @@ -462,7 +484,7 @@ class HomeFragment : Fragment() { private val apiChangeClickListener = View.OnClickListener { view -> view.context.selectHomepage(currentApiName) { api -> - homeViewModel.loadAndCancel(api, forceReload = true,fromUI = true) + homeViewModel.loadAndCancel(api, forceReload = true, fromUI = true) } /*val validAPIs = view.context?.filterProviderByPreferredMedia()?.toMutableList() ?: mutableListOf() 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 f6c3fead..163a60a1 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 @@ -11,6 +11,7 @@ import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.HomepageParentBinding +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.search.SearchFragment.Companion.filterSearchResponse @@ -154,7 +155,7 @@ open class ParentItemAdapter( class ParentViewHolder constructor( val binding: HomepageParentBinding, - // val viewModel: HomeViewModel, + // val viewModel: HomeViewModel, private val clickCallback: (SearchClickCallback) -> Unit, private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, private val expandCallback: ((String) -> Unit)? = null, @@ -162,7 +163,8 @@ open class ParentItemAdapter( RecyclerView.ViewHolder(binding.root) { val title: TextView = binding.homeChildMoreInfo private val recyclerView: RecyclerView = binding.homeChildRecyclerview - + private val startFocus = R.id.nav_rail_view + private val endFocus = FOCUS_SELF fun update(expand: HomeViewModel.ExpandableHomepageList) { val info = expand.list (recyclerView.adapter as? HomeChildItemAdapter?)?.apply { @@ -176,8 +178,13 @@ open class ParentItemAdapter( nextFocusDown = recyclerView.nextFocusDownId, ).apply { isHorizontal = info.isHorizontalImages + hasNext = expand.hasNext } - recyclerView.setLinearListLayout() + recyclerView.setLinearListLayout( + isHorizontal = true, + nextLeft = startFocus, + nextRight = endFocus, + ) } } @@ -192,7 +199,11 @@ open class ParentItemAdapter( isHorizontal = info.isHorizontalImages hasNext = expand.hasNext } - recyclerView.setLinearListLayout() + recyclerView.setLinearListLayout( + isHorizontal = true, + nextLeft = startFocus, + nextRight = endFocus, + ) title.text = info.name recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { 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 8557d26f..1684dfe5 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 @@ -30,6 +30,7 @@ import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage +import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST import com.lagradost.cloudstream3.ui.result.setLinearListLayout @@ -439,8 +440,8 @@ class HomeParentItemAdapterPreview( resumeRecyclerView.adapter = resumeAdapter bookmarkRecyclerView.adapter = bookmarkAdapter - resumeRecyclerView.setLinearListLayout() - bookmarkRecyclerView.setLinearListLayout() + resumeRecyclerView.setLinearListLayout(nextLeft = R.id.nav_rail_view, nextRight = FOCUS_SELF) + bookmarkRecyclerView.setLinearListLayout(nextLeft = R.id.nav_rail_view, nextRight = FOCUS_SELF) fixPaddingStatusbarMargin(topPadding) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt index 4bed0c9d..93857234 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerPipHelper.kt @@ -11,6 +11,7 @@ import android.util.Rational import androidx.annotation.RequiresApi import androidx.annotation.StringRes import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlin.math.roundToInt class PlayerPipHelper { @@ -88,22 +89,25 @@ class PlayerPipHelper { val ratioAccuracy = 100000 // To convert the float to int // java.lang.IllegalArgumentException: setPictureInPictureParams: Aspect ratio is too extreme (must be between 0.418410 and 2.390000) - val fixedRational = aspectRatio?.toFloat()?.coerceIn(mixAspectRatio, maxAspectRatio)?.let { - Rational((it * ratioAccuracy).roundToInt(), ratioAccuracy) - } + val fixedRational = + aspectRatio?.toFloat()?.coerceIn(mixAspectRatio, maxAspectRatio)?.let { + Rational((it * ratioAccuracy).roundToInt(), ratioAccuracy) + } - activity.setPictureInPictureParams( - PictureInPictureParams.Builder() - .apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - setSeamlessResizeEnabled(true) - setAutoEnterEnabled(isPlaying) + normalSafeApiCall { + activity.setPictureInPictureParams( + PictureInPictureParams.Builder() + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + setSeamlessResizeEnabled(true) + setAutoEnterEnabled(isPlaying) + } } - } - .setAspectRatio(fixedRational) - .setActions(actions) - .build() - ) + .setAspectRatio(fixedRational) + .setActions(actions) + .build() + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt index 26cb7900..b4e3062b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/LinearListLayout.kt @@ -4,19 +4,45 @@ import android.content.Context import android.view.View import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.CommonActivity +import com.lagradost.cloudstream3.CommonActivity.activity +import com.lagradost.cloudstream3.FocusDirection import com.lagradost.cloudstream3.mvvm.logError -fun RecyclerView?.setLinearListLayout(isHorizontal: Boolean = true) { - if (this == null) return +const val FOCUS_SELF = View.NO_ID - 1 +const val FOCUS_INHERIT = FOCUS_SELF - 1 +fun RecyclerView?.setLinearListLayout( + isHorizontal: Boolean = true, + nextLeft: Int = FOCUS_INHERIT, + nextRight: Int = FOCUS_INHERIT, + nextUp: Int = FOCUS_INHERIT, + nextDown: Int = FOCUS_INHERIT +) { + if (this == null) return + val ctx = this.context ?: return this.layoutManager = - this.context?.let { LinearListLayout(it).apply { if (isHorizontal) setHorizontal() else setVertical() } } - // ?: this.layoutManager + LinearListLayout(ctx).apply { + if (isHorizontal) setHorizontal() else setVertical() + nextFocusLeft = + if (nextLeft == FOCUS_INHERIT) this@setLinearListLayout.nextFocusLeftId else nextLeft + nextFocusRight = + if (nextRight == FOCUS_INHERIT) this@setLinearListLayout.nextFocusRightId else nextRight + nextFocusUp = + if (nextUp == FOCUS_INHERIT) this@setLinearListLayout.nextFocusUpId else nextUp + nextFocusDown = + if (nextDown == FOCUS_INHERIT) this@setLinearListLayout.nextFocusDownId else nextDown + } } open class LinearListLayout(context: Context?) : LinearLayoutManager(context) { + var nextFocusLeft: Int = View.NO_ID + var nextFocusRight: Int = View.NO_ID + var nextFocusUp: Int = View.NO_ID + var nextFocusDown: Int = View.NO_ID + fun setHorizontal() { orientation = HORIZONTAL } @@ -56,8 +82,37 @@ open class LinearListLayout(context: Context?) : linearSmoothScroller.targetPosition = position startSmoothScroll(linearSmoothScroller) }*/ + + /** from the current focus go to a direction */ + private fun getNextDirection(focused: View?, direction: FocusDirection): View? { + val id = when (direction) { + FocusDirection.Start -> if (isLayoutRTL) nextFocusRight else nextFocusLeft + FocusDirection.End -> if (isLayoutRTL) nextFocusLeft else nextFocusRight + FocusDirection.Up -> nextFocusUp + FocusDirection.Down -> nextFocusDown + } + + return when (id) { + View.NO_ID -> null + FOCUS_SELF -> focused + else -> CommonActivity.continueGetNextFocus( + activity ?: focused, + focused ?: return null, + direction, + id + ) + } + } + override fun onInterceptFocusSearch(focused: View, direction: Int): View? { val dir = if (orientation == HORIZONTAL) { + if (direction == View.FOCUS_DOWN) getNextDirection(focused, FocusDirection.Down)?.let { newFocus -> + return newFocus + } + if (direction == View.FOCUS_UP) getNextDirection(focused, FocusDirection.Up)?.let { newFocus -> + return newFocus + } + if (direction == View.FOCUS_DOWN || direction == View.FOCUS_UP) { // This scrolls the recyclerview before doing focus search, which // allows the focus search to work better. @@ -69,34 +124,45 @@ open class LinearListLayout(context: Context?) : } var ret = if (direction == View.FOCUS_RIGHT) 1 else -1 // only flip on horizontal layout - if (this.isLayoutRTL) { + if (isLayoutRTL) { ret = -ret } ret } else { - if (direction == View.FOCUS_RIGHT || direction == View.FOCUS_LEFT) return null + if (direction == View.FOCUS_RIGHT) getNextDirection(focused, FocusDirection.End)?.let { newFocus -> + return newFocus + } + if (direction == View.FOCUS_LEFT) getNextDirection(focused, FocusDirection.Start)?.let { newFocus -> + return newFocus + } + + if (direction == View.FOCUS_RIGHT || direction == View.FOCUS_LEFT) { + (focused.parent as? RecyclerView)?.focusSearch(direction) + return null + } + + //if (direction == View.FOCUS_RIGHT || direction == View.FOCUS_LEFT) return null if (direction == View.FOCUS_DOWN) 1 else -1 } - return try { - getPosition(getCorrectParent(focused))?.let { position -> - val lookfor = dir + position - //clamp(dir + position, 0, recyclerView.adapter?.itemCount ?: return null) + try { + val position = getPosition(getCorrectParent(focused)) ?: return null + val lookFor = dir + position - // refocus on the same view if going out of bounds, note that we only do it - // for out of bounds one way as we may override the start where item == -1 - if (lookfor >= itemCount) { - return getViewFromPos(itemCount - 1) ?: focused - } - - getViewFromPos(lookfor) ?: run { - scrollToPosition(lookfor) + // if out of bounds then refocus as specified + return if (lookFor >= itemCount) { + getNextDirection(focused, if(orientation == HORIZONTAL) FocusDirection.End else FocusDirection.Down) + } else if (lookFor < 0) { + getNextDirection(focused, if(orientation == HORIZONTAL) FocusDirection.Start else FocusDirection.Up) + } else { + getViewFromPos(lookFor) ?: run { + scrollToPosition(lookFor) null } } } catch (e: Exception) { logError(e) - null + return null } } 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 e1514d63..3ddaee61 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 @@ -294,7 +294,7 @@ open class ResultFragmentPhone : FullScreenPlayer(), super.onStop() } - private fun updateUI(id : Int?) { + private fun updateUI(id: Int?) { syncModel.updateUserData() viewModel.reloadEpisodes() } @@ -338,7 +338,12 @@ open class ResultFragmentPhone : FullScreenPlayer(), ) } - resultCastItems.layoutManager = object : LinearListLayout(view.context) { + resultCastItems.setLinearListLayout( + isHorizontal = true, + nextLeft = FOCUS_SELF, + nextRight = FOCUS_SELF + ) + /*resultCastItems.layoutManager = object : LinearListLayout(view.context) { override fun onRequestChildFocus( parent: RecyclerView, state: RecyclerView.State, @@ -356,7 +361,7 @@ open class ResultFragmentPhone : FullScreenPlayer(), } }.apply { this.orientation = RecyclerView.HORIZONTAL - } + }*/ resultCastItems.adapter = ActorAdaptor() resultEpisodes.adapter = @@ -597,8 +602,14 @@ open class ResultFragmentPhone : FullScreenPlayer(), EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, ep) ) } + DOWNLOAD_ACTION_LONG_CLICK -> { - viewModel.handleAction(EpisodeClickEvent(ACTION_DOWNLOAD_MIRROR, ep)) + viewModel.handleAction( + EpisodeClickEvent( + ACTION_DOWNLOAD_MIRROR, + ep + ) + ) } else -> DownloadButtonSetup.handleDownloadClick(click) 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 698a0ab5..be3de52b 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 @@ -307,7 +307,29 @@ class ResultFragmentTv : Fragment() { } } - resultEpisodes.setLinearListLayout(isHorizontal = false)/*.layoutManager = + resultEpisodes.setLinearListLayout( + isHorizontal = false, + nextUp = FOCUS_SELF, + nextDown = FOCUS_SELF, + nextRight = FOCUS_SELF, + ) + resultDubSelection.setLinearListLayout( + isHorizontal = false, + nextUp = FOCUS_SELF, + nextDown = FOCUS_SELF, + ) + resultRangeSelection.setLinearListLayout( + isHorizontal = false, + nextUp = FOCUS_SELF, + nextDown = FOCUS_SELF, + ) + resultSeasonSelection.setLinearListLayout( + isHorizontal = false, + nextUp = FOCUS_SELF, + nextDown = FOCUS_SELF, + ) + + /*.layoutManager = LinearListLayout(resultEpisodes.context, resultEpisodes.isRtl()).apply { setVertical() }*/ @@ -367,6 +389,11 @@ class ResultFragmentTv : Fragment() { ) resultCastItems.layoutManager = object : LinearListLayout(view.context) { + + override fun onInterceptFocusSearch(focused: View, direction: Int): View? { + return super.onInterceptFocusSearch(focused, direction) + } + override fun onRequestChildFocus( parent: RecyclerView, state: RecyclerView.State, @@ -383,8 +410,9 @@ class ResultFragmentTv : Fragment() { } } }.apply { - this.orientation = RecyclerView.HORIZONTAL + setHorizontal() } + resultCastItems.adapter = ActorAdaptor { toggleEpisodes(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 2f588c19..63213eb9 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 @@ -45,6 +45,8 @@ import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.updateChips 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.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.AppUtils.ownHide @@ -519,9 +521,12 @@ class SearchFragment : Fragment() { binding?.apply { searchHistoryRecycler.adapter = historyAdapter - searchHistoryRecycler.layoutManager = GridLayoutManager(context, 1) + searchHistoryRecycler.setLinearListLayout(isHorizontal = false, nextRight = FOCUS_SELF) + //searchHistoryRecycler.layoutManager = GridLayoutManager(context, 1) searchMasterRecycler.adapter = masterAdapter + //searchMasterRecycler.setLinearListLayout(isHorizontal = false, nextRight = FOCUS_SELF) + searchMasterRecycler.layoutManager = GridLayoutManager(context, 1) // Automatically search the specified query, this allows the app search to launch from intent diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 3c0b5b95..7b72fc3b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -23,6 +23,8 @@ import com.lagradost.cloudstream3.databinding.FragmentExtensionsBinding import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.plugins.RepositoryManager +import com.lagradost.cloudstream3.ui.result.FOCUS_SELF +import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -82,6 +84,14 @@ class ExtensionsFragment : Fragment() { setUpToolbar(R.string.extensions) + binding?.repoRecyclerView?.setLinearListLayout( + isHorizontal = false, + nextUp = R.id.settings_toolbar, //FOCUS_SELF, // back has no id so we cant :pensive: + nextDown = R.id.plugin_storage_appbar, + nextRight = FOCUS_SELF, + nextLeft = R.id.nav_rail_view + ) + binding?.repoRecyclerView?.adapter = RepoAdapter(false, { findNavController().navigate( R.id.navigation_settings_extensions_to_navigation_settings_plugins, @@ -126,11 +136,11 @@ class ExtensionsFragment : Fragment() { (binding?.repoRecyclerView?.adapter as? RepoAdapter)?.updateList(it) } - binding?.repoRecyclerView?.apply { + /*binding?.repoRecyclerView?.apply { context?.let { ctx -> layoutManager = LinearRecycleViewLayoutManager(ctx, nextFocusUpId, nextFocusDownId) } - } + }*/ // list_repositories?.setOnClickListener { // // Open webview on tv if browser fails diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt index 00e1806d..d8047c11 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt @@ -62,100 +62,101 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen super.onViewCreated(view, savedInstanceState) val metadata = data.plugin.second binding?.apply { - if (!pluginIcon.setImage(//plugin_icon?.height ?: - metadata.iconUrl?.replace( - "%size%", - "$iconSize" - )?.replace( - "%exact_size%", - "$iconSizeExact" - ), - null, - errorImageDrawable = R.drawable.ic_baseline_extension_24 - ) - ) { - pluginIcon.setImageResource(R.drawable.ic_baseline_extension_24) - } - pluginName.text = metadata.name.removeSuffix("Provider") - pluginVersion.text = metadata.version.toString() - pluginDescription.text = metadata.description ?: getString(R.string.no_data) - pluginSize.text = - if (metadata.fileSize == null) getString(R.string.no_data) else formatFileSize( - context, - metadata.fileSize - ) - pluginAuthor.text = - if (metadata.authors.isEmpty()) getString(R.string.no_data) else metadata.authors.joinToString( - ", " - ) - pluginStatus.text = resources.getStringArray(R.array.extension_statuses)[metadata.status] - pluginTypes.text = - if (metadata.tvTypes.isNullOrEmpty()) getString(R.string.no_data) else metadata.tvTypes.joinToString( - ", " - ) - pluginLang.text = if (metadata.language == null) - getString(R.string.no_data) - else - "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}" - - githubBtn.setOnClickListener { - if (metadata.repositoryUrl != null) { - openBrowser(metadata.repositoryUrl) + if (!pluginIcon.setImage(//plugin_icon?.height ?: + metadata.iconUrl?.replace( + "%size%", + "$iconSize" + )?.replace( + "%exact_size%", + "$iconSizeExact" + ), + null, + errorImageDrawable = R.drawable.ic_baseline_extension_24 + ) + ) { + pluginIcon.setImageResource(R.drawable.ic_baseline_extension_24) } - } + pluginName.text = metadata.name.removeSuffix("Provider") + pluginVersion.text = metadata.version.toString() + pluginDescription.text = metadata.description ?: getString(R.string.no_data) + pluginSize.text = + if (metadata.fileSize == null) getString(R.string.no_data) else formatFileSize( + context, + metadata.fileSize + ) + pluginAuthor.text = + if (metadata.authors.isEmpty()) getString(R.string.no_data) else metadata.authors.joinToString( + ", " + ) + pluginStatus.text = + resources.getStringArray(R.array.extension_statuses)[metadata.status] + pluginTypes.text = + if (metadata.tvTypes.isNullOrEmpty()) getString(R.string.no_data) else metadata.tvTypes.joinToString( + ", " + ) + pluginLang.text = if (metadata.language == null) + getString(R.string.no_data) + else + "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}" - if (!metadata.canVote()) { - downvote.alpha = .6f - upvote.alpha = .6f - } + githubBtn.setOnClickListener { + if (metadata.repositoryUrl != null) { + openBrowser(metadata.repositoryUrl) + } + } - if (data.isDownloaded) { - // On local plugins page the filepath is provided instead of url. - val plugin = - PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url] - if (plugin?.openSettings != null && context != null) { - actionSettings.isVisible = true - actionSettings.setOnClickListener { - try { - plugin.openSettings!!.invoke(requireContext()) - } catch (e: Throwable) { - Log.e( - "PluginAdapter", - "Failed to open ${metadata.name} settings: ${ - Log.getStackTraceString(e) - }" - ) + if (!metadata.canVote()) { + downvote.alpha = .6f + upvote.alpha = .6f + } + + if (data.isDownloaded) { + // On local plugins page the filepath is provided instead of url. + val plugin = + PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url] + if (plugin?.openSettings != null && context != null) { + actionSettings.isVisible = true + actionSettings.setOnClickListener { + try { + plugin.openSettings!!.invoke(requireContext()) + } catch (e: Throwable) { + Log.e( + "PluginAdapter", + "Failed to open ${metadata.name} settings: ${ + Log.getStackTraceString(e) + }" + ) + } } + } else { + actionSettings.isVisible = false } } else { actionSettings.isVisible = false } - } else { - actionSettings.isVisible = false - } - upvote.setOnClickListener { + upvote.setOnClickListener { + ioSafe { + metadata.vote(VotingApi.VoteType.UPVOTE).main { + updateVoting(it) + } + } + } + downvote.setOnClickListener { + ioSafe { + metadata.vote(VotingApi.VoteType.DOWNVOTE).main { + updateVoting(it) + } + + } + } + ioSafe { - metadata.vote(VotingApi.VoteType.UPVOTE).main { + metadata.getVotes().main { updateVoting(it) } } } - downvote.setOnClickListener { - ioSafe { - metadata.vote(VotingApi.VoteType.DOWNVOTE).main { - updateVoting(it) - } - - } - } - - ioSafe { - metadata.getVotes().main { - updateVoting(it) - } - } - } } private fun updateVoting(value: Int) { 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 1a6215db..172ea659 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,8 @@ import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding import com.lagradost.cloudstream3.mvvm.observe 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.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.ui.settings.appLanguages @@ -32,7 +34,7 @@ class PluginsFragment : Fragment() { container: ViewGroup?, savedInstanceState: Bundle?, ): View { - val localBinding = FragmentPluginsBinding.inflate(inflater,container,false) + val localBinding = FragmentPluginsBinding.inflate(inflater, container, false) binding = localBinding return localBinding.root//inflater.inflate(R.layout.fragment_plugins, container, false) } @@ -73,48 +75,51 @@ class PluginsFragment : Fragment() { setUpToolbar(name) binding?.settingsToolbar?.apply { - setOnMenuItemClickListener { menuItem -> - when (menuItem?.itemId) { - R.id.download_all -> { - PluginsViewModel.downloadAll(activity, url, pluginViewModel) - } - R.id.lang_filter -> { - val tempLangs = appLanguages.toMutableList() - val languageCodes = mutableListOf("none") + tempLangs.map { (_, _, iso) -> iso } - val languageNames = - mutableListOf(getString(R.string.no_data)) + tempLangs.map { (emoji, name, iso) -> - val flag = - emoji.ifBlank { SubtitleHelper.getFlagFromIso(iso) ?: "ERROR" } - "$flag $name" - } - val selectedList = - pluginViewModel.languages.map { it -> languageCodes.indexOf(it) } - - activity?.showMultiDialog( - languageNames, - selectedList, - getString(R.string.provider_lang_settings), - {}) { newList -> - pluginViewModel.languages = newList.map { it -> languageCodes[it] } - pluginViewModel.updateFilteredPlugins() + setOnMenuItemClickListener { menuItem -> + when (menuItem?.itemId) { + R.id.download_all -> { + PluginsViewModel.downloadAll(activity, url, pluginViewModel) } + + R.id.lang_filter -> { + val tempLangs = appLanguages.toMutableList() + val languageCodes = + mutableListOf("none") + tempLangs.map { (_, _, iso) -> iso } + val languageNames = + mutableListOf(getString(R.string.no_data)) + tempLangs.map { (emoji, name, iso) -> + val flag = + emoji.ifBlank { SubtitleHelper.getFlagFromIso(iso) ?: "ERROR" } + "$flag $name" + } + val selectedList = + pluginViewModel.languages.map { languageCodes.indexOf(it) } + + activity?.showMultiDialog( + languageNames, + selectedList, + getString(R.string.provider_lang_settings), + {}) { newList -> + pluginViewModel.languages = newList.map { languageCodes[it] } + pluginViewModel.updateFilteredPlugins() + } + } + + else -> {} } - else -> {} + return@setOnMenuItemClickListener true } - return@setOnMenuItemClickListener true - } - val searchView = - menu?.findItem(R.id.search_button)?.actionView as? SearchView + val searchView = + menu?.findItem(R.id.search_button)?.actionView as? SearchView - // Don't go back if active query - setNavigationOnClickListener { - if (searchView?.isIconified == false) { - searchView.isIconified = true - } else { - activity?.onBackPressed() + // Don't go back if active query + setNavigationOnClickListener { + if (searchView?.isIconified == false) { + searchView.isIconified = true + } else { + activity?.onBackPressed() + } } - } searchView?.setOnQueryTextFocusChangeListener { _, hasFocus -> if (!hasFocus) pluginViewModel.search(null) } @@ -137,7 +142,11 @@ class PluginsFragment : Fragment() { // Because onActionViewCollapsed doesn't wanna work we need this workaround :( - + binding?.pluginRecyclerView?.setLinearListLayout( + isHorizontal = false, + nextDown = FOCUS_SELF, + nextRight = FOCUS_SELF, + ) binding?.pluginRecyclerView?.adapter = PluginAdapter { @@ -167,11 +176,18 @@ class PluginsFragment : Fragment() { pluginViewModel.updatePluginList(context, url) binding?.tvtypesChipsScroll?.root?.isVisible = true - bindChips(binding?.tvtypesChipsScroll?.tvtypesChips, emptyList(), TvType.values().toList()) { list -> - pluginViewModel.tvTypes.clear() - pluginViewModel.tvTypes.addAll(list.map { it.name }) - pluginViewModel.updateFilteredPlugins() - } + bindChips( + binding?.tvtypesChipsScroll?.tvtypesChips, + emptyList(), + TvType.values().toList(), + callback = { list -> + pluginViewModel.tvTypes.clear() + pluginViewModel.tvTypes.addAll(list.map { it.name }) + pluginViewModel.updateFilteredPlugins() + }, + nextFocusDown = R.id.plugin_recycler_view, + nextFocusUp = null, + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 137e1457..991651dc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -24,6 +24,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WhoIsWatchingAdapter +import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState import com.lagradost.cloudstream3.ui.result.setImage @@ -194,7 +195,13 @@ object DataStoreHelper { builder.setContentView(binding.root) val accountName = context.getString(R.string.account) - binding.profilesRecyclerview.setLinearListLayout(isHorizontal = true) + binding.profilesRecyclerview.setLinearListLayout( + isHorizontal = true, + nextUp = FOCUS_SELF, + nextDown = FOCUS_SELF, + nextLeft = FOCUS_SELF, + nextRight = FOCUS_SELF + ) binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter( selectCallBack = { account -> setAccount(account) diff --git a/app/src/main/res/layout/fragment_child_downloads.xml b/app/src/main/res/layout/fragment_child_downloads.xml index a3cc8ce8..9afaea0b 100644 --- a/app/src/main/res/layout/fragment_child_downloads.xml +++ b/app/src/main/res/layout/fragment_child_downloads.xml @@ -1,39 +1,40 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/download_child_root" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?attr/primaryGrayBackground" + android:orientation="vertical" + tools:context=".ui.download.DownloadFragment"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@android:color/transparent"> + android:id="@+id/download_child_toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/primaryGrayBackground" + android:paddingTop="@dimen/navbar_height" + app:layout_scrollFlags="scroll|enterAlways" + app:navigationIconTint="?attr/iconColor" + app:titleTextColor="?attr/textColor" + tools:title="Overlord" /> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?attr/primaryBlackBackground" + android:nextFocusLeft="@id/nav_rail_view" + android:nextFocusUp="@id/download_child_toolbar" + android:padding="10dp" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:listitem="@layout/download_child_episode" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_extensions.xml b/app/src/main/res/layout/fragment_extensions.xml index a550efa4..b3583539 100644 --- a/app/src/main/res/layout/fragment_extensions.xml +++ b/app/src/main/res/layout/fragment_extensions.xml @@ -64,6 +64,7 @@ android:focusable="true" android:foreground="@drawable/outline_drawable" android:nextFocusRight="@id/add_repo_button_imageview" + android:nextFocusUp="@id/repo_recycler_view" android:orientation="horizontal" android:padding="10dp" @@ -84,13 +85,13 @@ android:textColor="?attr/textColor" /> + android:elevation="0dp" + app:cardCornerRadius="@dimen/storage_radius" + app:cardElevation="0dp" + app:cardMaxElevation="0dp"> diff --git a/app/src/main/res/layout/fragment_plugins.xml b/app/src/main/res/layout/fragment_plugins.xml index c207b2c3..ee86f12b 100644 --- a/app/src/main/res/layout/fragment_plugins.xml +++ b/app/src/main/res/layout/fragment_plugins.xml @@ -25,18 +25,22 @@ app:titleTextColor="?attr/textColor" tools:title="Overlord" /> - + - diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 2fec04c6..1fde999c 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -248,6 +248,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit style="@style/ResultButtonTV" android:nextFocusRight="@id/result_description" + android:nextFocusUp="@id/result_play_movie" android:nextFocusDown="@id/result_play_series" android:text="@string/play_movie_button" android:visibility="visible" @@ -537,10 +538,10 @@ https://developer.android.com/design/ui/tv/samples/jet-fit + tools:visibility="gone"> --> + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="10dp"> - + @@ -116,7 +118,10 @@ android:background="?attr/primaryBlackBackground" android:descendantFocusability="afterDescendants" + android:nextFocusLeft="@id/nav_rail_view" + android:nextFocusUp="@id/tvtypes_chips" + android:nextFocusDown="@id/search_clear_call_history" android:visibility="gone" tools:listitem="@layout/homepage_parent" /> @@ -134,20 +139,24 @@ android:background="?attr/primaryBlackBackground" android:descendantFocusability="afterDescendants" android:nextFocusLeft="@id/nav_rail_view" - android:visibility="visible" + android:nextFocusUp="@id/tvtypes_chips" + android:nextFocusDown="@id/search_clear_call_history" + android:paddingBottom="50dp" + android:visibility="visible" tools:listitem="@layout/search_history_item" /> + android:layout_height="50dp" + android:layout_gravity="bottom" + android:layout_margin="0dp" + android:nextFocusUp="@id/search_history_recycler" + android:padding="0dp" + android:text="@string/clear_history" + app:cornerRadius="0dp" + app:icon="@drawable/delete_all" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index 63c61393..4c4af404 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -11,11 +11,11 @@ tools:context=".ui.search.SearchFragment"> + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/navbar_width" + android:orientation="vertical" + android:paddingBottom="10dp"> - + @@ -118,38 +120,43 @@ android:background="?attr/primaryBlackBackground" android:descendantFocusability="afterDescendants" android:nextFocusLeft="@id/nav_rail_view" + android:nextFocusUp="@id/tvtypes_chips" + android:nextFocusDown="@id/search_clear_call_history" android:visibility="gone" tools:listitem="@layout/homepage_parent" /> + android:layout_height="match_parent" + android:background="?attr/primaryBlackBackground"> + android:layout_height="50dp" + android:layout_gravity="bottom" + android:layout_margin="0dp" + android:layout_marginStart="@dimen/navbar_width" + android:nextFocusUp="@id/search_history_recycler" + android:padding="0dp" + android:text="@string/clear_history" + app:cornerRadius="0dp" + app:icon="@drawable/delete_all" /> \ No newline at end of file diff --git a/app/src/main/res/layout/standard_toolbar.xml b/app/src/main/res/layout/standard_toolbar.xml index a28bdc80..bd1f251d 100644 --- a/app/src/main/res/layout/standard_toolbar.xml +++ b/app/src/main/res/layout/standard_toolbar.xml @@ -1,19 +1,20 @@ - + + android:id="@+id/settings_toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/primaryGrayBackground" + android:descendantFocusability="afterDescendants" + android:paddingTop="@dimen/navbar_height" + app:layout_scrollFlags="scroll|enterAlways" + app:navigationIconTint="?attr/iconColor" + app:titleTextColor="?attr/textColor" + tools:title="Overlord" /> \ No newline at end of file diff --git a/app/src/main/res/layout/tvtypes_chips.xml b/app/src/main/res/layout/tvtypes_chips.xml index 6b13546b..ee792602 100644 --- a/app/src/main/res/layout/tvtypes_chips.xml +++ b/app/src/main/res/layout/tvtypes_chips.xml @@ -6,6 +6,7 @@ android:paddingStart="8dp" android:paddingEnd="8dp" android:id="@+id/home_select_group" + android:descendantFocusability="afterDescendants" app:singleSelection="false" xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/app/src/main/res/layout/tvtypes_chips_scroll.xml b/app/src/main/res/layout/tvtypes_chips_scroll.xml index 66c7efda..8d006036 100644 --- a/app/src/main/res/layout/tvtypes_chips_scroll.xml +++ b/app/src/main/res/layout/tvtypes_chips_scroll.xml @@ -1,10 +1,13 @@ - + android:requiresFadingEdge="horizontal"> - + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 6e2a24f3..972a4caf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,8 +5,8 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:7.3.1") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20") + classpath("com.android.tools.build:gradle:8.0.2") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.5.0") // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index baa28c97..d4745142 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 30 17:11:15 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 5103ad09dc998d1cbfed197e83b5de594f8375ec Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:20:23 +0200 Subject: [PATCH 023/441] reverted gradle bump --- app/build.gradle.kts | 6 +++--- build.gradle.kts | 3 ++- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5c864117..9300775c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -115,11 +115,11 @@ android { compileOptions { isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { - jvmTarget = "17" + jvmTarget = "1.8" freeCompilerArgs = listOf("-Xjvm-default=compatibility") } lint { diff --git a/build.gradle.kts b/build.gradle.kts index 972a4caf..762e4588 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,8 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.0.2") + // we stay on low ver because prerelease build gradle is fucked + classpath("com.android.tools.build:gradle:7.3.1") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.5.0") diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4745142..baa28c97 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 30 17:11:15 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From ca6700e28de32e99080d9b975fac83303bb5a0cc Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Fri, 4 Aug 2023 15:21:20 +0000 Subject: [PATCH 024/441] More meaningful errors when adding repositories (#537) * More meaningful errors when adding repositories --- .../settings/extensions/ExtensionsFragment.kt | 21 +++++++++++++++++-- .../settings/extensions/PluginsViewModel.kt | 12 +++++++---- app/src/main/res/values/strings.xml | 2 ++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 7b72fc3b..8bc947c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -224,14 +224,31 @@ class ExtensionsFragment : Fragment() { showToast(R.string.error_invalid_data, Toast.LENGTH_SHORT) } } else { + val repository = RepositoryManager.parseRepository(url) + + // Exit if wrong repository + if (repository == null) { + showToast(R.string.no_repository_found_error, Toast.LENGTH_LONG) + return@ioSafe + } + val fixedName = if (!name.isNullOrBlank()) name - else RepositoryManager.parseRepository(url)?.name ?: "No name" + else repository.name val newRepo = RepositoryData(fixedName, url) RepositoryManager.addRepository(newRepo) extensionViewModel.loadStats() extensionViewModel.loadRepositories() - this@ExtensionsFragment.activity?.downloadAllPluginsDialog(url, fixedName) + + val plugins = RepositoryManager.getRepoPlugins(url) + if (plugins.isNullOrEmpty()) { + showToast(R.string.no_plugins_found_error, Toast.LENGTH_LONG) + } else { + this@ExtensionsFragment.activity?.downloadAllPluginsDialog( + url, + fixedName + ) + } } } dialog.dismissSafe(activity) 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 6c68ac17..471105be 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 @@ -86,13 +86,17 @@ class PluginsViewModel : ViewModel() { }.also { list -> main { showToast( - if (list.isEmpty()) { - txt( + when { + // No plugins at all + plugins.isEmpty() -> txt( + R.string.no_plugins_found_error, + ) + // All plugins downloaded + list.isEmpty() -> txt( R.string.batch_download_nothing_to_download_format, txt(R.string.plugin) ) - } else { - txt( + else -> txt( R.string.batch_download_start_format, list.size, txt(if (list.size == 1) R.string.plugin_singular else R.string.plugin) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4dd6eadc..3df4b8c1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -572,6 +572,8 @@ Started downloading %d %s… Downloaded %d %s All %s already downloaded + No plugins found in repository + Repository not found, check the URL and try VPN Batch download plugin plugins From bbbb7c4982d6f83f236883e2a9ed40d7a2b8eb61 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Sat, 5 Aug 2023 08:11:46 +0700 Subject: [PATCH 025/441] Extractor: added Rabbitstream (#536) * Extractor: added Rabbitstream * fix all request --------- Co-authored-by: Sofie99 --- .../cloudstream3/extractors/Rabbitstream.kt | 170 ++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 5 +- 2 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt new file mode 100644 index 00000000..b686f7d8 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt @@ -0,0 +1,170 @@ +package com.lagradost.cloudstream3.extractors + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64DecodeArray +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.M3u8Helper +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +// No License found in https://github.com/enimax-anime/key +// special credits to @enimax for providing key +class Megacloud : Rabbitstream() { + override val name = "Megacloud" + override val mainUrl = "https://megacloud.tv" + override val embed = "embed-2/ajax/e-1" + override val key = "https://raw.githubusercontent.com/enimax-anime/key/e6/key.txt" +} + +class Dokicloud : Rabbitstream() { + override val name = "Dokicloud" + override val mainUrl = "https://dokicloud.one" +} + +open class Rabbitstream : ExtractorApi() { + override val name = "Rabbitstream" + override val mainUrl = "https://rabbitstream.net" + override val requiresReferer = false + open val embed = "ajax/embed-4" + open val key = "https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt" + private var rawKey: String? = null + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val id = url.substringAfterLast("/").substringBefore("?") + + val response = app.get( + "$mainUrl/$embed/getSources?id=$id", + referer = mainUrl, + headers = mapOf("X-Requested-With" to "XMLHttpRequest") + ) + + val encryptedMap = response.parsedSafe() + val sources = encryptedMap?.sources + val decryptedSources = if (sources == null || encryptedMap.encrypted == false) { + response.parsedSafe() + } else { + val (key, encData) = extractRealKey(sources, getRawKey()) + val decrypted = decryptMapped>(encData, key) + SourcesResponses( + sources = decrypted, + tracks = encryptedMap.tracks + ) + } + + decryptedSources?.sources?.map { source -> + M3u8Helper.generateM3u8( + name, + source?.file ?: return@map, + "$mainUrl/", + ).forEach(callback) + } + + decryptedSources?.tracks?.map { track -> + subtitleCallback.invoke( + SubtitleFile( + track?.label ?: "", + track?.file ?: return@map + ) + ) + } + + } + + private suspend fun getRawKey(): String = rawKey ?: app.get(key).text.also { rawKey = it } + + private fun extractRealKey(originalString: String?, stops: String): Pair { + val table = parseJson>>(stops) + val decryptedKey = StringBuilder() + var offset = 0 + var encryptedString = originalString + + table.forEach { (start, end) -> + decryptedKey.append(encryptedString?.substring(start - offset, end - offset)) + encryptedString = encryptedString?.substring( + 0, + start - offset + ) + encryptedString?.substring(end - offset) + offset += end - start + } + return decryptedKey.toString() to encryptedString.toString() + } + + private inline fun decryptMapped(input: String, key: String): T? { + val decrypt = decrypt(input, key) + return AppUtils.tryParseJson(decrypt) + } + + private fun decrypt(input: String, key: String): String { + return decryptSourceUrl( + generateKey( + base64DecodeArray(input).copyOfRange(8, 16), + key.toByteArray() + ), input + ) + } + + private fun generateKey(salt: ByteArray, secret: ByteArray): ByteArray { + var key = md5(secret + salt) + var currentKey = key + while (currentKey.size < 48) { + key = md5(key + secret + salt) + currentKey += key + } + return currentKey + } + + private fun md5(input: ByteArray): ByteArray { + return MessageDigest.getInstance("MD5").digest(input) + } + + private fun decryptSourceUrl(decryptionKey: ByteArray, sourceUrl: String): String { + val cipherData = base64DecodeArray(sourceUrl) + val encrypted = cipherData.copyOfRange(16, cipherData.size) + val aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding") + aesCBC.init( + Cipher.DECRYPT_MODE, + SecretKeySpec(decryptionKey.copyOfRange(0, 32), "AES"), + IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size)) + ) + val decryptedData = aesCBC?.doFinal(encrypted) ?: throw ErrorLoadingException("Cipher not found") + return String(decryptedData, StandardCharsets.UTF_8) + } + + data class Tracks( + @JsonProperty("file") val file: String? = null, + @JsonProperty("label") val label: String? = null, + @JsonProperty("kind") val kind: String? = null, + ) + + data class Sources( + @JsonProperty("file") val file: String? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("label") val label: String? = null, + ) + + data class SourcesResponses( + @JsonProperty("sources") val sources: List? = emptyList(), + @JsonProperty("tracks") val tracks: List? = emptyList(), + ) + + data class SourcesEncrypted( + @JsonProperty("sources") val sources: String? = null, + @JsonProperty("encrypted") val encrypted: Boolean? = null, + @JsonProperty("tracks") val tracks: List? = emptyList(), + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index ed190bcc..83c61542 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -428,7 +428,10 @@ val extractorApis: MutableList = arrayListOf( Cda(), Dailymotion(), ByteShare(), - Ztreamhub() + Ztreamhub(), + Rabbitstream(), + Dokicloud(), + Megacloud(), ) From 44a2146c1211841ec5662e76eab00bff1014d30f Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Wed, 9 Aug 2023 23:44:17 +0200 Subject: [PATCH 026/441] fix voting api (#544) --- .../cloudstream3/plugins/VotingApi.kt | 101 ++++++++---------- .../extensions/PluginDetailsFragment.kt | 49 ++------- .../res/layout/fragment_plugin_details.xml | 15 +-- app/src/main/res/values/strings.xml | 1 + 4 files changed, 54 insertions(+), 112 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt index f099ad1a..a45ab5f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt @@ -8,22 +8,14 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.R import java.security.MessageDigest import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main -import kotlinx.coroutines.delay import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock object VotingApi { // please do not cheat the votes lol private const val LOGKEY = "VotingApi" - enum class VoteType(val value: Int) { - UPVOTE(1), - DOWNVOTE(-1), - NONE(0) - } - - private val apiDomain = "https://api.countapi.xyz" + private const val apiDomain = "https://counterapi.com/api" private fun transformUrl(url: String): String = // dont touch or all votes get reset MessageDigest @@ -35,12 +27,12 @@ object VotingApi { // please do not cheat the votes lol return getVotes(url) } - suspend fun SitePlugin.vote(requestType: VoteType): Int { - return vote(url, requestType) + fun SitePlugin.hasVoted(): Boolean { + return hasVoted(url) } - fun SitePlugin.getVoteType(): VoteType { - return getVoteType(url) + suspend fun SitePlugin.vote(): Int { + return vote(url) } fun SitePlugin.canVote(): Boolean { @@ -50,28 +42,31 @@ object VotingApi { // please do not cheat the votes lol // Plugin url to Int private val votesCache = mutableMapOf() - suspend fun getVotes(pluginUrl: String): Int { - val url = "${apiDomain}/get/cs3-votes/${transformUrl(pluginUrl)}" + private fun getRepository(pluginUrl: String) = pluginUrl + .split("/") + .drop(2) + .take(3) + .joinToString("-") + + private suspend fun readVote(pluginUrl: String): Int { + var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true" Log.d(LOGKEY, "Requesting: $url") - return votesCache[pluginUrl] ?: app.get(url).parsedSafe()?.value?.also { - votesCache[pluginUrl] = it - } ?: (0.also { - ioSafe { - createBucket(pluginUrl) + return app.get(url).parsedSafe()?.value ?: 0 + } + + private suspend fun writeVote(pluginUrl: String): Boolean { + var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}" + Log.d(LOGKEY, "Requesting: $url") + return app.get(url).parsedSafe()?.value != null + } + + suspend fun getVotes(pluginUrl: String): Int = + votesCache[pluginUrl] ?: readVote(pluginUrl).also { + votesCache[pluginUrl] = it } - }) - } - fun getVoteType(pluginUrl: String): VoteType { - return getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE - } - - private suspend fun createBucket(pluginUrl: String) { - val url = - "${apiDomain}/create?namespace=cs3-votes&key=${transformUrl(pluginUrl)}&value=0&update_lowerbound=-2&update_upperbound=2&enable_reset=0" - Log.d(LOGKEY, "Requesting: $url") - app.get(url) - } + fun hasVoted(pluginUrl: String) = + getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false fun canVote(pluginUrl: String): Boolean { if (!PluginManager.urlPlugins.contains(pluginUrl)) return false @@ -79,7 +74,7 @@ object VotingApi { // please do not cheat the votes lol } private val voteLock = Mutex() - suspend fun vote(pluginUrl: String, requestType: VoteType): Int { + suspend fun vote(pluginUrl: String): Int { // Prevent multiple requests at the same time. voteLock.withLock { if (!canVote(pluginUrl)) { @@ -90,33 +85,21 @@ object VotingApi { // please do not cheat the votes lol return getVotes(pluginUrl) } - val savedType: VoteType = - getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE - - val newType = if (requestType == savedType) VoteType.NONE else requestType - val changeValue = if (requestType == savedType) { - -requestType.value - } else if (savedType == VoteType.NONE) { - requestType.value - } else if (savedType != requestType) { - -savedType.value + requestType.value - } else 0 - - // Pre-emptively set vote key - setKey("cs3-votes/${transformUrl(pluginUrl)}", newType) - - val url = - "${apiDomain}/update/cs3-votes/${transformUrl(pluginUrl)}?amount=${changeValue}" - Log.d(LOGKEY, "Requesting: $url") - val res = app.get(url).parsedSafe()?.value - - if (res == null) { - // "Refund" key if the response is invalid - setKey("cs3-votes/${transformUrl(pluginUrl)}", savedType) - } else { - votesCache[pluginUrl] = res + if (hasVoted(pluginUrl)) { + main { + Toast.makeText(context, R.string.already_voted, Toast.LENGTH_SHORT) + .show() + } + return getVotes(pluginUrl) } - return res ?: 0 + + + if (writeVote(pluginUrl)) { + setKey("cs3-votes/${transformUrl(pluginUrl)}", true) + votesCache[pluginUrl] = votesCache[pluginUrl]?.plus(1) ?: 1 + } + + return getVotes(pluginUrl) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt index d8047c11..7d733be0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginDetailsFragment.kt @@ -13,10 +13,9 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentPluginDetailsBinding import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.plugins.VotingApi import com.lagradost.cloudstream3.plugins.VotingApi.canVote -import com.lagradost.cloudstream3.plugins.VotingApi.getVoteType import com.lagradost.cloudstream3.plugins.VotingApi.getVotes +import com.lagradost.cloudstream3.plugins.VotingApi.hasVoted import com.lagradost.cloudstream3.plugins.VotingApi.vote import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -106,7 +105,6 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen } if (!metadata.canVote()) { - downvote.alpha = .6f upvote.alpha = .6f } @@ -137,19 +135,11 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen upvote.setOnClickListener { ioSafe { - metadata.vote(VotingApi.VoteType.UPVOTE).main { + metadata.vote().main { updateVoting(it) } } } - downvote.setOnClickListener { - ioSafe { - metadata.vote(VotingApi.VoteType.DOWNVOTE).main { - updateVoting(it) - } - - } - } ioSafe { metadata.getVotes().main { @@ -163,33 +153,14 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen val metadata = data.plugin.second binding?.apply { pluginVotes.text = value.toString() - when (metadata.getVoteType()) { - VotingApi.VoteType.UPVOTE -> { - upvote.imageTintList = ColorStateList.valueOf( - context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary - ) - downvote.imageTintList = ColorStateList.valueOf( - context?.colorFromAttribute(R.attr.white) ?: R.color.white - ) - } - - VotingApi.VoteType.DOWNVOTE -> { - downvote.imageTintList = ColorStateList.valueOf( - context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary - ) - upvote.imageTintList = ColorStateList.valueOf( - context?.colorFromAttribute(R.attr.white) ?: R.color.white - ) - } - - VotingApi.VoteType.NONE -> { - upvote.imageTintList = ColorStateList.valueOf( - context?.colorFromAttribute(R.attr.white) ?: R.color.white - ) - downvote.imageTintList = ColorStateList.valueOf( - context?.colorFromAttribute(R.attr.white) ?: R.color.white - ) - } + if (metadata.hasVoted()) { + upvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary + ) + } else { + upvote.imageTintList = ColorStateList.valueOf( + context?.colorFromAttribute(R.attr.colorOnSurface) ?: R.color.white + ) } } } diff --git a/app/src/main/res/layout/fragment_plugin_details.xml b/app/src/main/res/layout/fragment_plugin_details.xml index 35ab9216..7a8f85e4 100644 --- a/app/src/main/res/layout/fragment_plugin_details.xml +++ b/app/src/main/res/layout/fragment_plugin_details.xml @@ -284,23 +284,10 @@ android:layout_gravity="center" android:gravity="center_horizontal|center_vertical"> - - tv_no_focus_tag + You have already voted From 72871c18b53d3c5c34f90311fe943e4681954b9e Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Wed, 9 Aug 2023 23:56:28 +0200 Subject: [PATCH 027/441] Translations update from Hosted Weblate (#535) Co-authored-by: Alexandru Co-authored-by: Alexthegib Co-authored-by: Alexthegib Co-authored-by: Amir Co-authored-by: Astrid Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> Co-authored-by: Eryk Michalak Co-authored-by: Ettore Atalan Co-authored-by: Fjuro Co-authored-by: Jan Haider Co-authored-by: Massimo Pissarello Co-authored-by: Milo Ivir Co-authored-by: PiterDev Co-authored-by: Rex_sa Co-authored-by: Reza Almanda Co-authored-by: Rudy Tantono Co-authored-by: Skrripy Co-authored-by: dabao1955 Co-authored-by: gallegonovato Co-authored-by: george kitsoukakis Co-authored-by: infoekcz --- app/src/main/res/values-ar/strings.xml | 5 +- app/src/main/res/values-cs/strings.xml | 7 ++- app/src/main/res/values-el/strings.xml | 49 ++++++++++++++++--- app/src/main/res/values-es/strings.xml | 4 +- app/src/main/res/values-fa/strings.xml | 10 +++- app/src/main/res/values-hr/strings.xml | 21 ++++---- app/src/main/res/values-in/strings.xml | 5 +- app/src/main/res/values-it/strings.xml | 12 +++-- app/src/main/res/values-ja/strings.xml | 9 +++- app/src/main/res/values-nl/strings.xml | 8 ++- app/src/main/res/values-pt/strings.xml | 8 ++- app/src/main/res/values-ro/strings.xml | 40 +++++++++++---- app/src/main/res/values-uk/strings.xml | 8 ++- fastlane/metadata/android/ar/changelogs/2.txt | 1 + .../metadata/android/ar/full_description.txt | 12 +++++ .../metadata/android/ar/short_description.txt | 1 + fastlane/metadata/android/ar/title.txt | 1 + .../metadata/android/cs-CZ/changelogs/2.txt | 1 + .../android/cs-CZ/full_description.txt | 10 ++++ .../android/cs-CZ/short_description.txt | 1 + fastlane/metadata/android/cs-CZ/title.txt | 1 + .../metadata/android/de-DE/changelogs/2.txt | 1 + .../android/de-DE/full_description.txt | 8 ++- .../android/de-DE/short_description.txt | 2 +- .../metadata/android/el-GR/changelogs/2.txt | 1 + .../android/el-GR/full_description.txt | 10 ++++ .../android/el-GR/short_description.txt | 1 + fastlane/metadata/android/el-GR/title.txt | 1 + .../metadata/android/it-IT/changelogs/2.txt | 1 + .../android/it-IT/full_description.txt | 12 ++--- .../android/it-IT/short_description.txt | 2 +- fastlane/metadata/android/it-IT/title.txt | 1 + fastlane/metadata/android/ro/changelogs/2.txt | 1 + .../metadata/android/ro/full_description.txt | 10 ++++ .../metadata/android/ro/short_description.txt | 1 + fastlane/metadata/android/ro/title.txt | 1 + .../metadata/android/uk/full_description.txt | 12 ++--- 37 files changed, 218 insertions(+), 61 deletions(-) create mode 100644 fastlane/metadata/android/ar/changelogs/2.txt create mode 100644 fastlane/metadata/android/ar/full_description.txt create mode 100644 fastlane/metadata/android/ar/short_description.txt create mode 100644 fastlane/metadata/android/ar/title.txt create mode 100644 fastlane/metadata/android/cs-CZ/changelogs/2.txt create mode 100644 fastlane/metadata/android/cs-CZ/full_description.txt create mode 100644 fastlane/metadata/android/cs-CZ/short_description.txt create mode 100644 fastlane/metadata/android/cs-CZ/title.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/2.txt create mode 100644 fastlane/metadata/android/el-GR/changelogs/2.txt create mode 100644 fastlane/metadata/android/el-GR/full_description.txt create mode 100644 fastlane/metadata/android/el-GR/short_description.txt create mode 100644 fastlane/metadata/android/el-GR/title.txt create mode 100644 fastlane/metadata/android/it-IT/changelogs/2.txt create mode 100644 fastlane/metadata/android/it-IT/title.txt create mode 100644 fastlane/metadata/android/ro/changelogs/2.txt create mode 100644 fastlane/metadata/android/ro/full_description.txt create mode 100644 fastlane/metadata/android/ro/short_description.txt create mode 100644 fastlane/metadata/android/ro/title.txt diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 41ee5ed0..95eff4a9 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -581,4 +581,7 @@ تعذر إنشاء واجهة المستخدم بشكل صحيح ، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور %s حدد الوضع لتصفية تنزيل المكونات الإضافية تعطيل - + \@string/default_subtitles + لا توجد اضافة في المستودع + المستودع لم يتم العثور عليه، تحقق من العنوان اوجرب شبكة افتراضية خاصة(vpn) + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index a2c3df3e..dea42030 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -571,6 +571,9 @@ \n \nPOZNÁMKA: Pokud je součet 10 nebo vyšší, přehrávač automaticky přeskočí načítání při načtení daného odkazu! Nepodařilo se správně vytvořit rozhraní. Toto je VÁŽNÁ CHYBA, kterou je potřeba ihned nahlásit %s - Zakázat + Vypnout Výběr režimu pro filtrování stahování doplňků - + V repozitáři nebyly nalezeny žádné doplňky + Repozitář nenalezen, zkontrolujte adresu URL a zkuste použít VPN + \@string/default_subtitles + \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index c24c0971..d638f2db 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -214,7 +214,7 @@ Να μην εμφανιστεί ξανά Παράλειψη της τρέχουσας ενημέρωσης Ενημέρωση - Προτιμώμενη ποιότητας παρακολούθησης + Προτιμώμενη ποιότητας παρακολούθησης (WiFi) Μέγιστος αριθμός χαρακτήρων τίτλου Ανάλυση προγράμματος αναπαραγωγής βίντεο Μέγεθος buffer για βίντεο @@ -452,7 +452,7 @@ Ανάμεικτοι τίτλοι τέλους -30 Κριτική - @string/ova + OVA Ενημερώσεις εφαρμογής Αντίγραφο ασφαλείας Extensions @@ -500,16 +500,53 @@ Ταξινόμηση με βάση Αλφαβητικά (Α προς Ω) Διάλεξε βιβλιοθήκη - Φαίνεται πως η λίστα είναι άδεια, δοκίμασε να μεταβείς σε μία άλλη + Αυτή η λίστα είναι άδεια. Δοκιμάστε μια άλλη. Αφαίρεση από παρακολουθημένα Περιηγητής Άνοιγμα με - Φαίνεται πως η βιβλιοθήκη σου είναι άδεια :( -\nΣυνδέσου σε έναν λογαριασμό που έχει βιβλιοθήκη, ή πρόσθεσε σειρές στην τοπική βιβλιοθήκη σου + Η βιβλιοθήκη σας είναι άδεια :( +\nΣυνδεθείτε με έναν λογαριασμό βιβλιοθήκης ή προσθέστε σειρές στην τοπική βιβλιοθήκη σας. Βρέθηκε αρχείο Ασφαλούς Λειτουργίας! \nΔεν πρόκειται να φορτωθούν extensions κατά το ξεκίνημα μέχρι να διαγραφεί το αρχείο. Αρχείο Καταγραφής Απέτυχε Πέτυχε Εκκίνηση - + Δε βρέθηκαν επεκτάσεις στο αποθετήριο + Δε βρέθηκε αποθετήριο, ελέγξτε την URL και δοκιμάστε VPN + Από εδώ μπορείτε να αλλάξετε τον τρόπο σειράς των πηγών. Εάν ένα βίντεο έχει υψηλότερη προτεραιότητα, θα εμφανίζεται ψηλότερα στην επιλογή πηγής. Το άθροισμα της προτεραιότητας πηγής και της ποιότητας, είναι η προτεραιότητα του βίντεο. +\n +\nΠηγή Α: 3 +\nΠοιότητα Β: 7 +\nΘα έχει συνδυασμένη προτεραιότητα βίντεο 10. +\n +\nΣΗΜΕΙΩΣΗ: Εάν το άθροισμα είναι 10 ή περισσότερο, η συσκευή αναπαραγωγής θα παραλείψει αυτόματα τη φόρτωση όταν φορτωθεί αυτός ο σύνδεσμος! + Δοκιμή παρόχου + Προτιμώμενη ποιότητας παρακολούθησης (Δεδομένα τηλεφώνου) + Διακομιστής μεσολάβησης raw.githubusercontent.com + Android TV + Ενημέρωση εγγεγραμμένων εκπομπών + Έγινε εγγραφή σε %s + Επαναφορά + Δημοσιεύθηκε το επεισόδιο %d! + Βοήθεια + Ποιότητες + Φόντο προφίλ + Επανεκκίνηση + Το GitHub δεν είναι προσβάσιμο. Ενεργοποίηση διακομιστή μεσολάβησης jsDelivr… + Παράκαμψη ISP + Αφαιρέθηκε η εγγραφή από %s + Εγγεγραμμένος + Προφίλ %d + Wi-Fi + Δεδομένα τηλεφώνου + Ορισμός προεπιλογής + Χρήση + Επεξεργασία + Προφίλ + Το UI δεν ήταν σε θέση να δημιουργηθεί σωστά, είναι ένα σφάλμα και θα πρέπει να αναφερθεί αμέσως %s + Επιλέξτε κατάσταση για φιλτράρισμα επεκτάσεων για λήψη + Απενεργοποιημένο + \@string/default_subtitles + Τέλος + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 1ac81c20..5fb756da 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -550,4 +550,6 @@ Selecciona el modo para filtrar la descarga de los plugins Desactivar @string/default_subtitles - + No se encontraron complementos en el repositorio + Repositorio no encontrado, comprueba la URL y prueba la VPN + \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 2e4b89b3..ba12d115 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -45,4 +45,12 @@ کارتونها استفاده شده برنامه - + بازگشت + آغاز قسمت بعد پس از پایان قسمت فعلی + تغییر ارائه دهنده + حذف + اطلاعات بیشتر + شرح + زبان زیرنویس + زیرنویس + \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 92920ab4..1e3fecb7 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -417,22 +417,22 @@ Dodaj repository Ime repositorya URL spremišta (repositorija) - Dodatak je učitan - Dodatak je izbrisan + Dodatak učitan + Dodatak izbrisan Nije moguće učitati %s 18+ Započelo preuzimanje %d %s… Preuzeto %d %s Sve %s je već preuzeto Skupno preuzimanje - plugin - plugins - Ovo će također izbrisati sve dodatke spremišta + dodatak + dodaci + Ovo će također izbrisati sve dodatke repozitorija Izbriši repository Preuzmi popis stranica koje želite koristiti Preuzeto: %d Onemogućeno: %d - Nije preuzeto: %d + Nepreuzeto: %d CloudStream nema instalirane web stranice prema zadanim postavkama. Morate instalirati stranice iz repozitorija. \n \nZbog bezumnog uklanjanja DMCA od strane Sky UK Limited 🤮 ne možemo povezati web mjesto repozitorija u aplikaciji. @@ -459,9 +459,9 @@ Podržano Jezik HLS Playlista - Automatski instaliraj ekstenzije + Automatski instaliraj dodatke Zasluge - Automatski instaliraj sve neinstalirane ekstenzije iz dodanih repozitorija. + Automatski instaliraj sve neinstalirane dodatke iz dodanih repozitorija. Preferirani video player Interni player Prvo instalirajte ekstenziju @@ -479,7 +479,7 @@ Ne Instaliranje ažuriranja aplikacije… Nije moguće instalirati novu verziju aplikacije - Ažurirano %d ekstenzija + Ažurirano %d dodataka Mješoviti početak Uvod Linkovi @@ -565,4 +565,5 @@ Kvalitete Pozadina profila Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s - + Odaberi modus za filtriranje preuzimanja dodataka + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index c413ba60..2ff4d1ee 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -572,4 +572,7 @@ UI tidak dapat dibuat dengan benar, ini adalah BUG UTAMA dan harus segera dilaporkan %s Nonaktif Pilih mode untuk memfilter unduhan plugin - + \@string/subtitle_default + Tidak ada plugin yang ditemukan di repositori + Repositori tidak ditemukan, periksa URL dan coba VPN + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 431b2a8c..64d27065 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -121,7 +121,7 @@ Pulsante di ridimensionamento del video Rimuovi bordi neri Sottotitoli - Impostazioni sottotitoli + Impostazioni sottotitoli lettore Sottotitoli Chromecast Impostazioni sottotitoli Chromecast Modalità Eigengravy @@ -226,7 +226,7 @@ Altri Film - Serie TV + Serie Cartoni Anime OVA @@ -568,4 +568,10 @@ Dati Mobili Qualità Sfondo profilo - + Nessun plugin trovato nel repository + Repository non trovato, controlla l\'URL e prova la VPN + Non è stato possibile creare correttamente l\'interfaccia utente, questo è un GRANDE BUG e dovrebbe essere segnalato immediatamente %s + Seleziona la modalità per filtrare il download dei plugin + \@string/default_subtitles + Disabilita + \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 7131ee25..464c6790 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -211,4 +211,11 @@ 窓の色 エッジタイプ ダウンロードを一時停止する - + 使用タイプの検索 + フォント + 言語をダウンロードする + 字幕の言語 + フォントサイズ + プロバイダーから探す + 言語の自動選択 + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index dff95be7..49e1aefb 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -568,4 +568,10 @@ \n \nNOTITIE: Als de som 10 of hoger is zal de speler automatisch het laden overslaan wanneer die link is geladen! Profiel %d - + Repository niet gevonden, controleer de URL en probeer een VPN + Geen plug-ins gevonden in de repository + Selecteer een modus om het downloaden van plug-ins te filteren + Uitzetten + De gebruikersinterface kon niet correct worden gemaakt, dit is een ERNSTIG PROBLEEM en moet onmiddellijk gerapporteerd worden %s + \@string/default_subtitles + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 773598bf..3c6b0636 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -546,4 +546,10 @@ \nTerá uma prioridade de vídeo combinada de 10. \n \nNOTA: Se a soma for 10 ou mais, o leitor saltará automaticamente o carregamento quando essa ligação for carregada! - + Selecionar o modo para filtrar a transferência de plug-ins + Não foi possível criar corretamente a interface do utilizador, trata-se de um GRANDE BUG e deve ser comunicado imediatamente %s + \@ string/legendas_padrão + Desativar + Não foram encontrados plugins no repositório + Repositório não encontrado, verifique o URL e tente a VPN + \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 294abcfd..4ca25e2a 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -29,7 +29,7 @@ Descărcări Setări Căutare… - Căutați %s... + Căutați %s… Fără date Mai multe opțiunii Episodul următor @@ -37,7 +37,7 @@ Distribuie Deschide în browser Săriți încărcarea - Se încarcă... + Se încarcă… În curs de vizualizare În așteptare Finalizat @@ -49,7 +49,7 @@ Stream Torrent Surse Subtitrare - Încercați să vă conectați din nou... + Încercați să vă conectați din nou… Înapoi Redă episodul Descărcare @@ -125,9 +125,9 @@ Modul Eigengravy Adăugați opțiunea de viteză în player Derulați spre înainte/înapoi - Glisați dintr-o parte în alta pentru a vă controla poziția într-un videoclip + Derulați dintr-o parte în alta pentru a controla timpul de difuzare a videoclipului Derulați pentru a modifica setările - Glisați spre stânga sau spre dreapta pentru a schimba luminozitatea sau volumul + Glisați în sus sau în jos pe partea stângă sau dreaptă pentru a schimba luminozitatea sau volumul Atingeți de două ori pentru a merge înainte/înapoi Atingeți de două ori pentru a pune pauză Atingeți de două ori partea stângă sau dreaptă a ecranului pentru a derula rapid înainte sau înapoi videoclipul @@ -442,8 +442,7 @@ Autori Raportarea accidentelor Adaugă depozit - Se pare că biblioteca ta este goală :( -\nConectează-te la un cont de bibliotecă sau adaugă emisiuni în biblioteca ta locală + Se pare că biblioteca ta este goală :( Conectează-te la un cont de bibliotecă sau adaugă emisiuni în biblioteca ta locală. Eliminați subtitrările închise din subtitrări Descărcați lista de site-uri pe care doriți să le utilizați Evaluare (Ridicat la Scăzut) @@ -501,7 +500,7 @@ Descriere Plugin Descărcat Sunteți sigur că vreți să ieșiți\? - Se pare că această listă este goală, încercați să treceți la o alta + Se pare că această listă este goală, încercați să treceți la o alta. Sortați după Player intern Prestabile @@ -548,4 +547,27 @@ Player video preferat Actualizări al aplicației Subtitrări - + Dezactivați + Aici puteți schimba modul în care sunt ordonate sursele. Dacă un videoclip are o prioritate mai mare, acesta va apărea mai sus în selecția surselor. Suma dintre prioritatea sursei și prioritatea calității reprezintă prioritatea video. +\n +\nSursa A: 3 +\nCalitate B: 7 +\nVa avea o prioritate video combinată de 10. +\n +\nNOTĂ: Dacă suma este 10 sau mai mare, playerul va sări automat peste încărcare atunci când este încărcat link-ul respectiv! + Nu s-a găsit plugin-uri în depozit + Nu s-a găsit depozitul, verificați URL-ul și încercați cu un VPN + Editați + Profiluri + Ajutor + Profilul %d + Wi-FI + Date mobile + Calități + Profil de fundal + Setați ca implicit + Utilizați + UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s + Selectați modul de filtrare a descărcării plugin-urilor + \@string/default_subtitles + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index e246c673..7d3c20fb 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -546,4 +546,10 @@ Редагувати Якості Фон профілю - + Не вдалося створити UI коректно, це ВАЖЛИВА ПОМИЛКА, про яку слід негайно повідомити %s + Виберіть режим для фільтрації завантаження плагінів + Вимкнути + \@string/default_subtitles + Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN + Не знайдено жодних плагінів у репозиторії + \ No newline at end of file diff --git a/fastlane/metadata/android/ar/changelogs/2.txt b/fastlane/metadata/android/ar/changelogs/2.txt new file mode 100644 index 00000000..911b9b32 --- /dev/null +++ b/fastlane/metadata/android/ar/changelogs/2.txt @@ -0,0 +1 @@ +- تمت إضافة سجل التغيير! diff --git a/fastlane/metadata/android/ar/full_description.txt b/fastlane/metadata/android/ar/full_description.txt new file mode 100644 index 00000000..9bbe01ef --- /dev/null +++ b/fastlane/metadata/android/ar/full_description.txt @@ -0,0 +1,12 @@ +يتيح لك كلاود ستريم -3 بث وتنزيل الأفلام والمسلسلات التلفزيونية والأنيمي. + +يأتي التطبيق بدون أي إعلانات وتحليلات. +و يدعم العديد من مواقع البث الاولي(التريلر) والأفلام والمزيد. وتشمل الميزات: + +إشارات مرجعية + +قم بتنزيل ودفق الأفلام والبرامج التلفزيونية والأنيمي + +تنزيلات الترجمة + +دعم كروم كاست diff --git a/fastlane/metadata/android/ar/short_description.txt b/fastlane/metadata/android/ar/short_description.txt new file mode 100644 index 00000000..f396ff81 --- /dev/null +++ b/fastlane/metadata/android/ar/short_description.txt @@ -0,0 +1 @@ +بث وتحميل الأفلام والأنمي والمسلسلات التلفزيونية. diff --git a/fastlane/metadata/android/ar/title.txt b/fastlane/metadata/android/ar/title.txt new file mode 100644 index 00000000..635e1390 --- /dev/null +++ b/fastlane/metadata/android/ar/title.txt @@ -0,0 +1 @@ +كلاود ستريم diff --git a/fastlane/metadata/android/cs-CZ/changelogs/2.txt b/fastlane/metadata/android/cs-CZ/changelogs/2.txt new file mode 100644 index 00000000..c100e834 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/2.txt @@ -0,0 +1 @@ +- Přidán seznam změn! diff --git a/fastlane/metadata/android/cs-CZ/full_description.txt b/fastlane/metadata/android/cs-CZ/full_description.txt new file mode 100644 index 00000000..3e2d8308 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 umožňuje streamovat a stahovat filmy, televizní seriály a anime. + +Aplikace je bez reklam a analytik a podporuje +spoustu stránek s trailery a filmy a další, např. + +Záložky + +Stahování titulků + +Podpora Chromecastu diff --git a/fastlane/metadata/android/cs-CZ/short_description.txt b/fastlane/metadata/android/cs-CZ/short_description.txt new file mode 100644 index 00000000..d934429d --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/short_description.txt @@ -0,0 +1 @@ +Streamování a stahování filmů, TV seriálů a anime. diff --git a/fastlane/metadata/android/cs-CZ/title.txt b/fastlane/metadata/android/cs-CZ/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/de-DE/changelogs/2.txt b/fastlane/metadata/android/de-DE/changelogs/2.txt new file mode 100644 index 00000000..fc9e0cb9 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/2.txt @@ -0,0 +1 @@ +- Änderungsprotokoll hinzugefügt! diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt index 20fa580c..df314372 100644 --- a/fastlane/metadata/android/de-DE/full_description.txt +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -1,14 +1,12 @@ -Mit CloudStream-3 kannst du Filme, TV-Serien und Anime streamen und herunterladen. Die App kommt ganz ohne Werbung und Analytik aus. Sie unterstützt mehrere Trailer-, Filmseiten und vieles mehr. Integrierte Features: - +Mit CloudStream-3 kannst du Filme, TV-Serien und Anime streamen und herunterladen. +Die App kommt ganz ohne Werbung und Analytik aus. +Sie unterstützt mehrere Trailer-, Filmseiten und vieles mehr. Integrierte Features: Lesezeichen - Herunterladen und Streamen von Filmen, Fernsehsendungen und Animes - Downloads von Untertiteln - Chromecast-Unterstützung diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt index 90f289be..d03eaf7d 100644 --- a/fastlane/metadata/android/de-DE/short_description.txt +++ b/fastlane/metadata/android/de-DE/short_description.txt @@ -1 +1 @@ -Streame und downloade Filme, TV-Serien und Animes. +Filme, Fernsehserien und Animes streamen und herunterladen. diff --git a/fastlane/metadata/android/el-GR/changelogs/2.txt b/fastlane/metadata/android/el-GR/changelogs/2.txt new file mode 100644 index 00000000..63b5f9cc --- /dev/null +++ b/fastlane/metadata/android/el-GR/changelogs/2.txt @@ -0,0 +1 @@ +- Προστέθηκε ο κατάλογος αλλαγών! diff --git a/fastlane/metadata/android/el-GR/full_description.txt b/fastlane/metadata/android/el-GR/full_description.txt new file mode 100644 index 00000000..8062d1c1 --- /dev/null +++ b/fastlane/metadata/android/el-GR/full_description.txt @@ -0,0 +1,10 @@ +Το CloudStream-3 σάς επιτρέπει να μεταδώσετε και να κατεβάζετε Ταινίες, Τηλεοπτικές σειρές και Anime. + +Η εφαρμογή έρχεται χωρίς διαφημίσεις και αναλυτικά στοιχεία και +υποστηρίζει πολλούς ιστότοπους με τρέιλερ ταινιών και πολλά άλλα, π.χ. + +Σελιδοδείκτες + +Λήψεις υποτίτλων + +Υποστήριξη Chromecast diff --git a/fastlane/metadata/android/el-GR/short_description.txt b/fastlane/metadata/android/el-GR/short_description.txt new file mode 100644 index 00000000..b868574a --- /dev/null +++ b/fastlane/metadata/android/el-GR/short_description.txt @@ -0,0 +1 @@ +Μετάδοση και λήψη ταινιών, σειρών και anime. diff --git a/fastlane/metadata/android/el-GR/title.txt b/fastlane/metadata/android/el-GR/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/el-GR/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/it-IT/changelogs/2.txt b/fastlane/metadata/android/it-IT/changelogs/2.txt new file mode 100644 index 00000000..04112af3 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/2.txt @@ -0,0 +1 @@ +- Aggiunto registro delle modifiche! diff --git a/fastlane/metadata/android/it-IT/full_description.txt b/fastlane/metadata/android/it-IT/full_description.txt index cc7dd597..fe80c88e 100644 --- a/fastlane/metadata/android/it-IT/full_description.txt +++ b/fastlane/metadata/android/it-IT/full_description.txt @@ -1,14 +1,10 @@ -CloudStream-3 ti consente di riprodurre in streaming e scaricare film, serie TV e anime. L'app viene fornita senza pubblicità e tracking. Supporta più siti di trailer e film e altro ancora. Le caratteristiche includono: - +CloudStream-3 ti consente di riprodurre in streaming e scaricare film, serie TV e anime. +L'app viene fornita senza pubblicità e tracking. +Supporta più siti di trailer e film e altro ancora, ad esempio: Preferiti - -Scarica e riproduci in streaming film, serie TV e anime - - -scarica sottotitoli - +Download di sottotitoli Supporto a Chromecast diff --git a/fastlane/metadata/android/it-IT/short_description.txt b/fastlane/metadata/android/it-IT/short_description.txt index 64c8e0d0..693131d5 100644 --- a/fastlane/metadata/android/it-IT/short_description.txt +++ b/fastlane/metadata/android/it-IT/short_description.txt @@ -1 +1 @@ -Scarica e riproduci in streaming film, serie TV e anime +Scarica e riproduci in streaming film, serie TV e anime. diff --git a/fastlane/metadata/android/it-IT/title.txt b/fastlane/metadata/android/it-IT/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/it-IT/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/ro/changelogs/2.txt b/fastlane/metadata/android/ro/changelogs/2.txt new file mode 100644 index 00000000..cc15b07a --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/2.txt @@ -0,0 +1 @@ +- Changelog adăugat! diff --git a/fastlane/metadata/android/ro/full_description.txt b/fastlane/metadata/android/ro/full_description.txt new file mode 100644 index 00000000..deb77fb6 --- /dev/null +++ b/fastlane/metadata/android/ro/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 vă permite să difuzați și să descărcați filme, seriale TV și anime. + +Aplicația vine fără reclame și analize și +suportă mai multe site-uri de rulare și filme, și nu numai, de ex. + +Marcaje + +Descărcări de subtitrări + +Suport pentru Chromecast diff --git a/fastlane/metadata/android/ro/short_description.txt b/fastlane/metadata/android/ro/short_description.txt new file mode 100644 index 00000000..1d8b97a1 --- /dev/null +++ b/fastlane/metadata/android/ro/short_description.txt @@ -0,0 +1 @@ +Transmiteți și descărcați filme, seriale TV și anime. diff --git a/fastlane/metadata/android/ro/title.txt b/fastlane/metadata/android/ro/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/ro/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt index 554b08ac..31770635 100644 --- a/fastlane/metadata/android/uk/full_description.txt +++ b/fastlane/metadata/android/uk/full_description.txt @@ -1,14 +1,10 @@ -CloudStream-3 дозволяє транслювати та завантажувати фільми, серіали та аніме. Застосунок не містить реклами та аналітики. Підтримує безліч сайтів з трейлерами та фільмами тощо. Особливості застосунку: - +CloudStream-3 дозволяє транслювати та завантажувати фільми, серіали та аніме. +Застосунок не містить реклами та аналітики й +підтримує безліч сайтів з трейлерами, фільмами тощо, а також багато іншого, наприклад: Закладки - -Завантаження та трансляція фільмів, серіалів та аніме - - -завантаження субтитрів - +Завантаження субтитрів Підтримка Chromecast From 3af0bf750ca06bc1cd191d73803f10f9853dffa0 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 21:56:47 +0000 Subject: [PATCH 028/441] chore(locales): fix locale issues --- app/src/main/res/values-ar/strings.xml | 4 ++-- app/src/main/res/values-cs/strings.xml | 4 ++-- app/src/main/res/values-el/strings.xml | 4 ++-- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 2 +- app/src/main/res/values-hr/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 4 ++-- app/src/main/res/values-it/strings.xml | 4 ++-- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 4 ++-- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 4 ++-- app/src/main/res/values-uk/strings.xml | 4 ++-- 13 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 95eff4a9..9b440e6f 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -581,7 +581,7 @@ تعذر إنشاء واجهة المستخدم بشكل صحيح ، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور %s حدد الوضع لتصفية تنزيل المكونات الإضافية تعطيل - \@string/default_subtitles + @string/default_subtitles لا توجد اضافة في المستودع المستودع لم يتم العثور عليه، تحقق من العنوان اوجرب شبكة افتراضية خاصة(vpn) - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index dea42030..f304199e 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -575,5 +575,5 @@ Výběr režimu pro filtrování stahování doplňků V repozitáři nebyly nalezeny žádné doplňky Repozitář nenalezen, zkontrolujte adresu URL a zkuste použít VPN - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index d638f2db..e88e4fc0 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -547,6 +547,6 @@ Το UI δεν ήταν σε θέση να δημιουργηθεί σωστά, είναι ένα σφάλμα και θα πρέπει να αναφερθεί αμέσως %s Επιλέξτε κατάσταση για φιλτράρισμα επεκτάσεων για λήψη Απενεργοποιημένο - \@string/default_subtitles + @string/default_subtitles Τέλος - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5fb756da..42e07c90 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -552,4 +552,4 @@ @string/default_subtitles No se encontraron complementos en el repositorio Repositorio no encontrado, comprueba la URL y prueba la VPN - \ No newline at end of file + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index ba12d115..f9d28e24 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -53,4 +53,4 @@ شرح زبان زیرنویس زیرنویس - \ No newline at end of file + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 1e3fecb7..35df36ac 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -566,4 +566,4 @@ Pozadina profila Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s Odaberi modus za filtriranje preuzimanja dodataka - \ No newline at end of file + diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 2ff4d1ee..6143a103 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -572,7 +572,7 @@ UI tidak dapat dibuat dengan benar, ini adalah BUG UTAMA dan harus segera dilaporkan %s Nonaktif Pilih mode untuk memfilter unduhan plugin - \@string/subtitle_default + @string/subtitle_default Tidak ada plugin yang ditemukan di repositori Repositori tidak ditemukan, periksa URL dan coba VPN - \ No newline at end of file + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 64d27065..dddc57c4 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -572,6 +572,6 @@ Repository non trovato, controlla l\'URL e prova la VPN Non è stato possibile creare correttamente l\'interfaccia utente, questo è un GRANDE BUG e dovrebbe essere segnalato immediatamente %s Seleziona la modalità per filtrare il download dei plugin - \@string/default_subtitles + @string/default_subtitles Disabilita - \ No newline at end of file + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 464c6790..af4ea695 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -218,4 +218,4 @@ フォントサイズ プロバイダーから探す 言語の自動選択 - \ No newline at end of file + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 49e1aefb..5f60ac14 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -573,5 +573,5 @@ Selecteer een modus om het downloaden van plug-ins te filteren Uitzetten De gebruikersinterface kon niet correct worden gemaakt, dit is een ERNSTIG PROBLEEM en moet onmiddellijk gerapporteerd worden %s - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 3c6b0636..b2504e84 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -552,4 +552,4 @@ Desativar Não foram encontrados plugins no repositório Repositório não encontrado, verifique o URL e tente a VPN - \ No newline at end of file + diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 4ca25e2a..1f288d2a 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -569,5 +569,5 @@ Utilizați UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s Selectați modul de filtrare a descărcării plugin-urilor - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 7d3c20fb..2c5d4197 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -549,7 +549,7 @@ Не вдалося створити UI коректно, це ВАЖЛИВА ПОМИЛКА, про яку слід негайно повідомити %s Виберіть режим для фільтрації завантаження плагінів Вимкнути - \@string/default_subtitles + @string/default_subtitles Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії - \ No newline at end of file + From 2d65aefc760575dce121960906725b9ad04c39be Mon Sep 17 00:00:00 2001 From: Cloudburst <18114966+C10udburst@users.noreply.github.com> Date: Thu, 10 Aug 2023 09:34:29 +0200 Subject: [PATCH 029/441] fix values-in --- app/src/main/res/values-in/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 6143a103..2bd86090 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -572,7 +572,6 @@ UI tidak dapat dibuat dengan benar, ini adalah BUG UTAMA dan harus segera dilaporkan %s Nonaktif Pilih mode untuk memfilter unduhan plugin - @string/subtitle_default Tidak ada plugin yang ditemukan di repositori Repositori tidak ditemukan, periksa URL dan coba VPN From ecd529f73bfd4b2182caa7767b4c32323d55a2ce Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Sat, 12 Aug 2023 15:44:35 +0000 Subject: [PATCH 030/441] TV UX improvements (#538) * Update styles.xml --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 13 +++++++++++- .../cloudstream3/ui/result/SelectAdaptor.kt | 20 +++---------------- .../main/res/color/button_selector_color.xml | 6 ++++++ app/src/main/res/layout/result_selection.xml | 2 +- app/src/main/res/values/styles.xml | 7 ++++++- 5 files changed, 28 insertions(+), 20 deletions(-) create mode 100644 app/src/main/res/color/button_selector_color.xml 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 4a807544..e0d50cc3 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 @@ -1273,7 +1273,18 @@ class GeneratorPlayer : FullScreenPlayer() { from, to ).apply { addListener(onEnd = { - if (!show) playerBinding?.skipChapterButton?.isVisible = false + if (show) { + if (!isShowing) { + // Automatically request focus if the menu is not opened + playerBinding?.skipChapterButton?.requestFocus() + } + } else { + playerBinding?.skipChapterButton?.isVisible = false + if (!isShowing) { + // Automatically return focus to play pause + playerBinding?.playerPausePlay?.requestFocus() + } + } }) addUpdateListener { valueAnimator -> val value = valueAnimator.animatedValue as Int diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt index bcf401ea..6fe45730 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt @@ -45,19 +45,9 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter) { @@ -79,10 +69,6 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter Unit ) { diff --git a/app/src/main/res/color/button_selector_color.xml b/app/src/main/res/color/button_selector_color.xml new file mode 100644 index 00000000..9975946d --- /dev/null +++ b/app/src/main/res/color/button_selector_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_selection.xml b/app/src/main/res/layout/result_selection.xml index 925c65c9..368c8384 100644 --- a/app/src/main/res/layout/result_selection.xml +++ b/app/src/main/res/layout/result_selection.xml @@ -2,7 +2,7 @@ 0dp + + - \ No newline at end of file + From 3ac462ae9625f8a546e0c95f947da00c6283eba3 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 12 Aug 2023 21:20:51 +0200 Subject: [PATCH 031/441] changed UI a bit for flashbang + fixed crash inf loading bug --- app/build.gradle.kts | 2 +- .../lagradost/cloudstream3/MainActivity.kt | 30 ++-- .../cloudstream3/ui/home/HomeFragment.kt | 23 ++- .../ui/home/HomeParentItemAdapterPreview.kt | 55 +++---- .../cloudstream3/ui/home/HomeViewModel.kt | 3 +- .../ui/settings/SettingsUpdates.kt | 4 +- .../lagradost/cloudstream3/utils/UIHelper.kt | 7 +- .../res/color/player_on_button_tv_attr.xml | 5 + app/src/main/res/color/white_attr_20.xml | 4 + .../res/drawable/player_button_tv_attr.xml | 15 ++ .../drawable/player_button_tv_attr_no_bg.xml | 9 ++ app/src/main/res/layout/fragment_home.xml | 47 ++++-- .../main/res/layout/fragment_home_head.xml | 24 ++-- .../main/res/layout/fragment_home_head_tv.xml | 135 +++++++++++------- app/src/main/res/layout/fragment_home_tv.xml | 52 ++++--- app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/colors.xml | 1 - app/src/main/res/values/strings.xml | 1 - app/src/main/res/values/styles.xml | 19 ++- 19 files changed, 252 insertions(+), 185 deletions(-) create mode 100644 app/src/main/res/color/player_on_button_tv_attr.xml create mode 100644 app/src/main/res/color/white_attr_20.xml create mode 100644 app/src/main/res/drawable/player_button_tv_attr.xml create mode 100644 app/src/main/res/drawable/player_button_tv_attr_no_bg.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9300775c..cfe89c05 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,7 +51,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.2" + versionName = "4.1.3" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index d6e275ed..a8160d33 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -278,7 +278,7 @@ var app = Requests(responseParser = object : ResponseParser { class MainActivity : AppCompatActivity(), ColorPickerDialogListener { companion object { const val TAG = "MAINACT" - + var lastError: String? = null /** * Setting this will automatically enter the query in the search * next time the search fragment is opened. @@ -599,22 +599,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } override fun dispatchKeyEvent(event: KeyEvent?): Boolean { - val start = System.currentTimeMillis() - try { - val response = CommonActivity.dispatchKeyEvent(this, event) - - if (response != null) - return response - } finally { - debugAssert({ - val end = System.currentTimeMillis() - val delta = end - start - delta > 100 - }) { - "Took over 100ms to navigate, smth is VERY wrong" - } - } - + val response = CommonActivity.dispatchKeyEvent(this, event) + if (response != null) + return response return super.dispatchKeyEvent(event) } @@ -1054,10 +1041,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val errorFile = filesDir.resolve("last_error") - var lastError: String? = null if (errorFile.exists() && errorFile.isFile) { lastError = errorFile.readText(Charset.defaultCharset()) errorFile.delete() + } else { + lastError = null } val settingsForProvider = SettingsJson() @@ -1167,16 +1155,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } //Automatically download not existing plugins, using mode specified. - val auto_download_plugin = AutoDownloadMode.getEnum( + val autoDownloadPlugin = AutoDownloadMode.getEnum( settingsManager.getInt( getString(R.string.auto_download_plugins_key), 0 ) ) ?: AutoDownloadMode.Disable - if (auto_download_plugin != AutoDownloadMode.Disable) { + if (autoDownloadPlugin != AutoDownloadMode.Disable) { PluginManager.downloadNotExistingPluginsAndLoad( this@MainActivity, - auto_download_plugin + autoDownloadPlugin ) } } 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 6f9a1654..fa0b6dfb 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 @@ -513,9 +513,13 @@ class HomeFragment : Fragment() { fixGrid() binding?.apply { - homeChangeApiLoading.setOnClickListener(apiChangeClickListener) + //homeChangeApiLoading.setOnClickListener(apiChangeClickListener) //homeChangeApiLoading.setOnClickListener(apiChangeClickListener) homeApiFab.setOnClickListener(apiChangeClickListener) + homeChangeApi.setOnClickListener(apiChangeClickListener) + homeSwitchAccount.setOnClickListener { v -> + DataStoreHelper.showWhoIsWatching(v?.context ?: return@setOnClickListener) + } homeRandom.setOnClickListener { if (listHomepageItems.isNotEmpty()) { activity.loadSearchResult(listHomepageItems.random()) @@ -527,21 +531,9 @@ class HomeFragment : Fragment() { mutableListOf(), homeViewModel ) - fixPaddingStatusbar(homeLoadingStatusbar) + //fixPaddingStatusbar(homeLoadingStatusbar) - if (isTvSettings()) { - homeApiFab.isVisible = false - if (isTrueTvSettings()) { - homeChangeApiLoading.isVisible = true - homeChangeApiLoading.isFocusable = true - homeChangeApiLoading.isFocusableInTouchMode = true - } - // home_bookmark_select?.isFocusable = true - // home_bookmark_select?.isFocusableInTouchMode = true - } else { - homeApiFab.isVisible = true - homeChangeApiLoading.isVisible = false - } + homeApiFab.isVisible = !isTvSettings() homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -574,6 +566,7 @@ class HomeFragment : Fragment() { observe(homeViewModel.apiName) { apiName -> currentApiName = apiName binding?.homeApiFab?.text = apiName + binding?.homeChangeApi?.text = apiName } observe(homeViewModel.page) { data -> 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 1684dfe5..943f784a 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 @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.ui.home import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat import androidx.core.view.isGone @@ -13,7 +12,6 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity @@ -41,10 +39,9 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes -import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarMargin import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView -import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.UIHelper.populateChips class HomeParentItemAdapterPreview( items: MutableList, @@ -245,7 +242,11 @@ class HomeParentItemAdapterPreview( private val previewViewpager: ViewPager2 = itemView.findViewById(R.id.home_preview_viewpager) - private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) + + private val previewViewpagerText: ViewGroup = + itemView.findViewById(R.id.home_preview_viewpager_text) + + // private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder) private var resumeRecyclerView: RecyclerView = itemView.findViewById(R.id.home_watch_child_recyclerview) @@ -254,7 +255,7 @@ class HomeParentItemAdapterPreview( itemView.findViewById(R.id.home_bookmarked_child_recyclerview) private var homeAccount: View? = - itemView.findViewById(R.id.home_switch_account) + itemView.findViewById(R.id.home_preview_switch_account) private var topPadding : View? = itemView.findViewById(R.id.home_padding) @@ -282,26 +283,8 @@ class HomeParentItemAdapterPreview( item.plot ?: "" homePreviewText.text = item.name - homePreviewTags.apply { - removeAllViews() - item.tags?.forEach { tag -> - val chip = Chip(context) - val chipDrawable = - ChipDrawable.createFromAttributes( - context, - null, - 0, - R.style.ChipFilledSemiTransparent - ) - chip.setChipDrawable(chipDrawable) - chip.text = tag - chip.isChecked = false - chip.isCheckable = false - chip.isFocusable = false - chip.isClickable = false - addView(chip) - } - } + populateChips(homePreviewTags,item.tags ?: emptyList(), R.style.ChipFilledSemiTransparent) + homePreviewTags.isGone = item.tags.isNullOrEmpty() @@ -324,7 +307,7 @@ class HomeParentItemAdapterPreview( } (binding as? FragmentHomeHeadBinding)?.apply { - homePreviewImage.setImage(item.posterUrl, item.posterHeaders) + //homePreviewImage.setImage(item.posterUrl, item.posterHeaders) homePreviewPlay.setOnClickListener { view -> viewModel.click( @@ -402,7 +385,6 @@ class HomeParentItemAdapterPreview( if (binding is FragmentHomeHeadTvBinding) { observe(viewModel.apiName) { name -> binding.homePreviewChangeApi.text = name - binding.homePreviewChangeApi2.text = name } } observe(viewModel.resumeWatching) { @@ -468,11 +450,6 @@ class HomeParentItemAdapterPreview( viewModel.loadAndCancel(api, forceReload = true, fromUI = true) } } - homePreviewChangeApi2.setOnClickListener { view -> - view.context.selectHomepage(viewModel.repo?.name) { api -> - viewModel.loadAndCancel(api, forceReload = true, fromUI = true) - } - } // This makes the hidden next buttons only available when on the info button // Otherwise you might be able to go to the next item without being at the info button @@ -517,10 +494,6 @@ class HomeParentItemAdapterPreview( } private fun updatePreview(preview: Resource>>) { - if (binding is FragmentHomeHeadTvBinding) { - binding.homePreviewChangeApi2.isGone = preview is Resource.Success - } - if (preview is Resource.Success) { homeNonePadding.apply { val params = layoutParams @@ -545,14 +518,18 @@ class HomeParentItemAdapterPreview( previewViewpager.fakeDragBy(1f) previewViewpager.endFakeDrag() previewCallback.onPageSelected(0) - previewHeader.isVisible = true + previewViewpager.isVisible = true + previewViewpagerText.isVisible = true + //previewHeader.isVisible = true } } else -> { previewAdapter.setItems(listOf(), false) previewViewpager.setCurrentItem(0, false) - previewHeader.isVisible = false + previewViewpager.isVisible = false + previewViewpagerText.isVisible = false + //previewHeader.isVisible = false } } } 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 a2dc9821..b1ced59e 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 @@ -18,6 +18,7 @@ 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 @@ -484,7 +485,7 @@ class HomeViewModel : ViewModel() { } else if (api == null) { // API is not found aka not loaded or removed, post the loading // progress if waiting for plugins, otherwise nothing - if(PluginManager.loadedLocalPlugins) { + if(PluginManager.loadedLocalPlugins || PluginManager.checkSafeModeFile() || lastError != null) { loadAndCancel(noneApi) } else { _page.postValue(Resource.Loading()) 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 9227409d..c304629a 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 @@ -177,7 +177,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { val prefNames = resources.getStringArray(R.array.auto_download_plugin) val prefValues = enumValues().sortedBy { x -> x.value }.map { x -> x.value } - val current = settingsManager.getInt(getString(R.string.auto_download_plugins_pref), 0) + val current = settingsManager.getInt(getString(R.string.auto_download_plugins_key), 0) activity?.showBottomDialog( prefNames.toList(), @@ -185,7 +185,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { getString(R.string.automatic_plugin_download_mode_title), true, {}) { - settingsManager.edit().putInt(getString(R.string.auto_download_plugins_pref), prefValues[it]).apply() + settingsManager.edit().putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } return@setOnPreferenceClickListener true 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 5a393ed5..038a2f11 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -24,6 +24,7 @@ import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.annotation.IdRes +import androidx.annotation.StyleRes import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.widget.PopupMenu @@ -81,7 +82,7 @@ object UIHelper { || Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) } - fun populateChips(view: ChipGroup?, tags: List) { + fun populateChips(view: ChipGroup?, tags: List, @StyleRes style : Int = R.style.ChipFilled) { if (view == null) return view.removeAllViews() val context = view.context ?: return @@ -92,7 +93,7 @@ object UIHelper { context, null, 0, - R.style.ChipFilled + style ) chip.setChipDrawable(chipDrawable) chip.text = tag @@ -100,7 +101,7 @@ object UIHelper { chip.isCheckable = false chip.isFocusable = false chip.isClickable = false - chip.setTextColor(context.colorFromAttribute(R.attr.textColor)) + chip.setTextColor(context.colorFromAttribute(R.attr.white)) view.addView(chip) } } diff --git a/app/src/main/res/color/player_on_button_tv_attr.xml b/app/src/main/res/color/player_on_button_tv_attr.xml new file mode 100644 index 00000000..feb1eeb0 --- /dev/null +++ b/app/src/main/res/color/player_on_button_tv_attr.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/white_attr_20.xml b/app/src/main/res/color/white_attr_20.xml new file mode 100644 index 00000000..e0237df0 --- /dev/null +++ b/app/src/main/res/color/white_attr_20.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/player_button_tv_attr.xml b/app/src/main/res/drawable/player_button_tv_attr.xml new file mode 100644 index 00000000..4c90a64e --- /dev/null +++ b/app/src/main/res/drawable/player_button_tv_attr.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml b/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml new file mode 100644 index 00000000..b9b927da --- /dev/null +++ b/app/src/main/res/drawable/player_button_tv_attr_no_bg.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index eb38e262..672a6d21 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -13,7 +13,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone" - tools:visibility="gone"> + tools:visibility="visible"> - + + + - + android:contentDescription="@string/account" + android:nextFocusLeft="@id/home_search" + android:padding="10dp" + android:src="@drawable/ic_outline_account_circle_24" /> + + tools:listitem="@layout/homepage_parent" + tools:visibility="gone" /> @@ -26,15 +26,6 @@ - - @@ -150,6 +141,7 @@ app:tint="?attr/white" /> + diff --git a/app/src/main/res/layout/fragment_home_head_tv.xml b/app/src/main/res/layout/fragment_home_head_tv.xml index d2c20bc4..03766d79 100644 --- a/app/src/main/res/layout/fragment_home_head_tv.xml +++ b/app/src/main/res/layout/fragment_home_head_tv.xml @@ -15,7 +15,6 @@ android:layout_height="0dp" /> @@ -28,7 +27,44 @@ - + + + + + + + + + - - - + + + tools:visibility="visible"> - + + + + - + android:padding="10dp" + android:src="@drawable/ic_outline_account_circle_24" + android:tag="@string/tv_no_focus_tag" + app:tint="@color/player_on_button_tv_attr" /> + - + tools:listitem="@layout/homepage_parent_tv" + tools:visibility="gone" /> + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7dd4c989..d9258c40 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -13,7 +13,6 @@ #161616 #e9eaee - #1AFFFFFF #9ba0a4 #DCDCDC diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 74515fbf..c80e0e76 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,7 +7,6 @@ auto_update auto_update_plugins auto_download_plugins_key - auto_download_plugins_pref skip_update_key prerelease_update manual_check_update diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 3ef56c22..e2f11221 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -62,7 +62,8 @@ @color/iconGrayBackground @color/boxItemBackground @color/iconColor - #FFF + @color/white + @color/black @style/CustomPreferenceThemeOverlay @@ -99,7 +100,7 @@ @@ -117,6 +118,7 @@ @color/textColor @color/grayTextColor @color/white + @color/black @color/whiteText @@ -158,7 +160,9 @@ @color/lightItemBackground @color/lightTextColor @color/lightGrayTextColor - #000 + @color/black + @color/white + @color/blackText @@ -170,6 +174,7 @@ @color/material_dynamic_neutral90 @color/material_dynamic_neutral60 @color/material_dynamic_neutral90 + @color/material_dynamic_neutral10 @color/material_on_primary_emphasis_medium @@ -747,13 +752,13 @@ @null @color/transparent @null - @drawable/player_button_tv + @drawable/player_button_tv_attr @color/white @color/transparent - @color/player_on_button_tv - @color/player_on_button_tv - @color/player_on_button_tv + @color/player_on_button_tv_attr + @color/player_on_button_tv_attr + @color/player_on_button_tv_attr wrap_content 40dp 16dp From e43b4808d1be1dfad1b308f971e54c835ee074b8 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 12 Aug 2023 21:23:43 +0200 Subject: [PATCH 032/441] phone fix --- app/src/main/res/layout/fragment_home.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 672a6d21..ac660ccd 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -93,6 +93,7 @@ +/> Date: Sat, 12 Aug 2023 21:52:37 +0200 Subject: [PATCH 033/441] should fix an issue with auto_download_plugins_key --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c80e0e76..ded7366b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,7 +6,7 @@ search_type_list auto_update auto_update_plugins - auto_download_plugins_key + auto_download_plugins_key2 skip_update_key prerelease_update manual_check_update From d2d2e41fb31a7da70adf6ef080540833a7070657 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Sat, 12 Aug 2023 20:25:30 +0000 Subject: [PATCH 034/441] Added Simkl (#548) --- .github/workflows/build_to_archive.yml | 2 + .github/workflows/prerelease.yml | 2 + app/build.gradle.kts | 25 +- .../com/lagradost/cloudstream3/MainAPI.kt | 29 +- .../syncproviders/AccountManager.kt | 7 +- .../cloudstream3/syncproviders/SyncAPI.kt | 31 +- .../cloudstream3/syncproviders/SyncRepo.kt | 4 +- .../syncproviders/providers/AniListApi.kt | 4 +- .../syncproviders/providers/LocalList.kt | 4 +- .../syncproviders/providers/MALApi.kt | 2 +- .../syncproviders/providers/SimklApi.kt | 848 ++++++++++++++++++ .../cloudstream3/ui/result/SyncViewModel.kt | 38 +- .../ui/settings/SettingsAccount.kt | 2 + app/src/main/res/drawable/simkl_logo.xml | 9 + app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/settings_account.xml | 43 +- 16 files changed, 988 insertions(+), 63 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt create mode 100644 app/src/main/res/drawable/simkl_logo.xml diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml index 9cd2c523..3b7aa9ae 100644 --- a/.github/workflows/build_to_archive.yml +++ b/.github/workflows/build_to_archive.yml @@ -56,6 +56,8 @@ jobs: 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 }} - uses: actions/checkout@v3 with: repository: "recloudstream/cloudstream-archive" diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 856d267c..58009a7a 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -48,6 +48,8 @@ jobs: 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 }} - name: Create pre-release uses: "marvinpinto/action-automatic-releases@latest" with: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cfe89c05..3c12652a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,4 @@ +import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import org.jetbrains.dokka.gradle.DokkaTask import java.io.ByteArrayOutputStream import java.net.URL @@ -54,17 +55,27 @@ android { versionName = "4.1.3" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") - resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") - resValue("bool", "is_prerelease", "false") + // Reads local.properties + val localProperties = gradleLocalProperties(rootDir) + buildConfigField( "String", "BUILDDATE", "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));" ) - + buildConfigField( + "String", + "SIMKL_CLIENT_ID", + "\"" + (System.getenv("SIMKL_CLIENT_ID") ?: localProperties["simkl.id"]) + "\"" + ) + buildConfigField( + "String", + "SIMKL_CLIENT_SECRET", + "\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\"" + ) testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" kapt { @@ -108,9 +119,9 @@ android { } } //toolchain { - // languageVersion.set(JavaLanguageVersion.of(17)) - // } - // jvmToolchain(17) + // languageVersion.set(JavaLanguageVersion.of(17)) + // } + // jvmToolchain(17) compileOptions { isCoreLibraryDesugaringEnabled = true @@ -211,7 +222,7 @@ dependencies { // Networking // implementation("com.squareup.okhttp3:okhttp:4.9.2") // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") - implementation("com.github.Blatzar:NiceHttp:0.4.2") + implementation("com.github.Blatzar:NiceHttp:0.4.3") // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 76abda97..7790f047 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -11,9 +11,12 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.syncproviders.providers.SimklApi import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson @@ -821,7 +824,8 @@ public enum class AutoDownloadMode(val value: Int) { ; companion object { - infix fun getEnum(value: Int): AutoDownloadMode? = AutoDownloadMode.values().firstOrNull { it.value == value } + infix fun getEnum(value: Int): AutoDownloadMode? = + AutoDownloadMode.values().firstOrNull { it.value == value } } } @@ -1143,6 +1147,7 @@ interface LoadResponse { companion object { private val malIdPrefix = malApi.idPrefix private val aniListIdPrefix = aniListApi.idPrefix + private val simklIdPrefix = simklApi.idPrefix var isTrailersEnabled = true fun LoadResponse.isMovie(): Boolean { @@ -1164,6 +1169,20 @@ interface LoadResponse { this.actors = actors?.map { (actor, role) -> ActorData(actor, role = role) } } + /** + * Internal helper function to add simkl ids from other databases. + */ + private fun LoadResponse.addSimklId( + database: SimklApi.Companion.SyncServices, + id: String? + ) { + normalSafeApiCall { + this.syncData[simklIdPrefix] = + SimklApi.addIdToString(this.syncData[simklIdPrefix], database, id.toString()) + ?: return@normalSafeApiCall + } + } + @JvmName("addActorsOnly") fun LoadResponse.addActors(actors: List?) { this.actors = actors?.map { actor -> ActorData(actor) } @@ -1179,10 +1198,16 @@ interface LoadResponse { fun LoadResponse.addMalId(id: Int?) { this.syncData[malIdPrefix] = (id ?: return).toString() + this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString()) } fun LoadResponse.addAniListId(id: Int?) { this.syncData[aniListIdPrefix] = (id ?: return).toString() + this.addSimklId(SimklApi.Companion.SyncServices.AniList, id.toString()) + } + + fun LoadResponse.addSimklId(id: Int?) { + this.addSimklId(SimklApi.Companion.SyncServices.Simkl, id.toString()) } fun LoadResponse.addImdbUrl(url: String?) { @@ -1264,6 +1289,7 @@ interface LoadResponse { fun LoadResponse.addImdbId(id: String?) { // TODO add imdb sync + this.addSimklId(SimklApi.Companion.SyncServices.Imdb, id) } fun LoadResponse.addTrackId(id: String?) { @@ -1276,6 +1302,7 @@ interface LoadResponse { fun LoadResponse.addTMDbId(id: String?) { // TODO add TMDb sync + this.addSimklId(SimklApi.Companion.SyncServices.Tmdb, id) } fun LoadResponse.addRating(text: String?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 8ce6bae2..8bf8dffa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -11,6 +11,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val malApi = MALApi(0) val aniListApi = AniListApi(0) val openSubtitlesApi = OpenSubtitlesApi(0) + val simklApi = SimklApi(0) val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() val localListApi = LocalList() @@ -18,19 +19,19 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { // used to login via app intent val OAuth2Apis get() = listOf( - malApi, aniListApi + malApi, aniListApi, simklApi ) // this needs init with context and can be accessed in settings val accountManagers get() = listOf( - malApi, aniListApi, openSubtitlesApi, //nginxApi + malApi, aniListApi, openSubtitlesApi, simklApi //nginxApi ) // used for active syncing val SyncApis get() = listOf( - SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi) + SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi), SyncRepo(simklApi) ) val inAppAuths diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt index 8c76c5bf..ed496326 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -10,7 +10,8 @@ enum class SyncIdName { MyAnimeList, Trakt, Imdb, - LocalList + Simkl, + LocalList, } interface SyncAPI : OAuth2API { @@ -35,9 +36,9 @@ interface SyncAPI : OAuth2API { 4 -> PlanToWatch 5 -> ReWatching */ - suspend fun score(id: String, status: SyncStatus): Boolean + suspend fun score(id: String, status: AbstractSyncStatus): Boolean - suspend fun getStatus(id: String): SyncStatus? + suspend fun getStatus(id: String): AbstractSyncStatus? suspend fun getResult(id: String): SyncResult? @@ -59,14 +60,24 @@ interface SyncAPI : OAuth2API { override var id: Int? = null, ) : SearchResponse - data class SyncStatus( - val status: Int, + abstract class AbstractSyncStatus { + abstract var status: Int + /** 1-10 */ - val score: Int?, - val watchedEpisodes: Int?, - var isFavorite: Boolean? = null, - var maxEpisodes: Int? = null, - ) + abstract var score: Int? + abstract var watchedEpisodes: Int? + abstract var isFavorite: Boolean? + abstract var maxEpisodes: Int? + } + + data class SyncStatus( + override var status: Int, + /** 1-10 */ + override var score: Int?, + override var watchedEpisodes: Int?, + override var isFavorite: Boolean? = null, + override var maxEpisodes: Int? = null, + ) : AbstractSyncStatus() data class SyncResult( /**Used to verify*/ diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt index 85b877e0..9363cb6f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt @@ -18,11 +18,11 @@ class SyncRepo(private val repo: SyncAPI) { repo.requireLibraryRefresh = value } - suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource { + suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Resource { return safeApiCall { repo.score(id, status) } } - suspend fun getStatus(id: String): Resource { + suspend fun getStatus(id: String): Resource { return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 0010ce25..d0c88901 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -158,7 +158,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) } - override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { + override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { val internalId = id.toIntOrNull() ?: return null val data = getDataAboutId(internalId) ?: return null @@ -171,7 +171,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) } - override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { + override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { return postDataAboutId( id.toIntOrNull() ?: return false, fromIntToAnimeStatus(status.status), diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt index 7dd43fe7..e6ca9711 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -45,11 +45,11 @@ class LocalList : SyncAPI { override val mainUrl = "" override val syncIdName = SyncIdName.LocalList - override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { + override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { return true } - override suspend fun getStatus(id: String): SyncAPI.SyncStatus? { + override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { return null } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index 5164b606..02826401 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -91,7 +91,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { return Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first() } - override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { + override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { return setScoreRequest( id.toIntOrNull() ?: return false, fromIntToAnimeStatus(status.status), diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt new file mode 100644 index 00000000..64afb0e2 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -0,0 +1,848 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import androidx.annotation.StringRes +import androidx.core.net.toUri +import androidx.fragment.app.FragmentActivity +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.debugAssert +import com.lagradost.cloudstream3.mvvm.debugPrint +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall +import com.lagradost.cloudstream3.syncproviders.AccountManager +import com.lagradost.cloudstream3.syncproviders.AuthAPI +import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.ui.library.ListSorting +import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson +import okhttp3.Interceptor +import okhttp3.Response +import java.math.BigInteger +import java.security.SecureRandom +import java.text.SimpleDateFormat +import java.time.Instant +import java.util.Date +import java.util.TimeZone + +class SimklApi(index: Int) : AccountManager(index), SyncAPI { + override var name = "Simkl" + override val key = "simkl-key" + override val redirectUrl = "simkl" + override val idPrefix = "simkl" + override var requireLibraryRefresh = true + override var mainUrl = "https://api.simkl.com" + override val icon = R.drawable.simkl_logo + override val requiresLogin = false + override val createAccountUrl = "$mainUrl/signup" + override val syncIdName = SyncIdName.Simkl + private val token: String? + get() = getKey(accountId, SIMKL_TOKEN_KEY).also { + debugAssert({ it == null }) { "No ${this.name} token!" } + } + + /** Automatically adds simkl auth headers */ + private val interceptor = HeaderInterceptor() + + /** + * This is required to override the reported last activity as simkl activites + * may not always update based on testing. + */ + private var lastScoreTime = -1L + + companion object { + private const val clientId = BuildConfig.SIMKL_CLIENT_ID + private const val clientSecret = BuildConfig.SIMKL_CLIENT_SECRET + private var lastLoginState = "" + + const val SIMKL_TOKEN_KEY: String = "simkl_token" + const val SIMKL_USER_KEY: String = "simkl_user" + const val SIMKL_CACHED_LIST: String = "simkl_cached_list" + const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time" + + /** 2014-09-01T09:10:11Z -> 1409562611 */ + private const val simklDateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + fun getUnixTime(string: String?): Long? { + return try { + SimpleDateFormat(simklDateFormat).apply { + this.timeZone = TimeZone.getTimeZone("UTC") + }.parse( + string ?: return null + )?.toInstant()?.epochSecond + } catch (e: Exception) { + logError(e) + return null + } + } + + /** 1409562611 -> 2014-09-01T09:10:11Z */ + fun getDateTime(unixTime: Long?): String? { + return try { + SimpleDateFormat(simklDateFormat).apply { + this.timeZone = TimeZone.getTimeZone("UTC") + }.format( + Date.from( + Instant.ofEpochSecond( + unixTime ?: return null + ) + ) + ) + } catch (e: Exception) { + null + } + } + + /** + * Set of sync services simkl is compatible with. + * Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id + */ + enum class SyncServices(val originalName: String) { + Simkl("simkl"), + Imdb("imdb"), + Tmdb("tmdb"), + AniList("anilist"), + Mal("mal"), + } + + /** + * The ID string is a way to keep a collection of services in one single ID using a map + * This adds a database service (like imdb) to the string and returns the new string. + */ + fun addIdToString(idString: String?, database: SyncServices, id: String?): String? { + if (id == null) return idString + return (readIdFromString(idString) + mapOf(database to id)).toJson() + } + + /** Read the id string to get all other ids */ + private fun readIdFromString(idString: String?): Map { + return tryParseJson(idString) ?: return emptyMap() + } + + fun getPosterUrl(poster: String): String { + return "https://wsrv.nl/?url=https://simkl.in/posters/${poster}_m.webp" + } + + private fun getUrlFromId(id: Int): String { + return "https://simkl.com/shows/$id" + } + + enum class SimklListStatusType( + var value: Int, + @StringRes val stringRes: Int, + val originalName: String? + ) { + Watching(0, R.string.type_watching, "watching"), + Completed(1, R.string.type_completed, "completed"), + Paused(2, R.string.type_on_hold, "hold"), + Dropped(3, R.string.type_dropped, "dropped"), + Planning(4, R.string.type_plan_to_watch, "plantowatch"), + ReWatching(5, R.string.type_re_watching, "watching"), + None(-1, R.string.none, null); + + companion object { + fun fromString(string: String): SimklListStatusType? { + return SimklListStatusType.values().firstOrNull { + it.originalName == string + } + } + } + } + + // ------------------- + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class TokenRequest( + @JsonProperty("code") val code: String, + @JsonProperty("client_id") val client_id: String = clientId, + @JsonProperty("client_secret") val client_secret: String = clientSecret, + @JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl", + @JsonProperty("grant_type") val grant_type: String = "authorization_code" + ) + + data class TokenResponse( + /** No expiration date */ + val access_token: String, + val token_type: String, + val scope: String + ) + // ------------------- + + /** https://simkl.docs.apiary.io/#reference/users/settings/receive-settings */ + data class SettingsResponse( + val user: User + ) { + data class User( + val name: String, + /** Url */ + val avatar: String + ) + } + + // ------------------- + data class ActivitiesResponse( + val all: String?, + val tv_shows: UpdatedAt, + val anime: UpdatedAt, + val movies: UpdatedAt, + ) { + data class UpdatedAt( + val all: String?, + val removed_from_list: String?, + val rated_at: String?, + ) + } + + /** https://simkl.docs.apiary.io/#reference/tv/episodes/get-tv-show-episodes */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class EpisodeMetadata( + @JsonProperty("title") val title: String?, + @JsonProperty("description") val description: String?, + @JsonProperty("season") val season: Int?, + @JsonProperty("episode") val episode: Int, + @JsonProperty("img") val img: String? + ) { + companion object { + fun convertToEpisodes(list: List?): List { + return list?.map { + MediaObject.Season.Episode(it.episode) + } ?: emptyList() + } + + fun convertToSeasons(list: List?): List { + return list?.filter { it.season != null }?.groupBy { + it.season + }?.map { (season, episodes) -> + MediaObject.Season(season!!, convertToEpisodes(episodes)) + } ?: emptyList() + } + } + } + + /** + * https://simkl.docs.apiary.io/#introduction/about-simkl-api/standard-media-objects + * Useful for finding shows from metadata + */ + @JsonInclude(JsonInclude.Include.NON_EMPTY) + open class MediaObject( + @JsonProperty("title") val title: String?, + @JsonProperty("year") val year: Int?, + @JsonProperty("ids") val ids: Ids?, + @JsonProperty("poster") val poster: String? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("seasons") val seasons: List? = null, + @JsonProperty("episodes") val episodes: List? = null + ) { + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class Season( + @JsonProperty("number") val number: Int, + @JsonProperty("episodes") val episodes: List + ) { + data class Episode(@JsonProperty("number") val number: Int) + } + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class Ids( + @JsonProperty("simkl") val simkl: Int?, + @JsonProperty("imdb") val imdb: String? = null, + @JsonProperty("tmdb") val tmdb: String? = null, + @JsonProperty("mal") val mal: String? = null, + @JsonProperty("anilist") val anilist: String? = null, + ) { + companion object { + fun fromMap(map: Map): Ids { + return Ids( + simkl = map[SyncServices.Simkl]?.toIntOrNull(), + imdb = map[SyncServices.Imdb], + tmdb = map[SyncServices.Tmdb], + mal = map[SyncServices.Mal], + anilist = map[SyncServices.AniList] + ) + } + } + } + + fun toSyncSearchResult(): SyncAPI.SyncSearchResult? { + return SyncAPI.SyncSearchResult( + this.title ?: return null, + "Simkl", + this.ids?.simkl?.toString() ?: return null, + getUrlFromId(this.ids.simkl), + this.poster?.let { getPosterUrl(it) }, + if (this.type == "movie") TvType.Movie else TvType.TvSeries + ) + } + } + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + class RatingMediaObject( + @JsonProperty("title") title: String?, + @JsonProperty("year") year: Int?, + @JsonProperty("ids") ids: Ids?, + @JsonProperty("rating") val rating: Int, + @JsonProperty("rated_at") val rated_at: String? = getDateTime(unixTime) + ) : MediaObject(title, year, ids) + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + class StatusMediaObject( + @JsonProperty("title") title: String?, + @JsonProperty("year") year: Int?, + @JsonProperty("ids") ids: Ids?, + @JsonProperty("to") val to: String, + @JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime) + ) : MediaObject(title, year, ids) + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + class HistoryMediaObject( + @JsonProperty("title") title: String?, + @JsonProperty("year") year: Int?, + @JsonProperty("ids") ids: Ids?, + @JsonProperty("seasons") seasons: List?, + @JsonProperty("episodes") episodes: List?, + ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + data class StatusRequest( + @JsonProperty("movies") val movies: List, + @JsonProperty("shows") val shows: List + ) + + /** https://simkl.docs.apiary.io/#reference/sync/get-all-items/get-all-items-in-the-user's-watchlist */ + data class AllItemsResponse( + val shows: List, + val anime: List, + val movies: List, + ) { + companion object { + fun merge(first: AllItemsResponse?, second: AllItemsResponse?): AllItemsResponse { + + // Replace the first item with the same id, or add the new item + fun MutableList.replaceOrAddItem(newItem: T, predicate: (T) -> Boolean) { + for (i in this.indices) { + if (predicate(this[i])) { + this[i] = newItem + return + } + } + this.add(newItem) + } + + // + fun merge( + first: List?, + second: List? + ): List { + return (first?.toMutableList() ?: mutableListOf()).apply { + second?.forEach { secondShow -> + this.replaceOrAddItem(secondShow) { + it.getIds().simkl == secondShow.getIds().simkl + } + } + } + } + + return AllItemsResponse( + merge(first?.shows, second?.shows), + merge(first?.anime, second?.anime), + merge(first?.movies, second?.movies), + ) + } + } + + interface Metadata { + val last_watched_at: String? + val status: String? + val user_rating: Int? + val last_watched: String? + val watched_episodes_count: Int? + val total_episodes_count: Int? + + fun getIds(): ShowMetadata.Show.Ids + fun toLibraryItem(): SyncAPI.LibraryItem + } + + data class MovieMetadata( + override val last_watched_at: String?, + override val status: String, + override val user_rating: Int?, + override val last_watched: String?, + override val watched_episodes_count: Int?, + override val total_episodes_count: Int?, + val movie: ShowMetadata.Show + ) : Metadata { + override fun getIds(): ShowMetadata.Show.Ids { + return this.movie.ids + } + + override fun toLibraryItem(): SyncAPI.LibraryItem { + return SyncAPI.LibraryItem( + this.movie.title, + "https://simkl.com/tv/${movie.ids.simkl}", + movie.ids.simkl.toString(), + this.watched_episodes_count, + this.total_episodes_count, + this.user_rating?.times(10), + getUnixTime(last_watched_at) ?: 0, + "Simkl", + TvType.Movie, + this.movie.poster?.let { getPosterUrl(it) }, + null, + null, + movie.ids.simkl + ) + } + } + + data class ShowMetadata( + override val last_watched_at: String?, + override val status: String, + override val user_rating: Int?, + override val last_watched: String?, + override val watched_episodes_count: Int?, + override val total_episodes_count: Int?, + val show: Show + ) : Metadata { + override fun getIds(): Show.Ids { + return this.show.ids + } + + override fun toLibraryItem(): SyncAPI.LibraryItem { + return SyncAPI.LibraryItem( + this.show.title, + "https://simkl.com/tv/${show.ids.simkl}", + show.ids.simkl.toString(), + this.watched_episodes_count, + this.total_episodes_count, + this.user_rating?.times(10), + getUnixTime(last_watched_at) ?: 0, + "Simkl", + TvType.Anime, + this.show.poster?.let { getPosterUrl(it) }, + null, + null, + show.ids.simkl + ) + } + + data class Show( + val title: String, + val poster: String?, + val year: Int?, + val ids: Ids, + ) { + data class Ids( + val simkl: Int, + val slug: String?, + val imdb: String?, + val zap2it: String?, + val tmdb: String?, + val offen: String?, + val tvdb: String?, + val mal: String?, + val anidb: String?, + val anilist: String?, + val traktslug: String? + ) { + fun matchesId(database: SyncServices, id: String): Boolean { + return when (database) { + SyncServices.Simkl -> this.simkl == id.toIntOrNull() + SyncServices.AniList -> this.anilist == id + SyncServices.Mal -> this.mal == id + SyncServices.Tmdb -> this.tmdb == id + SyncServices.Imdb -> this.imdb == id + } + } + } + } + } + } + } + + /** + * Appends api keys to the requests + **/ + private inner class HeaderInterceptor : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + debugPrint { "${this@SimklApi.name} made request to ${chain.request().url}" } + return chain.proceed( + chain.request() + .newBuilder() + .addHeader("Authorization", "Bearer $token") + .addHeader("simkl-api-key", clientId) + .build() + ) + } + } + + private suspend fun getUser(): SettingsResponse.User? { + return suspendSafeApiCall { + app.post("$mainUrl/users/settings", interceptor = interceptor) + .parsedSafe()?.user + } + } + + class SimklSyncStatus( + override var status: Int, + override var score: Int?, + override var watchedEpisodes: Int?, + val episodes: Array?, + override var isFavorite: Boolean? = null, + override var maxEpisodes: Int? = null, + ) : SyncAPI.AbstractSyncStatus() + + override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { + val realIds = readIdFromString(id) + val foundItem = getSyncListSmart()?.let { list -> + listOf(list.shows, list.anime, list.movies).flatten().firstOrNull { show -> + realIds.any { (database, id) -> + show.getIds().matchesId(database, id) + } + } + } + + // Search to get episodes + val searchResult = searchByIds(realIds)?.firstOrNull() + val episodes = getEpisodes(searchResult?.ids?.simkl, searchResult?.type) + + if (foundItem != null) { + return SimklSyncStatus( + status = foundItem.status?.let { SimklListStatusType.fromString(it)?.value } + ?: return null, + score = foundItem.user_rating, + watchedEpisodes = foundItem.watched_episodes_count, + maxEpisodes = foundItem.total_episodes_count, + episodes = episodes + ) + } else { + return if (searchResult != null) { + SimklSyncStatus( + status = SimklListStatusType.None.value, + score = 0, + watchedEpisodes = 0, + maxEpisodes = if (searchResult.type == "movie") 0 else null, + episodes = episodes + ) + } else { + null + } + } + } + + override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { + val parsedId = readIdFromString(id) + lastScoreTime = unixTime + + if (status.status == SimklListStatusType.None.value) { + return app.post( + "$mainUrl/sync/history/remove", + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + emptyList(), + emptyList() + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } + + val realScore = status.score + val ratingResponseSuccess = if (realScore != null) { + // Remove rating if score is 0 + val ratingsSuffix = if (realScore == 0) "/remove" else "" + debugPrint { "Rate ${this.name} item: rating=$realScore" } + app.post( + "$mainUrl/sync/ratings$ratingsSuffix", + json = StatusRequest( + // Not possible to know if TV or Movie + shows = listOf( + RatingMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + realScore + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } else { + true + } + + val simklStatus = status as? SimklSyncStatus + // All episodes if marked as completed + val watchedEpisodes = if (status.status == SimklListStatusType.Completed.value) { + simklStatus?.episodes?.size + } else { + status.watchedEpisodes + } + + // Only post episodes if available episodes and the status is correct + val episodeResponseSuccess = + if (simklStatus != null && watchedEpisodes != null && !simklStatus.episodes.isNullOrEmpty() && listOf( + SimklListStatusType.Paused.value, + SimklListStatusType.Dropped.value, + SimklListStatusType.Watching.value, + SimklListStatusType.Completed.value, + SimklListStatusType.ReWatching.value + ).contains(status.status) + ) { + val cutEpisodes = simklStatus.episodes.take(watchedEpisodes) + + val (seasons, episodes) = if (cutEpisodes.any { it.season != null }) { + EpisodeMetadata.convertToSeasons(cutEpisodes) to null + } else { + null to EpisodeMetadata.convertToEpisodes(cutEpisodes) + } + + debugPrint { "Synced history for ${status.watchedEpisodes} given size of ${simklStatus.episodes.size}: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" } + val episodeResponse = app.post( + "$mainUrl/sync/history", + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + seasons, + episodes + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ) + episodeResponse.isSuccessful + } else true + + val newStatus = + SimklListStatusType.values().firstOrNull { it.value == status.status }?.originalName + ?: SimklListStatusType.Watching.originalName + + val statusResponseSuccess = if (newStatus != null) { + debugPrint { "Add to ${this.name} list: status=$newStatus" } + app.post( + "$mainUrl/sync/add-to-list", + json = StatusRequest( + shows = listOf( + StatusMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + newStatus + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } else { + true + } + + debugPrint { "All scoring complete: rating=$ratingResponseSuccess, status=$statusResponseSuccess, episode=$episodeResponseSuccess" } + requireLibraryRefresh = true + return ratingResponseSuccess && statusResponseSuccess && episodeResponseSuccess + } + + + /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ + suspend fun searchByIds(serviceMap: Map): Array? { + if (serviceMap.isEmpty()) return emptyArray() + + return app.get( + "$mainUrl/search/id", + params = mapOf("client_id" to clientId) + serviceMap.map { (service, id) -> + service.originalName to id + } + ).parsedSafe() + } + + suspend fun getEpisodes(simklId: Int?, type: String?): Array? { + if (simklId == null) return null + val url = when (type) { + "anime" -> "https://api.simkl.com/anime/episodes/$simklId" + "tv" -> "https://api.simkl.com/tv/episodes/$simklId" + "movie" -> return null + else -> return null + } + return app.get(url, params = mapOf("client_id" to clientId)).parsedSafe() + } + + override suspend fun search(name: String): List? { + return app.get( + "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name) + ).parsedSafe>()?.mapNotNull { it.toSyncSearchResult() } + } + + override fun authenticate(activity: FragmentActivity?) { + lastLoginState = BigInteger(130, SecureRandom()).toString(32) + val url = + "https://simkl.com/oauth/authorize?response_type=code&client_id=$clientId&redirect_uri=$appString://${redirectUrl}&state=$lastLoginState" + openBrowser(url, activity) + } + + override fun loginInfo(): AuthAPI.LoginInfo? { + return getKey(accountId, SIMKL_USER_KEY)?.let { user -> + AuthAPI.LoginInfo( + name = user.name, + profilePicture = user.avatar, + accountIndex = accountIndex + ) + } + } + + override fun logOut() { + requireLibraryRefresh = true + removeAccountKeys() + } + + override suspend fun getResult(id: String): SyncAPI.SyncResult? { + return null + } + + private suspend fun getSyncListSince(since: Long?): AllItemsResponse { + val params = getDateTime(since)?.let { + mapOf("date_from" to it) + } ?: emptyMap() + + return app.get( + "$mainUrl/sync/all-items/", + params = params, + interceptor = interceptor + ).parsed() + } + + private suspend fun getActivities(): ActivitiesResponse? { + return app.post("$mainUrl/sync/activities", interceptor = interceptor).parsedSafe() + } + + private fun getSyncListCached(): AllItemsResponse? { + return getKey(accountId, SIMKL_CACHED_LIST) + } + + private suspend fun getSyncListSmart(): AllItemsResponse? { + if (token == null) return null + + val activities = getActivities() + val lastCacheUpdate = getKey(accountId, SIMKL_CACHED_LIST_TIME) + val lastRemoval = listOf( + activities?.tv_shows?.removed_from_list, + activities?.anime?.removed_from_list, + activities?.movies?.removed_from_list + ).maxOf { + getUnixTime(it) ?: -1 + } + val lastRealUpdate = + listOf( + activities?.tv_shows?.all, + activities?.anime?.all, + activities?.movies?.all, + ).maxOf { + getUnixTime(it) ?: -1 + } + + debugPrint { "Cache times: lastCacheUpdate=$lastCacheUpdate, lastRemoval=$lastRemoval, lastRealUpdate=$lastRealUpdate" } + val list = if (lastCacheUpdate == null || lastCacheUpdate < lastRemoval) { + debugPrint { "Full list update in ${this.name}." } + setKey(accountId, SIMKL_CACHED_LIST_TIME, lastRemoval) + getSyncListSince(null) + } else if (lastCacheUpdate < lastRealUpdate || lastCacheUpdate < lastScoreTime) { + debugPrint { "Partial list update in ${this.name}." } + setKey(accountId, SIMKL_CACHED_LIST_TIME, lastCacheUpdate) + AllItemsResponse.merge(getSyncListCached(), getSyncListSince(lastCacheUpdate)) + } else { + debugPrint { "Cached list update in ${this.name}." } + getSyncListCached() + } + debugPrint { "List sizes: movies=${list?.movies?.size}, shows=${list?.shows?.size}, anime=${list?.anime?.size}" } + + setKey(accountId, SIMKL_CACHED_LIST, list) + + return list + } + + + override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata? { + val list = getSyncListSmart() ?: return null + + val baseMap = + SimklListStatusType.values() + .filter { it.value >= 0 && it.value != SimklListStatusType.ReWatching.value } + .associate { + it.stringRes to emptyList() + } + + val syncMap = listOf(list.anime, list.movies, list.shows) + .flatten() + .groupBy { + it.status + } + .mapNotNull { (status, list) -> + val stringRes = + status?.let { SimklListStatusType.fromString(it)?.stringRes } + ?: return@mapNotNull null + val libraryList = list.map { it.toLibraryItem() } + stringRes to libraryList + }.toMap() + + return SyncAPI.LibraryMetadata( + (baseMap + syncMap).map { SyncAPI.LibraryList(txt(it.key), it.value) }, setOf( + ListSorting.AlphabeticalA, + ListSorting.AlphabeticalZ, + ListSorting.UpdatedNew, + ListSorting.UpdatedOld, + ListSorting.RatingHigh, + ListSorting.RatingLow, + ) + ) + } + + override fun getIdFromUrl(url: String): String { + val simklUrlRegex = Regex("""https://simkl\.com/[^/]*/(\d+).*""") + return simklUrlRegex.find(url)?.groupValues?.get(1) ?: "" + } + + override suspend fun handleRedirect(url: String): Boolean { + val uri = url.toUri() + val state = uri.getQueryParameter("state") + // Ensure consistent state + if (state != lastLoginState) return false + lastLoginState = "" + + val code = uri.getQueryParameter("code") ?: return false + val token = app.post( + "$mainUrl/oauth/token", json = TokenRequest(code) + ).parsedSafe() ?: return false + + switchToNewAccount() + setKey(accountId, SIMKL_TOKEN_KEY, token.access_token) + + val user = getUser() + if (user == null) { + removeKey(accountId, SIMKL_TOKEN_KEY) + switchToOldAccount() + return false + } + + setKey(accountId, SIMKL_USER_KEY, user) + registerAccount() + requireLibraryRefresh = true + + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt index 91415d26..a3e2ed87 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SyncViewModel.kt @@ -36,18 +36,18 @@ class SyncViewModel : ViewModel() { val metadata: LiveData> get() = _metaResponse - private val _userDataResponse: MutableLiveData?> = + private val _userDataResponse: MutableLiveData?> = MutableLiveData(null) - val userData: LiveData?> get() = _userDataResponse + val userData: LiveData?> get() = _userDataResponse // prefix, id - private var syncs = mutableMapOf() + private val syncs = mutableMapOf() //private val _syncIds: MutableLiveData> = // MutableLiveData(mutableMapOf()) //val syncIds: LiveData> get() = _syncIds - fun getSyncs() : Map { + fun getSyncs(): Map { return syncs } @@ -106,7 +106,7 @@ class SyncViewModel : ViewModel() { Log.i(TAG, "addFromUrl = $url") if (url == null || hasAddedFromUrl.contains(url)) return@ioSafe - if(!url.startsWith("http")) return@ioSafe + if (!url.startsWith("http")) return@ioSafe SyncUtil.getIdsFromUrl(url)?.let { (malId, aniListId) -> hasAddedFromUrl.add(url) @@ -150,7 +150,8 @@ class SyncViewModel : ViewModel() { val user = userData.value if (user is Resource.Success) { - _userDataResponse.postValue(Resource.Success(user.value.copy(watchedEpisodes = episodes))) + user.value.watchedEpisodes = episodes + _userDataResponse.postValue(Resource.Success(user.value)) } } @@ -158,7 +159,8 @@ class SyncViewModel : ViewModel() { Log.i(TAG, "setScore = $score") val user = userData.value if (user is Resource.Success) { - _userDataResponse.postValue(Resource.Success(user.value.copy(score = score))) + user.value.score = score + _userDataResponse.postValue(Resource.Success(user.value)) } } @@ -167,7 +169,8 @@ class SyncViewModel : ViewModel() { if (which < -1 || which > 5) return // validate input val user = userData.value if (user is Resource.Success) { - _userDataResponse.postValue(Resource.Success(user.value.copy(status = which))) + user.value.status = which + _userDataResponse.postValue(Resource.Success(user.value)) } } @@ -185,17 +188,16 @@ class SyncViewModel : ViewModel() { fun modifyMaxEpisode(episodeNum: Int) { Log.i(TAG, "modifyMaxEpisode = $episodeNum") modifyData { status -> - status.copy( - watchedEpisodes = maxOf( - episodeNum, - status.watchedEpisodes ?: return@modifyData null - ) + status.watchedEpisodes = maxOf( + episodeNum, + status.watchedEpisodes ?: return@modifyData null ) + status } } /// modifies the current sync data, return null if you don't want to change it - private fun modifyData(update: ((SyncAPI.SyncStatus) -> (SyncAPI.SyncStatus?))) = + private fun modifyData(update: ((SyncAPI.AbstractSyncStatus) -> (SyncAPI.AbstractSyncStatus?))) = ioSafe { syncs.amap { (prefix, id) -> repos.firstOrNull { it.idPrefix == prefix }?.let { repo -> @@ -245,8 +247,12 @@ class SyncViewModel : ViewModel() { // shitty way to sort anilist first, as it has trailers while mal does not if (syncs.containsKey(aniListApi.idPrefix)) { try { // swap can throw error - Collections.swap(current, current.indexOfFirst { it.first == aniListApi.idPrefix }, 0) - } catch (t : Throwable) { + Collections.swap( + current, + current.indexOfFirst { it.first == aniListApi.idPrefix }, + 0 + ) + } catch (t: Throwable) { logError(t) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 33316020..b3225d5c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API @@ -257,6 +258,7 @@ class SettingsAccount : PreferenceFragmentCompat() { listOf( R.string.mal_key to malApi, R.string.anilist_key to aniListApi, + R.string.simkl_key to simklApi, R.string.opensubtitles_key to openSubtitlesApi, ) diff --git a/app/src/main/res/drawable/simkl_logo.xml b/app/src/main/res/drawable/simkl_logo.xml new file mode 100644 index 00000000..eb29fb5b --- /dev/null +++ b/app/src/main/res/drawable/simkl_logo.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ded7366b..13251c7c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -449,6 +449,7 @@ Put the title under the poster anilist_key + simkl_key mal_key opensubtitles_key nginx_key diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml index d4bae8c4..d3dbcb31 100644 --- a/app/src/main/res/xml/settings_account.xml +++ b/app/src/main/res/xml/settings_account.xml @@ -1,27 +1,32 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> + android:icon="@drawable/mal_logo" + android:key="@string/mal_key" /> - - - - + android:icon="@drawable/ic_anilist_icon" + android:key="@string/anilist_key" /> - - - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file From 3ab9e11350aab59c4ada14ec33e6f72fa324f2e9 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 12 Aug 2023 23:41:53 +0200 Subject: [PATCH 035/441] fixed SimklApi subscription --- .../cloudstream3/syncproviders/providers/SimklApi.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 64afb0e2..64cebfc6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -316,9 +316,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** https://simkl.docs.apiary.io/#reference/sync/get-all-items/get-all-items-in-the-user's-watchlist */ data class AllItemsResponse( - val shows: List, - val anime: List, - val movies: List, + @JsonProperty("shows") + val shows: List = emptyList(), + @JsonProperty("anime") + val anime: List = emptyList(), + @JsonProperty("movies") + val movies: List = emptyList(), ) { companion object { fun merge(first: AllItemsResponse?, second: AllItemsResponse?): AllItemsResponse { From 0eb241e6cb4a6d2f5bff7728f7ea0ac7b0a0e904 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 12 Aug 2023 23:54:37 +0200 Subject: [PATCH 036/441] fixed fab expand --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt | 1 + 1 file changed, 1 insertion(+) 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 3ddaee61..633ee762 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 @@ -905,6 +905,7 @@ open class ResultFragmentPhone : FullScreenPlayer(), observe(viewModel.watchStatus) { watchType -> binding?.resultBookmarkFab?.apply { + setText(watchType.stringRes) if (watchType == WatchType.NONE) { context?.colorFromAttribute(R.attr.white) } else { From 74867bed1cc96b3a494fb107a6ff9c251579b525 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Sun, 13 Aug 2023 21:07:36 +0530 Subject: [PATCH 037/441] Update SpeedoStream.kt (#552) Fixes YoMovies Provider. --- .../java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt index 90104ace..3f6fff2f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt @@ -13,7 +13,7 @@ class SpeedoStream1 : SpeedoStream() { open class SpeedoStream : ExtractorApi() { override val name = "SpeedoStream" - override val mainUrl = "https://speedostream.com" + override val mainUrl = "https://speedostream.mom" override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?): List { From 4d98690adbc8a903b3545e094d5b9ca7099e408a Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 15 Aug 2023 02:05:07 +0200 Subject: [PATCH 038/441] small fix to home load --- .../cloudstream3/plugins/PluginManager.kt | 4 + .../cloudstream3/ui/home/HomeViewModel.kt | 14 ++- .../settings/extensions/ExtensionsFragment.kt | 107 +++++++++++------- .../main/res/layout/fragment_extensions.xml | 1 + 4 files changed, 81 insertions(+), 45 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 4c32088a..87b0ba3b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -165,6 +165,9 @@ object PluginManager { var loadedLocalPlugins = false private set + + var loadedOnlinePlugins = false + private set private val gson = Gson() private suspend fun maybeLoadPlugin(context: Context, file: File) { @@ -278,6 +281,7 @@ object PluginManager { } // ioSafe { + loadedOnlinePlugins = true afterPluginsLoadedEvent.invoke(false) // } 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 b1ced59e..e8cf8863 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 @@ -103,7 +103,7 @@ class HomeViewModel : ViewModel() { private var currentShuffledList: List = listOf() private fun autoloadRepo(): APIRepository { - return APIRepository(synchronized(apis) { apis.first { it.hasMainPage }}) + return APIRepository(synchronized(apis) { apis.first { it.hasMainPage } }) } private val _availableWatchStatusTypes = @@ -185,8 +185,9 @@ class HomeViewModel : ViewModel() { } private var onGoingLoad: Job? = null - private var isCurrentlyLoadingName : String? = null + private var isCurrentlyLoadingName: String? = null private fun loadAndCancel(api: MainAPI) { + //println("loaded ${api.name}") onGoingLoad?.cancel() isCurrentlyLoadingName = api.name onGoingLoad = load(api) @@ -290,7 +291,7 @@ class HomeViewModel : ViewModel() { } } - private fun load(api: MainAPI) : Job = ioSafe { + private fun load(api: MainAPI): Job = ioSafe { repo = //if (api != null) { APIRepository(api) //} else { @@ -455,9 +456,9 @@ class HomeViewModel : ViewModel() { fromUI: Boolean = false ) = ioSafe { + //println("trying to load $preferredApiName") // Since plugins are loaded in stages this function can get called multiple times. // The issue with this is that the homepage may be fetched multiple times while the first request is loading - val api = getApiFromNameNull(preferredApiName) // api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true val currentPage = page.value @@ -467,6 +468,7 @@ class HomeViewModel : ViewModel() { return@ioSafe } + val api = getApiFromNameNull(preferredApiName) if (preferredApiName == noneApi.name) { // just set to random if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) @@ -485,10 +487,12 @@ class HomeViewModel : ViewModel() { } else if (api == null) { // API is not found aka not loaded or removed, post the loading // progress if waiting for plugins, otherwise nothing - if(PluginManager.loadedLocalPlugins || PluginManager.checkSafeModeFile() || lastError != null) { + if (PluginManager.loadedOnlinePlugins || PluginManager.checkSafeModeFile() || lastError != null) { loadAndCancel(noneApi) } else { _page.postValue(Resource.Loading()) + if (preferredApiName != null) + _apiName.postValue(preferredApiName) } } else { // if the api is found, then set it to it and save key diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 8bc947c5..553e7675 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui.settings.extensions import android.content.ClipboardManager import android.content.Context import android.content.DialogInterface +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -12,6 +13,8 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.core.view.marginBottom +import androidx.core.view.marginTop import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController @@ -33,7 +36,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import com.lagradost.cloudstream3.widget.LinearRecycleViewLayoutManager class ExtensionsFragment : Fragment() { var binding: FragmentExtensionsBinding? = null @@ -84,51 +86,76 @@ class ExtensionsFragment : Fragment() { setUpToolbar(R.string.extensions) - binding?.repoRecyclerView?.setLinearListLayout( - isHorizontal = false, - nextUp = R.id.settings_toolbar, //FOCUS_SELF, // back has no id so we cant :pensive: - nextDown = R.id.plugin_storage_appbar, - nextRight = FOCUS_SELF, - nextLeft = R.id.nav_rail_view - ) - binding?.repoRecyclerView?.adapter = RepoAdapter(false, { - findNavController().navigate( - R.id.navigation_settings_extensions_to_navigation_settings_plugins, - PluginsFragment.newInstance( - it.name, - it.url, - false - ) + binding?.repoRecyclerView?.apply { + setLinearListLayout( + isHorizontal = false, + nextUp = R.id.settings_toolbar, //FOCUS_SELF, // back has no id so we cant :pensive: + nextDown = R.id.plugin_storage_appbar, + nextRight = FOCUS_SELF, + nextLeft = R.id.nav_rail_view ) - }, { repo -> - // Prompt user before deleting repo - main { - val builder = AlertDialog.Builder(context ?: view.context) - val dialogClickListener = - DialogInterface.OnClickListener { _, which -> - when (which) { - DialogInterface.BUTTON_POSITIVE -> { - ioSafe { - RepositoryManager.removeRepository(view.context, repo) - extensionViewModel.loadStats() - extensionViewModel.loadRepositories() - } - } - DialogInterface.BUTTON_NEGATIVE -> {} - } + if (!isTrueTvSettings()) + binding?.addRepoButton?.let { button -> + button.post { + setPadding( + paddingLeft, + paddingTop, + paddingRight, + button.measuredHeight + button.marginTop + button.marginBottom + ) } + } - builder.setTitle(R.string.delete_repository) - .setMessage( - context?.getString(R.string.delete_repository_plugins) - ) - .setPositiveButton(R.string.delete, dialogClickListener) - .setNegativeButton(R.string.cancel, dialogClickListener) - .show().setDefaultFocus() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> + val dy = scrollY - oldScrollY + if (dy > 0) { //check for scroll down + binding?.addRepoButton?.shrink() // hide + } else if (dy < -5) { + binding?.addRepoButton?.extend() // show + } + } } - }) + adapter = RepoAdapter(false, { + findNavController().navigate( + R.id.navigation_settings_extensions_to_navigation_settings_plugins, + PluginsFragment.newInstance( + it.name, + it.url, + false + ) + ) + }, { repo -> + // Prompt user before deleting repo + main { + val builder = AlertDialog.Builder(context ?: view.context) + val dialogClickListener = + DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + ioSafe { + RepositoryManager.removeRepository(view.context, repo) + extensionViewModel.loadStats() + extensionViewModel.loadRepositories() + } + } + + DialogInterface.BUTTON_NEGATIVE -> {} + } + } + + builder.setTitle(R.string.delete_repository) + .setMessage( + context?.getString(R.string.delete_repository_plugins) + ) + .setPositiveButton(R.string.delete, dialogClickListener) + .setNegativeButton(R.string.cancel, dialogClickListener) + .show().setDefaultFocus() + } + }) + } observe(extensionViewModel.repositories) { binding?.repoRecyclerView?.isVisible = it.isNotEmpty() diff --git a/app/src/main/res/layout/fragment_extensions.xml b/app/src/main/res/layout/fragment_extensions.xml index b3583539..71dd372b 100644 --- a/app/src/main/res/layout/fragment_extensions.xml +++ b/app/src/main/res/layout/fragment_extensions.xml @@ -11,6 +11,7 @@ Date: Tue, 15 Aug 2023 18:37:33 +0000 Subject: [PATCH 039/441] Fix episode removal in simkl (#555) --- .../syncproviders/providers/SimklApi.kt | 75 ++++++++++++------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 64cebfc6..b4a9d789 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -60,8 +60,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { private var lastScoreTime = -1L companion object { - private const val clientId = BuildConfig.SIMKL_CLIENT_ID - private const val clientSecret = BuildConfig.SIMKL_CLIENT_SECRET + private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID + private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET private var lastLoginState = "" const val SIMKL_TOKEN_KEY: String = "simkl_token" @@ -498,6 +498,9 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { val episodes: Array?, override var isFavorite: Boolean? = null, override var maxEpisodes: Int? = null, + /** Save seen episodes separately to know the change from old to new. + * Required to remove seen episodes if count decreases */ + val oldEpisodes: Int, ) : SyncAPI.AbstractSyncStatus() override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { @@ -521,7 +524,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { score = foundItem.user_rating, watchedEpisodes = foundItem.watched_episodes_count, maxEpisodes = foundItem.total_episodes_count, - episodes = episodes + episodes = episodes, + oldEpisodes = foundItem.watched_episodes_count ?: 0, ) } else { return if (searchResult != null) { @@ -530,7 +534,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { score = 0, watchedEpisodes = 0, maxEpisodes = if (searchResult.type == "movie") 0 else null, - episodes = episodes + episodes = episodes, + oldEpisodes = 0, ) } else { null @@ -604,32 +609,46 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { SimklListStatusType.ReWatching.value ).contains(status.status) ) { - val cutEpisodes = simklStatus.episodes.take(watchedEpisodes) - - val (seasons, episodes) = if (cutEpisodes.any { it.season != null }) { - EpisodeMetadata.convertToSeasons(cutEpisodes) to null - } else { - null to EpisodeMetadata.convertToEpisodes(cutEpisodes) + suspend fun postEpisodes( + url: String, + rawEpisodes: List + ): Boolean { + val (seasons, episodes) = if (rawEpisodes.any { it.season != null }) { + EpisodeMetadata.convertToSeasons(rawEpisodes) to null + } else { + null to EpisodeMetadata.convertToEpisodes(rawEpisodes) + } + debugPrint { "Synced history using $url: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" } + return app.post( + url, + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + null, + null, + MediaObject.Ids.fromMap(parsedId), + seasons, + episodes + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful } - debugPrint { "Synced history for ${status.watchedEpisodes} given size of ${simklStatus.episodes.size}: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" } - val episodeResponse = app.post( - "$mainUrl/sync/history", - json = StatusRequest( - shows = listOf( - HistoryMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - seasons, - episodes - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ) - episodeResponse.isSuccessful + // If episodes decrease: remove all episodes beyond watched episodes. + val removeResponse = if (simklStatus.oldEpisodes > watchedEpisodes) { + val removeEpisodes = simklStatus.episodes + .drop(watchedEpisodes) + postEpisodes("$mainUrl/sync/history/remove", removeEpisodes) + } else { + true + } + val cutEpisodes = simklStatus.episodes.take(watchedEpisodes) + val addResponse = postEpisodes("$mainUrl/sync/history/", cutEpisodes) + + removeResponse && addResponse } else true val newStatus = From d536dffaf559425bdb7b02232c3bfac8a951133d Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Wed, 16 Aug 2023 19:45:39 +0530 Subject: [PATCH 040/441] Fix Trailers not Working (#559) * Fix Trailers not Working * smol tip --- app/build.gradle.kts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3c12652a..b228fea0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -165,7 +165,7 @@ dependencies { androidTestImplementation("androidx.test:core") //implementation("io.karn:khttp-android:0.1.2") //okhttp instead -// implementation("org.jsoup:jsoup:1.13.1") + // implementation("org.jsoup:jsoup:1.13.1") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") implementation("androidx.preference:preference-ktx:1.2.0") @@ -220,8 +220,8 @@ dependencies { implementation("androidx.work:work-runtime-ktx:2.8.1") // Networking -// implementation("com.squareup.okhttp3:okhttp:4.9.2") -// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") + // implementation("com.squareup.okhttp3:okhttp:4.9.2") + // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") implementation("com.github.Blatzar:NiceHttp:0.4.3") // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") @@ -243,11 +243,9 @@ dependencies { // used for subtitle decoding https://github.com/albfernandez/juniversalchardet implementation("com.github.albfernandez:juniversalchardet:2.4.0") - // slow af yt - //implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT") - // newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204 - implementation("com.github.TeamNewPipe:NewPipeExtractor:8495ad619e") + // this should be updated frequently to avoid trailer fu*kery + implementation("com.github.TeamNewPipe:NewPipeExtractor:1f08d28") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") // Library/extensions searching with Levenshtein distance From d247640dcf2a3985390cb0e3d5fd4c8d82e7d4ad Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Wed, 16 Aug 2023 19:48:15 +0530 Subject: [PATCH 041/441] Play n Dowload button fix for NS*W results. (#557) * Play n Dowload button fix for NS*W results. * Revert MainAPI Changes * Tweaked ResultViewModel --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 011d133d..2fe3b012 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 @@ -1707,7 +1707,7 @@ class ResultViewModel2 : ViewModel() { else -> { if (response.type.isLiveStream()) R.string.play_livestream_button - else if (response.type.isMovieType()) // this wont break compatibility as you only need to override isMovieType + else if (response.isMovie()) // this wont break compatibility as you only need to override isMovieType R.string.play_movie_button else null } @@ -2340,4 +2340,4 @@ class ResultViewModel2 : ViewModel() { } } } -} \ No newline at end of file +} From 20da3807a2cb98b60b22e4ad65a96c037c254c58 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 17 Aug 2023 01:00:43 +0200 Subject: [PATCH 042/441] fixed search query for intent --- .../com/lagradost/cloudstream3/MainActivity.kt | 12 ++++++++---- .../cloudstream3/ui/search/SearchFragment.kt | 15 +++++++++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a8160d33..15b16078 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -286,7 +286,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { * * This is a very bad solution but I was unable to find a better one. **/ - private var nextSearchQuery: String? = null + var nextSearchQuery: String? = null /** * Fires every time a new batch of plugins have been loaded, no guarantee about how often this is run and on which thread @@ -362,9 +362,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { loadRepository(url) return true } else if (safeURI(str)?.scheme == appStringSearch) { + val query = str.substringAfter("$appStringSearch://") nextSearchQuery = - URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8") - + try { + URLDecoder.decode(query, "UTF-8") + } catch (t : Throwable) { + logError(t) + query + } // Use both navigation views to support both layouts. // It might be better to use the QuickSearch. activity?.findViewById(R.id.nav_view)?.selectedItemId = @@ -1315,7 +1320,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) { bundle?.apply { this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery) - nextSearchQuery = null } } } 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 63213eb9..bdf82377 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 @@ -84,7 +84,7 @@ class SearchFragment : Fragment() { fun newInstance(query: String): Bundle { return Bundle().apply { - putString(SEARCH_QUERY, query) + if(query.isNotBlank()) putString(SEARCH_QUERY, query) } } } @@ -211,7 +211,7 @@ class SearchFragment : Fragment() { reloadRepos() binding?.apply { - val adapter: RecyclerView.Adapter? = + val adapter: RecyclerView.Adapter = SearchAdapter( ArrayList(), searchAutofitResults, @@ -530,11 +530,18 @@ class SearchFragment : Fragment() { searchMasterRecycler.layoutManager = GridLayoutManager(context, 1) // Automatically search the specified query, this allows the app search to launch from intent - arguments?.getString(SEARCH_QUERY)?.let { query -> + var sq = arguments?.getString(SEARCH_QUERY) ?: savedInstanceState?.getString(SEARCH_QUERY) + if(sq.isNullOrBlank()) { + sq = MainActivity.nextSearchQuery + } + + sq?.let { query -> if (query.isBlank()) return@let mainSearch.setQuery(query, true) // Clear the query as to not make it request the same query every time the page is opened - arguments?.putString(SEARCH_QUERY, null) + arguments?.remove(SEARCH_QUERY) + savedInstanceState?.remove(SEARCH_QUERY) + MainActivity.nextSearchQuery = null } } From c2b951a07866077e05d15e385111d2d74fe7e542 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 17 Aug 2023 01:19:24 +0200 Subject: [PATCH 043/441] fixed #560 lock locks orientation --- .../ui/player/FullScreenPlayer.kt | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 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 9739b627..0f3c189d 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 @@ -2,9 +2,11 @@ package com.lagradost.cloudstream3.ui.player import android.animation.ObjectAnimator import android.annotation.SuppressLint +import android.app.Activity import android.content.Context import android.content.pm.ActivityInfo import android.content.res.ColorStateList +import android.content.res.Configuration import android.content.res.Resources import android.graphics.Color import android.media.AudioManager @@ -16,6 +18,7 @@ import android.util.DisplayMetrics import android.view.KeyEvent import android.view.LayoutInflater import android.view.MotionEvent +import android.view.Surface import android.view.View import android.view.ViewGroup import android.view.WindowManager @@ -56,6 +59,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.Vector2 import kotlin.math.* + const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking const val MINIMUM_VERTICAL_SWIPE = 2.0f // in percentage const val MINIMUM_HORIZONTAL_SWIPE = 2.0f // in percentage @@ -292,6 +296,36 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.getCurrentPreferredSubtitle() == null } + open fun lockOrientation(activity: Activity) { + val display = + (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + val rotation = display.rotation + val currentOrientation = activity.resources.configuration.orientation + var orientation = 0 + when (currentOrientation) { + Configuration.ORIENTATION_LANDSCAPE -> orientation = + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE + + Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED, Configuration.ORIENTATION_PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + //Configuration.ORIENTATION_PORTRAIT -> orientation = + // if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + } + activity.requestedOrientation = orientation + } + + private fun updateOrientation() { + activity?.apply { + if(lockRotation) { + if(isLocked) { + lockOrientation(this) + } + else { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + } + } + } + } + protected fun enterFullscreen() { if (isFullScreenPlayer) { activity?.hideSystemUI() @@ -301,8 +335,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { activity?.window?.attributes = params } } - if (lockRotation) - activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + updateOrientation() } protected fun exitFullscreen() { @@ -561,6 +594,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } isLocked = !isLocked + updateOrientation() + if (isLocked && isShowing) { playerBinding?.playerHolder?.postDelayed({ if (isLocked && isShowing) { From 590c74111cb945366ebd6a11278f2f9f827043c5 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 17 Aug 2023 23:10:21 +0200 Subject: [PATCH 044/441] fuck it we ball, m3u8 download is now fixed --- .../lagradost/cloudstream3/extractors/Cda.kt | 10 +- .../cloudstream3/utils/M3u8Helper.kt | 282 +++++++++--------- .../utils/VideoDownloadManager.kt | 221 ++++++-------- 3 files changed, 237 insertions(+), 276 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt index 6a2f399d..42f6eddb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt @@ -1,13 +1,11 @@ package com.lagradost.cloudstream3.extractors -import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.USER_AGENT +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.Companion.generateM3u8 -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson -import android.util.Log +import com.lagradost.cloudstream3.utils.Qualities import java.net.URLDecoder open class Cda: ExtractorApi() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 6c5117b4..6770e303 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -1,17 +1,16 @@ package com.lagradost.cloudstream3.utils +import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.logError -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.delay import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec import kotlin.math.pow - +/** backwards api surface */ class M3u8Helper { companion object { - private val generator = M3u8Helper() suspend fun generateM3u8( source: String, streamUrl: String, @@ -20,34 +19,59 @@ class M3u8Helper { headers: Map = mapOf(), name: String = source ): List { - return generator.m3u8Generation( - M3u8Stream( - streamUrl = streamUrl, - quality = quality, - headers = headers, - ), null - ) - .map { stream -> - ExtractorLink( - source, - name = name, - stream.streamUrl, - referer, - stream.quality ?: Qualities.Unknown.value, - true, - stream.headers, - ) - } + return M3u8Helper2.generateM3u8(source, streamUrl, referer, quality, headers, name) } } + + data class M3u8Stream( + val streamUrl: String, + val quality: Int? = null, + val headers: Map = mapOf() + ) + + suspend fun m3u8Generation(m3u8: M3u8Stream, returnThis: Boolean? = true): List { + return M3u8Helper2.m3u8Generation(m3u8, returnThis) + } +} + +object M3u8Helper2 { + suspend fun generateM3u8( + source: String, + streamUrl: String, + referer: String, + quality: Int? = null, + headers: Map = mapOf(), + name: String = source + ): List { + return m3u8Generation( + M3u8Helper.M3u8Stream( + streamUrl = streamUrl, + quality = quality, + headers = headers, + ), null + ) + .map { stream -> + ExtractorLink( + source, + name = name, + stream.streamUrl, + referer, + stream.quality ?: Qualities.Unknown.value, + true, + stream.headers, + ) + } + } + private val ENCRYPTION_DETECTION_REGEX = Regex("#EXT-X-KEY:METHOD=([^,]+),") private val ENCRYPTION_URL_IV_REGEX = Regex("#EXT-X-KEY:METHOD=([^,]+),URI=\"([^\"]+)\"(?:,IV=(.*))?") private val QUALITY_REGEX = Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""") private val TS_EXTENSION_REGEX = - Regex("""(.*\.ts.*|.*\.jpg.*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts + Regex("""#EXTINF:.*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways + //Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts private fun absoluteExtensionDetermination(url: String): String? { val split = url.split("/") @@ -73,7 +97,7 @@ class M3u8Helper { } }.iterator() - private fun getDecrypter( + fun getDecrypter( secretKey: ByteArray, data: ByteArray, iv: ByteArray = "".toByteArray() @@ -91,13 +115,8 @@ class M3u8Helper { return st != null && (st.value.isNotEmpty() || st.destructured.component1() != "NONE") } - data class M3u8Stream( - val streamUrl: String, - val quality: Int? = null, - val headers: Map = mapOf() - ) - private fun selectBest(qualities: List): M3u8Stream? { + private fun selectBest(qualities: List): M3u8Helper.M3u8Stream? { val result = qualities.sortedBy { if (it.quality != null && it.quality <= 1080) it.quality else 0 }.filter { @@ -113,19 +132,16 @@ class M3u8Helper { } private fun isNotCompleteUrl(url: String): Boolean { - return !url.contains("https://") && !url.contains("http://") + return !url.startsWith("https://") && !url.startsWith("http://") } - suspend fun m3u8Generation(m3u8: M3u8Stream, returnThis: Boolean? = true): List { -// return listOf(m3u8) - val list = mutableListOf() + suspend fun m3u8Generation(m3u8: M3u8Helper.M3u8Stream, returnThis: Boolean? = true): List { + val list = mutableListOf() val m3u8Parent = getParentLink(m3u8.streamUrl) val response = app.get(m3u8.streamUrl, headers = m3u8.headers, verify = false).text -// var hasAnyContent = false for (match in QUALITY_REGEX.findAll(response)) { -// hasAnyContent = true var (quality, m3u8Link, m3u8Link2) = match.destructured if (m3u8Link.isEmpty()) m3u8Link = m3u8Link2 if (absoluteExtensionDetermination(m3u8Link) == "m3u8") { @@ -136,21 +152,21 @@ class M3u8Helper { println(m3u8.streamUrl) } list += m3u8Generation( - M3u8Stream( + M3u8Helper.M3u8Stream( m3u8Link, quality.toIntOrNull(), m3u8.headers ), false ) } - list += M3u8Stream( + list += M3u8Helper.M3u8Stream( m3u8Link, quality.toIntOrNull(), m3u8.headers ) } if (returnThis != false) { - list += M3u8Stream( + list += M3u8Helper.M3u8Stream( m3u8.streamUrl, Qualities.Unknown.value, m3u8.headers @@ -160,113 +176,111 @@ class M3u8Helper { return list } + data class LazyHlsDownloadData( + private val encryptionData: ByteArray, + private val encryptionIv: ByteArray, + private val isEncrypted: Boolean, + private val allTsLinks: List, + private val relativeUrl: String, + private val headers: Map, + ) { + val size get() = allTsLinks.size - data class HlsDownloadData( - val bytes: ByteArray, - val currentIndex: Int, - val totalTs: Int, - val errored: Boolean = false - ) - - suspend fun hlsYield( - qualities: List, - startIndex: Int = 0 - ): Iterator { - if (qualities.isEmpty()) return listOf( - HlsDownloadData( - byteArrayOf(), - 1, - 1, - true - ) - ).iterator() - - var selected = selectBest(qualities) - if (selected == null) { - selected = qualities[0] + suspend fun resolveLinkSafe( + index: Int, + tries: Int = 3, + failDelay: Long = 3000 + ): ByteArray? { + for (i in 0 until tries) { + try { + return resolveLink(index) + } catch (e: IllegalArgumentException) { + return null + } catch (t: Throwable) { + delay(failDelay) + } + } + return null } + + @Throws + suspend fun resolveLink(index: Int): ByteArray { + if (index < 0 || index >= size) throw IllegalArgumentException("index must be in the bounds of the ts") + val url = allTsLinks[index] + + val tsResponse = app.get(url, headers = headers, verify = false) + val tsData = tsResponse.body.bytes() + if (tsData.isEmpty()) throw ErrorLoadingException("no data") + + return if (isEncrypted) { + getDecrypter(encryptionData, tsData, encryptionIv) + } else { + tsData + } + } + } + + @Throws + suspend fun hslLazy( + qualities: List + ): LazyHlsDownloadData { + if (qualities.isEmpty()) throw IllegalArgumentException("qualities must be non empty") + val selected = selectBest(qualities) ?: qualities.first() val headers = selected.headers - val streams = qualities.map { m3u8Generation(it, false) }.flatten() - //val sslVerification = if (headers.containsKey("ssl_verification")) headers["ssl_verification"].toBoolean() else true - + // this selects the best quality of the qualities offered, + // due to the recursive nature of m3u8, we only go 2 depth val secondSelection = selectBest(streams.ifEmpty { listOf(selected) }) - if (secondSelection != null) { - val m3u8Response = - runBlocking { - app.get( - secondSelection.streamUrl, - headers = headers, - verify = false - ).text - } + ?: throw IllegalArgumentException("qualities has no streams") - var encryptionUri: String? - var encryptionIv = byteArrayOf() - var encryptionData = byteArrayOf() + val m3u8Response = + app.get( + secondSelection.streamUrl, + headers = headers, + verify = false + ).text - val encryptionState = isEncrypted(m3u8Response) + println("m3u8Response=$m3u8Response") - if (encryptionState) { - val match = - ENCRYPTION_URL_IV_REGEX.find(m3u8Response)!!.destructured // its safe to assume that its not going to be null - encryptionUri = match.component2() + // encryption, this is because crunchy uses it + var encryptionIv = byteArrayOf() + var encryptionData = byteArrayOf() - if (isNotCompleteUrl(encryptionUri)) { - encryptionUri = "${getParentLink(secondSelection.streamUrl)}/$encryptionUri" - } + val encryptionState = isEncrypted(m3u8Response) - encryptionIv = match.component3().toByteArray() - val encryptionKeyResponse = - runBlocking { app.get(encryptionUri, headers = headers, verify = false) } - encryptionData = encryptionKeyResponse.body?.bytes() ?: byteArrayOf() + if (encryptionState) { + // its safe to assume that its not going to be null + val match = + ENCRYPTION_URL_IV_REGEX.find(m3u8Response)!!.groupValues + + var encryptionUri = match[1] + + if (isNotCompleteUrl(encryptionUri)) { + encryptionUri = "${getParentLink(secondSelection.streamUrl)}/$encryptionUri" } - val allTs = TS_EXTENSION_REGEX.findAll(m3u8Response) - val allTsList = allTs.toList() - val totalTs = allTsList.size - if (totalTs == 0) { - return listOf(HlsDownloadData(byteArrayOf(), 1, 1, true)).iterator() - } - var lastYield = 0 - - val relativeUrl = getParentLink(secondSelection.streamUrl) - var retries = 0 - val tsByteGen = sequence { - loop@ for ((index, ts) in allTs.withIndex()) { - val url = if ( - isNotCompleteUrl(ts.destructured.component1()) - ) "$relativeUrl/${ts.destructured.component1()}" else ts.destructured.component1() - val c = index + 1 + startIndex - - while (lastYield != c) { - try { - val tsResponse = - runBlocking { app.get(url, headers = headers, verify = false) } - var tsData = tsResponse.body?.bytes() ?: byteArrayOf() - - if (encryptionState) { - tsData = getDecrypter(encryptionData, tsData, encryptionIv) - yield(HlsDownloadData(tsData, c, totalTs)) - lastYield = c - break - } - yield(HlsDownloadData(tsData, c, totalTs)) - lastYield = c - } catch (e: Exception) { - logError(e) - if (retries == 3) { - yield(HlsDownloadData(byteArrayOf(), c, totalTs, true)) - break@loop - } - ++retries - Thread.sleep(2_000) - } - } - } - } - return tsByteGen.iterator() + encryptionIv = match[2].toByteArray() + val encryptionKeyResponse = app.get(encryptionUri, headers = headers, verify = false) + encryptionData = encryptionKeyResponse.body.bytes() } - return listOf(HlsDownloadData(byteArrayOf(), 1, 1, true)).iterator() + val relativeUrl = getParentLink(secondSelection.streamUrl) + val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts -> + val value = ts.groupValues[1] + if (isNotCompleteUrl(value)) { + "$relativeUrl/${value}" + } else { + value + } + }.toList() + if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty") + + return LazyHlsDownloadData( + encryptionData = encryptionData, + encryptionIv = encryptionIv, + isEncrypted = encryptionState, + allTsLinks = allTsList, + relativeUrl = relativeUrl, + headers = headers + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index c138ea75..f4eb37b7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -15,7 +15,6 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData import androidx.preference.PreferenceManager import androidx.work.Data @@ -32,18 +31,15 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly import java.io.BufferedInputStream @@ -51,11 +47,9 @@ import java.io.File import java.io.InputStream import java.io.OutputStream import java.lang.Thread.sleep -import java.net.URI import java.net.URL import java.net.URLConnection import java.util.* -import kotlin.math.roundToInt const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" const val DOWNLOAD_CHANNEL_NAME = "Downloads" @@ -92,7 +86,7 @@ object VideoDownloadManager { @DrawableRes const val pressToStopIcon = R.drawable.exo_icon_stop - private var updateCount : Int = 0 + private var updateCount: Int = 0 private val downloadDataUpdateCount = MutableLiveData() enum class DownloadType { @@ -687,7 +681,8 @@ object VideoDownloadManager { return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream) } - fun downloadThing( + @Throws + suspend fun downloadThing( context: Context, link: IDownloadableMinimum, name: String, @@ -696,9 +691,9 @@ object VideoDownloadManager { tryResume: Boolean, parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, - ): Int { + ): Int = withContext(Dispatchers.IO) { if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { - return ERROR_UNKNOWN + return@withContext ERROR_UNKNOWN } val basePath = context.getBasePath() @@ -714,7 +709,7 @@ object VideoDownloadManager { } val stream = setupStream(context, name, relativePath, extension, tryResume) - if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode val resume = stream.resume!! val fileStream = stream.fileStream!! @@ -766,7 +761,7 @@ object VideoDownloadManager { } val bytesTotal = contentLength + resumeLength - if (extension == "mp4" && bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG + if (extension == "mp4" && bytesTotal < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG parentId?.let { setKey( @@ -845,11 +840,13 @@ object VideoDownloadManager { DownloadActionType.Pause -> { isPaused = true; updateNotification() } + DownloadActionType.Stop -> { isStopped = true; updateNotification() removeKey(KEY_RESUME_PACKAGES, event.first.toString()) saveQueue() } + DownloadActionType.Resume -> { isPaused = false; updateNotification() } @@ -917,15 +914,17 @@ object VideoDownloadManager { } // RETURN MESSAGE - return when { + return@withContext when { isFailed -> { parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } ERROR_CONNECTION_ERROR } + isStopped -> { parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } deleteFile() } + else -> { parentId?.let { id -> downloadProgressEvent.invoke( @@ -989,6 +988,7 @@ object VideoDownloadManager { found.delete() this.createDirectory(directoryName) } + this.isDirectory -> this.createDirectory(directoryName) else -> this.parentFile?.createDirectory(directoryName) } @@ -1107,7 +1107,8 @@ object VideoDownloadManager { return SUCCESS_STOPPED } - private fun downloadHLS( + @Throws + private suspend fun downloadHLS( context: Context, link: ExtractorLink, name: String, @@ -1115,16 +1116,8 @@ object VideoDownloadManager { parentId: Int?, startIndex: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit - ): Int { + ): Int = withContext(Dispatchers.IO) { val extension = "mp4" - fun logcatPrint(vararg items: Any?) { - items.forEach { - println("[HLS]: $it") - } - } - - val m3u8Helper = M3u8Helper() - logcatPrint("initialised the HLS downloader.") val m3u8 = M3u8Helper.M3u8Stream( link.url, link.quality, mapOf("referer" to link.referer) @@ -1139,54 +1132,40 @@ object VideoDownloadManager { ) else folder val stream = setupStream(context, name, relativePath, extension, realIndex > 0) - if (stream.errorCode != SUCCESS_STREAM) return stream.errorCode + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - if (!stream.resume!!) realIndex = 0 - val fileLengthAdd = stream.fileLength!! - val tsIterator = runBlocking { - m3u8Helper.hlsYield(listOf(m3u8), realIndex) - } + if (stream.resume != true) realIndex = 0 + val fileLengthAdd = stream.fileLength ?: 0 + val items = M3u8Helper2.hslLazy(listOf(m3u8)) val displayName = getDisplayName(name, extension) val fileStream = stream.fileStream!! - val firstTs = tsIterator.next() - var isDone = false var isFailed = false var isPaused = false - var bytesDownloaded = firstTs.bytes.size.toLong() + fileLengthAdd - var tsProgress = 1L + realIndex - val totalTs = firstTs.totalTs.toLong() + var bytesDownloaded = fileLengthAdd + var tsProgress: Long = realIndex.toLong() + 1 // we don't want div by zero + val totalTs: Long = items.size.toLong() fun deleteFile(): Int { return delete(context, name, relativePath, extension, parentId, basePath.first) } - /* - Most of the auto generated m3u8 out there have TS of the same size. - And only the last TS might have a different size. - - But oh well, in cases of handmade m3u8 streams this will go all over the place ¯\_(ツ)_/¯ - So ya, this calculates an estimate of how many bytes the file is going to be. - - > (bytesDownloaded/tsProgress)*totalTs - */ fun updateInfo() { - parentId?.let { - setKey( - KEY_DOWNLOAD_INFO, - it.toString(), - DownloadedFileInfo( - (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - relativePath ?: "", - displayName, - tsProgress.toString(), - basePath = basePath.second - ) + setKey( + KEY_DOWNLOAD_INFO, + (parentId ?: return).toString(), + DownloadedFileInfo( + // approx bytes + totalBytes = (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), + relativePath = relativePath ?: "", + displayName = displayName, + extraInfo = tsProgress.toString(), + basePath = basePath.second ) - } + ) } updateInfo() @@ -1210,9 +1189,7 @@ object VideoDownloadManager { (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), ) ) - } catch (e: Exception) { - // IDK MIGHT ERROR - } + } catch (_: Throwable) {} } createNotificationCallback.invoke( @@ -1226,24 +1203,6 @@ object VideoDownloadManager { ) } - fun stopIfError(ts: M3u8Helper.HlsDownloadData): Int? { - if (ts.errored || ts.bytes.isEmpty()) { - val error: Int = if (!ts.errored) { - logcatPrint("Error: No stream was found.") - ERROR_UNKNOWN - } else { - logcatPrint("Error: Failed to fetch data.") - ERROR_CONNECTION_ERROR - } - isFailed = true - fileStream.close() - deleteFile() - updateNotification() - return error - } - return null - } - val notificationCoroutine = main { while (true) { if (!isDone) { @@ -1261,11 +1220,11 @@ object VideoDownloadManager { DownloadActionType.Stop -> { isFailed = true } + DownloadActionType.Pause -> { - isPaused = - true // Pausing is not supported since well...I need to know the index of the ts it was paused at - // it may be possible to store it in a variable, but when the app restarts it will be lost + isPaused = true } + DownloadActionType.Resume -> { isPaused = false } @@ -1278,32 +1237,22 @@ object VideoDownloadManager { try { if (parentId != null) downloadEvent -= downloadEventListener - } catch (e: Exception) { - logError(e) + } catch (t: Throwable) { + logError(t) } try { parentId?.let { downloadStatus.remove(it) } - } catch (e: Exception) { - logError(e) - // IDK MIGHT ERROR + } catch (t: Throwable) { + logError(t) } notificationCoroutine.cancel() } - stopIfError(firstTs).let { - if (it != null) { - closeAll() - return it - } - } - if (parentId != null) downloadEvent += downloadEventListener - fileStream.write(firstTs.bytes) - fun onFailed() { fileStream.close() deleteFile() @@ -1311,31 +1260,29 @@ object VideoDownloadManager { closeAll() } - for (ts in tsIterator) { + for (idx in realIndex until items.size) { while (isPaused) { if (isFailed) { onFailed() - return SUCCESS_STOPPED + return@withContext SUCCESS_STOPPED } - sleep(100) + delay(100) } if (isFailed) { onFailed() - return SUCCESS_STOPPED + return@withContext SUCCESS_STOPPED } - stopIfError(ts).let { - if (it != null) { - closeAll() - return it - } + val bytes = items.resolveLinkSafe(idx) ?: run { + isFailed = true + onFailed() + return@withContext ERROR_CONNECTION_ERROR } - fileStream.write(ts.bytes) - tsProgress = ts.currentIndex.toLong() - bytesDownloaded += ts.bytes.size.toLong() - logcatPrint("Download progress ${((tsProgress.toFloat() / totalTs.toFloat()) * 100).roundToInt()}%") + fileStream.write(bytes) + tsProgress = idx.toLong() + 1 + bytesDownloaded += bytes.size.toLong() updateInfo() } isDone = true @@ -1344,7 +1291,7 @@ object VideoDownloadManager { closeAll() updateInfo() - return SUCCESS_DOWNLOAD_DONE + return@withContext SUCCESS_DOWNLOAD_DONE } fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { @@ -1379,7 +1326,7 @@ object VideoDownloadManager { ) } - private fun downloadSingleEpisode( + private suspend fun downloadSingleEpisode( context: Context, source: String?, folder: String?, @@ -1405,25 +1352,29 @@ object VideoDownloadManager { null )?.extraInfo?.toIntOrNull() } else null - return downloadHLS(context, link, name, folder, ep.id, startIndex) { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback, - meta.hlsProgress, - meta.hlsTotal - ) + return suspendSafeApiCall { + downloadHLS(context, link, name, folder, ep.id, startIndex) { meta -> + main { + createNotification( + context, + source, + link.name, + ep, + meta.type, + meta.bytesDownloaded, + meta.bytesTotal, + notificationCallback, + meta.hlsProgress, + meta.hlsTotal + ) + } } - }.also { extractorJob.cancel() } + }.also { + extractorJob.cancel() + } ?: ERROR_UNKNOWN } - return normalSafeApiCall { + return suspendSafeApiCall { downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta -> main { createNotification( @@ -1468,17 +1419,15 @@ object VideoDownloadManager { DownloadResumePackage(item, index) ) val connectionResult = withContext(Dispatchers.IO) { - normalSafeApiCall { - downloadSingleEpisode( - context, - item.source, - item.folder, - item.ep, - link, - notificationCallback, - resume - ).also { println("Single episode finished with return code: $it") } - } + downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + resume + ).also { println("Single episode finished with return code: $it") } } if (connectionResult != null && connectionResult > 0) { // SUCCESS removeKey(KEY_RESUME_PACKAGES, id.toString()) From 61d63b17d819d7899e04314293d8f01b651ef22a Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Fri, 18 Aug 2023 02:41:59 +0530 Subject: [PATCH 045/441] chore: acra improvements and media3 bump (#562) * Acra Bump * Media3 bump --- app/build.gradle.kts | 20 +++++++++---------- .../lagradost/cloudstream3/AcraApplication.kt | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b228fea0..3b215dbc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -181,22 +181,22 @@ dependencies { // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09") // Media 3 - implementation("androidx.media3:media3-common:1.1.0") - implementation("androidx.media3:media3-exoplayer:1.1.0") - implementation("androidx.media3:media3-datasource-okhttp:1.1.0") - implementation("androidx.media3:media3-ui:1.1.0") - implementation("androidx.media3:media3-session:1.1.0") - implementation("androidx.media3:media3-cast:1.1.0") - implementation("androidx.media3:media3-exoplayer-hls:1.1.0") - implementation("androidx.media3:media3-exoplayer-dash:1.1.0") + implementation("androidx.media3:media3-common:1.1.1") + implementation("androidx.media3:media3-exoplayer:1.1.1") + implementation("androidx.media3:media3-datasource-okhttp:1.1.1") + implementation("androidx.media3:media3-ui:1.1.1") + implementation("androidx.media3:media3-session:1.1.1") + implementation("androidx.media3:media3-cast:1.1.1") + implementation("androidx.media3:media3-exoplayer-hls:1.1.1") + implementation("androidx.media3:media3-exoplayer-dash:1.1.1") // Custom ffmpeg extension for audio codecs implementation("com.github.recloudstream:media-ffmpeg:1.1.0") //implementation("com.google.android.exoplayer:extension-leanback:2.14.0") // Bug reports - implementation("ch.acra:acra-core:5.8.4") - implementation("ch.acra:acra-toast:5.8.4") + implementation("ch.acra:acra-core:5.11.0") + implementation("ch.acra:acra-toast:5.11.0") compileOnly("com.google.auto.service:auto-service-annotations:1.0") //either for java sources: diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 069287b0..61d467c4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -121,10 +121,10 @@ class AcraApplication : Application() { buildConfigClass = BuildConfig::class.java reportFormat = StringFormat.JSON - reportContent = arrayOf( + reportContent = listOf( ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE, ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL, - ReportField.STACK_TRACE + ReportField.STACK_TRACE, ) // removed this due to bug when starting the app, moved it to when it actually crashes @@ -213,4 +213,4 @@ class AcraApplication : Application() { } } -} \ No newline at end of file +} From 8f6e8a8e99c349489f05294e2d46a9ce58afc1ae Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 18 Aug 2023 01:46:29 +0200 Subject: [PATCH 046/441] fixed #547 fuck inheritance --- app/build.gradle.kts | 2 +- .../ui/result/ResultFragmentPhone.kt | 53 ++++++++++++------- .../ui/result/ResultTrailerPlayer.kt | 3 -- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3b215dbc..f72ec0b0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -231,7 +231,7 @@ dependencies { // API because cba maintaining it myself implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") - implementation("com.github.discord:OverlappingPanels:0.1.3") + implementation("com.github.discord:OverlappingPanels:0.1.5") // debugImplementation because LeakCanary should only run in debug builds. //debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") 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 633ee762..ae0b7419 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 @@ -23,7 +23,6 @@ import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.RecyclerView import com.discord.panels.OverlappingPanelsLayout import com.discord.panels.PanelsChildGestureRegionObserver import com.google.android.gms.cast.framework.CastButtonFactory @@ -62,8 +61,6 @@ import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable -import com.lagradost.cloudstream3.utils.AppUtils.isLtr -import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.openBrowser import com.lagradost.cloudstream3.utils.ExtractorLink @@ -81,8 +78,13 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.VideoDownloadHelper -open class ResultFragmentPhone : FullScreenPlayer(), - PanelsChildGestureRegionObserver.GestureRegionsListener { +open class ResultFragmentPhone : FullScreenPlayer() { + private val gestureRegionsListener = object : PanelsChildGestureRegionObserver.GestureRegionsListener { + override fun onGestureRegionsUpdate(gestureRegions: List) { + binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + } + } + protected lateinit var viewModel: ResultViewModel2 protected lateinit var syncModel: SyncViewModel @@ -210,15 +212,20 @@ open class ResultFragmentPhone : FullScreenPlayer(), loadTrailer() } + override fun onDestroy() { + super.onDestroy() + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { + unregister(it) + } + removeGestureRegionsUpdateListener(gestureRegionsListener) + } + } + override fun onDestroyView() { //somehow this still leaks and I dont know why???? // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt - PanelsChildGestureRegionObserver.Provider.get().let { obs -> - resultBinding?.resultCastItems?.let { - obs.unregister(it) - } - obs.removeGestureRegionsUpdateListener(this) - } + updateUIEvent -= ::updateUI binding = null resultBinding = null @@ -287,6 +294,8 @@ open class ResultFragmentPhone : FullScreenPlayer(), it.colorFromAttribute(R.attr.primaryBlackBackground) } super.onResume() + PanelsChildGestureRegionObserver.Provider.get() + .addGestureRegionsUpdateListener(gestureRegionsListener) } override fun onStop() { @@ -323,7 +332,16 @@ open class ResultFragmentPhone : FullScreenPlayer(), setUrl(storedData.url) syncModel.addFromUrl(storedData.url) val api = APIHolder.getApiFromNameNull(storedData.apiName) - PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(this) + + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { + register(it) + } + addGestureRegionsUpdateListener(gestureRegionsListener) + } + + + // ===== ===== ===== resultBinding?.apply { @@ -374,9 +392,8 @@ open class ResultFragmentPhone : FullScreenPlayer(), DownloadButtonSetup.handleDownloadClick(downloadClickEvent) } ) - resultCastItems.let { - PanelsChildGestureRegionObserver.Provider.get().register(it) - } + + resultScroll.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> val dy = scrollY - oldScrollY if (dy > 0) { //check for scroll down @@ -1055,11 +1072,7 @@ open class ResultFragmentPhone : FullScreenPlayer(), override fun onPause() { super.onPause() - PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(this) - } - - override fun onGestureRegionsUpdate(gestureRegions: List) { - binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(gestureRegionsListener) } private fun setRecommendations(rec: List?, validApiName: String?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 1f663e31..eb8cb9b3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -118,9 +118,6 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { override fun onTracksInfoChanged() {} override fun exitedPipMode() {} - - override fun onGestureRegionsUpdate(gestureRegions: List) {} - private fun updateFullscreen(fullscreen: Boolean) { isFullScreenPlayer = fullscreen lockRotation = fullscreen From e95dc1db2a94a4f3f5583bc626e331129be77a38 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Fri, 18 Aug 2023 21:16:03 +0530 Subject: [PATCH 047/441] fix: cast items recycler (finally) (#564) * turn cast items visible(tools) * prevent cast gesture listener from permanent RIP in one lifecycle --- .../ui/result/ResultFragmentPhone.kt | 20 +++++++++---------- app/src/main/res/layout/fragment_result.xml | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) 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 ae0b7419..a932a57c 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 @@ -212,19 +212,17 @@ open class ResultFragmentPhone : FullScreenPlayer() { loadTrailer() } - override fun onDestroy() { - super.onDestroy() - PanelsChildGestureRegionObserver.Provider.get().apply { - resultBinding?.resultCastItems?.let { - unregister(it) - } - removeGestureRegionsUpdateListener(gestureRegionsListener) - } - } - override fun onDestroyView() { + //somehow this still leaks and I dont know why???? // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt + PanelsChildGestureRegionObserver.Provider.get().let { obs -> + resultBinding?.resultCastItems?.let { + obs.unregister(it) + } + + obs.removeGestureRegionsUpdateListener(gestureRegionsListener) + } updateUIEvent -= ::updateUI binding = null @@ -1127,4 +1125,4 @@ open class ResultFragmentPhone : FullScreenPlayer() { } } } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index ee3477b0..87de7186 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -476,7 +476,7 @@ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:itemCount="2" tools:listitem="@layout/cast_item" - tools:visibility="gone" /> + tools:visibility="visible" /> --> - \ No newline at end of file + From 56cb3d718188bf95e16cc062280bc5a16e42ea24 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 00:48:00 +0200 Subject: [PATCH 048/441] refactored download system for better preference + bugfixes --- .../cloudstream3/DownloaderTestImpl.kt | 2 +- .../com/lagradost/cloudstream3/MainAPI.kt | 2 +- .../ui/download/DownloadChildAdapter.kt | 2 +- .../ui/download/EasyDownloadButton.kt | 264 ----- .../ui/download/button/BaseFetchButton.kt | 61 +- .../ui/download/button/DownloadButton.kt | 17 +- .../ui/download/button/PieFetchButton.kt | 53 +- .../cloudstream3/utils/M3u8Helper.kt | 23 +- .../cloudstream3/utils/VideoDownloadHelper.kt | 6 +- .../utils/VideoDownloadManager.kt | 1016 ++++++++--------- .../main/res/drawable/baseline_stop_24.xml | 10 + 11 files changed, 604 insertions(+), 852 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt create mode 100644 app/src/main/res/drawable/baseline_stop_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt index 379a91e4..0a2db2bd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt @@ -50,7 +50,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do companion object { private const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" private var instance: DownloaderTestImpl? = null /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 7790f047..80332445 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -29,7 +29,7 @@ import java.util.* import kotlin.math.absoluteValue const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" //val baseHeader = mapOf("User-Agent" to USER_AGENT) val mapper = JsonMapper.builder().addModule(KotlinModule()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt index b4774cf8..1d7b5a83 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt @@ -23,7 +23,7 @@ data class VisualDownloadChildCached( val data: VideoDownloadHelper.DownloadEpisodeCached, ) -data class DownloadClickEvent(val action: Int, val data: EasyDownloadButton.IMinimumData) +data class DownloadClickEvent(val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached) class DownloadChildAdapter( var cardList: List, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt deleted file mode 100644 index 77878432..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt +++ /dev/null @@ -1,264 +0,0 @@ -package com.lagradost.cloudstream3.ui.download - -import android.animation.ObjectAnimator -import android.text.format.Formatter.formatShortFileSize -import android.view.View -import android.view.animation.DecelerateInterpolator -import android.widget.ImageView -import android.widget.TextView -import androidx.core.view.isGone -import androidx.core.view.isVisible -import androidx.core.widget.ContentLoadingProgressBar -import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.Coroutines -import com.lagradost.cloudstream3.utils.IDisposable -import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons -import com.lagradost.cloudstream3.utils.VideoDownloadManager - -class EasyDownloadButton : IDisposable { - interface IMinimumData { - val id: Int - } - - private var _clickCallback: ((DownloadClickEvent) -> Unit)? = null - private var _imageChangeCallback: ((Pair) -> Unit)? = null - - override fun dispose() { - try { - _clickCallback = null - _imageChangeCallback = null - downloadProgressEventListener?.let { VideoDownloadManager.downloadProgressEvent -= it } - downloadStatusEventListener?.let { VideoDownloadManager.downloadStatusEvent -= it } - } catch (e: Exception) { - e.printStackTrace() - } - } - - private var downloadProgressEventListener: ((Triple) -> Unit)? = null - private var downloadStatusEventListener: ((Pair) -> Unit)? = - null - - fun setUpMaterialButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - downloadButton: MaterialButton, - textView: TextView?, - data: IMinimumData, - clickCallback: (DownloadClickEvent) -> Unit, - ) { - setUpDownloadButton( - setupCurrentBytes, - setupTotalBytes, - progressBar, - textView, - data, - downloadButton, - { - downloadButton.setIconResource(it.first) - downloadButton.text = it.second - }, - clickCallback - ) - } - - fun setUpMoreButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - downloadImage: ImageView, - textView: TextView?, - textViewProgress: TextView?, - clickableView: View, - isTextPercentage: Boolean, - data: IMinimumData, - clickCallback: (DownloadClickEvent) -> Unit, - ) { - setUpDownloadButton( - setupCurrentBytes, - setupTotalBytes, - progressBar, - textViewProgress, - data, - clickableView, - { (image, text) -> - downloadImage.isVisible = textViewProgress?.isGone ?: true - downloadImage.setImageResource(image) - textView?.text = text - }, - clickCallback, isTextPercentage - ) - } - - fun setUpButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - downloadImage: ImageView, - textView: TextView?, - data: IMinimumData, - clickCallback: (DownloadClickEvent) -> Unit, - ) { - setUpDownloadButton( - setupCurrentBytes, - setupTotalBytes, - progressBar, - textView, - data, - downloadImage, - { - downloadImage.setImageResource(it.first) - }, - clickCallback - ) - } - - private fun setUpDownloadButton( - setupCurrentBytes: Long?, - setupTotalBytes: Long?, - progressBar: ContentLoadingProgressBar, - textView: TextView?, - data: IMinimumData, - downloadView: View, - downloadImageChangeCallback: (Pair) -> Unit, - clickCallback: (DownloadClickEvent) -> Unit, - isTextPercentage: Boolean = false - ) { - _clickCallback = clickCallback - _imageChangeCallback = downloadImageChangeCallback - var lastState: VideoDownloadManager.DownloadType? = null - var currentBytes = setupCurrentBytes ?: 0 - var totalBytes = setupTotalBytes ?: 0 - var needImageUpdate = true - - fun changeDownloadImage(state: VideoDownloadManager.DownloadType) { - lastState = state - if (currentBytes <= 0) needImageUpdate = true - val img = if (currentBytes > 0) { - when (state) { - VideoDownloadManager.DownloadType.IsPaused -> Pair( - R.drawable.ic_baseline_play_arrow_24, - R.string.download_paused - ) - VideoDownloadManager.DownloadType.IsDownloading -> Pair( - R.drawable.netflix_pause, - R.string.downloading - ) - else -> Pair(R.drawable.ic_baseline_delete_outline_24, R.string.downloaded) - } - } else { - Pair(R.drawable.netflix_download, R.string.download) - } - _imageChangeCallback?.invoke( - Pair( - img.first, - downloadView.context.getString(img.second) - ) - ) - } - - fun fixDownloadedBytes(setCurrentBytes: Long, setTotalBytes: Long, animate: Boolean) { - currentBytes = setCurrentBytes - totalBytes = setTotalBytes - - if (currentBytes == 0L) { - changeDownloadImage(VideoDownloadManager.DownloadType.IsStopped) - textView?.visibility = View.GONE - progressBar.visibility = View.GONE - } else { - if (lastState == VideoDownloadManager.DownloadType.IsStopped) { - changeDownloadImage(VideoDownloadManager.getDownloadState(data.id)) - } - textView?.visibility = View.VISIBLE - progressBar.visibility = View.VISIBLE - val currentMbString = formatShortFileSize(textView?.context, setCurrentBytes) - val totalMbString = formatShortFileSize(textView?.context, setTotalBytes) - - textView?.text = - if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else - textView?.context?.getString(R.string.download_size_format) - ?.format(currentMbString, totalMbString) - - progressBar.let { bar -> - bar.max = (setTotalBytes / 1000).toInt() - - if (animate) { - val animation: ObjectAnimator = ObjectAnimator.ofInt( - bar, - "progress", - bar.progress, - (setCurrentBytes / 1000).toInt() - ) - animation.duration = 500 - animation.setAutoCancel(true) - animation.interpolator = DecelerateInterpolator() - animation.start() - } else { - bar.progress = (setCurrentBytes / 1000).toInt() - } - } - } - } - - fixDownloadedBytes(currentBytes, totalBytes, false) - changeDownloadImage(VideoDownloadManager.getDownloadState(data.id)) - - downloadProgressEventListener = { downloadData: Triple -> - if (data.id == downloadData.first) { - if (downloadData.second != currentBytes || downloadData.third != totalBytes) { // TO PREVENT WASTING UI TIME - Coroutines.runOnMainThread { - fixDownloadedBytes(downloadData.second, downloadData.third, true) - changeDownloadImage(VideoDownloadManager.getDownloadState(data.id)) - } - } - } - } - - downloadStatusEventListener = - { downloadData: Pair -> - if (data.id == downloadData.first) { - if (lastState != downloadData.second || needImageUpdate) { // TO PREVENT WASTING UI TIME - Coroutines.runOnMainThread { - changeDownloadImage(downloadData.second) - } - } - } - } - - downloadProgressEventListener?.let { VideoDownloadManager.downloadProgressEvent += it } - downloadStatusEventListener?.let { VideoDownloadManager.downloadStatusEvent += it } - - downloadView.setOnClickListener { - if (currentBytes <= 0 || totalBytes <= 0) { - _clickCallback?.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) - } else { - val list = arrayListOf( - Pair(DOWNLOAD_ACTION_PLAY_FILE, R.string.popup_play_file), - Pair(DOWNLOAD_ACTION_DELETE_FILE, R.string.popup_delete_file), - ) - - // DON'T RESUME A DOWNLOADED FILE lastState != VideoDownloadManager.DownloadType.IsDone && - if ((currentBytes * 100 / totalBytes) < 98) { - list.add( - if (lastState == VideoDownloadManager.DownloadType.IsDownloading) - Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download) - else - Pair(DOWNLOAD_ACTION_RESUME_DOWNLOAD, R.string.popup_resume_download) - ) - } - - it.popupMenuNoIcons( - list - ) { - _clickCallback?.invoke(DownloadClickEvent(itemId, data)) - } - } - } - - downloadView.setOnLongClickListener { - clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data)) - return@setOnLongClickListener true - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index 05f630a0..b43f1aac 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -22,7 +22,7 @@ data class DownloadMetadata( val progressPercentage: Long get() = if (downloadedLength < 1024) 0 else maxOf( 0, - minOf(100, (downloadedLength * 100L) / totalLength) + minOf(100, (downloadedLength * 100L) / (totalLength + 1)) ) } @@ -101,38 +101,41 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : open fun setProgress(downloadedBytes: Long, totalBytes: Long) { isZeroBytes = downloadedBytes == 0L - val steps = 10000L - progressBar.max = steps.toInt() - // div by zero error and 1 byte off is ok impo - val progress = (downloadedBytes * steps / (totalBytes + 1L)).toInt() + progressBar.post { + val steps = 10000L + progressBar.max = steps.toInt() + // div by zero error and 1 byte off is ok impo - val animation = ProgressBarAnimation( - progressBar, - progressBar.progress.toFloat(), - progress.toFloat() - ).apply { - fillAfter = true - duration = - if (progress > progressBar.progress) // we don't want to animate backward changes in progress - 100 - else - 0L - } + val progress = (downloadedBytes * steps / (totalBytes + 1L)).toInt() - if (isZeroBytes) { - progressText?.isVisible = false - } else { - progressText?.apply { - val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes) - val totalMbString = Formatter.formatShortFileSize(context, totalBytes) - text = - //if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else - context?.getString(R.string.download_size_format) - ?.format(currentMbString, totalMbString) + val animation = ProgressBarAnimation( + progressBar, + progressBar.progress.toFloat(), + progress.toFloat() + ).apply { + fillAfter = true + duration = + if (progress > progressBar.progress) // we don't want to animate backward changes in progress + 100 + else + 0L } - } - progressBar.startAnimation(animation) + if (isZeroBytes) { + progressText?.isVisible = false + } else { + progressText?.apply { + val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes) + val totalMbString = Formatter.formatShortFileSize(context, totalBytes) + text = + //if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else + context?.getString(R.string.download_size_format) + ?.format(currentMbString, totalMbString) + } + } + + progressBar.startAnimation(animation) + } } fun downloadStatusEvent(data: Pair) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt index bb2ba7b1..d97a4b88 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt @@ -21,14 +21,17 @@ class DownloadButton(context: Context, attributeSet: AttributeSet) : } override fun setStatus(status: DownloadStatusTell?) { - super.setStatus(status) - val txt = when (status) { - DownloadStatusTell.IsPaused -> R.string.download_paused - DownloadStatusTell.IsDownloading -> R.string.downloading - DownloadStatusTell.IsDone -> R.string.downloaded - else -> R.string.download + mainText?.post { + val txt = when (status) { + DownloadStatusTell.IsPaused -> R.string.download_paused + DownloadStatusTell.IsDownloading -> R.string.downloading + DownloadStatusTell.IsDone -> R.string.downloaded + else -> R.string.download + } + mainText?.setText(txt) } - mainText?.setText(txt) + super.setStatus(status) + } override fun setDefaultClickListener( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index 0b7a7fea..d20fcf93 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -174,7 +174,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : currentMetaData.apply { // DON'T RESUME A DOWNLOADED FILE lastState != VideoDownloadManager.DownloadType.IsDone && - if ((downloadedLength * 100 / totalLength) < 98) { + if (progressPercentage < 98) { list.add( if (status == VideoDownloadManager.DownloadType.IsDownloading) Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download) @@ -248,33 +248,34 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : //progressBar.isVisible = // status != null && status != DownloadStatusTell.Complete && status != DownloadStatusTell.Error //progressBarBackground.isVisible = status != null && status != DownloadStatusTell.Complete - val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading + progressBarBackground.post { + val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading + if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { + val animation = AnimationUtils.loadAnimation(context, waitingAnimation) + progressBarBackground.startAnimation(animation) + } else { + progressBarBackground.clearAnimation() + } - if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { - val animation = AnimationUtils.loadAnimation(context, waitingAnimation) - progressBarBackground.startAnimation(animation) - } else { - progressBarBackground.clearAnimation() + val progressDrawable = + if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline + + progressBarBackground.background = + ContextCompat.getDrawable(context, progressDrawable) + + val drawable = getDrawableFromStatus(status) + statusView.setImageDrawable(drawable) + val isDrawable = drawable != null + + statusView.isVisible = isDrawable + val hide = hideWhenIcon && isDrawable + if (hide) { + progressBar.clearAnimation() + progressBarBackground.clearAnimation() + } + progressBarBackground.isGone = hide + progressBar.isGone = hide } - - val progressDrawable = - if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline - - progressBarBackground.background = - ContextCompat.getDrawable(context, progressDrawable) - - val drawable = getDrawableFromStatus(status) - statusView.setImageDrawable(drawable) - val isDrawable = drawable != null - - statusView.isVisible = isDrawable - val hide = hideWhenIcon && isDrawable - if (hide) { - progressBar.clearAnimation() - progressBarBackground.clearAnimation() - } - progressBarBackground.isGone = hide - progressBar.isGone = hide } override fun resetView() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 6770e303..1fb3a72d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -186,6 +186,27 @@ object M3u8Helper2 { ) { val size get() = allTsLinks.size + suspend fun resolveLinkWhileSafe( + index: Int, + tries: Int = 3, + failDelay: Long = 3000, + condition : (() -> Boolean) + ): ByteArray? { + for (i in 0 until tries) { + if(!condition()) return null + + try { + val out = resolveLink(index) + return if(condition()) out else null + } catch (e: IllegalArgumentException) { + return null + } catch (t: Throwable) { + delay(failDelay) + } + } + return null + } + suspend fun resolveLinkSafe( index: Int, tries: Int = 3, @@ -240,8 +261,6 @@ object M3u8Helper2 { verify = false ).text - println("m3u8Response=$m3u8Response") - // encryption, this is because crunchy uses it var encryptionIv = byteArrayOf() var encryptionData = byteArrayOf() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt index a76cc115..d1614bc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt @@ -2,20 +2,18 @@ package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.ui.download.EasyDownloadButton - object VideoDownloadHelper { data class DownloadEpisodeCached( @JsonProperty("name") val name: String?, @JsonProperty("poster") val poster: String?, @JsonProperty("episode") val episode: Int, @JsonProperty("season") val season: Int?, - @JsonProperty("id") override val id: Int, + @JsonProperty("id") val id: Int, @JsonProperty("parentId") val parentId: Int, @JsonProperty("rating") val rating: Int?, @JsonProperty("description") val description: String?, @JsonProperty("cacheTime") val cacheTime: Long, - ) : EasyDownloadButton.IMinimumData + ) data class DownloadHeaderCached( @JsonProperty("apiName") val apiName: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index f4eb37b7..0334103f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -15,7 +15,6 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri -import androidx.lifecycle.MutableLiveData import androidx.preference.PreferenceManager import androidx.work.Data import androidx.work.ExistingWorkPolicy @@ -30,6 +29,8 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService @@ -42,13 +43,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly -import java.io.BufferedInputStream +import java.io.Closeable import java.io.File +import java.io.IOException import java.io.InputStream import java.io.OutputStream -import java.lang.Thread.sleep import java.net.URL -import java.net.URLConnection import java.util.* const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" @@ -60,34 +60,31 @@ object VideoDownloadManager { private var currentDownloads = mutableListOf() private const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" - @DrawableRes - const val imgDone = R.drawable.rddone + @get:DrawableRes + val imgDone get() = R.drawable.rddone - @DrawableRes - const val imgDownloading = R.drawable.rdload + @get:DrawableRes + val imgDownloading get() = R.drawable.rdload - @DrawableRes - const val imgPaused = R.drawable.rdpause + @get:DrawableRes + val imgPaused get() = R.drawable.rdpause - @DrawableRes - const val imgStopped = R.drawable.rderror + @get:DrawableRes + val imgStopped get() = R.drawable.rderror - @DrawableRes - const val imgError = R.drawable.rderror + @get:DrawableRes + val imgError get() = R.drawable.rderror - @DrawableRes - const val pressToPauseIcon = R.drawable.ic_baseline_pause_24 + @get:DrawableRes + val pressToPauseIcon get() = R.drawable.ic_baseline_pause_24 - @DrawableRes - const val pressToResumeIcon = R.drawable.ic_baseline_play_arrow_24 + @get:DrawableRes + val pressToResumeIcon get() = R.drawable.ic_baseline_play_arrow_24 - @DrawableRes - const val pressToStopIcon = R.drawable.exo_icon_stop - - private var updateCount: Int = 0 - private val downloadDataUpdateCount = MutableLiveData() + @get:DrawableRes + val pressToStopIcon get() = R.drawable.baseline_stop_24 enum class DownloadType { IsPaused, @@ -251,9 +248,8 @@ object VideoDownloadManager { total: Long, notificationCallback: (Int, Notification) -> Unit, hlsProgress: Long? = null, - hlsTotal: Long? = null, - - ): Notification? { + hlsTotal: Long? = null + ): Notification? { try { if (total <= 0) return null// crash, invalid data @@ -336,14 +332,28 @@ object VideoDownloadManager { } val bigText = - if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { - (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix" - } else if (state == DownloadType.IsFailed) { - downloadFormat.format(context.getString(R.string.download_failed), rowTwo) - } else if (state == DownloadType.IsDone) { - downloadFormat.format(context.getString(R.string.download_done), rowTwo) - } else { - downloadFormat.format(context.getString(R.string.download_canceled), rowTwo) + when (state) { + DownloadType.IsDownloading, DownloadType.IsPaused -> { + (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix" + } + + DownloadType.IsFailed -> { + downloadFormat.format( + context.getString(R.string.download_failed), + rowTwo + ) + } + + DownloadType.IsDone -> { + downloadFormat.format(context.getString(R.string.download_done), rowTwo) + } + + else -> { + downloadFormat.format( + context.getString(R.string.download_canceled), + rowTwo + ) + } } val bodyStyle = NotificationCompat.BigTextStyle() @@ -351,14 +361,28 @@ object VideoDownloadManager { builder.setStyle(bodyStyle) } else { val txt = - if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { - rowTwo - } else if (state == DownloadType.IsFailed) { - downloadFormat.format(context.getString(R.string.download_failed), rowTwo) - } else if (state == DownloadType.IsDone) { - downloadFormat.format(context.getString(R.string.download_done), rowTwo) - } else { - downloadFormat.format(context.getString(R.string.download_canceled), rowTwo) + when (state) { + DownloadType.IsDownloading, DownloadType.IsPaused -> { + rowTwo + } + + DownloadType.IsFailed -> { + downloadFormat.format( + context.getString(R.string.download_failed), + rowTwo + ) + } + + DownloadType.IsDone -> { + downloadFormat.format(context.getString(R.string.download_done), rowTwo) + } + + else -> { + downloadFormat.format( + context.getString(R.string.download_canceled), + rowTwo + ) + } } builder.setContentText(txt) @@ -681,6 +705,171 @@ object VideoDownloadManager { return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream) } + /** This class handles the notifications, as well as the relevant key */ + data class DownloadMetaData( + private val id: Int?, + var bytesDownloaded: Long = 0, + var totalBytes: Long? = null, + + // notification metadata + private var lastUpdatedMs: Long = 0, + private val createNotificationCallback: (CreateNotificationMetadata) -> Unit, + + private var internalType: DownloadType = DownloadType.IsPending, + + // how many segments that we have downloaded + var hlsProgress: Int = 0, + // how many segments that exist + var hlsTotal: Int? = null, + // this is how many segments that has been written to the file + // will always be <= hlsProgress as we may keep some in a buffer + var hlsWrittenProgress: Long = 0, + + // this is used for copy with metadata on how much we have downloaded for setKey + private var downloadFileInfoTemplate: DownloadedFileInfo? = null + ) : Closeable { + val approxTotalBytes: Long + get() = totalBytes ?: hlsTotal?.let { total -> + (bytesDownloaded * (total / hlsProgress.toFloat())).toLong() + } ?: 0L + + private val isHLS get() = hlsTotal != null + + private val downloadEventListener = { event: Pair -> + if (event.first == id) { + when (event.second) { + DownloadActionType.Pause -> { + type = DownloadType.IsPaused + } + + DownloadActionType.Stop -> { + type = DownloadType.IsStopped + removeKey(KEY_RESUME_PACKAGES, event.first.toString()) + saveQueue() + } + + DownloadActionType.Resume -> { + type = DownloadType.IsDownloading + } + } + } + } + + private fun updateFileInfo() { + if (id == null) return + downloadFileInfoTemplate?.let { template -> + setKey( + KEY_DOWNLOAD_INFO, + id.toString(), + template.copy( + totalBytes = approxTotalBytes, + extraInfo = if (isHLS) hlsWrittenProgress.toString() else null + ) + ) + } + } + + fun setDownloadFileInfoTemplate(template: DownloadedFileInfo) { + downloadFileInfoTemplate = template + updateFileInfo() + } + + init { + if (id != null) { + downloadEvent += downloadEventListener + } + } + + override fun close() { + // as we may need to resume hls downloads, we save the current written index + if (isHLS) { + updateFileInfo() + } + if (id != null) { + downloadEvent -= downloadEventListener + downloadStatus -= id + } + } + + var type + get() = internalType + set(value) { + internalType = value + notify() + } + + companion object { + const val UPDATE_RATE_MS: Long = 1000L + } + + @JvmName("DownloadMetaDataNotify") + private fun notify() { + lastUpdatedMs = System.currentTimeMillis() + try { + val bytes = approxTotalBytes + + // notification creation + if (isHLS) { + createNotificationCallback( + CreateNotificationMetadata( + internalType, + bytesDownloaded, + bytes, + hlsTotal = hlsTotal?.toLong(), + hlsProgress = hlsProgress.toLong() + ) + ) + } else { + createNotificationCallback( + CreateNotificationMetadata( + internalType, + bytesDownloaded, + bytes, + ) + ) + } + + // as hls has an approx file size we want to update this metadata + if (isHLS) { + updateFileInfo() + } + + // push all events, this *should* not crash, TODO MUTEX? + if (id != null) { + downloadStatus[id] = type + downloadProgressEvent(Triple(id, bytesDownloaded, bytes)) + downloadStatusEvent(id to type) + } + } catch (t: Throwable) { + logError(t) + } + } + + private fun checkNotification() { + if (lastUpdatedMs + UPDATE_RATE_MS > System.currentTimeMillis()) return + notify() + } + + + /** adds the length and pushes a notification if necessary */ + fun addBytes(length: Long) { + bytesDownloaded += length + // we don't want to update the notification after it is paused, + // download progress may not stop directly when we "pause" it + if (type == DownloadType.IsDownloading) checkNotification() + } + + /** adds the length + hsl progress and pushes a notification if necessary */ + fun addSegment(length: Long) { + hlsProgress += 1 + addBytes(length) + } + + fun setWrittenSegment(segmentIndex: Int) { + hlsWrittenProgress = segmentIndex.toLong() + 1L + } + } + @Throws suspend fun downloadThing( context: Context, @@ -692,253 +881,225 @@ object VideoDownloadManager { parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, ): Int = withContext(Dispatchers.IO) { + // we cant download torrents with this implementation, aria2c might be used in the future if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { return@withContext ERROR_UNKNOWN } - val basePath = context.getBasePath() - - val displayName = getDisplayName(name, extension) - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.first.isDownloadDir()) getRelativePath( - folder - ) else folder - - fun deleteFile(): Int { - return delete(context, name, relativePath, extension, parentId, basePath.first) - } - - val stream = setupStream(context, name, relativePath, extension, tryResume) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - - val resume = stream.resume!! - val fileStream = stream.fileStream!! - val fileLength = stream.fileLength!! - - // CONNECT - val connection: URLConnection = - URL(link.url.replace(" ", "%20")).openConnection() // IDK OLD PHONES BE WACK - - // SET CONNECTION SETTINGS - connection.connectTimeout = 10000 - connection.setRequestProperty("Accept-Encoding", "identity") - connection.setRequestProperty("user-agent", USER_AGENT) - if (link.referer.isNotEmpty()) connection.setRequestProperty("referer", link.referer) - - // extra stuff - connection.setRequestProperty( - "sec-ch-ua", - "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"" + var fileStream: OutputStream? = null + var requestStream: InputStream? = null + val metadata = DownloadMetaData( + totalBytes = 0, + bytesDownloaded = 0, + createNotificationCallback = createNotificationCallback, + id = parentId, ) + try { + // get the file path + val (baseFile, basePath) = context.getBasePath() + val displayName = getDisplayName(name, extension) + val relativePath = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( + folder + ) else folder - connection.setRequestProperty("sec-ch-ua-mobile", "?0") - connection.setRequestProperty("accept", "*/*") - // dataSource.setRequestProperty("Sec-Fetch-Site", "none") //same-site - connection.setRequestProperty("sec-fetch-user", "?1") - connection.setRequestProperty("sec-fetch-mode", "navigate") - connection.setRequestProperty("sec-fetch-dest", "video") - link.headers.entries.forEach { - connection.setRequestProperty(it.key, it.value) - } + // set up the download file + val stream = setupStream(context, name, relativePath, extension, tryResume) + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode + fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN + val resume = stream.resume ?: return@withContext ERROR_UNKNOWN + val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN + val resumeAt = (if (resume) fileLength else 0) + metadata.bytesDownloaded = resumeAt + metadata.type = DownloadType.IsPending - if (resume) - connection.setRequestProperty("Range", "bytes=${fileLength}-") - val resumeLength = (if (resume) fileLength else 0) + // set up a connection + val request = app.get( + link.url.replace(" ", "%20"), + headers = link.headers + mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", + "sec-fetch-mode" to "navigate", + "sec-fetch-dest" to "video", + "sec-fetch-user" to "?1", + "sec-ch-ua-mobile" to "?0", + ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap(), + referer = link.referer, + verify = false + ) - // ON CONNECTION - connection.connect() + // init variables + val contentLength = request.size ?: 0 + metadata.totalBytes = contentLength + resumeAt - val contentLength = try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // fuck android - connection.contentLengthLong - } else { - connection.getHeaderField("content-length").toLongOrNull() - ?: connection.contentLength.toLong() - } - } catch (e: Exception) { - logError(e) - 0L - } - val bytesTotal = contentLength + resumeLength - - if (extension == "mp4" && bytesTotal < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG - - parentId?.let { - setKey( - KEY_DOWNLOAD_INFO, - it.toString(), + // save + metadata.setDownloadFileInfoTemplate( DownloadedFileInfo( - bytesTotal, - relativePath ?: "", - displayName, - basePath = basePath.second + totalBytes = metadata.approxTotalBytes, + relativePath = relativePath ?: "", + displayName = displayName, + basePath = basePath ) ) + + // total length is less than 5mb, that is too short and something has gone wrong + if (extension == "mp4" && metadata.approxTotalBytes < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION + + // read the buffer into the filestream, this is equivalent of transferTo + requestStream = request.body.byteStream() + metadata.type = DownloadType.IsDownloading + + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var read: Int + while (requestStream.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it } >= 0) { + fileStream.write(buffer, 0, read) + + // wait until not paused + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then break to delete + if (metadata.type == DownloadType.IsStopped) break + metadata.addBytes(read.toLong()) + } + + if (metadata.type == DownloadType.IsStopped) { + return@withContext delete( + context, + name, + relativePath, + extension, + parentId, + baseFile + ) + } + + metadata.type = DownloadType.IsDone + return@withContext SUCCESS_DOWNLOAD_DONE + } catch (e: IOException) { + // some sort of IO error, this should not happened + // we just rethrow it + logError(e) + throw e + } catch (t: Throwable) { + // some sort of network error, will error + + // note that when failing we don't want to delete the file, + // only user interaction has that power + metadata.type = DownloadType.IsFailed + return@withContext ERROR_CONNECTION_ERROR + } finally { + fileStream?.closeQuietly() + requestStream?.closeQuietly() + metadata.close() } + } - // Could use connection.contentType for mime types when creating the file, - // however file is already created and players don't go of file type - // https://stackoverflow.com/questions/23714383/what-are-all-the-possible-values-for-http-content-type-header - // might receive application/octet-stream - /*if (!connection.contentType.isNullOrEmpty() && !connection.contentType.startsWith("video")) { - return ERROR_WRONG_CONTENT // CONTENT IS NOT VIDEO, SHOULD NEVER HAPPENED, BUT JUST IN CASE - }*/ + @Throws + private suspend fun downloadHLS( + context: Context, + link: ExtractorLink, + name: String, + folder: String?, + parentId: Int?, + startIndex: Int?, + createNotificationCallback: (CreateNotificationMetadata) -> Unit, + parallelConnections: Int = 3 + ): Int = withContext(Dispatchers.IO) { + require(parallelConnections >= 1) - // READ DATA FROM CONNECTION - val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream) - val buffer = ByteArray(1024) - var count: Int - var bytesDownloaded = resumeLength + val metadata = DownloadMetaData( + createNotificationCallback = createNotificationCallback, + id = parentId + ) + val extension = "mp4" - var isPaused = false - var isStopped = false - var isDone = false - var isFailed = false + var fileStream: OutputStream? = null + try { + // the start .ts index + var startAt = startIndex ?: 0 - // TO NOT REUSE CODE - fun updateNotification() { - val type = when { - isDone -> DownloadType.IsDone - isStopped -> DownloadType.IsStopped - isFailed -> DownloadType.IsFailed - isPaused -> DownloadType.IsPaused - else -> DownloadType.IsDownloading - } + // set up the file data + val (baseFile, basePath) = context.getBasePath() + val relativePath = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( + folder + ) else folder + val displayName = getDisplayName(name, extension) + val stream = setupStream(context, name, relativePath, extension, startAt > 0) + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode + if (stream.resume != true) startAt = 0 + fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN - parentId?.let { id -> - try { - downloadStatus[id] = type - downloadStatusEvent.invoke(Pair(id, type)) - downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal)) - } catch (e: Exception) { - // IDK MIGHT ERROR - } - } - - createNotificationCallback.invoke( - CreateNotificationMetadata( - type, - bytesDownloaded, - bytesTotal + // push the metadata + metadata.bytesDownloaded = stream.fileLength ?: 0 + metadata.hlsProgress = startAt + metadata.type = DownloadType.IsPending + metadata.setDownloadFileInfoTemplate( + DownloadedFileInfo( + totalBytes = 0, + relativePath = relativePath ?: "", + displayName = displayName, + basePath = basePath ) ) - /*createNotification( - context, - source, - link.name, - ep, - type, - bytesDownloaded, - bytesTotal - )*/ - } - val downloadEventListener = { event: Pair -> - if (event.first == parentId) { - when (event.second) { - DownloadActionType.Pause -> { - isPaused = true; updateNotification() + // do the initial get request to fetch the segments + val m3u8 = M3u8Helper.M3u8Stream( + link.url, link.quality, mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() + ) + val items = M3u8Helper2.hslLazy(listOf(m3u8)) + + metadata.hlsTotal = items.size + metadata.type = DownloadType.IsDownloading + + // does several connections in parallel instead of a regular for loop to improve + // download speed + (startAt until items.size).chunked(parallelConnections).forEach { subset -> + // wait until not paused + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then break to delete + if (metadata.type == DownloadType.IsStopped) return@forEach + + subset.amap { idx -> + idx to items.resolveLinkSafe(idx)?.also { bytes -> + metadata.addSegment(bytes.size.toLong()) } - - DownloadActionType.Stop -> { - isStopped = true; updateNotification() - removeKey(KEY_RESUME_PACKAGES, event.first.toString()) - saveQueue() - } - - DownloadActionType.Resume -> { - isPaused = false; updateNotification() + }.forEach { (idx, bytes) -> + if (bytes == null) { + metadata.type = DownloadType.IsFailed + return@withContext ERROR_CONNECTION_ERROR } + fileStream.write(bytes) + metadata.setWrittenSegment(idx) } } - } - if (parentId != null) - downloadEvent += downloadEventListener - - // UPDATE DOWNLOAD NOTIFICATION - val notificationCoroutine = main { - while (true) { - if (!isPaused) { - updateNotification() - } - for (i in 1..10) { - delay(100) - } - } - } - - // THE REAL READ - try { - while (true) { - count = connectionInputStream.read(buffer) - if (count < 0) break - bytesDownloaded += count - // downloadProgressEvent.invoke(Pair(id, bytesDownloaded)) // Updates too much for any UI to keep up with - while (isPaused) { - sleep(100) - if (isStopped) { - break - } - } - if (isStopped) { - break - } - fileStream.write(buffer, 0, count) - } - } catch (e: Exception) { - logError(e) - isFailed = true - updateNotification() - } - - // REMOVE AND EXIT ALL - fileStream.close() - connectionInputStream.close() - notificationCoroutine.cancel() - - try { - if (parentId != null) - downloadEvent -= downloadEventListener - } catch (e: Exception) { - logError(e) - } - - try { - parentId?.let { - downloadStatus.remove(it) - } - } catch (e: Exception) { - // IDK MIGHT ERROR - } - - // RETURN MESSAGE - return@withContext when { - isFailed -> { - parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } - ERROR_CONNECTION_ERROR + if (metadata.type == DownloadType.IsStopped) { + return@withContext delete( + context, + name, + relativePath, + extension, + parentId, + baseFile + ) } - isStopped -> { - parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } - deleteFile() - } - - else -> { - parentId?.let { id -> - downloadProgressEvent.invoke( - Triple( - id, - bytesDownloaded, - bytesTotal - ) - ) - } - isDone = true - updateNotification() - SUCCESS_DOWNLOAD_DONE - } + metadata.type = DownloadType.IsDone + return@withContext SUCCESS_DOWNLOAD_DONE + } catch (t: Throwable) { + logError(t) + metadata.type = DownloadType.IsFailed + return@withContext ERROR_UNKNOWN + } finally { + fileStream?.closeQuietly() + metadata.close() } } @@ -1107,192 +1268,6 @@ object VideoDownloadManager { return SUCCESS_STOPPED } - @Throws - private suspend fun downloadHLS( - context: Context, - link: ExtractorLink, - name: String, - folder: String?, - parentId: Int?, - startIndex: Int?, - createNotificationCallback: (CreateNotificationMetadata) -> Unit - ): Int = withContext(Dispatchers.IO) { - val extension = "mp4" - - val m3u8 = M3u8Helper.M3u8Stream( - link.url, link.quality, mapOf("referer" to link.referer) - ) - - var realIndex = startIndex ?: 0 - val basePath = context.getBasePath() - - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.first.isDownloadDir()) getRelativePath( - folder - ) else folder - - val stream = setupStream(context, name, relativePath, extension, realIndex > 0) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - - if (stream.resume != true) realIndex = 0 - val fileLengthAdd = stream.fileLength ?: 0 - val items = M3u8Helper2.hslLazy(listOf(m3u8)) - - val displayName = getDisplayName(name, extension) - - val fileStream = stream.fileStream!! - - var isDone = false - var isFailed = false - var isPaused = false - var bytesDownloaded = fileLengthAdd - var tsProgress: Long = realIndex.toLong() + 1 // we don't want div by zero - val totalTs: Long = items.size.toLong() - - fun deleteFile(): Int { - return delete(context, name, relativePath, extension, parentId, basePath.first) - } - - fun updateInfo() { - setKey( - KEY_DOWNLOAD_INFO, - (parentId ?: return).toString(), - DownloadedFileInfo( - // approx bytes - totalBytes = (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - relativePath = relativePath ?: "", - displayName = displayName, - extraInfo = tsProgress.toString(), - basePath = basePath.second - ) - ) - } - - updateInfo() - - fun updateNotification() { - val type = when { - isDone -> DownloadType.IsDone - isFailed -> DownloadType.IsFailed - isPaused -> DownloadType.IsPaused - else -> DownloadType.IsDownloading - } - - parentId?.let { id -> - try { - downloadStatus[id] = type - downloadStatusEvent.invoke(Pair(id, type)) - downloadProgressEvent.invoke( - Triple( - id, - bytesDownloaded, - (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - ) - ) - } catch (_: Throwable) {} - } - - createNotificationCallback.invoke( - CreateNotificationMetadata( - type, - bytesDownloaded, - (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - tsProgress, - totalTs - ) - ) - } - - val notificationCoroutine = main { - while (true) { - if (!isDone) { - updateNotification() - } - for (i in 1..10) { - delay(100) - } - } - } - - val downloadEventListener = { event: Pair -> - if (event.first == parentId) { - when (event.second) { - DownloadActionType.Stop -> { - isFailed = true - } - - DownloadActionType.Pause -> { - isPaused = true - } - - DownloadActionType.Resume -> { - isPaused = false - } - } - updateNotification() - } - } - - fun closeAll() { - try { - if (parentId != null) - downloadEvent -= downloadEventListener - } catch (t: Throwable) { - logError(t) - } - try { - parentId?.let { - downloadStatus.remove(it) - } - } catch (t: Throwable) { - logError(t) - } - notificationCoroutine.cancel() - } - - if (parentId != null) - downloadEvent += downloadEventListener - - fun onFailed() { - fileStream.close() - deleteFile() - updateNotification() - closeAll() - } - - for (idx in realIndex until items.size) { - while (isPaused) { - if (isFailed) { - onFailed() - return@withContext SUCCESS_STOPPED - } - delay(100) - } - - if (isFailed) { - onFailed() - return@withContext SUCCESS_STOPPED - } - - val bytes = items.resolveLinkSafe(idx) ?: run { - isFailed = true - onFailed() - return@withContext ERROR_CONNECTION_ERROR - } - - fileStream.write(bytes) - tsProgress = idx.toLong() + 1 - bytesDownloaded += bytes.size.toLong() - updateInfo() - } - isDone = true - fileStream.close() - updateNotification() - - closeAll() - updateInfo() - return@withContext SUCCESS_DOWNLOAD_DONE - } fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { return getFileName(context, metadata.name, metadata.episode, metadata.season) @@ -1353,22 +1328,30 @@ object VideoDownloadManager { )?.extraInfo?.toIntOrNull() } else null return suspendSafeApiCall { - downloadHLS(context, link, name, folder, ep.id, startIndex) { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback, - meta.hlsProgress, - meta.hlsTotal - ) + downloadHLS( + context, + link, + name, + folder, + ep.id, + startIndex, + createNotificationCallback = { meta -> + main { + createNotification( + context, + source, + link.name, + ep, + meta.type, + meta.bytesDownloaded, + meta.bytesTotal, + notificationCallback, + meta.hlsProgress, + meta.hlsTotal + ) + } } - } + ) }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN @@ -1392,7 +1375,7 @@ object VideoDownloadManager { }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN } - fun downloadCheck( + suspend fun downloadCheck( context: Context, notificationCallback: (Int, Notification) -> Unit, ): Int? { if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) { @@ -1407,42 +1390,55 @@ object VideoDownloadManager { currentDownloads.add(id) - main { - try { - for (index in (pkg.linkIndex ?: 0) until item.links.size) { - val link = item.links[index] - val resume = pkg.linkIndex == index + try { + for (index in (pkg.linkIndex ?: 0) until item.links.size) { + val link = item.links[index] + val resume = pkg.linkIndex == index - setKey( - KEY_RESUME_PACKAGES, - id.toString(), - DownloadResumePackage(item, index) + setKey( + KEY_RESUME_PACKAGES, + id.toString(), + DownloadResumePackage(item, index) + ) + + var connectionResult = + downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + resume + ) + //.also { println("Single episode finished with return code: $it") } + + // retry every link at least once + if (connectionResult <= 0) { + connectionResult = downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + true ) - val connectionResult = withContext(Dispatchers.IO) { - downloadSingleEpisode( - context, - item.source, - item.folder, - item.ep, - link, - notificationCallback, - resume - ).also { println("Single episode finished with return code: $it") } - } - if (connectionResult != null && connectionResult > 0) { // SUCCESS - removeKey(KEY_RESUME_PACKAGES, id.toString()) - break - } else if (index == item.links.lastIndex) { - downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) - } } - } catch (e: Exception) { - logError(e) - } finally { - currentDownloads.remove(id) - // Because otherwise notifications will not get caught by the workmanager - downloadCheckUsingWorker(context) + + if (connectionResult > 0) { // SUCCESS + removeKey(KEY_RESUME_PACKAGES, id.toString()) + break + } else if (index == item.links.lastIndex) { + downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) + } } + } catch (e: Exception) { + logError(e) + } finally { + currentDownloads.remove(id) + // Because otherwise notifications will not get caught by the work manager + downloadCheckUsingWorker(context) } } return null @@ -1538,26 +1534,13 @@ object VideoDownloadManager { return context.getKey(KEY_RESUME_PACKAGES, id.toString()) } - fun downloadFromResume( + suspend fun downloadFromResume( context: Context, pkg: DownloadResumePackage, notificationCallback: (Int, Notification) -> Unit, setKey: Boolean = true ) { if (!currentDownloads.any { it == pkg.item.ep.id }) { -// if (currentDownloads.size == maxConcurrentDownloads) { -// main { -//// showToast( // can be replaced with regular Toast -//// context, -//// "${pkg.item.ep.mainName}${pkg.item.ep.episode?.let { " ${context.getString(R.string.episode)} $it " } ?: " "}${ -//// context.getString( -//// R.string.queued -//// ) -//// }", -//// Toast.LENGTH_SHORT -//// ) -// } -// } downloadQueue.addLast(pkg) downloadCheck(context, notificationCallback) if (setKey) saveQueue() @@ -1590,7 +1573,7 @@ object VideoDownloadManager { return false }*/ - fun downloadEpisode( + suspend fun downloadEpisode( context: Context?, source: String?, folder: String?, @@ -1599,13 +1582,12 @@ object VideoDownloadManager { notificationCallback: (Int, Notification) -> Unit, ) { if (context == null) return - if (links.isNotEmpty()) { - downloadFromResume( - context, - DownloadResumePackage(DownloadItem(source, folder, ep, links), null), - notificationCallback - ) - } + if (links.isEmpty()) return + downloadFromResume( + context, + DownloadResumePackage(DownloadItem(source, folder, ep, links), null), + notificationCallback + ) } /** Worker stuff */ diff --git a/app/src/main/res/drawable/baseline_stop_24.xml b/app/src/main/res/drawable/baseline_stop_24.xml new file mode 100644 index 00000000..100cb1fc --- /dev/null +++ b/app/src/main/res/drawable/baseline_stop_24.xml @@ -0,0 +1,10 @@ + + + From a05616e3e8c6c0373f88a7c6dcc022d42ca7188d Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 01:37:48 +0200 Subject: [PATCH 049/441] fix --- .../cloudstream3/extractors/Mp4Upload.kt | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt index 93a280ed..e746b286 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt @@ -10,24 +10,39 @@ open class Mp4Upload : ExtractorApi() { override var name = "Mp4Upload" override var mainUrl = "https://www.mp4upload.com" private val srcRegex = Regex("""player\.src\("(.*?)"""") - override val requiresReferer = true + private val srcRegex2 = Regex("""player\.src\([\w\W]*src: "(.*?)"""") + override val requiresReferer = true + private val idMatch = Regex("""mp4upload\.com/(embed-|)([A-Za-z0-9]*)""") override suspend fun getUrl(url: String, referer: String?): List? { - with(app.get(url)) { - getAndUnpack(this.text).let { unpackedText -> - val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull() - srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link -> - return listOf( - ExtractorLink( - name, - name, - link, - url, - quality ?: Qualities.Unknown.value, - ) - ) - } - } + val realUrl = idMatch.find(url)?.groupValues?.get(2)?.let { id -> + "$mainUrl/embed-$id.html" + } ?: url + val response = app.get(realUrl) + val unpackedText = getAndUnpack(response.text) + val quality = + unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull() + srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + name, + name, + link, + url, + quality ?: Qualities.Unknown.value, + ) + ) + } + srcRegex2.find(unpackedText)?.groupValues?.get(1)?.let { link -> + return listOf( + ExtractorLink( + name, + name, + link, + url, + quality ?: Qualities.Unknown.value, + ) + ) } return null } From 35e1b8b4dcbe5172afc8f4cf23a673a84f99c194 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 01:38:40 +0200 Subject: [PATCH 050/441] bump --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f72ec0b0..71015e31 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,7 +52,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.3" + versionName = "4.1.4" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") From e20e3dcfd3ce450b0efe61d64db4a34413652c72 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 04:46:47 +0200 Subject: [PATCH 051/441] fixed some bugs caused by new download update --- app/build.gradle.kts | 2 +- .../utils/DownloadFileWorkManager.kt | 17 +- .../utils/VideoDownloadManager.kt | 271 ++++++++++-------- 3 files changed, 165 insertions(+), 125 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 71015e31..708a2083 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,7 +52,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.4" + versionName = "4.1.5" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt index c1eb649b..aa424c08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt @@ -7,6 +7,7 @@ import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO @@ -25,15 +26,16 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo override suspend fun doWork(): Result { val key = workerParams.inputData.getString("key") try { - println("KEY $key") if (key == DOWNLOAD_CHECK) { - downloadCheck(applicationContext, ::handleNotification)?.let { - awaitDownload(it) - } + downloadCheck(applicationContext, ::handleNotification) } else if (key != null) { - val info = applicationContext.getKey(WORK_KEY_INFO, key) + val info = + applicationContext.getKey(WORK_KEY_INFO, key) val pkg = - applicationContext.getKey(WORK_KEY_PACKAGE, key) + applicationContext.getKey( + WORK_KEY_PACKAGE, + key + ) if (info != null) { downloadEpisode( applicationContext, @@ -43,10 +45,8 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo info.links, ::handleNotification ) - awaitDownload(info.ep.id) } else if (pkg != null) { downloadFromResume(applicationContext, pkg, ::handleNotification) - awaitDownload(pkg.item.ep.id) } removeKeys(key) } @@ -73,6 +73,7 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo VideoDownloadManager.DownloadType.IsDone, VideoDownloadManager.DownloadType.IsFailed, VideoDownloadManager.DownloadType.IsStopped -> { isDone = true } + else -> Unit } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 0334103f..ef0d9d8a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -26,6 +26,7 @@ import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType @@ -723,7 +724,7 @@ object VideoDownloadManager { var hlsTotal: Int? = null, // this is how many segments that has been written to the file // will always be <= hlsProgress as we may keep some in a buffer - var hlsWrittenProgress: Long = 0, + var hlsWrittenProgress: Int = 0, // this is used for copy with metadata on how much we have downloaded for setKey private var downloadFileInfoTemplate: DownloadedFileInfo? = null @@ -798,6 +799,15 @@ object VideoDownloadManager { notify() } + fun onDelete() { + bytesDownloaded = 0 + hlsWrittenProgress = 0 + hlsProgress = 0 + + //internalType = DownloadType.IsStopped + notify() + } + companion object { const val UPDATE_RATE_MS: Long = 1000L } @@ -842,6 +852,9 @@ object VideoDownloadManager { } } catch (t: Throwable) { logError(t) + if (BuildConfig.DEBUG) { + throw t + } } } @@ -866,7 +879,7 @@ object VideoDownloadManager { } fun setWrittenSegment(segmentIndex: Int) { - hlsWrittenProgress = segmentIndex.toLong() + 1L + hlsWrittenProgress = segmentIndex + 1 } } @@ -916,16 +929,18 @@ object VideoDownloadManager { // set up a connection val request = app.get( link.url.replace(" ", "%20"), - headers = link.headers + mapOf( - "Accept-Encoding" to "identity", - "accept" to "*/*", - "user-agent" to USER_AGENT, - "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", - "sec-fetch-mode" to "navigate", - "sec-fetch-dest" to "video", - "sec-fetch-user" to "?1", - "sec-ch-ua-mobile" to "?0", - ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap(), + headers = link.headers.appendAndDontOverride( + mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", + "sec-fetch-mode" to "navigate", + "sec-fetch-dest" to "video", + "sec-fetch-user" to "?1", + "sec-ch-ua-mobile" to "?0", + ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap() + ), referer = link.referer, verify = false ) @@ -964,14 +979,14 @@ object VideoDownloadManager { } if (metadata.type == DownloadType.IsStopped) { - return@withContext delete( - context, - name, - relativePath, - extension, - parentId, - baseFile - ) + // we need to close before delete + fileStream.closeQuietly() + metadata.onDelete() + if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { + return@withContext SUCCESS_STOPPED + } else { + return@withContext ERROR_DELETING_FILE + } } metadata.type = DownloadType.IsDone @@ -995,6 +1010,18 @@ object VideoDownloadManager { } } + /** Helper function to make sure duplicate attributes don't get overriden or inserted without lowercase cmp + * example: map("a" to 1) appendAndDontOverride map("A" to 2, "a" to 3, "c" to 4) = map("a" to 1, "c" to 4) + * */ + private fun Map.appendAndDontOverride(rhs: Map): Map { + val out = this.toMutableMap() + val current = this.keys.map { it.lowercase() } + for ((key, value) in rhs) { + if (current.contains(key.lowercase())) continue + out[key] = value + } + return out + } @Throws private suspend fun downloadHLS( @@ -1047,11 +1074,13 @@ object VideoDownloadManager { // do the initial get request to fetch the segments val m3u8 = M3u8Helper.M3u8Stream( - link.url, link.quality, mapOf( - "Accept-Encoding" to "identity", - "accept" to "*/*", - "user-agent" to USER_AGENT, - ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() + link.url, link.quality, link.headers.appendAndDontOverride( + mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() + ) ) val items = M3u8Helper2.hslLazy(listOf(m3u8)) @@ -1081,14 +1110,14 @@ object VideoDownloadManager { } if (metadata.type == DownloadType.IsStopped) { - return@withContext delete( - context, - name, - relativePath, - extension, - parentId, - baseFile - ) + // we need to close before delete + fileStream.closeQuietly() + metadata.onDelete() + if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { + return@withContext SUCCESS_STOPPED + } else { + return@withContext ERROR_DELETING_FILE + } } metadata.type = DownloadType.IsDone @@ -1194,7 +1223,7 @@ object VideoDownloadManager { return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace( '/', File.separatorChar - ) + ).replace("${File.separatorChar}${File.separatorChar}", File.separatorChar.toString()) } /** @@ -1224,7 +1253,7 @@ object VideoDownloadManager { return this != null && this.filePath == getDownloadDir()?.filePath } - private fun delete( + /*private fun delete( context: Context, name: String, folder: String?, @@ -1235,7 +1264,7 @@ object VideoDownloadManager { val displayName = getDisplayName(name, extension) // delete all subtitle files - if (extension == "mp4") { + if (extension != "vtt" && extension != "srt") { try { delete(context, name, folder, "vtt", parentId, basePath) delete(context, name, folder, "srt", parentId, basePath) @@ -1248,9 +1277,9 @@ object VideoDownloadManager { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.isDownloadDir()) { val relativePath = getRelativePath(folder) val lastContent = - context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName) - if (lastContent != null) { - context.contentResolver.delete(lastContent, null, null) + context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName) ?: return ERROR_DELETING_FILE + if(context.contentResolver.delete(lastContent, null, null) <= 0) { + return ERROR_DELETING_FILE } } else { val dir = basePath?.gotoDir(folder) @@ -1260,13 +1289,12 @@ object VideoDownloadManager { // Cleans up empty directory if (dir.listFiles()?.isEmpty() == true) dir.delete() } -// } parentId?.let { downloadDeleteEvent.invoke(parentId) } } return SUCCESS_STOPPED - } + }*/ fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { @@ -1377,71 +1405,71 @@ object VideoDownloadManager { suspend fun downloadCheck( context: Context, notificationCallback: (Int, Notification) -> Unit, - ): Int? { - if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) { - val pkg = downloadQueue.removeFirst() - val item = pkg.item - val id = item.ep.id - if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT - downloadEvent.invoke(Pair(id, DownloadActionType.Resume)) - /** ID needs to be returned to the work-manager to properly await notification */ - return id - } + ) { + if (!(currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0)) return - currentDownloads.add(id) - - try { - for (index in (pkg.linkIndex ?: 0) until item.links.size) { - val link = item.links[index] - val resume = pkg.linkIndex == index - - setKey( - KEY_RESUME_PACKAGES, - id.toString(), - DownloadResumePackage(item, index) - ) - - var connectionResult = - downloadSingleEpisode( - context, - item.source, - item.folder, - item.ep, - link, - notificationCallback, - resume - ) - //.also { println("Single episode finished with return code: $it") } - - // retry every link at least once - if (connectionResult <= 0) { - connectionResult = downloadSingleEpisode( - context, - item.source, - item.folder, - item.ep, - link, - notificationCallback, - true - ) - } - - if (connectionResult > 0) { // SUCCESS - removeKey(KEY_RESUME_PACKAGES, id.toString()) - break - } else if (index == item.links.lastIndex) { - downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) - } - } - } catch (e: Exception) { - logError(e) - } finally { - currentDownloads.remove(id) - // Because otherwise notifications will not get caught by the work manager - downloadCheckUsingWorker(context) - } + val pkg = downloadQueue.removeFirst() + val item = pkg.item + val id = item.ep.id + if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT + downloadEvent.invoke(Pair(id, DownloadActionType.Resume)) + /** ID needs to be returned to the work-manager to properly await notification */ + // return id } - return null + + currentDownloads.add(id) + try { + for (index in (pkg.linkIndex ?: 0) until item.links.size) { + val link = item.links[index] + val resume = pkg.linkIndex == index + + setKey( + KEY_RESUME_PACKAGES, + id.toString(), + DownloadResumePackage(item, index) + ) + + var connectionResult = + downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + resume + ) + //.also { println("Single episode finished with return code: $it") } + + // retry every link at least once + if (connectionResult <= 0) { + connectionResult = downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + true + ) + } + + if (connectionResult > 0) { // SUCCESS + removeKey(KEY_RESUME_PACKAGES, id.toString()) + break + } else if (index == item.links.lastIndex) { + downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) + } + } + } catch (e: Exception) { + logError(e) + } finally { + currentDownloads.remove(id) + // Because otherwise notifications will not get caught by the work manager + downloadCheckUsingWorker(context) + } + + // return id } fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? { @@ -1500,23 +1528,21 @@ object VideoDownloadManager { return success } - private fun deleteFile(context: Context, id: Int): Boolean { - val info = - context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false - downloadEvent.invoke(Pair(id, DownloadActionType.Stop)) - downloadProgressEvent.invoke(Triple(id, 0, 0)) - downloadStatusEvent.invoke(Pair(id, DownloadType.IsStopped)) - downloadDeleteEvent.invoke(id) - val base = basePathToFile(context, info.basePath) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) { + private fun deleteFile( + context: Context, + folder: UniFile?, + relativePath: String, + displayName: String + ): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && folder.isDownloadDir()) { val cr = context.contentResolver ?: return false val fileUri = - cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName) + cr.getExistingDownloadUriOrNullQ(relativePath, displayName) ?: return true // FILE NOT FOUND, ALREADY DELETED return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0 } else { - val file = base?.gotoDir(info.relativePath)?.findFile(info.displayName) + val file = folder?.gotoDir(relativePath)?.findFile(displayName) // val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName) // val dFile = File(normalPath) if (file?.exists() != true) return true @@ -1530,6 +1556,17 @@ object VideoDownloadManager { } } + private fun deleteFile(context: Context, id: Int): Boolean { + val info = + context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false + downloadEvent.invoke(Pair(id, DownloadActionType.Stop)) + downloadProgressEvent.invoke(Triple(id, 0, 0)) + downloadStatusEvent.invoke(id to DownloadType.IsStopped) + downloadDeleteEvent.invoke(id) + val base = basePathToFile(context, info.basePath) + return deleteFile(context, base, info.relativePath, info.displayName) + } + fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? { return context.getKey(KEY_RESUME_PACKAGES, id.toString()) } @@ -1544,10 +1581,12 @@ object VideoDownloadManager { downloadQueue.addLast(pkg) downloadCheck(context, notificationCallback) if (setKey) saveQueue() + //ret } else { - downloadEvent.invoke( + downloadEvent( Pair(pkg.item.ep.id, DownloadActionType.Resume) ) + //null } } From f571596bbc69d1fca24fb99dcef86227f03a4a80 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Sat, 19 Aug 2023 19:34:21 +0530 Subject: [PATCH 052/441] fix: expand resume watching sheet and ft: ripple on profile drawable when pressed (#566) * add ripple to profile icon on home * Update HomeParentItemAdapterPreview.kt --- .../cloudstream3/ui/home/HomeParentItemAdapterPreview.kt | 4 ++-- app/src/main/res/layout/fragment_home_head.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 943f784a..13497a99 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 @@ -539,7 +539,7 @@ class HomeParentItemAdapterPreview( resumeAdapter.updateList(resumeWatching) if (binding is FragmentHomeHeadBinding) { - binding.homeBookmarkParentItemTitle.setOnClickListener { + binding.homeWatchParentItemTitle.setOnClickListener { viewModel.popup( HomeViewModel.ExpandableHomepageList( HomePageList( @@ -578,4 +578,4 @@ class HomeParentItemAdapterPreview( } } } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/fragment_home_head.xml b/app/src/main/res/layout/fragment_home_head.xml index e13b96a8..90386ccf 100644 --- a/app/src/main/res/layout/fragment_home_head.xml +++ b/app/src/main/res/layout/fragment_home_head.xml @@ -61,7 +61,7 @@ android:layout_height="match_parent" android:layout_gravity="center" android:layout_marginStart="-50dp" - android:background="?android:attr/selectableItemBackgroundBorderless" + android:foreground="?android:attr/selectableItemBackgroundBorderless" android:contentDescription="@string/account" android:nextFocusLeft="@id/home_search" android:padding="10dp" @@ -288,4 +288,4 @@ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listitem="@layout/home_result_grid" /> - \ No newline at end of file + From b3abf1e45fbf2a532b551f9d69dbf7c674765ba7 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 17:03:27 +0200 Subject: [PATCH 053/441] fixed decryption --- .../cloudstream3/utils/M3u8Helper.kt | 24 ++++++++----------- .../utils/VideoDownloadManager.kt | 2 ++ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 1fb3a72d..5c0b45de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -88,21 +88,17 @@ object M3u8Helper2 { } } - private val defaultIvGen = sequence { - var initial = 1 + private fun defaultIv(index: Int) : ByteArray { + return toBytes16Big(index+1) + } - while (true) { - yield(toBytes16Big(initial)) - ++initial - } - }.iterator() - - fun getDecrypter( + fun getDecrypted( secretKey: ByteArray, data: ByteArray, - iv: ByteArray = "".toByteArray() + iv: ByteArray = byteArrayOf(), + index : Int, ): ByteArray { - val ivKey = if (iv.isEmpty()) defaultIvGen.next() else iv + val ivKey = if (iv.isEmpty()) defaultIv(index) else iv val c = Cipher.getInstance("AES/CBC/PKCS5Padding") val skSpec = SecretKeySpec(secretKey, "AES") val ivSpec = IvParameterSpec(ivKey) @@ -234,7 +230,7 @@ object M3u8Helper2 { if (tsData.isEmpty()) throw ErrorLoadingException("no data") return if (isEncrypted) { - getDecrypter(encryptionData, tsData, encryptionIv) + getDecrypted(encryptionData, tsData, encryptionIv, index) } else { tsData } @@ -272,13 +268,13 @@ object M3u8Helper2 { val match = ENCRYPTION_URL_IV_REGEX.find(m3u8Response)!!.groupValues - var encryptionUri = match[1] + var encryptionUri = match[2] if (isNotCompleteUrl(encryptionUri)) { encryptionUri = "${getParentLink(secondSelection.streamUrl)}/$encryptionUri" } - encryptionIv = match[2].toByteArray() + encryptionIv = match[3].toByteArray() val encryptionKeyResponse = app.get(encryptionUri, headers = headers, verify = false) encryptionData = encryptionKeyResponse.body.bytes() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index ef0d9d8a..dc3eaa25 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -803,6 +803,8 @@ object VideoDownloadManager { bytesDownloaded = 0 hlsWrittenProgress = 0 hlsProgress = 0 + if (id != null) + downloadDeleteEvent(id) //internalType = DownloadType.IsStopped notify() From 98b641714073e6bb84f74bd9ea3e19a03ed857aa Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 21:37:14 +0200 Subject: [PATCH 054/441] made downloader faster with parallel downloads --- .../ui/result/ResultViewModel2.kt | 6 +- .../utils/VideoDownloadManager.kt | 438 ++++++++++++++++-- 2 files changed, 395 insertions(+), 49 deletions(-) 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 2fe3b012..bdd27091 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 @@ -593,10 +593,8 @@ class ResultViewModel2 : ViewModel() { folder, if (link.url.contains(".srt")) ".srt" else "vtt", false, - null - ) { - // no notification - } + null, createNotificationCallback = {} + ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index dc3eaa25..d8ef7e85 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -40,14 +40,20 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly import java.io.Closeable import java.io.File import java.io.IOException -import java.io.InputStream import java.io.OutputStream import java.net.URL import java.util.* @@ -710,6 +716,8 @@ object VideoDownloadManager { data class DownloadMetaData( private val id: Int?, var bytesDownloaded: Long = 0, + var bytesWritten: Long = 0, + var totalBytes: Long? = null, // notification metadata @@ -732,10 +740,21 @@ object VideoDownloadManager { val approxTotalBytes: Long get() = totalBytes ?: hlsTotal?.let { total -> (bytesDownloaded * (total / hlsProgress.toFloat())).toLong() - } ?: 0L + } ?: bytesDownloaded private val isHLS get() = hlsTotal != null + private var stopListener: (() -> Unit)? = null + + /** on cancel button pressed or failed invoke this once and only once */ + fun setOnStop(callback: (() -> Unit)) { + stopListener = callback + } + + fun removeStopListener() { + stopListener = null + } + private val downloadEventListener = { event: Pair -> if (event.first == id) { when (event.second) { @@ -747,6 +766,8 @@ object VideoDownloadManager { type = DownloadType.IsStopped removeKey(KEY_RESUME_PACKAGES, event.first.toString()) saveQueue() + stopListener?.invoke() + stopListener = null } DownloadActionType.Resume -> { @@ -783,13 +804,14 @@ object VideoDownloadManager { override fun close() { // as we may need to resume hls downloads, we save the current written index - if (isHLS) { + if (isHLS || totalBytes == null) { updateFileInfo() } if (id != null) { downloadEvent -= downloadEventListener downloadStatus -= id } + stopListener = null } var type @@ -846,6 +868,11 @@ object VideoDownloadManager { updateFileInfo() } + if (internalType == DownloadType.IsStopped || internalType == DownloadType.IsFailed) { + stopListener?.invoke() + stopListener = null + } + // push all events, this *should* not crash, TODO MUTEX? if (id != null) { downloadStatus[id] = type @@ -874,6 +901,10 @@ object VideoDownloadManager { if (type == DownloadType.IsDownloading) checkNotification() } + fun addBytesWritten(length: Long) { + bytesWritten += length + } + /** adds the length + hsl progress and pushes a notification if necessary */ fun addSegment(length: Long) { hlsProgress += 1 @@ -885,6 +916,173 @@ object VideoDownloadManager { } } + /** bytes have the size end-start where the byte range is [start,end) + * note that ByteArray is a pointer and therefore cant be stored without cloning it */ + data class LazyStreamDownloadResponse( + val bytes: ByteArray, + val startByte: Long, + val endByte: Long, + ) { + val size get() = endByte - startByte + + override fun toString(): String { + return "$startByte->$endByte" + } + + override fun equals(other: Any?): Boolean { + if (other !is LazyStreamDownloadResponse) return false + return other.startByte == startByte && other.endByte == endByte + } + + override fun hashCode(): Int { + return Objects.hash(startByte, endByte) + } + } + + data class LazyStreamDownloadData( + private val url: String, + private val headers: Map, + private val referer: String, + /** This specifies where chunck i starts and ends, + * bytes=${chuckStartByte[ i ]}-${chuckStartByte[ i+1 ] -1} + * where out of bounds => bytes=${chuckStartByte[ i ]}- */ + private val chuckStartByte: LongArray, + val totalLength: Long?, + val downloadLength: Long?, + val chuckSize: Long, + val bufferSize: Int, + ) { + val size get() = chuckStartByte.size + + /** returns what byte it has downloaded, + * so start at 10 and download 4 bytes = return 14 + * + * the range is [startByte, endByte) to be able to do [a, b) [b, c) ect + * + * [a, null) will return inclusive to eof = [a, eof] + * + * throws an error if initial get request fails, can be specified as return startByte + * */ + @Throws + private suspend fun resolve( + startByte: Long, + endByte: Long?, + callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) + ): Long = withContext(Dispatchers.IO) { + var currentByte: Long = startByte + val stopAt = endByte ?: Long.MAX_VALUE + if (currentByte >= stopAt) return@withContext currentByte + + val request = app.get( + url, + headers = headers + mapOf( + // range header is inclusive so [startByte, endByte-1] = [startByte, endByte) + // if nothing at end the server will continue until eof + "Range" to "bytes=$startByte-" // ${endByte?.minus(1)?.toString() ?: "" } + ), + referer = referer, + verify = false + ) + val requestStream = request.body.byteStream() + + val buffer = ByteArray(bufferSize) + var read: Int + + try { + while (requestStream.read(buffer, 0, bufferSize).also { read = it } >= 0) { + val start = currentByte + currentByte += read.toLong() + + // this stops overflow + if (currentByte >= stopAt) { + callback(LazyStreamDownloadResponse(buffer, start, stopAt)) + break + } else { + callback(LazyStreamDownloadResponse(buffer, start, currentByte)) + } + } + } catch (e: CancellationException) { + throw e + } catch (t: Throwable) { + logError(t) + } finally { + requestStream.closeQuietly() + } + + return@withContext currentByte + } + + /** retries the resolve n times and returns true if successful */ + suspend fun resolveSafe( + index: Int, + retries: Int = 3, + callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) + ): Boolean { + var start = chuckStartByte.getOrNull(index) ?: return false + val end = chuckStartByte.getOrNull(index + 1) + + for (i in 0 until retries) { + try { + // in case + start = resolve(start, end, callback) + // no end defined, so we don't care exactly where it ended + if (end == null) return true + // we have download more or exactly what we needed + if (start >= end) return true + } catch (e: IllegalStateException) { + return false + } catch (e: CancellationException) { + return false + } catch (t: Throwable) { + continue + } + } + return false + } + + } + + @Throws + suspend fun streamLazy( + url: String, + headers: Map, + referer: String, + startByte: Long, + /** how many bytes every connection should be, by default it is 10 MiB */ + chuckSize: Long = (1 shl 20) * 10, + /** maximum bytes in the buffer that responds */ + bufferSize: Int = DEFAULT_BUFFER_SIZE + ): LazyStreamDownloadData { + // we don't want to make a separate connection for every 1kb + require(chuckSize > 1000) + + val contentLength = + app.head(url = url, headers = headers, referer = referer, verify = false).size + + var downloadLength: Long? = null + var totalLength: Long? = null + + val ranges = if (contentLength == null) { + LongArray(1) { startByte } + } else { + downloadLength = contentLength - startByte + totalLength = contentLength + LongArray((downloadLength / chuckSize).toInt()) { idx -> + startByte + idx * chuckSize + } + } + return LazyStreamDownloadData( + url = url, + headers = headers, + referer = referer, + chuckStartByte = ranges, + downloadLength = downloadLength, + totalLength = totalLength, + chuckSize = chuckSize, + bufferSize = bufferSize + ) + } + @Throws suspend fun downloadThing( context: Context, @@ -895,6 +1093,7 @@ object VideoDownloadManager { tryResume: Boolean, parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, + parallelConnections: Int = 3 ): Int = withContext(Dispatchers.IO) { // we cant download torrents with this implementation, aria2c might be used in the future if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { @@ -902,7 +1101,7 @@ object VideoDownloadManager { } var fileStream: OutputStream? = null - var requestStream: InputStream? = null + //var requestStream: InputStream? = null val metadata = DownloadMetaData( totalBytes = 0, bytesDownloaded = 0, @@ -926,11 +1125,13 @@ object VideoDownloadManager { val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN val resumeAt = (if (resume) fileLength else 0) metadata.bytesDownloaded = resumeAt + metadata.bytesWritten = resumeAt metadata.type = DownloadType.IsPending - // set up a connection - val request = app.get( - link.url.replace(" ", "%20"), + val items = streamLazy( + url = link.url.replace(" ", "%20"), + referer = link.referer, + startByte = resumeAt, headers = link.headers.appendAndDontOverride( mapOf( "Accept-Encoding" to "identity", @@ -941,17 +1142,12 @@ object VideoDownloadManager { "sec-fetch-dest" to "video", "sec-fetch-user" to "?1", "sec-ch-ua-mobile" to "?0", - ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap() - ), - referer = link.referer, - verify = false + ) + ) ) - // init variables - val contentLength = request.size ?: 0 - metadata.totalBytes = contentLength + resumeAt - - // save + metadata.totalBytes = items.totalLength + metadata.type = DownloadType.IsDownloading metadata.setDownloadFileInfoTemplate( DownloadedFileInfo( totalBytes = metadata.approxTotalBytes, @@ -961,23 +1157,166 @@ object VideoDownloadManager { ) ) - // total length is less than 5mb, that is too short and something has gone wrong - if (extension == "mp4" && metadata.approxTotalBytes < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION + val currentMutex = Mutex() + val current = (0 until items.size).iterator() - // read the buffer into the filestream, this is equivalent of transferTo - requestStream = request.body.byteStream() - metadata.type = DownloadType.IsDownloading + val fileMutex = Mutex() + // start to data + val pendingData: HashMap = + hashMapOf() - val buffer = ByteArray(DEFAULT_BUFFER_SIZE) - var read: Int - while (requestStream.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it } >= 0) { - fileStream.write(buffer, 0, read) + val jobs = (0 until parallelConnections).map { + launch { - // wait until not paused - while (metadata.type == DownloadType.IsPaused) delay(100) - // if stopped then break to delete - if (metadata.type == DownloadType.IsStopped) break - metadata.addBytes(read.toLong()) + // this may seem a bit complex but it more or less acts as a queue system + // imagine we do the downloading [0,3] and it response in the order 0,2,3,1 + // file: [_,_,_,_] queue: [_,_,_,_] Initial condition + // file: [X,_,_,_] queue: [_,_,_,_] + added 0 directly to file + // file: [X,_,_,_] queue: [_,_,X,_] + added 2 to queue + // file: [X,_,_,_] queue: [_,_,X,X] + added 3 to queue + // file: [X,X,_,_] queue: [_,_,X,X] + added 1 directly to file + // file: [X,X,X,X] queue: [_,_,_,_] write the queue and remove from it + + val callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) = + callback@{ response -> + if (!isActive) return@callback + fileMutex.withLock { + // wait until not paused + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then throw + if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed) { + this.cancel() + return@callback + } + + val responseSize = response.size + metadata.addBytes(response.size) + + if (response.startByte == metadata.bytesWritten) { + // if we are first in the queue then write it directly + fileStream.write( + response.bytes, + 0, + responseSize.toInt() + ) + metadata.addBytesWritten(responseSize) + } else { + // otherwise append to queue, we need to clone the bytes as they will be overridden otherwise + pendingData[response.startByte] = + response.copy(bytes = response.bytes.clone()) + } + + while (true) { + // remove the current queue start, so no possibility of + // while(true) { continue } in case size = 0, and removed extra + // garbage + val pending = pendingData.remove(metadata.bytesWritten) ?: break + + val size = pending.size + + fileStream.write( + pending.bytes, + 0, + size.toInt() + ) + metadata.addBytesWritten(size) + } + } + } + + // this will take up the first available job and resolve + while (true) { + if (!isActive) return@launch + fileMutex.withLock { + if (metadata.type == DownloadType.IsStopped) return@launch + } + + // just in case, we never want this to fail due to multithreading + val index = currentMutex.withLock { + if (!current.hasNext()) return@launch + current.nextInt() + } + + // in case something has gone wrong set to failed if the fail is not caused by + // user cancellation + if (!items.resolveSafe(index, callback = callback)) { + fileMutex.withLock { + if (metadata.type != DownloadType.IsStopped) { + metadata.type = DownloadType.IsFailed + } + } + return@launch + } + } + } + } + + // fast stop as the jobs may be in a slow request + metadata.setOnStop { + jobs.forEach { job -> + try { + job.cancel() + } catch (t: Throwable) { + logError(t) + } + } + } + + jobs.forEach { it.join() } + + // jobs are finished so we don't want to stop them anymore + metadata.removeStopListener() + + // set up a connection + //val request = app.get( + // link.url.replace(" ", "%20"), + // headers = link.headers.appendAndDontOverride( + // mapOf( + // "Accept-Encoding" to "identity", + // "accept" to "*/*", + // "user-agent" to USER_AGENT, + // "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", + // "sec-fetch-mode" to "navigate", + // "sec-fetch-dest" to "video", + // "sec-fetch-user" to "?1", + // "sec-ch-ua-mobile" to "?0", + // ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap() + // ), + // referer = link.referer, + // verify = false + //) + + // init variables + //val contentLength = request.size ?: 0 + //metadata.totalBytes = contentLength + resumeAt + //// save + //metadata.setDownloadFileInfoTemplate( + // DownloadedFileInfo( + // totalBytes = metadata.approxTotalBytes, + // relativePath = relativePath ?: "", + // displayName = displayName, + // basePath = basePath + // ) + //) + //// total length is less than 5mb, that is too short and something has gone wrong + //if (extension == "mp4" && metadata.approxTotalBytes < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION + //// read the buffer into the filestream, this is equivalent of transferTo + //requestStream = request.body.byteStream() + //metadata.type = DownloadType.IsDownloading + //val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + //var read: Int + //while (requestStream.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it } >= 0) { + // fileStream.write(buffer, 0, read) + // // wait until not paused + // while (metadata.type == DownloadType.IsPaused) delay(100) + // // if stopped then break to delete + // if (metadata.type == DownloadType.IsStopped) break + // metadata.addBytes(read.toLong()) + //} + + + if (metadata.type == DownloadType.IsFailed) { + return@withContext ERROR_CONNECTION_ERROR } if (metadata.type == DownloadType.IsStopped) { @@ -1003,11 +1342,12 @@ object VideoDownloadManager { // note that when failing we don't want to delete the file, // only user interaction has that power + metadata.removeStopListener() metadata.type = DownloadType.IsFailed return@withContext ERROR_CONNECTION_ERROR } finally { fileStream?.closeQuietly() - requestStream?.closeQuietly() + //requestStream?.closeQuietly() metadata.close() } } @@ -1388,20 +1728,28 @@ object VideoDownloadManager { } return suspendSafeApiCall { - downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback - ) - } - } + downloadThing( + context, + link, + name, + folder, + "mp4", + tryResume, + ep.id, + createNotificationCallback = { meta -> + main { + createNotification( + context, + source, + link.name, + ep, + meta.type, + meta.bytesDownloaded, + meta.bytesTotal, + notificationCallback + ) + } + }) }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN } From 61ca0a56bea81ef7bd18f943006df56ff4b8e707 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sat, 19 Aug 2023 21:38:29 +0200 Subject: [PATCH 055/441] Translations update from Hosted Weblate (#546) Co-authored-by: Alexandru Co-authored-by: Alexthegib Co-authored-by: Alexthegib Co-authored-by: Amir Co-authored-by: Astrid Co-authored-by: Carlos Luiz Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> Co-authored-by: Danilo Co-authored-by: Eryk Michalak Co-authored-by: Ettore Atalan Co-authored-by: Fjuro Co-authored-by: Htet Oo Hlaing Co-authored-by: Imprevisible Co-authored-by: Jan Haider Co-authored-by: Jimuel Mallari Co-authored-by: Massimo Pissarello Co-authored-by: Milo Ivir Co-authored-by: PiterDev Co-authored-by: Rex_sa Co-authored-by: Reza Almanda Co-authored-by: Rudy Tantono Co-authored-by: Skrripy Co-authored-by: dabao1955 Co-authored-by: gallegonovato Co-authored-by: george kitsoukakis Co-authored-by: infoekcz Co-authored-by: tuan041 --- app/src/main/res/values-ar/strings.xml | 3 +- app/src/main/res/values-bp/strings.xml | 79 ++- app/src/main/res/values-cs/strings.xml | 3 +- app/src/main/res/values-es/strings.xml | 3 +- app/src/main/res/values-fil/strings.xml | 2 + app/src/main/res/values-fr/strings.xml | 24 +- app/src/main/res/values-in/strings.xml | 4 +- app/src/main/res/values-it/strings.xml | 3 +- app/src/main/res/values-my/strings.xml | 556 ++++++++++++++++++ app/src/main/res/values-nl/strings.xml | 5 +- app/src/main/res/values-pl/strings.xml | 5 +- app/src/main/res/values-pt/strings.xml | 3 +- app/src/main/res/values-qt/strings.xml | 3 +- app/src/main/res/values-uk/strings.xml | 37 +- app/src/main/res/values-vi/strings.xml | 15 +- fastlane/metadata/android/pt/changelogs/2.txt | 1 + .../metadata/android/pt/full_description.txt | 10 + .../metadata/android/pt/short_description.txt | 1 + fastlane/metadata/android/pt/title.txt | 1 + fastlane/metadata/android/vi/changelogs/2.txt | 1 + .../metadata/android/vi/full_description.txt | 10 + .../metadata/android/vi/short_description.txt | 1 + fastlane/metadata/android/vi/title.txt | 1 + 23 files changed, 734 insertions(+), 37 deletions(-) create mode 100644 app/src/main/res/values-fil/strings.xml create mode 100644 app/src/main/res/values-my/strings.xml create mode 100644 fastlane/metadata/android/pt/changelogs/2.txt create mode 100644 fastlane/metadata/android/pt/full_description.txt create mode 100644 fastlane/metadata/android/pt/short_description.txt create mode 100644 fastlane/metadata/android/pt/title.txt create mode 100644 fastlane/metadata/android/vi/changelogs/2.txt create mode 100644 fastlane/metadata/android/vi/full_description.txt create mode 100644 fastlane/metadata/android/vi/short_description.txt create mode 100644 fastlane/metadata/android/vi/title.txt diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 9b440e6f..987211a5 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -584,4 +584,5 @@ @string/default_subtitles لا توجد اضافة في المستودع المستودع لم يتم العثور عليه، تحقق من العنوان اوجرب شبكة افتراضية خاصة(vpn) - + لقد صوتت بالفعل + \ No newline at end of file diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 38424e56..2a3bdb27 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -10,7 +10,7 @@ %dm Poster - @string/result_poster_img_des + Pôster Episode Poster Main Poster Next Random @@ -66,7 +66,7 @@ Erro Carregando Links Armazenamento Interno Dub - Leg + Sub Deletar Arquivo Assistir Arquivo Retomar Download @@ -257,7 +257,7 @@ Não mostrar de novo Pular essa Atualização Atualizar - Qualidade preferida + Qualidade preferida de reprodução (Wi-fi) Máximo de caracteres do título de vídeos Resolução do player de vídeo Tamanho do buffer do vídeo @@ -428,4 +428,75 @@ Começa o próximo episódio quando o atual termina Ativar NSFW em fornecedores compatíveis Fornecedores - + Reverter + Ações + votou com sucesso + Baixando atualização do aplicativo… + Referencias + Atualizações do App + Tocar com CloudStream + Automaticamente instale todos os plugins não instalados dos repositórios adicionados. + Reproduzir Trailer + Navegador + Copia de Segurança + A Barra de Progresso pode ser usada quando o player estiver oculto + Inscrever + Essa lista está vazia. Tente mudar para outra. + Reproduzir Livestream + Log do Teste + Baixa plugins automaticamente + Selecione o modo para filtrar os plugins baixados + Teste falhou + A Barra de Progresso pode ser usada quando o player estiver visível + Organizar + Sim + Você tem certeza que deseja sair\? + Instalando atualização do aplicativo… + Editar + Perfis + Exibindo Player - procure na Barra de Progresso + remover dos assitidos + Extensões + Alfabética(A => Z) + Abrir com + Selecionar Biblioteca + Passou + Sua biblioteca está vazia :0 +\nEntre numa conta de biblioteca ou adicione Midias para sua biblioteca local. + Qualidade preferida de reprodução (Dados Móveis) + Legado + Biblioteca + Não + Trilhas Sonoras + Votação(Baixa para Alta) + Atualização iniciada + Conteúdo +18 + Ajuda + Processo de configuração de Redo + Não pudemos instalar a nova versão do App + instalador de pacotes + Organizar por + Votação(Alta para Baixa) + Alfabética(Z => A) + Qualidade + Perfil de plano de fundo + Aqui você pode alterar como as fontes são ordenadas. Se um vídeo tiver uma prioridade mais alta, aparecerá mais alto na seleção da fonte. A soma da prioridade da fonte e da prioridade da qualidade é a prioridade do vídeo. +\n +\nFonte A: 3 +\nQualidade B: 7 +\nTerá uma prioridade de vídeo combinada de 10. +\n +\nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado! + Arquivo de modo de segurança encontrado! +\nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. + Inscrevel em %d + Episódio %d Lançado + Selecionar padrão + Disinscrevel em %d + Alguns aparelhos não possuem suporte para este pacote de instalação. Tente a opção legada se a atualização não instalar. + Dados móveis + Perfil %d + Atualizando shows inscritos + Player oculto - Procure na barra de progresso + Conteúdo +18 + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index f304199e..165dbbb4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -576,4 +576,5 @@ V repozitáři nebyly nalezeny žádné doplňky Repozitář nenalezen, zkontrolujte adresu URL a zkuste použít VPN @string/default_subtitles - + Již jste hlasovali + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 42e07c90..6326211e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -552,4 +552,5 @@ @string/default_subtitles No se encontraron complementos en el repositorio Repositorio no encontrado, comprueba la URL y prueba la VPN - + Ya has votado + \ No newline at end of file diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-fil/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 36c1cf1f..4cc56207 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -530,4 +530,26 @@ Joueur représenté - Montant de la recherche Joueur caché - Montant de la recherche Impossible d\'accéder à GitHub. Activation du proxy jsDelivr… - + Vous avez déjà voté + Désactivé + Ici, vous pouvez modifier la façon dont les sources sont ordonnées. Si une vidéo a une priorité plus élevée, elle apparaîtra plus haut dans la sélection de la source. La somme de la priorité source et de la priorité qualité est la priorité vidéo. +\n +\nSource A : 3 +\nQualité B : 7 +\nLa priorité vidéo combinée sera de 10. +\n +\nREMARQUE : Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé ! + Aucun plugin trouvé dans ce dossier + Dossier non trouvé, vérifiez l\'url et essayé un VPN + Données mobiles + Définir par défaut + Utiliser + Modifier + Profils + Aide + Profil %d + Wi-Fi + Qualités + L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s + Sélectionnez le mode pour filtrer le téléchargement des plugins + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 2bd86090..87a01ff9 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -574,4 +574,6 @@ Pilih mode untuk memfilter unduhan plugin Tidak ada plugin yang ditemukan di repositori Repositori tidak ditemukan, periksa URL dan coba VPN - + Kamu sudah voting + \@string/default_subtitles + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index dddc57c4..7cca78ca 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -574,4 +574,5 @@ Seleziona la modalità per filtrare il download dei plugin @string/default_subtitles Disabilita - + Hai già votato + \ No newline at end of file diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml new file mode 100644 index 00000000..cde13ba5 --- /dev/null +++ b/app/src/main/res/values-my/strings.xml @@ -0,0 +1,556 @@ + + + သရုပ်ဆောင်များ: %s + %dရက် %ddနာရီ %ddမိနစ် + %ddနာရီ %ddမိနစ် + %ddမိနစ် + ပိုစတာ + အပိုင်း ပိုစတာ + မိန်း ပိုစတာ + နောက် ကျပန်း + နောက်သို့ + နောက်ခံပုံရိပ်ကို အကြိုကြည့်ရန် + အဆင့်: %.1f + အပ်ဒိတ်အသစ်! +\n%s -> %s + စစ်ထုတ်မှု + %d မိနစ် + CloudStream + CloudStream ဖြင့်ကြည့်ရန် + ပင်မ + ရှာရန် + ရှာရန်… + ရှာရန် %s… + အချက်အလက်မရှိပါ + အခြားရွေးစရာများ + နောက်အပိုင်း + ကဏ္ဍများ + မျှဝေမည် + ဘရောက်ဇာတွင်ဖွင့်ရန် + ဘရောက်ဇာ + မစောင့်တော့ပါ + ကြည့်နေသည် + ကြည့်ပြီး + ကြည့်ခြင်းရပ်ထားသော + ဘာမျှ + လင့်များချိတ်ဆက်ရာတွင်အချို့အယွင်း + ဖုန်း သိုလှောင်ရုံ + စာမှတ်များ စစ်ထုတ်မှု + စာမှတ်များ + ဖယ်ရှားရန် + ကြည့်ရှုမှုအခြေအနေသတ်မှတ်ခြင်း + မိတ္တူကူးရန် + ပိတ်ရန် + ရှင်းလင်းရန် + သိမ်းဆည်းရန် + ကြည့်ရှုမှုအရှိန် + နောက်ခံ အရောင် + ဝင်းဒိုး အရောင် + အစွန်းနားပုံစံ + စာတန်းထိုး အမြင့် + ဖောင့် + ဖောင့် အရွယ်အစား + အမျိုးအစားများအသုံးပြု၍ရှာရန် + %d အက်ပ်ဖန်တီးသူတွေကိုကျေးဇူးတင်ကြောင်းပို့မည် + အလိုအလျောက် ဘာသာစကားရွေးချယ်ခြင်း + ဒေါင်းလုဒ် လုပ်ထားသော ဘာသာစကားများ + မူရင်းပုံစံအတိုင်းပြန်ထားရန်ဖိထားပါ + ဆက်လက်ကြည့်ရှုမည် + ဖယ်ရှားရန် + ပိုမို၍ + \@string/home_play + ဒီဟာကတောရပ်တစ်ခုပါ ဗီပီအန်တစ်ခုသုံးဖို့အကြံပြုပါတယ် + ဖော်ပြချက် + ဇာတ်လမ်းသွား မတွေ့ပါ + ဖော်ပြချက် မရိှပါ + Logcat ပြရန် 🐈 + Log + ရုပ်ပုံထပ် + ကြည့်ရှုမှု စခရင်အရွယ်အစားချိန်ညိှမှု + စာတန်းထိုးများ + ကြည့်ရှုမှုစာတန်းထိုးပြုပြင်စရာများ + Eigengravy လုပ်ဆောင်မှု + ရစ်ရန်ဘယ်ညာဆွဲပါ + သင်ရောက်နေတဲ့နေရာပြောင်းရန်ဘယ်ညာဆွဲပါ + ပြုပြင်စရာရိှပါက ပွတ်ဆွဲပါ + နောက်အပိုင်းကို အလိုအလျောက် ဖွင့်ပါ + ကျော်ရန်နှစ်ချက်နှိပ်ပါ + ရပ်ရန်နှစ်ချက်နှိပ်ပါ + ကျော်လိုသောပမာဏ (စက္ကန့်များ) + ရှေ့သို့ကျော်ရန် သို့ နောက်သို့ရစ်ရန် ဘယ် သို့ ညာ ပေါ်မှာနှစ်ချက်နှိပ်ပါ + ရပ်ရန် အလယ်တွင်နှစ်ချက်နှိပ်ပါ + ဖုန်းအလင်းအမှောင်အတိုင်းသုံးမည် + အက်ပ်ကြည့်ရှုမှုထဲမှာ ဖုန်းအလင်းအမှောင်အတိုင်းသုံးမည် + ကြည့်ရှုမှုတိုးတက်ခြင်းကိုအပ်ဒိတ်လုပ်ပါ + အရန်သိမ်းဖိုင်မှပြန်သိုလှောင်မည် + အရန်သိမ်းမည် + အရန်သိမ်းဖိုင်များရယူပြီး + အရန်သိမ်းဖိုင်မှပြန်သိုလှောငးခြင်မအောင်မြင်ပါ %s + သိုလှောင်ပြီး + သိုလှောင်ရုံခွင့်ပြုချက်မရိှပါ။ပြန်ကြိုးစားပါ။ + အရန်သိမ်းနေစဥ်အချို့အယွင်း %s + လိုက်ဘရီ + အပ်ဒိတ်များနှင့်အရန်သိမ်းဆည်းမှု + နက်နက်ရှိုင်းရှိုင်းရှာခြင်း + သင့်ကိုဝန်ဆောင်မှုပေးသူအလိုက်ရှာဖွေမှုရလဒ်များပေးမည် + ချို့ယွင်းမှုအကြီးစားဖြစ်မှသာဒေတာများပေးပို့ပါ + anime များအတွက်ဖြည့်စွက်အပိုင်းကိုပြရန် + ထွေလာများကိုပြရန် + Kitsu မှ ပိုစတာများကိုပြရန် + အလိုအလျောက် ဖြည့်စွက်လုပ်ဆောင်ချက်များကိုအပ်ဒိတ်တင်ခြင်း + အပိုလုပ်ဆောင်ချက်များကိုစစ်ထုတ်ရန်မုဒ်ရွေးပါ + အက်ပ်အပ်ဒိတ်များပြရန် + အစီအစဥ်ချခြင်းကိုပြန်စမည် + အက်ပ်ထည့်သွင်းခြင်း + အချို့ဖုန်းတွေက အက်ပ်ထည့်သွင်းခြင်းလုပ်ဆောင်ချက်အသစ်ကို မပံ့ပိုးပါဘူး။အကယ်၍အလုပ်မဖြစ်ပါကသမားရိုးကျနည်းလမ်းကိုအသုံးပြုပါ။ + Github + ဤဝန်ဆောင်မှုပေးသူသည် Chromecast ကိုမပံ့ပိုးပါ + လင့်များမတွေ့ပါ + ကလစ်ဘုတ်သို့မိတ္တူကူးပြီး + အပိုင်းကြည့်မည် + အတွဲ + အတွဲမရှိပါ + အပိုင်း + အပိုင်းများ + %d-%d + ဒါကအပြီးဖျက်ခြင်းဖြစ်ပါသည် %s +\nသင်သေချာပါသလား။ + %dမိနစ် +\nကျန်ရိှသည် + ထုတ်လွှင့်နေဆဲ + ထုတ်လွှင့်မှုပြီးဆုံး + အခြေအနေ + ခုနစ် + အဆင့်သတ်မှတ်ချက် + ကြာချိန် + ဆိုဒ် + အကျဥ်းချုပ် + နောက်အစီအစဥ် + စာတန်းထိုးမထည့် + ပုံသေ + \@string/default_subtitles + ကျန်ရှိသော + အက်ပ် + ရုပ်ရှင်များ + ဇာတ်လမ်းတွဲများ + ကာတွန်းများ + Anime + ေတာရပ်များ + မှတ်တမ်းရုပ်ရှင်များ + OVA + အာရှ ဒရာမာများ + တိုက်ရိုက်ထုတ်လွှင်မှုများ + အပြာဗီဒီယိုများ + အခြား + ရုပ်ရှင် + ဇာတ်လမ်းတွဲ + OVA + တောရပ် + မှတ်တမ်းရုပ်ရှင် + အာရှ ဒရာမာ + တိုက်ရိုက်ထုတ်လွှင့်မှု + အပြာဗီဒီယို + ဗီဒီယို + ရင်းမြစ်အချို့အယွင်း + အဝေးထိန်းချုပ်မှုအချို့အယွင်း + တင်ဆက်သူ အချို့အယွင်း + မျှော်လင့်မထားသော အချို့အယွင်း + Chromecast အပိုင်း + Chromecast ဖန်သားပြင် + အက်ပ်တွင်းဖွင့် + ဖွင့်ရန် %s + ဘရောက်ဇာထဲမှာ ဖွင့်ရန် + လင့်ကူးယူရန် + အလိုအလျောက်ဒေါင်းလုဒ် + လင့်များကို ပြန်စစ်ရန် + အရည်အသွေး အမှတ်အသား + နောက်ခံအသံ အမှတ်အသား + စာတန်း အမှတ်အသား + ခေါင်းစဥ် + အပ်ဒိတ်မရှိပါ + အပ်ဒိတ်စစ်ရန် + လော့ခ်ခတ်ရန် + ရင်းမြစ် + OPကိုကျော်ရန် + ဒီအပ်ဒိတ်ကိုကျော်ပါ + အပ်ဒိတ် + ခေါင်းစဥ်အတွက်စာလုံးရေပြည့်ခြင်း + ကြည့်ရှုမှု အရည်အသွေး + ဗီဒီယိုရှေ့ပြေးသိမ်းဆည်းမှုပမာဏ + ကြည့်ရှုမှုဘားမြင်တွေ့ရချိန်တွင်ပြသသောပမာဏ + ဝှက်ထားသောကြည့်ရှုပြီးသောပမာဏ + ဝှက်ထားသည့်အခါ အသုံးပြုသည့် ရှာဖွေမှုပမာဏ + Android TV ကဲ့သို့သော မမ်မိုရီနည်းသော စက်ပစ္စည်းများတွင် သတ်မှတ်နှုန်း အလွန်မြင့်မားပါက ပျက်စီးမှုများ ဖြစ်စေသည်။ + DNS over HTTPS + raw.githubusercontent.com ပရောက်စီ + GitHub သို့ မရောက်ရှိနိုင်ပါ။ jsDelivr ပရောက်စီကို ဖွင့်နေသည်… + ကလုန်း ဆိုဒ် + ဆိုဒ်ကိုဖယ်ရှားရန် + မတူညီသော URL တစ်ခုဖြင့် ရှိပြီးသား ဝဘ်ဆိုက်တစ်ခု၏ ပုံတူတစ်ခုကို ထည့်ပါ + ဒေါင်းလုဒ်လမ်းကြောင်း + NGINX ဆာဗာ URL + Dubbed/Subbed Anime ကိုပြသပါ + မျက်နှာပြင်နှင့် အံကိုက် + ဆန့်သည် + ချဲ့သည် + ရှင်းလင်းချက် + ISP ရှောင်လွှဲမှုများ + လင့်များ + အက်ပ်အပ်ဒိတ်များ + Extensions + ဆောင်ရွက်ချက်များ + Cache + Android တီဗွီ + စာတန်းထိုးများ + ပုံသေများ + ပုံပန်းသဏ္ဌာန် + အထွေထွေ + ပင်မစာမျက်နှာမှာကျပန်းခလုတ်ကိုပြပါ + %s အပိုင်း %d + အပိုင်း %d ထုတ်လွှင့်ပြသမည် + ပိုစတာ + ပံ့ပိုးပေးသောဝန်ဆောင်မှုပြောင်းရန် + အရှိန် (%.2fx) + ဒေါင်းလုဒ်များ + ပြင်ဆင်ရန် + ခဏစောင့်ပါ… + ကြည့်ဆဲ + ကြည့်ရန် + ပြန်ကြည့်နေသည် + စာတန်းထိုး + ရုပ်ရှင်ကြည့်မည် + ထွေလာ ကြည့်မည် + လိုက်ခ် ကြည့်မည် + တောရပ် ကြည့်မည် + ရင်းမြစ်များ + ချိတ်ဆက်မှုပြန်ကြိုးစား… + နောက်သို့ + အပိုင်း ကြည့်မည် + ဒေါင်းလုဒ် + ဒေါင်းလုဒ် လုပ်ပြီး + ဒေါင်းလုဒ် လုပ်နေသည် + ဒေါင်းလုဒ် ရပ်ထား + ဒေါင်းလုဒ်စတင် + ဒေါင်းလုဒ် မအောင်မြင် + ဒေါင်းလုဒ် ပယ်ဖျက်ပြီး + ဒေါင်းလုဒ်ပြီးစီး + အပ်ဒိတ်စတင် + တိုက်ရိုက်ကြည့်မည် + နောက်ခံအသံ + ရှာဖွေမှုရလဒ်များတွင်ရွေးချယ်ထားသောဗီဒီယိုအရည်အသွေးကိုဝှက်ထားရန် + စာတန်းထိုး + ဖိုင်ဖျက်ရန် + ဖိုင်ကို ဖွင့်ရန် + ဒေါင်းလုဒ် ဆက်လုပ်ရန် + ဒေါင်းလုဒ် ရပ်ရန် + အလိုအလျောက်အက်ပ်ချို့ယွင်းချက်ပေးပို့ခြင်းကိုပိတ်မည် + ပိုမို၍ + ပ့ံပိုးပေးသောဝန်ဆောင်မှုများအသုံးပြု၍ရှာရန် + ဝုက်ရန် + ကျေးဇူးတင်ကြောင်းမပို့ရသေး + ကြည့်မည် + အချက်အလက် + စာတန်းထိုး ဘာသာစကား + ဒီမှာနေရာချခြင်းဖြင့်ဖောင့်များကိုသွင်းပါ %s + အတည်ပြု + ပယ်ဖျက်ရန် + စာတန်းထိုး ပြုပြင်ခြင်း + စာသား အရောင် + အနားကွပ် အရောင် + ဒီပံ့ပိုးမှုကောင်းမွန်စွာအလုပ်လုပ်ရန်ဗီပီအန်တစ်ခုလိုနိုင်ပါတယ် + အသေးစိတ်အချက်အလက်များပြမထားပါ။ဝဘ်ဆိုဒ်ပေါ်မှာမရှိလျှင်ကြည့်ရှု၍မရနိုင်ပါ။ + ဖြည့်စွက်လုပ်ဆောင်ချက်များကို အလိုအလျောက်ဒေါင်းလုဒ်လုပ်ခြင်း + ရီပိုစစ်ထရီများမှမထည့်သွင်းရသေးသောဖြည့်စွက်လုပ်ဆောင်ချက်များအားလုံးကိုထည့်သွင်းပါ။ + ပြန်ကြည့်ခြင်းကိုအသေးစား ကြည့်ရှုမှုတွင်ဆက်ပြပါ + အနက်ရောင်ဘောင်များကို ဖယ်ရှားရန် + အက်ပ်ထဲဝင်လိုက်သည့်နှင့်အက်ပ်အပ်ဒိတ်ကိုစစ်ဆေးပါ။ + Chromecast စာတန်းထိုးများ + Chromecast စာတန်းထိုး ပြုပြင်ရန် + ကြည့်ရှုမှုပုံစံထဲမှာအရိှန်ရွေးစရာတစ်ခုထည့်ရန် + အသံအတိုးအကျယ်နှင့်အလင်းအမှောင်များကိုချိန်ညိှရန် ဘယ် သို့ ညာ ဘက်တွင် အပေါ်အောက်ဆွဲပါ + ယခုကြည့်နေသောအပိုင်းပြီးပါကနောက်အပိုင်းကိုဖွင့်ပါ + သင့်၏အပိုင်းကြည်ရှုမှုရောက်ရှိနေရာကိုအလိုအလျောက်သိမ်းဆည်းပါ + ရှာရန် + အကောင့်များ + အချက်အလက် + ဒေတာများမပို့ရန် + ကြည့်ရှုပြီးသောအချိန်ပမာဏ + Android TV ကဲ့သို့သော သိုလှောင်မှုနေရာနည်းပါးသော စက်ပစ္စည်းများတွင် အလွန်မြင့်မားစွာ သတ်မှတ်ပါက ပြဿနာများ ဖြစ်လာနိုင်သည်။ + ISP ပိတ်ဆို့ခြင်းကို ကျော်လွှားရန်အတွက် အသုံးဝင်သည် + jsDelivr ကို အသုံးပြု၍ GitHub ပိတ်ဆို့ခြင်းကို ကျော်ဖြတ်သည်။ အပ်ဒိတ်များကို ရက်အနည်းငယ်ကြာအောင် နှောင့်နှေးစေနိုင်သည်။ + အရန်သိမ်းထားသော + လက်ဟန်များ + ကြည့်ရှုမှုလုပ်ဆောင်ချက်များ + အပြင်အဆင် + လုပ်ဆောင်ချက်များ + ကျပန်းခလုတ် + ရှေ့ပြေးအပ်ဒိတ်များကိုထည့်သွင်းပါ + ပုံမှန်အပ်ဒိတ်များအစား ရှေ့ပြေးအက်ဒိတ်များကိုရှာပါ + တူညီသောအက်ပ်ရေးသားသူများ၏ ဝတ္ထုရှည်များဖတ်နိုင်သည့် အက်ပ် + တူညီသောအက်ပ်ရေးသားသူများ၏ Anime အက်ပ် + Discord ကိုဝင်ရန် + အက်ပ်ရေးသားသူများထံ ကျေးဇူးတင်စာပို့မည် + ပေးခဲ့သောစာအရေအတွက် + အက်ပ်ဘာသာစကား + မူလအခြေအနေများကိုပြန်ထားပါ + စိတ်မကောင်းပါ။အက်ပ်ရပ်တန့်သွားပါတယ်။အမည်မဖော်ထားတဲ့တင်ပြချက်ကို အက်ပ်ရေးသားသူများထံ ပို့မှာဖြစ်ပါတယ် + %s %d%s + အတွဲ + %d %s + အပိုင်း + အပိုင်းများမတွေ့ပါ + ဖိုင်ကိုဖျက်ရန် + ဖျက်ရန် + ရပ်ရန် + စရန် + မအောင်မြင်ပါ + ကျော်ဖြတ်ပြီး + ကြည့်လက်စ + -30 + +30 + အသုံးပြုပြီးသော + ကာတွန်း + Anime + ဒေါင်းလုဒ် အချို့အယွင်း၊သိုလှောင်ရုံခွင့်ပြုချက်တွေကိုစစ်ဆေးပါ + ဒေါင်းလုဒ် ကြေးမုံ + စာတန်းထိုးများကို ဒေါင်းလုဒ်လုပ်ရန် + ပိုစတာပေါ်ရှိ UI အစိတ်အပိုင်းများကို ပြောင်းပါ + ပြန်ညိှ + နောက်ထပ်မပြရန် + ဝိုင်ဖိုင်ဖြင့်ကြည့်စဥ်ဗီဒီယိုအရည်အသွေး + မိုဘိုင်းဒေတာဖြင့်ကြည့်စဥ်ဗီဒီယိုအရည်အသွေး + ဗီဒီယိုရှေ့ပြေးသိမ်းဆည်းမှုအကွာအဝေး + ဗီဒီယိုcacheအများ + ဗီဒီယို cache နှင့် ရုပ်ပုံ cache များကိူရှင်းလင်းရန် + ပံ့ပိုးပေးထားသည့် ဝန်ဆောင်မှုများပေါ်တွင် အပြာဗီဒီယို ကို ဖွင့်ပါ + ဝန်ဆောင်မှုပံ့ပိုးသူဘာသာစကား + အက်ပ်အပြင်အဆင် + ဦးစားပေးမီဒီယာ + အက်ပ် အပြင်အဆင် + ပိုစတာခေါင်းစဉ်တည်နေရာ + ခေါင်းစဉ်ကို ပိုစတာအောက်မှာ ထားပါ + ဖုန်းအပြင်အဆင် + အင်မြူလိတ်တာ အပြင်အဆင် + အဓိကအရောင် + အကြံပြုသည် + ဒီဗွီဒီ + 4K + ထုတ်လွှင့်ရန် လင့်ခ် နှင့်ချိတ်ပါ + ရည်ညွှန်းသည် + ရှေ့သို့ + နောက်သို့ + အပ်ဒိတ်လုပ်ပြီး %d ဖြည့်စွက်များ + ဒေါင်းလုဒ်မလုပ်ရသေး: %d + ဝဘ်ဘရောက်ဇာ + အက်ပ်မတွေ့ပါ + ဘာသာစကားအားလုံး + ကျော်ရန် %s + အစမှပြန်စ + ရောထားသောအဆုံးပိုင်း + ရောထားသောအစပိုင်း + ခရက်ဒစ်များ + အစ + သေချာသည် + သမားရိုးကျ + ထည့်သွင်းသူ + ထွက်ချိန်တွင် အက်ပ်ကို အပ်ဒိတ်လုပ်ပါမည် + CloudStream တွင် မူရင်းအတိုင်း ထည့်သွင်းထားသည့်ဆိုက်များ မရှိပါ။ ရီပိုစစ်ထရီများ မှ ဆိုဒ် များကို ထည့်သွင်းရန်လိုအပ်သည်။ +\n +\nSky UK Limited မှ ဦးနှောက်မဲ့ DMCA ကို ဖယ်ရှားလိုက်ခြင်းကြောင့် 🤮 ကျွန်ုပ်တို့သည် ရီပိုစစ်ထရီဆိုဒ်ကို အက်ပ်တွင် ချိတ်ဆက်၍မရပါ။ +\n +\nကျွန်ုပ်တို့၏ Discord တွင်ပါဝင်ပါ သို့မဟုတ် အွန်လိုင်းတွင်ရှာဖွေပါ။ + အခြားသူများ၏ရီပိုစစ်ထရီများကိုရှာဖွေမည် + အသံများ + အသံဖိုင်များ + ဗီဒီယိုအသံဖိုင်များ + ပြန်စတင်ချိန်မှာအသုံးပြုပါ + ရပ်ရန် + လုံခြုံသောမုဖွင့်ရန် + ဖုန်းတွင်းကြည့်ရှုမှု + ဦးစားပေး ဗီဒီယိုဖွင့်စက် + ပြဿနာဖြစ်စေသည့်အရာကို သင်ရှာဖွေရာတွင် အထောက်အကူဖြစ်စေရန်အတွက် ပျက်စီးမှုတစ်ခုကြောင့် အဆက်များအားလုံးကို ပိတ်ထားသည်။ + အဆင့်သတ်မှတ်ချက်များ: %s + ပျက်စီးမှုအချက်အလက်ကို ကြည့်ပါ + ဖော်ပြချက် + ဗားရှင်း + အခြေအနေ + အရွယ်အစား + ရေးသားသူများ + HLS ဖွင့်စဥ် + ကြည့်ရှုခဲ့သည်များ + အက်ပ်၏ ဗားရှင်းအသစ်ကို ထည့်သွင်း၍မရပါ + အစပိုင်း/အဆုံးပိုင်းအတွက် ကျော်နိုင်သော ပေါ့ပ်အပ်များကို ပြပါ + စာသားအလွန်များသဖြင့်ကလစ်ဘုတ်တွင် သိမ်းဆည်း၍မရပါ။ + ပံ့ပိုးပေးသူများ + အပြင်အဆင် + အလိုအလျောက် + တီဗွီအပြင်အဆင် + သင့်စကားဝှက် + သင့်ယူဇာနိမ်း + သင့်အီးမေးလ် လိပ်စာ + 127.0.0.1 + သင့်ဆိုဒ် + example.com + အရိပ် + ထမြောက်မှု + စာတန်းထိုးများ ထပ်တူပြုရန် + စာတန်းထိုးများ အလွန်စောနေပါက %d ms ဒီဟာကိုသုံးပါ + The quick brown fox jumps over the lazy dog + တင်ပြီး %s + ဖိုင်မှတင်သွင်းပြီး + ရုံရိုက် + အကြည် + အကြည် + UHD + HDR + SDR + Web + WP + SD + ကြည့်ရှုမှု + ပိုစတာပုံရိပ် + စာတန်းထိုးများမှ bloat ကိုဖယ်ရှားပါ + နှစ်သက်ရာ မီဒီယာဘာသာစကားဖြင့် စစ်ထုတ်ပါ + အပိုများ + ဒေတာမမှန်ပါ + URL မမှန်ပါ + အချို့အယွင်း + စာတန်းထိုးများမှ ပိတ်ထားသော စာတန်းများကို ဖယ်ရှားပါ + ထွေလာ + ဖြည့်စွက်များ + ရီပိုစစ်ထရီ ဖြည့်စွက်များအားလုံးကိုဖျက်မည်ဖြစ်သည် + ရီပိုစစ်ထရီ ကိုဖျက်ရန် + ဤရီပိုစစ်ထရီမှ ဖြည့်စွက်များအားလုံးကို ဒေါင်းလုဒ်လုပ်မှာလား\? + %s (ပိတ်ပြီး) + ထောက်ပံ့ထားသော + ဘာသာစကား + အဆက်များကိုအရင်သွင်းပါ + VLC + MPV + ဝဘ်ထဲတွင်ဖွင့်ရန် + အစပိုင်း + အဆုံးပိုင်း + ကြည့်ရှုခဲ့သည်များကိုရှင်းရန် + ကြည့်ပြီးသည်မှဖယ်ရှားရန် + သင်ထွက်ရန်သေချာပြီလား + မသေချာပါ + အက်ပ်အပ်ဒိတ်အားဒေါင်းလုဒ်လုပ်နေသည်… + အက်ပ်အပ်ဒိတ်အားသွင်းနေသည်… + အပ်ဒိတ်ဖြစ်မှု (အသစ် မှ အဟောင်း) + သင့်လိုက်ဘရီသည် ဗလာဖြစ်နေသည် :( +\nအကောင့်ဝင်ပါ သို့မဟုတ် သင့်ဖုန်းလိုက်ဘရီတွင် ကြည့်စရာများထည့်ပါ။ + သုံးရန် + တည်းဖြတ်ရန် + အရည်အသွေးများ + စာတန်းထိုး ကုဒ်လုပ်ခြင်း + ပံ့ပိုးပေးသူ စစ်ဆေးမှု + %s %s + အကောင့်ဝင်မည် + အကောင့်ပြောင်းမည် + ချိန်ညိှခြင်း + /%d + အင်တာနက်မှ တင်သွင်းမည် + နောက်ခံ + အကောင့်မဝင်ရောက်နိုင်ပါ %s + ဘာမျှ + အနည်းဆုံး + အနားကွပ် + ကျပန်း + ချုံ့ပြီး + 1000 ms + စာတန်းထိုး ကြန့်ကြာမှု + စာတန်းထိုးများအလွန်နောက်ကျနေပါက %d ms ဒီဟာကိုသုံးပါ + စာတန်းထိုး ကြန့်ကြာမှု သတ်မှတ်ထားခြင်းမရှိ + ရုံရိုက် + ရုံရိုက် + TC + အရည်အသွေး + အစီအစဥ်ချခြင်းကိုကျော်မည် + ချို့ယွင်းမှုသတင်းပေးပို့ခြင်း + ဘာတွေကြည့်ချင်လဲ + ပြီးပြီ + အဆက်များ + မသွင်းနိုင်ပါ %s + သင့်စက်ပစ္စည်းနှင့် ကိုက်ညီစေရန် အက်ပ်၏အသွင်အပြင်ကို ပြောင်းလဲပါ + ရီပိုစစ်ထရီထည့်ရန် + အသက်ပြည့်ပြီးသူများသာ + ရီပိုစစ်ထရီအမည် + ရီပိုစစ်ထရီ URL + ဖြည့်စွက်များ ထည့်ပြီး + ဤဘာသာစကားများဖြင့် ဗီဒီယိုများကို ကြည့်ရှုပါ + ဖြည့်စွက်များ ဒေါင်းလုဒ်လုပ်ပြီး + ဖြည့်စွက်များဖျက်ပြီး + ဒေါင်းလုဒ်လုပ်ခြင်း စတင်သည် %d %s… + ဒေါင်းလုဒ်လုပ်ပြီး %d %s + အားလုံး %s ဒေါင်းလုဒ်လုပ်ပြီးသား + ရီပိုစစ်ထရီထဲတွင်ဖြည့်စွက်များမတွေ့ပါ + ရီပိုစစ်ထရီမတွေ့ပါ၊URLကိုပြန်စစ်ပြီးဗီပီအန်ဖြင့်ကြိုးစားကြည့်ပါ + အသုတ်လိုက် ဒေါင်းလုဒ် + ဖြည့်စွက် + သင်အသုံးပြုလိုသောဆိုက်များစာရင်းကို ဒေါင်းလုဒ်လုပ်ပါ + ဒေါင်းလုဒ်လုပ်ပြီး: %d + ပိတ်ပြီး: %d + ဘာသာစကားကုဒ် (en) + အကောင့် + အကောင့်ထွက်မည် + အကောင့်ထည့်မည် + အကောင့်ဖွင့်မည် + စောင့်ကြည့်ခြင်းထည့်မည် + ထည့်ပြီး %s + အဆင့်သတ်မှတ်ထားပြီး + %d / 10 + /\?\? + %s ချိတ်ဆက်ပြီး + ပိတ်ပါ + ပုံမှန် + အားလုံး + အပြည့် + ဖိုင်ဒေါင်းလုဒ်လုပ်ပြီး + အဓိက + ထောက်ပံ့သည် + ရင်းမြစ် + မကြာမီလာမည်… + TS + Blu-ray + အရည်အသွေးနှင့်ခေါင်းစဥ် + ခေါင်းစဥ် + အိုင်ဒီမမှန်ပါ + အများမြင်နိုင်သော + စာတန်းထိုးအားလုံးကို စာလုံးအကြီးပြောင်းပါ + ပြန်စတင်မည် + ကြည့်ပြီးအဖြစ်မှတ်ရန် + အစီအစဥ်ချမှု + အစီအစဥ် + အဆင့်သတ်မှတ်ချက် (အမြင့်ဆုံးမှအနိမ့်ဆုံးသို့) + အဆင့်သတ်မှတ်ချက် (အနိမ့်ဆုံး မှ အမြင့်ဆုံးသို့) + အပ်ဒိတ်ဖြစ်မှု (အဟောင် မှ အသစ်) + အက္ခရာစဥ်လိုက် (A မှ Z) + အက္ခရာစဥ်လိုက် (Z မှ A) + ပြောင်းပြန် + စာရင်းသွင်းထားသောရှိုးများကိုအပ်ဒိတ်လုပ်နေသည် + စာရင်းသွင်းပြီး + စာရင်းသွင်းပြီး %s + စာရင်းသွင်းမှုပယ်ဖျက်ပြီး %s + ဤစာရင်းသည် ဗလာဖြစ်နေသည်။ အခြားတစ်ခုသို့ ပြောင်းကြည့်ပါ။ + Safe mode ဖိုင်ကို တွေ့ရှိခဲ့သည်။ +\nဖိုင်ကိုမဖယ်ရှားမချင်း စတင်ဖွင့်စတွင် မည်သည့် extension များကိုမျှ မတင်ပါ။ + အပိုင်းသစ် %d ထွက်ပြီ + ပရိုဖိုင် %d + ဝိုင်ဖိုင် + မိုဘိုင်းဒေတာ + ပုံသေထားရန် + ပရိုဖိုင်များ + အကူအညီ + ဤနေရာတွင် သင်သည် အရင်းအမြစ်များကို မည်ကဲ့သို့ အစီအစဥ်ချမည်ကို ပြောင်းလဲနိုင်သည်။ ဗီဒီယိုတစ်ခုတွင် ပိုမိုဦးစားပေးပါက ရင်းမြစ်ရွေးချယ်မှုတွင် ပိုမိုမြင့်မားလာမည်ဖြစ်သည်။ အရင်းအမြစ် ဦးစားပေးနှင့် အရည်အသွေး ဦးစားပေး၏ ပေါင်းစုသည် ဗီဒီယို ဦးစားပေးဖြစ်သည်။ +\n +\nအရင်းအမြစ် A: 3 +\nအရည်အသွေး B: 7 +\nပေါင်းစပ်ဗီဒီယို ဦးစားပေး 10 ခု ရှိပါမည်။ +\n +\nမှတ်ချက်- ပေါင်းလဒ်သည် 10 သို့မဟုတ် ထို့ထက်ပိုပါက ထိုလင့်ခ်ကို တင်သည့်အခါ ဗီဒီယိုဖွင့်စက်သည် အလိုအလျောက် ဒေါင်းလုဒ်ကို ကျော်သွားမည်ဖြစ်သည် + ပရိုဖိုင်နောက်ခံ + UI ကို မှန်ကန်စွာ ဖန်တီး၍မရပါ၊ ၎င်းသည် အဓိက ချို့ယွင်းချက်တစ်ခုဖြစ်ပြီး ချက်ချင်းသတင်းပို့သင့်သည်။ %s + သင်နဂိုတည်းကသတ်မှတ်ပြီး + လိုက်ဘရီရွေးချယ်ရန် + ဖြင့်ဖွင့်မည် + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 5f60ac14..c33bf107 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -573,5 +573,6 @@ Selecteer een modus om het downloaden van plug-ins te filteren Uitzetten De gebruikersinterface kon niet correct worden gemaakt, dit is een ERNSTIG PROBLEEM en moet onmiddellijk gerapporteerd worden %s - @string/default_subtitles - + \@string/default_subtitles + Je hebt al gestemd + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 6db36065..38c56f0d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -553,4 +553,7 @@ Wybierz tryb filtrowania pobieranych rozszerzeń Wyłączać @string/default_subtitles - + Nie znaleziono żadnych wtyczek w repozytorium + Już oddano głos + Nie znaleziono tego repozytorium, sprawdź adres URL lub spróbuj połączyć się przez VPN + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b2504e84..75d1ddbc 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -552,4 +552,5 @@ Desativar Não foram encontrados plugins no repositório Repositório não encontrado, verifique o URL e tente a VPN - + Você já votou + \ No newline at end of file diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index f763d795..cce4e7d3 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -248,4 +248,5 @@ aoaaaaaoooghhh oooooh uuaagh @string/home_play - + oouuhhh ahhooo-ahah + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2c5d4197..14b2334f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -80,14 +80,14 @@ Сюжет не знайдено Опис не знайдено Показати Logcat 🐈 - Продовження відтворення в мініатюрному плеєрі поверх інших застосунків + Продовжує відтворення в мініатюрному плеєрі поверх інших застосунків Прибирає чорні рамки Субтитри Субтитри Chromecast Налаштування субтитрів Chromecast Режим Eigengravy Проведіть пальцем, щоб змінити налаштування - Проведіть пальцем вгору або вниз, ліворуч або праворуч, щоб змінити яскравість чи гучність + Проведіть вгору або вниз з лівого або правого боку, щоб змінити яскравість чи гучність Відтворювати наступний епізод після закінчення поточного Головна CloudStream @@ -121,8 +121,8 @@ Колір тексту Колір контуру Автовідтворення наступного епізоду - Проведіть пальцем з боку в бік, щоб керувати своїм положенням у відео - %d Бананів для розробників + Проведіть з боку в бік, щоб керувати своїм положенням у відео + %d бананів для розробників Кнопка зміни розміру плеєра @string/home_play Для коректної роботи цього постачальника може знадобитися VPN @@ -133,7 +133,7 @@ Проведіть пальцем, щоб перемотати Двічі торкніться, щоб перемотати Двічі торкніться для паузи - Крок перемотки (Секунди) + Крок перемотки (секунди) Натисніть двічі посередині, щоб призупинити відтворення відео Використовувати яскравість системи Оновити прогрес перегляду @@ -150,8 +150,8 @@ Надає результати пошуку, розділені за постачальниками Надсилає дані лише про збої Не надсилає даних - Показати заповнюючий епізод для аніме - Показати трейлери + Показувати філери до аніме + Показувати трейлери Приховати вибрану якість відео в результатах пошуку Автоматичне завантаження плагінів Показувати оновлення застосунку @@ -214,12 +214,12 @@ Завантажити дзеркало Перевірити наявність оновлень Заблокувати - Пропустити OP + Пропускати OP Не показувати знову Оновити Бажана якість перегляду (WiFi) Заголовок - Перемикання елементів інтерфейсу на плакаті + Перемикання елементів інтерфейсу на постері Оновлення не знайдено Двічі торкніться праворуч або ліворуч, щоб перемотати відео вперед або назад Використовуйте системну яскравість у плеєрі замість темної накладки @@ -227,10 +227,10 @@ Торренти Автоматична синхронізація прогресу поточного епізоду Відсутні дозволи на зберігання. Будь ласка, спробуйте ще раз. - Показати постери від Kitsu + Показувати постери від Kitsu Автоматичне оновлення плагінів Автоматично встановлювати всі ще не встановлені плагіни з доданих репозиторіїв. - Автоматично шукати нові оновлення після запуску застосунку. + Автоматично шукає нові оновлення після запуску застосунку. Оновлення до бета-версій Посилання скопійовано в буфер обміну Деякі телефони не підтримують новий інсталятор пакетів. Спробуйте стару версію, якщо оновлення не встановлюються. @@ -354,7 +354,7 @@ DNS через HTTPS Шлях завантаження Додайте клон існуючого сайту, з іншою URL-адресою - Відображати мітку Дубляж/Субтитри в аніме + Відображати мітку Дубляж/Субтитри до аніме Застереження Розширення Дії @@ -382,7 +382,7 @@ Підтримка Фон Blu-ray - Видалити закриті титри з субтитрів + Видаляти закриті титри з субтитрів DVD Недійсні дані Фільтрувати за бажаною мовою медіа @@ -400,7 +400,7 @@ HD TS TC - Видалити роздуття субтитрів + Видаляти роздуття субтитрів Referer Далі Дивіться відео на цих мовах @@ -451,8 +451,8 @@ Вбудований плеєр VLC MPV - Відтворення веб-відео - Веб-браузер + Web Video Cast + Веббраузер Ендінґ Коротке повторення Пропустити %s @@ -462,7 +462,7 @@ Вступ Очистити історію Історія - Показувати спливаючі вікна для опенінґу/ендінґу + Показує спливаюче вікно для пропуску опенінґу/ендінґу Забагато тексту. Не вдалося зберегти в буфер обміну. Позначити як переглянуте Ви впевнені що хочете вийти\? @@ -552,4 +552,5 @@ @string/default_subtitles Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії - + Ви вже проголосували + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 4b394227..f7abd6db 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -267,7 +267,7 @@ Cập nhật Chất lượng xem ưu tiên (WiFi) Kí tự tối đa trên tiêu đề - Độ phân giải trình phát video + Nội dung trình phát video 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 @@ -380,8 +380,8 @@ Web Ảnh áp phích Trình phát - Độ phân giải và Tiêu đề - Tiêu đề + Độ phân giải và Tên nguồn + Tên nguồn Độ phân giải Id không hợp lệ Lỗi dữ liệu @@ -561,4 +561,11 @@ \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 phẩm chất - + Bạn đã bình chọn + Vô hiệu hoá + Không tìm thấy tiện ích, hãy kiểm tra URL và thử VPN + Không tìm thấy plugin + 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 + \@string/default_subtitles + \ No newline at end of file diff --git a/fastlane/metadata/android/pt/changelogs/2.txt b/fastlane/metadata/android/pt/changelogs/2.txt new file mode 100644 index 00000000..1153e632 --- /dev/null +++ b/fastlane/metadata/android/pt/changelogs/2.txt @@ -0,0 +1 @@ +- Adicionado o registo de alterações! diff --git a/fastlane/metadata/android/pt/full_description.txt b/fastlane/metadata/android/pt/full_description.txt new file mode 100644 index 00000000..48bf36ce --- /dev/null +++ b/fastlane/metadata/android/pt/full_description.txt @@ -0,0 +1,10 @@ +O CloudStream-3 permite-lhe transmitir e descarregar filmes, séries de TV e anime. + +A aplicação é fornecida sem quaisquer anúncios e análises e +suporta vários sites de trailers e filmes, e muito mais, por exemplo + +Marcadores + +Downloads de legendas + +Suporte para Chromecast diff --git a/fastlane/metadata/android/pt/short_description.txt b/fastlane/metadata/android/pt/short_description.txt new file mode 100644 index 00000000..d0392f34 --- /dev/null +++ b/fastlane/metadata/android/pt/short_description.txt @@ -0,0 +1 @@ +Transmita e transfira filmes, séries de TV e anime. diff --git a/fastlane/metadata/android/pt/title.txt b/fastlane/metadata/android/pt/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/pt/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/vi/changelogs/2.txt b/fastlane/metadata/android/vi/changelogs/2.txt new file mode 100644 index 00000000..e03e458e --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/2.txt @@ -0,0 +1 @@ +- Đã thêm Nhật ký thay đổi! diff --git a/fastlane/metadata/android/vi/full_description.txt b/fastlane/metadata/android/vi/full_description.txt new file mode 100644 index 00000000..90ea7ab7 --- /dev/null +++ b/fastlane/metadata/android/vi/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 cho phép bạn xem và tải xuống phim lẻ, phim bộ và anime. + +Ứng dụng không có quảng cáo hay và phân tích nào, +đồng thời hỗ trợ nhiều trang web xem phim, v.v. + +Đánh dấu + +Tải phụ đề + +Hỗ trợ Chromecast diff --git a/fastlane/metadata/android/vi/short_description.txt b/fastlane/metadata/android/vi/short_description.txt new file mode 100644 index 00000000..e4e20bd5 --- /dev/null +++ b/fastlane/metadata/android/vi/short_description.txt @@ -0,0 +1 @@ +Xem và tải xuống phim lẻ, phim bộ và anime. diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/vi/title.txt @@ -0,0 +1 @@ +CloudStream From 6948bf807373d349844701c4e2eb7e3a438179e0 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Sat, 19 Aug 2023 19:38:45 +0000 Subject: [PATCH 056/441] chore(locales): fix locale issues --- .../lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 2 ++ app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fil/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 4 ++-- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-my/strings.xml | 6 +++--- app/src/main/res/values-nl/strings.xml | 4 ++-- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-qt/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 4 ++-- 16 files changed, 22 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index d76eba1e..2c81ad1f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -69,6 +69,7 @@ val appLanguages = arrayListOf( Triple("", "Esperanto", "eo"), Triple("", "español", "es"), Triple("", "فارسی", "fa"), + Triple("", "fil", "fil"), Triple("", "français", "fr"), Triple("", "galego", "gl"), Triple("", "हिन्दी", "hi"), @@ -84,6 +85,7 @@ val appLanguages = arrayListOf( Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), Triple("", "bahasa Melayu", "ms"), + Triple("", "ဗမာစာ", "my"), Triple("", "Nederlands", "nl"), Triple("", "norsk nynorsk", "nn"), Triple("", "norsk bokmål", "no"), diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 987211a5..0c11f7e9 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -585,4 +585,4 @@ لا توجد اضافة في المستودع المستودع لم يتم العثور عليه، تحقق من العنوان اوجرب شبكة افتراضية خاصة(vpn) لقد صوتت بالفعل - \ No newline at end of file + diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 2a3bdb27..425293e4 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -499,4 +499,4 @@ Atualizando shows inscritos Player oculto - Procure na barra de progresso Conteúdo +18 - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 165dbbb4..46bd860d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -577,4 +577,4 @@ Repozitář nenalezen, zkontrolujte adresu URL a zkuste použít VPN @string/default_subtitles Již jste hlasovali - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 6326211e..8e9f9c2c 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -553,4 +553,4 @@ No se encontraron complementos en el repositorio Repositorio no encontrado, comprueba la URL y prueba la VPN Ya has votado - \ No newline at end of file + diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index a6b3daec..42eba3cc 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4cc56207..208e6140 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -552,4 +552,4 @@ Qualités L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s Sélectionnez le mode pour filtrer le téléchargement des plugins - \ No newline at end of file + diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 87a01ff9..d514bcc4 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -575,5 +575,5 @@ Tidak ada plugin yang ditemukan di repositori Repositori tidak ditemukan, periksa URL dan coba VPN Kamu sudah voting - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7cca78ca..0c34e89a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -575,4 +575,4 @@ @string/default_subtitles Disabilita Hai già votato - \ No newline at end of file + diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index cde13ba5..0cb44373 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -58,7 +58,7 @@ ဆက်လက်ကြည့်ရှုမည် ဖယ်ရှားရန် ပိုမို၍ - \@string/home_play + @string/home_play ဒီဟာကတောရပ်တစ်ခုပါ ဗီပီအန်တစ်ခုသုံးဖို့အကြံပြုပါတယ် ဖော်ပြချက် ဇာတ်လမ်းသွား မတွေ့ပါ @@ -128,7 +128,7 @@ နောက်အစီအစဥ် စာတန်းထိုးမထည့် ပုံသေ - \@string/default_subtitles + @string/default_subtitles ကျန်ရှိသော အက်ပ် ရုပ်ရှင်များ @@ -553,4 +553,4 @@ သင်နဂိုတည်းကသတ်မှတ်ပြီး လိုက်ဘရီရွေးချယ်ရန် ဖြင့်ဖွင့်မည် - \ No newline at end of file + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index c33bf107..d19726fd 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -573,6 +573,6 @@ Selecteer een modus om het downloaden van plug-ins te filteren Uitzetten De gebruikersinterface kon niet correct worden gemaakt, dit is een ERNSTIG PROBLEEM en moet onmiddellijk gerapporteerd worden %s - \@string/default_subtitles + @string/default_subtitles Je hebt al gestemd - \ No newline at end of file + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 38c56f0d..a170d610 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -556,4 +556,4 @@ Nie znaleziono żadnych wtyczek w repozytorium Już oddano głos Nie znaleziono tego repozytorium, sprawdź adres URL lub spróbuj połączyć się przez VPN - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 75d1ddbc..908ddb0d 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -553,4 +553,4 @@ Não foram encontrados plugins no repositório Repositório não encontrado, verifique o URL e tente a VPN Você já votou - \ No newline at end of file + diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index cce4e7d3..9c68c008 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -249,4 +249,4 @@ oooooh uuaagh @string/home_play oouuhhh ahhooo-ahah - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 14b2334f..e0db1c0e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index f7abd6db..217d2791 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -567,5 +567,5 @@ Không tìm thấy plugin 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 - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + From a3009af4f585c010cd5018037123fefd644f8921 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Sat, 19 Aug 2023 19:48:10 +0000 Subject: [PATCH 057/441] Add Native Crash Handler (#565) * Add NativeCrashHandler * Safer init --- app/CMakeLists.txt | 6 +++ app/build.gradle.kts | 6 +++ app/src/main/cpp/native-lib.cpp | 28 ++++++++++++ .../lagradost/cloudstream3/AcraApplication.kt | 1 + .../cloudstream3/NativeCrashHandler.kt | 44 +++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 app/CMakeLists.txt create mode 100644 app/src/main/cpp/native-lib.cpp create mode 100644 app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 00000000..7f7fd14c --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,6 @@ +# Set this to the minimum version your project supports. +cmake_minimum_required(VERSION 3.18) +project(CrashHandler) +find_library(log-lib log) +add_library(native-lib SHARED src/main/cpp/native-lib.cpp) +target_link_libraries(native-lib ${log-lib}) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 708a2083..d6515289 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,6 +32,12 @@ android { enable = true } + externalNativeBuild { + cmake { + path("CMakeLists.txt") + } + } + signingConfigs { create("prerelease") { if (prereleaseStoreFile != null) { diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp new file mode 100644 index 00000000..f4cb531f --- /dev/null +++ b/app/src/main/cpp/native-lib.cpp @@ -0,0 +1,28 @@ +#include +#include +#include + +#define TAG "CloudStream Crash Handler" +volatile sig_atomic_t gSignalStatus = 0; +void handleNativeCrash(int signal) { + gSignalStatus = signal; +} + +extern "C" JNIEXPORT void JNICALL +Java_com_lagradost_cloudstream3_NativeCrashHandler_initNativeCrashHandler(JNIEnv *env, jobject) { + #define REGISTER_SIGNAL(X) signal(X, handleNativeCrash); + REGISTER_SIGNAL(SIGSEGV) + #undef REGISTER_SIGNAL +} + +//extern "C" JNIEXPORT void JNICALL +//Java_com_lagradost_cloudstream3_NativeCrashHandler_triggerNativeCrash(JNIEnv *env, jobject thiz) { +// int *p = nullptr; +// *p = 0; +//} + +extern "C" JNIEXPORT int JNICALL +Java_com_lagradost_cloudstream3_NativeCrashHandler_getSignalStatus(JNIEnv *env, jobject) { + //__android_log_print(ANDROID_LOG_INFO, TAG, "Got signal status %d", gSignalStatus); + return gSignalStatus; +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 61d467c4..32702657 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -106,6 +106,7 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) : class AcraApplication : Application() { override fun onCreate() { super.onCreate() + NativeCrashHandler.initCrashHandler() Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt new file mode 100644 index 00000000..e5cb2702 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt @@ -0,0 +1,44 @@ +package com.lagradost.cloudstream3 + +import com.lagradost.cloudstream3.MainActivity.Companion.lastError +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +object NativeCrashHandler { + // external fun triggerNativeCrash() + private external fun initNativeCrashHandler() + private external fun getSignalStatus(): Int + + private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch { + while (true) { + delay(10_000) + val signal = getSignalStatus() + // Signal is initialized to zero + if (signal == 0) continue + + // Do not crash in safe mode! + if (lastError != null) continue + if (checkSafeModeFile()) continue + + throw RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n") + } + } + + fun initCrashHandler() { + try { + System.loadLibrary("native-lib") + initNativeCrashHandler() + } catch (t: Throwable) { + // Make debug crash. + if (BuildConfig.DEBUG) throw t + logError(t) + return + } + + initSignalPolling() + } +} \ No newline at end of file From c4852ce440736105d32a18f1774ee65dead98808 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 20 Aug 2023 01:29:50 +0200 Subject: [PATCH 058/441] made HSL downloader even faster --- .../cloudstream3/utils/M3u8Helper.kt | 5 + .../utils/VideoDownloadManager.kt | 215 +++++++++++------- 2 files changed, 132 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 5c0b45de..11dfa441 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.utils import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.app +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec @@ -196,6 +197,8 @@ object M3u8Helper2 { return if(condition()) out else null } catch (e: IllegalArgumentException) { return null + } catch (e : CancellationException) { + return null } catch (t: Throwable) { delay(failDelay) } @@ -213,6 +216,8 @@ object M3u8Helper2 { return resolveLink(index) } catch (e: IllegalArgumentException) { return null + } catch (e : CancellationException) { + return null } catch (t: Throwable) { delay(failDelay) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index d8ef7e85..89094f3f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -43,6 +43,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -1083,6 +1084,39 @@ object VideoDownloadManager { ) } + /** Helper function to make sure duplicate attributes don't get overriden or inserted without lowercase cmp + * example: map("a" to 1) appendAndDontOverride map("A" to 2, "a" to 3, "c" to 4) = map("a" to 1, "c" to 4) + * */ + private fun Map.appendAndDontOverride(rhs: Map): Map { + val out = this.toMutableMap() + val current = this.keys.map { it.lowercase() } + for ((key, value) in rhs) { + if (current.contains(key.lowercase())) continue + out[key] = value + } + return out + } + + private fun List.cancel() { + forEach { job -> + try { + job.cancel() + } catch (t: Throwable) { + logError(t) + } + } + } + + private suspend fun List.join() { + forEach { job -> + try { + job.join() + } catch (t: Throwable) { + logError(t) + } + } + } + @Throws suspend fun downloadThing( context: Context, @@ -1166,8 +1200,9 @@ object VideoDownloadManager { hashMapOf() val jobs = (0 until parallelConnections).map { - launch { + launch(Dispatchers.IO) { + // @downloadexplanation // this may seem a bit complex but it more or less acts as a queue system // imagine we do the downloading [0,3] and it response in the order 0,2,3,1 // file: [_,_,_,_] queue: [_,_,_,_] Initial condition @@ -1177,6 +1212,10 @@ object VideoDownloadManager { // file: [X,X,_,_] queue: [_,_,X,X] + added 1 directly to file // file: [X,X,X,X] queue: [_,_,_,_] write the queue and remove from it + // note that this is a bit more complex compared to hsl as ever segment + // will return several bytearrays, and is therefore chained by the byte + // so every request has a front and back byte instead of an index + // this *requires* that no gap exist due because of resolve val callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) = callback@{ response -> if (!isActive) return@callback @@ -1228,10 +1267,11 @@ object VideoDownloadManager { while (true) { if (!isActive) return@launch fileMutex.withLock { - if (metadata.type == DownloadType.IsStopped) return@launch + if (metadata.type == DownloadType.IsStopped + || metadata.type == DownloadType.IsFailed) return@launch } - // just in case, we never want this to fail due to multithreading + // mutex just in case, we never want this to fail due to multithreading val index = currentMutex.withLock { if (!current.hasNext()) return@launch current.nextInt() @@ -1253,68 +1293,14 @@ object VideoDownloadManager { // fast stop as the jobs may be in a slow request metadata.setOnStop { - jobs.forEach { job -> - try { - job.cancel() - } catch (t: Throwable) { - logError(t) - } - } + jobs.cancel() } - jobs.forEach { it.join() } + jobs.join() // jobs are finished so we don't want to stop them anymore metadata.removeStopListener() - // set up a connection - //val request = app.get( - // link.url.replace(" ", "%20"), - // headers = link.headers.appendAndDontOverride( - // mapOf( - // "Accept-Encoding" to "identity", - // "accept" to "*/*", - // "user-agent" to USER_AGENT, - // "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", - // "sec-fetch-mode" to "navigate", - // "sec-fetch-dest" to "video", - // "sec-fetch-user" to "?1", - // "sec-ch-ua-mobile" to "?0", - // ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap() - // ), - // referer = link.referer, - // verify = false - //) - - // init variables - //val contentLength = request.size ?: 0 - //metadata.totalBytes = contentLength + resumeAt - //// save - //metadata.setDownloadFileInfoTemplate( - // DownloadedFileInfo( - // totalBytes = metadata.approxTotalBytes, - // relativePath = relativePath ?: "", - // displayName = displayName, - // basePath = basePath - // ) - //) - //// total length is less than 5mb, that is too short and something has gone wrong - //if (extension == "mp4" && metadata.approxTotalBytes < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION - //// read the buffer into the filestream, this is equivalent of transferTo - //requestStream = request.body.byteStream() - //metadata.type = DownloadType.IsDownloading - //val buffer = ByteArray(DEFAULT_BUFFER_SIZE) - //var read: Int - //while (requestStream.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it } >= 0) { - // fileStream.write(buffer, 0, read) - // // wait until not paused - // while (metadata.type == DownloadType.IsPaused) delay(100) - // // if stopped then break to delete - // if (metadata.type == DownloadType.IsStopped) break - // metadata.addBytes(read.toLong()) - //} - - if (metadata.type == DownloadType.IsFailed) { return@withContext ERROR_CONNECTION_ERROR } @@ -1342,7 +1328,6 @@ object VideoDownloadManager { // note that when failing we don't want to delete the file, // only user interaction has that power - metadata.removeStopListener() metadata.type = DownloadType.IsFailed return@withContext ERROR_CONNECTION_ERROR } finally { @@ -1352,19 +1337,6 @@ object VideoDownloadManager { } } - /** Helper function to make sure duplicate attributes don't get overriden or inserted without lowercase cmp - * example: map("a" to 1) appendAndDontOverride map("A" to 2, "a" to 3, "c" to 4) = map("a" to 1, "c" to 4) - * */ - private fun Map.appendAndDontOverride(rhs: Map): Map { - val out = this.toMutableMap() - val current = this.keys.map { it.lowercase() } - for ((key, value) in rhs) { - if (current.contains(key.lowercase())) continue - out[key] = value - } - return out - } - @Throws private suspend fun downloadHLS( context: Context, @@ -1429,28 +1401,95 @@ object VideoDownloadManager { metadata.hlsTotal = items.size metadata.type = DownloadType.IsDownloading + + val currentMutex = Mutex() + val current = (0 until items.size).iterator() + + val fileMutex = Mutex() + val pendingData: HashMap = hashMapOf() + + // see @downloadexplanation for explanation of this download strategy, + // this keeps all jobs working at all times, // does several connections in parallel instead of a regular for loop to improve // download speed - (startAt until items.size).chunked(parallelConnections).forEach { subset -> - // wait until not paused - while (metadata.type == DownloadType.IsPaused) delay(100) - // if stopped then break to delete - if (metadata.type == DownloadType.IsStopped) return@forEach + val jobs = (0 until parallelConnections).map { + launch(Dispatchers.IO) { + while (true) { + if (!isActive) return@launch + fileMutex.withLock { + if (metadata.type == DownloadType.IsStopped + || metadata.type == DownloadType.IsFailed + ) return@launch + } - subset.amap { idx -> - idx to items.resolveLinkSafe(idx)?.also { bytes -> - metadata.addSegment(bytes.size.toLong()) + // mutex just in case, we never want this to fail due to multithreading + val index = currentMutex.withLock { + if (!current.hasNext()) return@launch + current.nextInt() + } + + // in case something has gone wrong set to failed if the fail is not caused by + // user cancellation + val bytes = items.resolveLinkSafe(index) ?: run { + fileMutex.withLock { + if (metadata.type != DownloadType.IsStopped) { + metadata.type = DownloadType.IsFailed + } + } + return@launch + } + + try { + fileMutex.lock() + // user pause + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then break to delete + if (metadata.type == DownloadType.IsStopped || !isActive) return@launch + + // send notification, no matter the actual write order + metadata.addSegment(bytes.size.toLong()) + + // directly write the bytes if you are first + if (metadata.hlsWrittenProgress == index) { + fileStream.write(bytes) + metadata.setWrittenSegment(index) + } else { + // no need to clone as there will be no modification of this bytearray + pendingData[index] = bytes + } + + // write the cached bytes submitted by other threads + while (true) { + fileStream.write( + pendingData.remove(metadata.hlsWrittenProgress) ?: break + ) + metadata.setWrittenSegment(metadata.hlsWrittenProgress) + } + } catch (t : Throwable) { + // this is in case of write fail + if (metadata.type != DownloadType.IsStopped) { + metadata.type = DownloadType.IsFailed + } + } finally { + fileMutex.unlock() + } } - }.forEach { (idx, bytes) -> - if (bytes == null) { - metadata.type = DownloadType.IsFailed - return@withContext ERROR_CONNECTION_ERROR - } - fileStream.write(bytes) - metadata.setWrittenSegment(idx) } } + // fast stop as the jobs may be in a slow request + metadata.setOnStop { + jobs.cancel() + } + + jobs.join() + + metadata.removeStopListener() + + if (metadata.type == DownloadType.IsFailed) { + return@withContext ERROR_CONNECTION_ERROR + } + if (metadata.type == DownloadType.IsStopped) { // we need to close before delete fileStream.closeQuietly() From 4e28e5f8cc4d811184799cb3aa024665cc0aee3d Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 20 Aug 2023 03:58:31 +0200 Subject: [PATCH 059/441] fixed not downloading the last 20MiB on mp4 downloader + bump + mb/s notification --- app/build.gradle.kts | 2 +- .../utils/VideoDownloadManager.kt | 79 ++++++++++++++----- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d6515289..50125aa3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,7 +58,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.5" + versionName = "4.1.6" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 89094f3f..507abc34 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -30,7 +30,6 @@ import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall @@ -256,7 +255,8 @@ object VideoDownloadManager { total: Long, notificationCallback: (Int, Notification) -> Unit, hlsProgress: Long? = null, - hlsTotal: Long? = null + hlsTotal: Long? = null, + bytesPerSecond: Long ): Notification? { try { if (total <= 0) return null// crash, invalid data @@ -327,22 +327,29 @@ object VideoDownloadManager { val totalMbString: String val suffix: String + val mbFormat = "%.1f MB" + if (hlsProgress != null && hlsTotal != null) { progressPercentage = hlsProgress.toLong() * 100 / hlsTotal progressMbString = hlsProgress.toString() totalMbString = hlsTotal.toString() - suffix = " - %.1f MB".format(progress / 1000000f) + suffix = " - $mbFormat".format(progress / 1000000f) } else { progressPercentage = progress * 100 / total - progressMbString = "%.1f MB".format(progress / 1000000f) - totalMbString = "%.1f MB".format(total / 1000000f) + progressMbString = mbFormat.format(progress / 1000000f) + totalMbString = mbFormat.format(total / 1000000f) suffix = "" } + val mbPerSecondString = + if (state == DownloadType.IsDownloading) { + " ($mbFormat/s)".format(bytesPerSecond.toFloat() / 1000000f) + } else "" + val bigText = when (state) { DownloadType.IsDownloading, DownloadType.IsPaused -> { - (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix" + (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix$mbPerSecondString" } DownloadType.IsFailed -> { @@ -608,6 +615,7 @@ object VideoDownloadManager { val bytesTotal: Long, val hlsProgress: Long? = null, val hlsTotal: Long? = null, + val bytesPerSecond: Long ) data class StreamData( @@ -723,6 +731,7 @@ object VideoDownloadManager { // notification metadata private var lastUpdatedMs: Long = 0, + private var lastDownloadedBytes: Long = 0, private val createNotificationCallback: (CreateNotificationMetadata) -> Unit, private var internalType: DownloadType = DownloadType.IsPending, @@ -738,6 +747,12 @@ object VideoDownloadManager { // this is used for copy with metadata on how much we have downloaded for setKey private var downloadFileInfoTemplate: DownloadedFileInfo? = null ) : Closeable { + fun setResumeLength(length: Long) { + bytesDownloaded = length + bytesWritten = length + lastDownloadedBytes = length + } + val approxTotalBytes: Long get() = totalBytes ?: hlsTotal?.let { total -> (bytesDownloaded * (total / hlsProgress.toFloat())).toLong() @@ -839,6 +854,13 @@ object VideoDownloadManager { @JvmName("DownloadMetaDataNotify") private fun notify() { + // max 10 sec between notifications, min 0.1s, this is to stop div by zero + val dt = (System.currentTimeMillis() - lastUpdatedMs).coerceIn(100, 10000) + + val bytesPerSecond = + ((bytesDownloaded - lastDownloadedBytes) * 1000L) / dt + + lastDownloadedBytes = bytesDownloaded lastUpdatedMs = System.currentTimeMillis() try { val bytes = approxTotalBytes @@ -851,7 +873,8 @@ object VideoDownloadManager { bytesDownloaded, bytes, hlsTotal = hlsTotal?.toLong(), - hlsProgress = hlsProgress.toLong() + hlsProgress = hlsProgress.toLong(), + bytesPerSecond = bytesPerSecond ) ) } else { @@ -860,6 +883,7 @@ object VideoDownloadManager { internalType, bytesDownloaded, bytes, + bytesPerSecond = bytesPerSecond ) ) } @@ -1057,21 +1081,29 @@ object VideoDownloadManager { // we don't want to make a separate connection for every 1kb require(chuckSize > 1000) - val contentLength = + var contentLength = app.head(url = url, headers = headers, referer = referer, verify = false).size + if (contentLength != null && contentLength <= 0) contentLength = null var downloadLength: Long? = null var totalLength: Long? = null val ranges = if (contentLength == null) { + // is the equivalent of [startByte..EOF] as we don't know the size we can only do one + // connection LongArray(1) { startByte } } else { downloadLength = contentLength - startByte totalLength = contentLength - LongArray((downloadLength / chuckSize).toInt()) { idx -> + // div with ceiling as + // this makes the last part "unknown ending" and it will break at EOF + // so eg startByte = 0, downloadLength = 13, chuckSize = 10 + // = LongArray(2) { 0, 10 } = [0,10) + [10..EOF] + LongArray(((downloadLength + chuckSize - 1) / chuckSize).toInt()) { idx -> startByte + idx * chuckSize } } + return LazyStreamDownloadData( url = url, headers = headers, @@ -1158,8 +1190,7 @@ object VideoDownloadManager { val resume = stream.resume ?: return@withContext ERROR_UNKNOWN val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN val resumeAt = (if (resume) fileLength else 0) - metadata.bytesDownloaded = resumeAt - metadata.bytesWritten = resumeAt + metadata.setResumeLength(resumeAt) metadata.type = DownloadType.IsPending val items = streamLazy( @@ -1268,7 +1299,8 @@ object VideoDownloadManager { if (!isActive) return@launch fileMutex.withLock { if (metadata.type == DownloadType.IsStopped - || metadata.type == DownloadType.IsFailed) return@launch + || metadata.type == DownloadType.IsFailed + ) return@launch } // mutex just in case, we never want this to fail due to multithreading @@ -1374,7 +1406,7 @@ object VideoDownloadManager { fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN // push the metadata - metadata.bytesDownloaded = stream.fileLength ?: 0 + metadata.setResumeLength(stream.fileLength ?: 0) metadata.hlsProgress = startAt metadata.type = DownloadType.IsPending metadata.setDownloadFileInfoTemplate( @@ -1446,12 +1478,15 @@ object VideoDownloadManager { // if stopped then break to delete if (metadata.type == DownloadType.IsStopped || !isActive) return@launch + val segmentLength = bytes.size.toLong() // send notification, no matter the actual write order - metadata.addSegment(bytes.size.toLong()) + metadata.addSegment(segmentLength) // directly write the bytes if you are first if (metadata.hlsWrittenProgress == index) { fileStream.write(bytes) + + metadata.addBytesWritten(segmentLength) metadata.setWrittenSegment(index) } else { // no need to clone as there will be no modification of this bytearray @@ -1460,12 +1495,14 @@ object VideoDownloadManager { // write the cached bytes submitted by other threads while (true) { - fileStream.write( - pendingData.remove(metadata.hlsWrittenProgress) ?: break - ) + val cache = pendingData.remove(metadata.hlsWrittenProgress) ?: break + val cacheLength = cache.size.toLong() + + fileStream.write(cache) + metadata.addBytesWritten(cacheLength) metadata.setWrittenSegment(metadata.hlsWrittenProgress) } - } catch (t : Throwable) { + } catch (t: Throwable) { // this is in case of write fail if (metadata.type != DownloadType.IsStopped) { metadata.type = DownloadType.IsFailed @@ -1756,7 +1793,8 @@ object VideoDownloadManager { meta.bytesTotal, notificationCallback, meta.hlsProgress, - meta.hlsTotal + meta.hlsTotal, + meta.bytesPerSecond ) } } @@ -1785,7 +1823,8 @@ object VideoDownloadManager { meta.type, meta.bytesDownloaded, meta.bytesTotal, - notificationCallback + notificationCallback, + bytesPerSecond = meta.bytesPerSecond ) } }) From afcbdeecc86151081aa8a135b3c5900bf1ec8c7e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 22 Aug 2023 04:00:05 +0200 Subject: [PATCH 060/441] changes to downloader for stable resume --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 6 +- .../ui/settings/SettingsUpdates.kt | 11 +- .../utils/DownloadFileWorkManager.kt | 22 +- .../utils/VideoDownloadManager.kt | 532 ++++++------------ 4 files changed, 191 insertions(+), 380 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 e0d50cc3..9e601fc7 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 @@ -520,10 +520,10 @@ class GeneratorPlayer : FullScreenPlayer() { if (uri == null) return@normalSafeApiCall val ctx = context ?: AcraApplication.context ?: return@normalSafeApiCall // RW perms for the path - val flags = + ctx.contentResolver.takePersistableUriPermission( + uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - ctx.contentResolver.takePersistableUriPermission(uri, flags) + ) val file = UniFile.fromUri(ctx, uri) println("Loaded subtitle file. Selected URI path: $uri - Name: ${file.name}") 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 c304629a..62e46c08 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 @@ -116,13 +116,14 @@ class SettingsUpdates : PreferenceFragmentCompat() { null, "txt", false - ).fileStream - fileStream?.writer()?.write(text) - } catch (e: Exception) { - logError(e) + ).openNew() + fileStream.writer().write(text) + dialog.dismissSafe(activity) + } catch (t: Throwable) { + logError(t) + showToast(t.message) } finally { fileStream?.closeQuietly() - dialog.dismissSafe(activity) } } binding.closeBtt.setOnClickListener { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt index aa424c08..421e4420 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt @@ -16,6 +16,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadCheck import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadEpisode import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadFromResume import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadStatusEvent +import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadResumePackage import kotlinx.coroutines.delay const val DOWNLOAD_CHECK = "DownloadCheck" @@ -36,15 +37,20 @@ class DownloadFileWorkManager(val context: Context, private val workerParams: Wo WORK_KEY_PACKAGE, key ) + if (info != null) { - downloadEpisode( - applicationContext, - info.source, - info.folder, - info.ep, - info.links, - ::handleNotification - ) + getDownloadResumePackage(applicationContext, info.ep.id)?.let { dpkg -> + downloadFromResume(applicationContext, dpkg, ::handleNotification) + } ?: run { + downloadEpisode( + applicationContext, + info.source, + info.folder, + info.ep, + info.links, + ::handleNotification + ) + } } else if (pkg != null) { downloadFromResume(applicationContext, pkg, ::handleNotification) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 507abc34..a81e4b3a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -9,9 +9,7 @@ import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.os.Environment -import android.provider.MediaStore import androidx.annotation.DrawableRes -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri @@ -32,6 +30,7 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -44,6 +43,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -301,6 +301,8 @@ object VideoDownloadManager { if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { builder.setProgress((total / 1000).toInt(), (progress / 1000).toInt(), false) + } else if (state == DownloadType.IsPending) { + builder.setProgress(0,0,true) } val rowTwoExtra = if (ep.name != null) " - ${ep.name}\n" else "" @@ -352,6 +354,10 @@ object VideoDownloadManager { (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix$mbPerSecondString" } + DownloadType.IsPending -> { + (if (linkName == null) "" else "$linkName\n") + rowTwo + } + DownloadType.IsFailed -> { downloadFormat.format( context.getString(R.string.download_failed), @@ -363,7 +369,7 @@ object VideoDownloadManager { downloadFormat.format(context.getString(R.string.download_done), rowTwo) } - else -> { + DownloadType.IsStopped -> { downloadFormat.format( context.getString(R.string.download_canceled), rowTwo @@ -377,7 +383,7 @@ object VideoDownloadManager { } else { val txt = when (state) { - DownloadType.IsDownloading, DownloadType.IsPaused -> { + DownloadType.IsDownloading, DownloadType.IsPaused, DownloadType.IsPending -> { rowTwo } @@ -392,7 +398,7 @@ object VideoDownloadManager { downloadFormat.format(context.getString(R.string.download_done), rowTwo) } - else -> { + DownloadType.IsStopped -> { downloadFormat.format( context.getString(R.string.download_canceled), rowTwo @@ -480,54 +486,6 @@ object VideoDownloadManager { return tempName.replace(" ", " ").trim(' ') } - @RequiresApi(Build.VERSION_CODES.Q) - private fun ContentResolver.getExistingFolderStartName(relativePath: String): List>? { - try { - val projection = arrayOf( - MediaStore.MediaColumns._ID, - MediaStore.MediaColumns.DISPLAY_NAME, // unused (for verification use only) - //MediaStore.MediaColumns.RELATIVE_PATH, // unused (for verification use only) - ) - - val selection = - "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath'" - - val result = this.query( - MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), - projection, selection, null, null - ) - val list = ArrayList>() - - result.use { c -> - if (c != null && c.count >= 1) { - c.moveToFirst() - while (true) { - val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) - val name = - c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)) - val uri = ContentUris.withAppendedId( - MediaStore.Downloads.EXTERNAL_CONTENT_URI, id - ) - list.add(Pair(name, uri)) - if (c.isLast) { - break - } - c.moveToNext() - } - - /* - val cDisplayName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)) - val cRelativePath = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))*/ - - } - } - return list - } catch (e: Exception) { - logError(e) - return null - } - } - /** * Used for getting video player subs. * @return List of pairs for the files in this format: @@ -538,76 +496,12 @@ object VideoDownloadManager { basePath: String? ): List>? { val base = basePathToFile(context, basePath) - val folder = base?.gotoDir(relativePath, false) + val folder = base?.gotoDir(relativePath, false) ?: return null + if (!folder.isDirectory) return null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) { - return context.contentResolver?.getExistingFolderStartName(relativePath) - } else { -// val normalPath = -// "${Environment.getExternalStorageDirectory()}${File.separatorChar}${relativePath}".replace( -// '/', -// File.separatorChar -// ) -// val folder = File(normalPath) - if (folder?.isDirectory == true) { - return folder.listFiles()?.map { Pair(it.name ?: "", it.uri) } - } - } - return null -// } + return folder.listFiles()?.map { (it.name ?: "") to it.uri } } - @RequiresApi(Build.VERSION_CODES.Q) - private fun ContentResolver.getExistingDownloadUriOrNullQ( - relativePath: String, - displayName: String - ): Uri? { - try { - val projection = arrayOf( - MediaStore.MediaColumns._ID, - //MediaStore.MediaColumns.DISPLAY_NAME, // unused (for verification use only) - //MediaStore.MediaColumns.RELATIVE_PATH, // unused (for verification use only) - ) - - val selection = - "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath' AND " + "${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'" - - val result = this.query( - MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), - projection, selection, null, null - ) - - result.use { c -> - if (c != null && c.count >= 1) { - c.moveToFirst().let { - val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) - /* - val cDisplayName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)) - val cRelativePath = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))*/ - - return ContentUris.withAppendedId( - MediaStore.Downloads.EXTERNAL_CONTENT_URI, id - ) - } - } - } - return null - } catch (e: Exception) { - logError(e) - return null - } - } - - @RequiresApi(Build.VERSION_CODES.Q) - fun ContentResolver.getFileLength(fileUri: Uri): Long? { - return try { - this.openFileDescriptor(fileUri, "r") - .use { it?.statSize ?: 0 } - } catch (e: Exception) { - logError(e) - null - } - } data class CreateNotificationMetadata( val type: DownloadType, @@ -619,16 +513,39 @@ object VideoDownloadManager { ) data class StreamData( - val errorCode: Int, - val resume: Boolean? = null, - val fileLength: Long? = null, - val fileStream: OutputStream? = null, - ) + private val fileLength: Long, + val file: UniFile, + //val fileStream: OutputStream, + ) { + fun open() : OutputStream { + return file.openOutputStream(resume) + } + + fun openNew() : OutputStream { + return file.openOutputStream(false) + } + + val resume: Boolean get() = fileLength > 0L + val startAt: Long get() = if (resume) fileLength else 0L + val exists: Boolean get() = file.exists() + } + + + //class ADownloadException(val id: Int) : RuntimeException(message = "Download error $id") + + fun UniFile.createFileOrThrow(displayName: String): UniFile { + return this.createFile(displayName) ?: throw IOException("Could not create file") + } + + fun UniFile.deleteOrThrow() { + if (!this.delete()) throw IOException("Could not delete file") + } /** * Sets up the appropriate file and creates a data stream from the file. * Used for initializing downloads. * */ + @Throws(IOException::class) fun setupStream( context: Context, name: String, @@ -637,88 +554,24 @@ object VideoDownloadManager { tryResume: Boolean, ): StreamData { val displayName = getDisplayName(name, extension) - val fileStream: OutputStream - val fileLength: Long - var resume = tryResume - val baseFile = context.getBasePath() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.first?.isDownloadDir() == true) { - val cr = context.contentResolver ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) + val (baseFile, _) = context.getBasePath() - val currentExistingFile = - cr.getExistingDownloadUriOrNullQ( - folder ?: "", - displayName - ) // CURRENT FILE WITH THE SAME PATH + val subDir = baseFile?.gotoDir(folder) ?: throw IOException() + val foundFile = subDir.findFile(displayName) - fileLength = - if (currentExistingFile == null || !resume) 0 else (cr.getFileLength( - currentExistingFile - ) - ?: 0)// IF NOT RESUME THEN 0, OTHERWISE THE CURRENT FILE SIZE - - if (!resume && currentExistingFile != null) { // DELETE FILE IF FILE EXITS AND NOT RESUME - val rowsDeleted = context.contentResolver.delete(currentExistingFile, null, null) - if (rowsDeleted < 1) { - println("ERROR DELETING FILE!!!") - } - } - - var appendFile = false - val newFileUri = if (resume && currentExistingFile != null) { - appendFile = true - currentExistingFile - } else { - val contentUri = - MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI - //val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - val currentMimeType = when (extension) { - - // Absolutely ridiculous, if text/vtt is used as mimetype scoped storage prevents - // downloading to /Downloads yet it works with null - - "vtt" -> null // "text/vtt" - "mp4" -> "video/mp4" - "srt" -> null // "application/x-subrip"//"text/plain" - else -> null - } - val newFile = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) - put(MediaStore.MediaColumns.TITLE, name) - if (currentMimeType != null) - put(MediaStore.MediaColumns.MIME_TYPE, currentMimeType) - put(MediaStore.MediaColumns.RELATIVE_PATH, folder) - } - - cr.insert( - contentUri, - newFile - ) ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) - } - - fileStream = cr.openOutputStream(newFileUri, "w" + (if (appendFile) "a" else "")) - ?: return StreamData(ERROR_CONTENT_RESOLVER_NOT_FOUND) + val (file, fileLength) = if (foundFile == null || !foundFile.exists()) { + subDir.createFileOrThrow(displayName) to 0L } else { - val subDir = baseFile.first?.gotoDir(folder) - val rFile = subDir?.findFile(displayName) - if (rFile?.exists() != true) { - fileLength = 0 - if (subDir?.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE) + if (tryResume) { + foundFile to foundFile.size() } else { - if (resume) { - fileLength = rFile.size() - } else { - fileLength = 0 - if (!rFile.delete()) return StreamData(ERROR_DELETING_FILE) - if (subDir.createFile(displayName) == null) return StreamData(ERROR_CREATE_FILE) - } + foundFile.deleteOrThrow() + subDir.createFileOrThrow(displayName) to 0L } - fileStream = (subDir.findFile(displayName) - ?: subDir.createFile(displayName))!!.openOutputStream() -// fileStream = FileOutputStream(rFile, false) - if (fileLength == 0L) resume = false } - return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream) + + return StreamData(fileLength, file) } /** This class handles the notifications, as well as the relevant key */ @@ -938,6 +791,8 @@ object VideoDownloadManager { fun setWrittenSegment(segmentIndex: Int) { hlsWrittenProgress = segmentIndex + 1 + // in case of abort we need to save every written progress + updateFileInfo() } } @@ -1185,18 +1040,16 @@ object VideoDownloadManager { // set up the download file val stream = setupStream(context, name, relativePath, extension, tryResume) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN - val resume = stream.resume ?: return@withContext ERROR_UNKNOWN - val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN - val resumeAt = (if (resume) fileLength else 0) - metadata.setResumeLength(resumeAt) + + fileStream = stream.open() + + metadata.setResumeLength(stream.startAt) metadata.type = DownloadType.IsPending val items = streamLazy( url = link.url.replace(" ", "%20"), referer = link.referer, - startByte = resumeAt, + startByte = stream.startAt, headers = link.headers.appendAndDontOverride( mapOf( "Accept-Encoding" to "identity", @@ -1230,6 +1083,19 @@ object VideoDownloadManager { val pendingData: HashMap = hashMapOf() + val fileChecker = launch(Dispatchers.IO) { + while (isActive) { + if (stream.exists) { + delay(5000) + continue + } + fileMutex.withLock { + metadata.type = DownloadType.IsStopped + } + break + } + } + val jobs = (0 until parallelConnections).map { launch(Dispatchers.IO) { @@ -1329,9 +1195,11 @@ object VideoDownloadManager { } jobs.join() + fileChecker.cancel() // jobs are finished so we don't want to stop them anymore metadata.removeStopListener() + if (!stream.exists) metadata.type = DownloadType.IsStopped if (metadata.type == DownloadType.IsFailed) { return@withContext ERROR_CONNECTION_ERROR @@ -1341,11 +1209,8 @@ object VideoDownloadManager { // we need to close before delete fileStream.closeQuietly() metadata.onDelete() - if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { - return@withContext SUCCESS_STOPPED - } else { - return@withContext ERROR_DELETING_FILE - } + deleteFile(context, baseFile, relativePath ?: "", displayName) + return@withContext SUCCESS_STOPPED } metadata.type = DownloadType.IsDone @@ -1400,13 +1265,13 @@ object VideoDownloadManager { folder ) else folder val displayName = getDisplayName(name, extension) - val stream = setupStream(context, name, relativePath, extension, startAt > 0) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - if (stream.resume != true) startAt = 0 - fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN + val stream = + setupStream(context, name, relativePath, extension, startAt > 0) + if (!stream.resume) startAt = 0 + fileStream = stream.open() // push the metadata - metadata.setResumeLength(stream.fileLength ?: 0) + metadata.setResumeLength(stream.startAt) metadata.hlsProgress = startAt metadata.type = DownloadType.IsPending metadata.setDownloadFileInfoTemplate( @@ -1433,13 +1298,25 @@ object VideoDownloadManager { metadata.hlsTotal = items.size metadata.type = DownloadType.IsDownloading - val currentMutex = Mutex() - val current = (0 until items.size).iterator() + val current = (startAt until items.size).iterator() val fileMutex = Mutex() val pendingData: HashMap = hashMapOf() + val fileChecker = launch(Dispatchers.IO) { + while (isActive) { + if (stream.exists) { + delay(5000) + continue + } + fileMutex.withLock { + metadata.type = DownloadType.IsStopped + } + break + } + } + // see @downloadexplanation for explanation of this download strategy, // this keeps all jobs working at all times, // does several connections in parallel instead of a regular for loop to improve @@ -1476,7 +1353,7 @@ object VideoDownloadManager { // user pause while (metadata.type == DownloadType.IsPaused) delay(100) // if stopped then break to delete - if (metadata.type == DownloadType.IsStopped || !isActive) return@launch + if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed || !isActive) return@launch val segmentLength = bytes.size.toLong() // send notification, no matter the actual write order @@ -1499,11 +1376,13 @@ object VideoDownloadManager { val cacheLength = cache.size.toLong() fileStream.write(cache) + metadata.addBytesWritten(cacheLength) metadata.setWrittenSegment(metadata.hlsWrittenProgress) } } catch (t: Throwable) { // this is in case of write fail + logError(t) if (metadata.type != DownloadType.IsStopped) { metadata.type = DownloadType.IsFailed } @@ -1520,9 +1399,12 @@ object VideoDownloadManager { } jobs.join() + fileChecker.cancel() metadata.removeStopListener() + if (!stream.exists) metadata.type = DownloadType.IsStopped + if (metadata.type == DownloadType.IsFailed) { return@withContext ERROR_CONNECTION_ERROR } @@ -1531,11 +1413,8 @@ object VideoDownloadManager { // we need to close before delete fileStream.closeQuietly() metadata.onDelete() - if (deleteFile(context, baseFile, relativePath ?: "", displayName)) { - return@withContext SUCCESS_STOPPED - } else { - return@withContext ERROR_DELETING_FILE - } + deleteFile(context, baseFile, relativePath ?: "", displayName) + return@withContext SUCCESS_STOPPED } metadata.type = DownloadType.IsDone @@ -1564,6 +1443,11 @@ object VideoDownloadManager { directoryName: String?, createMissingDirectories: Boolean = true ): UniFile? { + if(directoryName == null) return this + + return directoryName.split(File.separatorChar).filter { it.isNotBlank() }.fold(this) { file: UniFile?, directory -> + file?.createDirectory(directory) + } // May give this error on scoped storage. // W/DocumentsContract: Failed to create document @@ -1571,7 +1455,7 @@ object VideoDownloadManager { // Not present in latest testing. -// println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}") + println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}") try { // Creates itself from parent if doesn't exist. @@ -1671,49 +1555,6 @@ object VideoDownloadManager { return this != null && this.filePath == getDownloadDir()?.filePath } - /*private fun delete( - context: Context, - name: String, - folder: String?, - extension: String, - parentId: Int?, - basePath: UniFile? - ): Int { - val displayName = getDisplayName(name, extension) - - // delete all subtitle files - if (extension != "vtt" && extension != "srt") { - try { - delete(context, name, folder, "vtt", parentId, basePath) - delete(context, name, folder, "srt", parentId, basePath) - } catch (e: Exception) { - logError(e) - } - } - - // If scoped storage and using download dir (not accessible with UniFile) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.isDownloadDir()) { - val relativePath = getRelativePath(folder) - val lastContent = - context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName) ?: return ERROR_DELETING_FILE - if(context.contentResolver.delete(lastContent, null, null) <= 0) { - return ERROR_DELETING_FILE - } - } else { - val dir = basePath?.gotoDir(folder) - val file = dir?.findFile(displayName) - val success = file?.delete() - if (success != true) return ERROR_DELETING_FILE else { - // Cleans up empty directory - if (dir.listFiles()?.isEmpty() == true) dir.delete() - } - parentId?.let { - downloadDeleteEvent.invoke(parentId) - } - } - return SUCCESS_STOPPED - }*/ - fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { return getFileName(context, metadata.name, metadata.episode, metadata.season) @@ -1765,70 +1606,60 @@ object VideoDownloadManager { } } - if (link.isM3u8 || URL(link.url).path.endsWith(".m3u8")) { - val startIndex = if (tryResume) { - context.getKey( - KEY_DOWNLOAD_INFO, - ep.id.toString(), - null - )?.extraInfo?.toIntOrNull() - } else null - return suspendSafeApiCall { - downloadHLS( + val callback: (CreateNotificationMetadata) -> Unit = { meta -> + main { + createNotification( + context, + source, + link.name, + ep, + meta.type, + meta.bytesDownloaded, + meta.bytesTotal, + notificationCallback, + meta.hlsProgress, + meta.hlsTotal, + meta.bytesPerSecond + ) + } + } + + try { + if (link.isM3u8 || normalSafeApiCall { URL(link.url).path.endsWith(".m3u8") } == true) { + val startIndex = if (tryResume) { + context.getKey( + KEY_DOWNLOAD_INFO, + ep.id.toString(), + null + )?.extraInfo?.toIntOrNull() + } else null + + return downloadHLS( context, link, name, folder, ep.id, startIndex, - createNotificationCallback = { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback, - meta.hlsProgress, - meta.hlsTotal, - meta.bytesPerSecond - ) - } - } + callback ) - }.also { - extractorJob.cancel() - } ?: ERROR_UNKNOWN + } else { + return downloadThing( + context, + link, + name, + folder, + "mp4", + tryResume, + ep.id, + callback + ) + } + } catch (t: Throwable) { + return ERROR_UNKNOWN + } finally { + extractorJob.cancel() } - - return suspendSafeApiCall { - downloadThing( - context, - link, - name, - folder, - "mp4", - tryResume, - ep.id, - createNotificationCallback = { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback, - bytesPerSecond = meta.bytesPerSecond - ) - } - }) - }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN } suspend fun downloadCheck( @@ -1911,26 +1742,10 @@ object VideoDownloadManager { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return null val base = basePathToFile(context, info.basePath) + val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName) + if (file?.exists() != true) return null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && base.isDownloadDir()) { - val cr = context.contentResolver ?: return null - val fileUri = - cr.getExistingDownloadUriOrNullQ(info.relativePath, info.displayName) - ?: return null - val fileLength = cr.getFileLength(fileUri) ?: return null - if (fileLength == 0L) return null - return DownloadedFileInfoResult(fileLength, info.totalBytes, fileUri) - } else { - - val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName) - -// val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName) -// val dFile = File(normalPath) - - if (file?.exists() != true) return null - - return DownloadedFileInfoResult(file.size(), info.totalBytes, file.uri) - } + return DownloadedFileInfoResult(file.size(), info.totalBytes, file.uri) } catch (e: Exception) { logError(e) return null @@ -1943,6 +1758,7 @@ object VideoDownloadManager { fun UniFile.size(): Long { val len = length() return if (len <= 1) { + println("LEN:::::::>>>>>>>>>>>>>>>>>>>>>>>$len") val inputStream = this.openInputStream() return inputStream.available().toLong().also { inputStream.closeQuietly() } } else { @@ -1962,32 +1778,20 @@ object VideoDownloadManager { relativePath: String, displayName: String ): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && folder.isDownloadDir()) { - val cr = context.contentResolver ?: return false - val fileUri = - cr.getExistingDownloadUriOrNullQ(relativePath, displayName) - ?: return true // FILE NOT FOUND, ALREADY DELETED - - return cr.delete(fileUri, null, null) > 0 // IF DELETED ROWS IS OVER 0 - } else { - val file = folder?.gotoDir(relativePath)?.findFile(displayName) -// val normalPath = context.getNormalPath(getFile(info.relativePath), info.displayName) -// val dFile = File(normalPath) - if (file?.exists() != true) return true - return try { - file.delete() - } catch (e: Exception) { - logError(e) - val cr = context.contentResolver - cr.delete(file.uri, null, null) > 0 - } + val file = folder?.gotoDir(relativePath)?.findFile(displayName) ?: return false + if (!file.exists()) return true + return try { + file.delete() + } catch (e: Exception) { + logError(e) + (context.contentResolver?.delete(file.uri, null, null) ?: return false) > 0 } } private fun deleteFile(context: Context, id: Int): Boolean { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false - downloadEvent.invoke(Pair(id, DownloadActionType.Stop)) + downloadEvent.invoke(id to DownloadActionType.Stop) downloadProgressEvent.invoke(Triple(id, 0, 0)) downloadStatusEvent.invoke(id to DownloadType.IsStopped) downloadDeleteEvent.invoke(id) From 3ea6b1a8d507899ae5a9b295e9f29ded7e0e0448 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 06:25:06 +0200 Subject: [PATCH 061/441] fixed resume download + migrated filesystem to SafeFile --- .../ui/player/DownloadedPlayerActivity.kt | 4 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 8 +- .../ui/settings/SettingsGeneral.kt | 42 +- .../cloudstream3/utils/BackupUtils.kt | 23 +- .../utils/VideoDownloadManager.kt | 378 +++++++----------- .../cloudstream3/utils/storage/MediaFile.kt | 369 +++++++++++++++++ .../cloudstream3/utils/storage/SafeFile.kt | 244 +++++++++++ .../utils/storage/UniFileWrapper.kt | 116 ++++++ 8 files changed, 924 insertions(+), 260 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 6f40e145..03405faf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -6,11 +6,11 @@ import android.os.Bundle import android.util.Log import android.view.KeyEvent import androidx.appcompat.app.AppCompatActivity -import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.storage.SafeFile const val DTAG = "PlayerActivity" @@ -50,7 +50,7 @@ class DownloadedPlayerActivity : AppCompatActivity() { } private fun playUri(uri: Uri) { - val name = UniFile.fromUri(this, uri).name + val name = SafeFile.fromUri(this, uri)?.name() this.navigate( R.id.global_to_navigation_player, GeneratorPlayer.newInstance( DownloadFileGenerator( 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 9e601fc7..341b4ad3 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 @@ -52,6 +52,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx +import com.lagradost.cloudstream3.utils.storage.SafeFile import kotlinx.coroutines.Job import java.util.* import kotlin.math.abs @@ -525,10 +526,11 @@ class GeneratorPlayer : FullScreenPlayer() { Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION ) - val file = UniFile.fromUri(ctx, uri) - println("Loaded subtitle file. Selected URI path: $uri - Name: ${file.name}") + val file = SafeFile.fromUri(ctx, uri) + val fileName = file?.name() + println("Loaded subtitle file. Selected URI path: $uri - Name: $fileName") // DO NOT REMOVE THE FILE EXTENSION FROM NAME, IT'S NEEDED FOR MIME TYPES - val name = file.name ?: uri.toString() + val name = fileName ?: uri.toString() val subtitleData = SubtitleData( name, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index 2c81ad1f..f46aac9b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -3,9 +3,7 @@ package com.lagradost.cloudstream3.ui.settings import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Build import android.os.Bundle -import android.os.Environment import android.view.View import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts @@ -13,7 +11,6 @@ import androidx.appcompat.app.AlertDialog import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty -import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -41,7 +38,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath -import java.io.File +import com.lagradost.cloudstream3.utils.storage.SafeFile fun getCurrentLocale(context: Context): String { val res = context.resources @@ -139,8 +136,9 @@ class SettingsGeneral : PreferenceFragmentCompat() { context.contentResolver.takePersistableUriPermission(uri, flags) - val file = UniFile.fromUri(context, uri) - println("Selected URI path: $uri - Full path: ${file.filePath}") + val file = SafeFile.fromUri(context, uri) + val filePath = file?.filePath() + println("Selected URI path: $uri - Full path: $filePath") // Stores the real URI using download_path_key // Important that the URI is stored instead of filepath due to permissions. @@ -149,7 +147,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { // From URI -> File path // File path here is purely for cosmetic purposes in settings - (file.filePath ?: uri.toString()).let { + (filePath ?: uri.toString()).let { PreferenceManager.getDefaultSharedPreferences(context) .edit().putString(getString(R.string.download_path_pref), it).apply() } @@ -306,25 +304,23 @@ class SettingsGeneral : PreferenceFragmentCompat() { } return@setOnPreferenceClickListener true } + fun getDownloadDirs(): List { return normalSafeApiCall { - val defaultDir = VideoDownloadManager.getDownloadDir()?.filePath + context?.let { ctx -> + val defaultDir = VideoDownloadManager.getDefaultDir(ctx)?.filePath() - // app_name_download_path = Cloudstream and does not change depending on release. - // DOES NOT WORK ON SCOPED STORAGE. - val secondaryDir = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) null else Environment.getExternalStorageDirectory().absolutePath + - File.separator + resources.getString(R.string.app_name_download_path) - val first = listOf(defaultDir, secondaryDir) - (try { - val currentDir = context?.getBasePath()?.let { it.first?.filePath ?: it.second } + val first = listOf(defaultDir) + (try { + val currentDir = ctx.getBasePath().let { it.first?.filePath() ?: it.second } - (first + - requireContext().getExternalFilesDirs("").mapNotNull { it.path } + - currentDir) - } catch (e: Exception) { - first - }).filterNotNull().distinct() + (first + + ctx.getExternalFilesDirs("").mapNotNull { it.path } + + currentDir) + } catch (e: Exception) { + first + }).filterNotNull().distinct() + } } ?: emptyList() } @@ -339,7 +335,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { val currentDir = settingsManager.getString(getString(R.string.download_path_pref), null) - ?: VideoDownloadManager.getDownloadDir().toString() + ?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx).toString() } activity?.showBottomDialog( dirs + listOf("Custom"), 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 5bd0cd15..2da54678 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -1,11 +1,8 @@ package com.lagradost.cloudstream3.utils import android.annotation.SuppressLint -import android.content.ContentValues import android.content.Context import android.net.Uri -import android.os.Build -import android.provider.MediaStore import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -36,9 +33,9 @@ import com.lagradost.cloudstream3.utils.DataStore.mapper import com.lagradost.cloudstream3.utils.DataStore.setKeyRaw import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.requestRW -import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath -import com.lagradost.cloudstream3.utils.VideoDownloadManager.isDownloadDir -import java.io.IOException +import com.lagradost.cloudstream3.utils.VideoDownloadManager.setupStream +import okhttp3.internal.closeQuietly +import java.io.OutputStream import java.io.PrintWriter import java.lang.System.currentTimeMillis import java.text.SimpleDateFormat @@ -147,6 +144,8 @@ object BackupUtils { @SuppressLint("SimpleDateFormat") fun FragmentActivity.backup() { + var fileStream: OutputStream? = null + var printStream: PrintWriter? = null try { if (!checkWrite()) { showToast(getString(R.string.backup_failed), Toast.LENGTH_LONG) @@ -154,13 +153,16 @@ object BackupUtils { return } - val subDir = getBasePath().first val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) val ext = "json" val displayName = "CS3_Backup_${date}" val backupFile = getBackup() + val stream = setupStream(this, displayName, null, ext, false) + fileStream = stream.openNew() + printStream = PrintWriter(fileStream) + printStream.print(mapper.writeValueAsString(backupFile)) - val steam = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + /*val steam = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && subDir?.isDownloadDir() == true ) { val cr = this.contentResolver @@ -198,7 +200,7 @@ object BackupUtils { val printStream = PrintWriter(steam) printStream.print(mapper.writeValueAsString(backupFile)) - printStream.close() + printStream.close()*/ showToast( R.string.backup_success, @@ -214,6 +216,9 @@ object BackupUtils { } catch (e: Exception) { logError(e) } + } finally { + printStream?.closeQuietly() + fileStream?.closeQuietly() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index a81e4b3a..37c02be4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -8,7 +8,6 @@ import android.content.* import android.graphics.Bitmap import android.net.Uri import android.os.Build -import android.os.Environment import androidx.annotation.DrawableRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -20,7 +19,6 @@ import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager import com.bumptech.glide.load.model.GlideUrl import com.fasterxml.jackson.annotation.JsonProperty -import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -31,19 +29,19 @@ import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall -import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import com.lagradost.cloudstream3.utils.storage.MediaFileContentType +import com.lagradost.cloudstream3.utils.storage.SafeFile import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel -import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -160,24 +158,33 @@ object VideoDownloadManager { @JsonProperty("pkg") val pkg: DownloadResumePackage, ) - private const val SUCCESS_DOWNLOAD_DONE = 1 - private const val SUCCESS_STREAM = 3 - private const val SUCCESS_STOPPED = 2 + data class DownloadStatus( + /** if you should retry with the same args and hope for a better result */ + val retrySame: Boolean, + /** if you should try the next mirror */ + val tryNext: Boolean, + /** if the result is what the user intended */ + val success: Boolean, + ) - // will not download the next one, but is still classified as an error - private const val ERROR_DELETING_FILE = 3 - private const val ERROR_CREATE_FILE = -2 - private const val ERROR_UNKNOWN = -10 + /** Invalid input, just skip to the next one as the same args will give the same error */ + private val DOWNLOAD_INVALID_INPUT = + DownloadStatus(retrySame = false, tryNext = true, success = false) - //private const val ERROR_OPEN_FILE = -3 - private const val ERROR_TOO_SMALL_CONNECTION = -4 + /** no need to try any other mirror as we have downloaded the file */ + private val DOWNLOAD_SUCCESS = + DownloadStatus(retrySame = false, tryNext = false, success = true) - //private const val ERROR_WRONG_CONTENT = -5 - private const val ERROR_CONNECTION_ERROR = -6 + /** the user pressed stop, so no need to download anything else */ + private val DOWNLOAD_STOPPED = + DownloadStatus(retrySame = false, tryNext = false, success = true) - //private const val ERROR_MEDIA_STORE_URI_CANT_BE_CREATED = -7 - //private const val ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM = -8 - private const val ERROR_CONTENT_RESOLVER_NOT_FOUND = -9 + /** 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) + + /** 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) private const val KEY_RESUME_PACKAGES = "download_resume" const val KEY_DOWNLOAD_INFO = "download_info" @@ -209,15 +216,15 @@ object VideoDownloadManager { } } - /** Will return IsDone if not found or error */ - fun getDownloadState(id: Int): DownloadType { - return try { - downloadStatus[id] ?: DownloadType.IsDone - } catch (e: Exception) { - logError(e) - DownloadType.IsDone - } - } + ///** Will return IsDone if not found or error */ + //fun getDownloadState(id: Int): DownloadType { + // return try { + // downloadStatus[id] ?: DownloadType.IsDone + // } catch (e: Exception) { + // logError(e) + // DownloadType.IsDone + // } + //} private val cachedBitmaps = hashMapOf() fun Context.getImageBitmapFromUrl(url: String, headers: Map? = null): Bitmap? { @@ -302,7 +309,7 @@ object VideoDownloadManager { if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { builder.setProgress((total / 1000).toInt(), (progress / 1000).toInt(), false) } else if (state == DownloadType.IsPending) { - builder.setProgress(0,0,true) + builder.setProgress(0, 0, true) } val rowTwoExtra = if (ep.name != null) " - ${ep.name}\n" else "" @@ -496,10 +503,11 @@ object VideoDownloadManager { basePath: String? ): List>? { val base = basePathToFile(context, basePath) - val folder = base?.gotoDir(relativePath, false) ?: return null - if (!folder.isDirectory) return null + val folder = base?.gotoDirectory(relativePath, false) ?: return null + if (folder.isDirectory() != false) return null - return folder.listFiles()?.map { (it.name ?: "") to it.uri } + return folder.listFiles() + ?.mapNotNull { (it.name() ?: "") to (it.uri() ?: return@mapNotNull null) } } @@ -514,37 +522,29 @@ object VideoDownloadManager { data class StreamData( private val fileLength: Long, - val file: UniFile, + val file: SafeFile, //val fileStream: OutputStream, ) { - fun open() : OutputStream { - return file.openOutputStream(resume) + @Throws(IOException::class) + fun open(): OutputStream { + return file.openOutputStreamOrThrow(resume) } - fun openNew() : OutputStream { - return file.openOutputStream(false) + @Throws(IOException::class) + fun openNew(): OutputStream { + return file.openOutputStreamOrThrow(false) + } + + fun delete(): Boolean { + return file.delete() } val resume: Boolean get() = fileLength > 0L val startAt: Long get() = if (resume) fileLength else 0L - val exists: Boolean get() = file.exists() + val exists: Boolean get() = file.exists() == true } - //class ADownloadException(val id: Int) : RuntimeException(message = "Download error $id") - - fun UniFile.createFileOrThrow(displayName: String): UniFile { - return this.createFile(displayName) ?: throw IOException("Could not create file") - } - - fun UniFile.deleteOrThrow() { - if (!this.delete()) throw IOException("Could not delete file") - } - - /** - * Sets up the appropriate file and creates a data stream from the file. - * Used for initializing downloads. - * */ @Throws(IOException::class) fun setupStream( context: Context, @@ -552,19 +552,39 @@ object VideoDownloadManager { folder: String?, extension: String, tryResume: Boolean, + ): StreamData { + val (base, _) = context.getBasePath() + return setupStream( + base ?: throw IOException("Bad config"), + name, + folder, + extension, + tryResume + ) + } + + /** + * Sets up the appropriate file and creates a data stream from the file. + * Used for initializing downloads. + * */ + @Throws(IOException::class) + fun setupStream( + baseFile: SafeFile, + name: String, + folder: String?, + extension: String, + tryResume: Boolean, ): StreamData { val displayName = getDisplayName(name, extension) - val (baseFile, _) = context.getBasePath() - - val subDir = baseFile?.gotoDir(folder) ?: throw IOException() + val subDir = baseFile.gotoDirectoryOrThrow(folder) val foundFile = subDir.findFile(displayName) - val (file, fileLength) = if (foundFile == null || !foundFile.exists()) { + val (file, fileLength) = if (foundFile == null || foundFile.exists() != true) { subDir.createFileOrThrow(displayName) to 0L } else { if (tryResume) { - foundFile to foundFile.size() + foundFile to foundFile.lengthOrThrow() } else { foundFile.deleteOrThrow() subDir.createFileOrThrow(displayName) to 0L @@ -1004,21 +1024,20 @@ object VideoDownloadManager { } } - @Throws suspend fun downloadThing( context: Context, link: IDownloadableMinimum, name: String, - folder: String?, + folder: String, extension: String, tryResume: Boolean, parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, parallelConnections: Int = 3 - ): Int = withContext(Dispatchers.IO) { + ): DownloadStatus = withContext(Dispatchers.IO) { // we cant download torrents with this implementation, aria2c might be used in the future - if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { - return@withContext ERROR_UNKNOWN + if (link.url.startsWith("magnet") || link.url.endsWith(".torrent") || parallelConnections < 1) { + return@withContext DOWNLOAD_INVALID_INPUT } var fileStream: OutputStream? = null @@ -1033,13 +1052,10 @@ object VideoDownloadManager { // get the file path val (baseFile, basePath) = context.getBasePath() val displayName = getDisplayName(name, extension) - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( - folder - ) else folder + if (baseFile == null) return@withContext DOWNLOAD_BAD_CONFIG // set up the download file - val stream = setupStream(context, name, relativePath, extension, tryResume) + val stream = setupStream(baseFile, name, folder, extension, tryResume) fileStream = stream.open() @@ -1069,7 +1085,7 @@ object VideoDownloadManager { metadata.setDownloadFileInfoTemplate( DownloadedFileInfo( totalBytes = metadata.approxTotalBytes, - relativePath = relativePath ?: "", + relativePath = folder, displayName = displayName, basePath = basePath ) @@ -1202,19 +1218,19 @@ object VideoDownloadManager { if (!stream.exists) metadata.type = DownloadType.IsStopped if (metadata.type == DownloadType.IsFailed) { - return@withContext ERROR_CONNECTION_ERROR + return@withContext DOWNLOAD_FAILED } if (metadata.type == DownloadType.IsStopped) { // we need to close before delete fileStream.closeQuietly() metadata.onDelete() - deleteFile(context, baseFile, relativePath ?: "", displayName) - return@withContext SUCCESS_STOPPED + stream.delete() + return@withContext DOWNLOAD_STOPPED } metadata.type = DownloadType.IsDone - return@withContext SUCCESS_DOWNLOAD_DONE + return@withContext DOWNLOAD_SUCCESS } catch (e: IOException) { // some sort of IO error, this should not happened // we just rethrow it @@ -1226,7 +1242,7 @@ object VideoDownloadManager { // note that when failing we don't want to delete the file, // only user interaction has that power metadata.type = DownloadType.IsFailed - return@withContext ERROR_CONNECTION_ERROR + return@withContext DOWNLOAD_FAILED } finally { fileStream?.closeQuietly() //requestStream?.closeQuietly() @@ -1234,39 +1250,36 @@ object VideoDownloadManager { } } - @Throws private suspend fun downloadHLS( context: Context, link: ExtractorLink, name: String, - folder: String?, + folder: String, parentId: Int?, startIndex: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, parallelConnections: Int = 3 - ): Int = withContext(Dispatchers.IO) { - require(parallelConnections >= 1) + ): DownloadStatus = withContext(Dispatchers.IO) { + if (parallelConnections < 1) return@withContext DOWNLOAD_INVALID_INPUT val metadata = DownloadMetaData( createNotificationCallback = createNotificationCallback, id = parentId ) - val extension = "mp4" - var fileStream: OutputStream? = null try { + val extension = "mp4" + // the start .ts index var startAt = startIndex ?: 0 // set up the file data val (baseFile, basePath) = context.getBasePath() - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( - folder - ) else folder + if (baseFile == null) return@withContext DOWNLOAD_BAD_CONFIG + val displayName = getDisplayName(name, extension) val stream = - setupStream(context, name, relativePath, extension, startAt > 0) + setupStream(baseFile, name, folder, extension, startAt > 0) if (!stream.resume) startAt = 0 fileStream = stream.open() @@ -1277,7 +1290,7 @@ object VideoDownloadManager { metadata.setDownloadFileInfoTemplate( DownloadedFileInfo( totalBytes = 0, - relativePath = relativePath ?: "", + relativePath = folder, displayName = displayName, basePath = basePath ) @@ -1406,99 +1419,29 @@ object VideoDownloadManager { if (!stream.exists) metadata.type = DownloadType.IsStopped if (metadata.type == DownloadType.IsFailed) { - return@withContext ERROR_CONNECTION_ERROR + return@withContext DOWNLOAD_FAILED } if (metadata.type == DownloadType.IsStopped) { // we need to close before delete fileStream.closeQuietly() metadata.onDelete() - deleteFile(context, baseFile, relativePath ?: "", displayName) - return@withContext SUCCESS_STOPPED + stream.delete() + return@withContext DOWNLOAD_STOPPED } metadata.type = DownloadType.IsDone - return@withContext SUCCESS_DOWNLOAD_DONE + return@withContext DOWNLOAD_SUCCESS } catch (t: Throwable) { logError(t) metadata.type = DownloadType.IsFailed - return@withContext ERROR_UNKNOWN + return@withContext DOWNLOAD_FAILED } finally { fileStream?.closeQuietly() metadata.close() } } - - /** - * Guarantees a directory is present with the dir name (if createMissingDirectories is true). - * Works recursively when '/' is present. - * Will remove any file with the dir name if present and add directory. - * Will not work if the parent directory does not exist. - * - * @param directoryName if null will use the current path. - * @return UniFile / null if createMissingDirectories = false and folder is not found. - * */ - private fun UniFile.gotoDir( - directoryName: String?, - createMissingDirectories: Boolean = true - ): UniFile? { - if(directoryName == null) return this - - return directoryName.split(File.separatorChar).filter { it.isNotBlank() }.fold(this) { file: UniFile?, directory -> - file?.createDirectory(directory) - } - - // May give this error on scoped storage. - // W/DocumentsContract: Failed to create document - // java.lang.IllegalArgumentException: Parent document isn't a directory - - // Not present in latest testing. - - println("Going to dir $directoryName from ${this.uri} ---- ${this.filePath}") - - try { - // Creates itself from parent if doesn't exist. - if (!this.exists() && createMissingDirectories && !this.name.isNullOrBlank()) { - if (this.parentFile != null) { - this.parentFile?.createDirectory(this.name) - } else if (this.filePath != null) { - UniFile.fromFile(File(this.filePath!!).parentFile)?.createDirectory(this.name) - } - } - - val allDirectories = directoryName?.split("/") - return if (allDirectories?.size == 1 || allDirectories == null) { - val found = this.findFile(directoryName) - when { - directoryName.isNullOrBlank() -> this - found?.isDirectory == true -> found - - !createMissingDirectories -> null - // Below creates directories - found?.isFile == true -> { - found.delete() - this.createDirectory(directoryName) - } - - this.isDirectory -> this.createDirectory(directoryName) - else -> this.parentFile?.createDirectory(directoryName) - } - } else { - var currentDirectory = this - allDirectories.forEach { - // If the next directory is not found it returns the deepest directory possible. - val nextDir = currentDirectory.gotoDir(it, createMissingDirectories) - currentDirectory = nextDir ?: return null - } - currentDirectory - } - } catch (e: Exception) { - logError(e) - return null - } - } - private fun getDisplayName(name: String, extension: String): String { return "$name.$extension" } @@ -1510,33 +1453,22 @@ object VideoDownloadManager { * As of writing UniFile is used for everything but download directory on scoped storage. * Special ContentResolver fuckery is needed for that as UniFile doesn't work. * */ - fun getDownloadDir(): UniFile? { + fun getDefaultDir(context: Context): SafeFile? { // See https://www.py4u.net/discuss/614761 - return UniFile.fromFile( - File( - Environment.getExternalStorageDirectory().absolutePath + File.separatorChar + - Environment.DIRECTORY_DOWNLOADS - ) + return SafeFile.fromMedia( + context, MediaFileContentType.Downloads ) } - @Deprecated("TODO fix UniFile to work with download directory.") - private fun getRelativePath(folder: String?): String { - return (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace( - '/', - File.separatorChar - ).replace("${File.separatorChar}${File.separatorChar}", File.separatorChar.toString()) - } - /** * Turns a string to an UniFile. Used for stored string paths such as settings. * Should only be used to get a download path. * */ - private fun basePathToFile(context: Context, path: String?): UniFile? { + private fun basePathToFile(context: Context, path: String?): SafeFile? { return when { - path.isNullOrBlank() -> getDownloadDir() - path.startsWith("content://") -> UniFile.fromUri(context, path.toUri()) - else -> UniFile.fromFile(File(path)) + path.isNullOrBlank() -> getDefaultDir(context) + path.startsWith("content://") -> SafeFile.fromUri(context, path.toUri()) + else -> SafeFile.fromFile(context, File(path)) } } @@ -1545,17 +1477,12 @@ object VideoDownloadManager { * Returns the file and a string to be stored for future file retrieval. * UniFile.filePath is not sufficient for storage. * */ - fun Context.getBasePath(): Pair { + fun Context.getBasePath(): Pair { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val basePathSetting = settingsManager.getString(getString(R.string.download_path_key), null) return basePathToFile(this, basePathSetting) to basePathSetting } - fun UniFile?.isDownloadDir(): Boolean { - return this != null && this.filePath == getDownloadDir()?.filePath - } - - fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { return getFileName(context, metadata.name, metadata.episode, metadata.season) } @@ -1596,7 +1523,7 @@ object VideoDownloadManager { link: ExtractorLink, notificationCallback: (Int, Notification) -> Unit, tryResume: Boolean = false, - ): Int { + ): DownloadStatus { val name = getFileName(context, ep) // Make sure this is cancelled when download is done or cancelled. @@ -1638,7 +1565,7 @@ object VideoDownloadManager { context, link, name, - folder, + folder ?: "", ep.id, startIndex, callback @@ -1648,7 +1575,7 @@ object VideoDownloadManager { context, link, name, - folder, + folder ?: "", "mp4", tryResume, ep.id, @@ -1656,7 +1583,7 @@ object VideoDownloadManager { ) } } catch (t: Throwable) { - return ERROR_UNKNOWN + return DOWNLOAD_FAILED } finally { extractorJob.cancel() } @@ -1698,10 +1625,8 @@ object VideoDownloadManager { notificationCallback, resume ) - //.also { println("Single episode finished with return code: $it") } - // retry every link at least once - if (connectionResult <= 0) { + if (connectionResult.retrySame) { connectionResult = downloadSingleEpisode( context, item.source, @@ -1713,11 +1638,12 @@ object VideoDownloadManager { ) } - if (connectionResult > 0) { // SUCCESS + if (connectionResult.success) { // SUCCESS removeKey(KEY_RESUME_PACKAGES, id.toString()) break - } else if (index == item.links.lastIndex) { + } else if (!connectionResult.tryNext || index >= item.links.lastIndex) { downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) + break } } } catch (e: Exception) { @@ -1731,62 +1657,69 @@ object VideoDownloadManager { // return id } - fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? { - val res = getDownloadFileInfo(context, id) - if (res == null) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) - return res + /* fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? { + val res = getDownloadFileInfo(context, id) + if (res == null) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) + return res + } + */ + fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? = + getDownloadFileInfo(context, id, removeKeys = true) + + private fun DownloadedFileInfo.toFile(context: Context): SafeFile? { + return basePathToFile(context, this.basePath)?.gotoDirectory(relativePath) + ?.findFile(displayName) } - private fun getDownloadFileInfo(context: Context, id: Int): DownloadedFileInfoResult? { + private fun getDownloadFileInfo( + context: Context, + id: Int, + removeKeys: Boolean = false + ): DownloadedFileInfoResult? { try { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return null - val base = basePathToFile(context, info.basePath) - val file = base?.gotoDir(info.relativePath, false)?.findFile(info.displayName) - if (file?.exists() != true) return null + val file = info.toFile(context) - return DownloadedFileInfoResult(file.size(), info.totalBytes, file.uri) + // only delete the key if the file is not found + if (file == null || !file.existsOrThrow()) { + if (removeKeys) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) + return null + } + + return DownloadedFileInfoResult( + file.lengthOrThrow(), + info.totalBytes, + file.uriOrThrow() + ) } catch (e: Exception) { logError(e) return null } } - /** - * Gets the true download size as Scoped Storage sometimes wrongly returns 0. - * */ - fun UniFile.size(): Long { - val len = length() - return if (len <= 1) { - println("LEN:::::::>>>>>>>>>>>>>>>>>>>>>>>$len") - val inputStream = this.openInputStream() - return inputStream.available().toLong().also { inputStream.closeQuietly() } - } else { - len - } - } - fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { val success = deleteFile(context, id) if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) return success } - private fun deleteFile( + /*private fun deleteFile( context: Context, - folder: UniFile?, + folder: SafeFile?, relativePath: String, displayName: String ): Boolean { - val file = folder?.gotoDir(relativePath)?.findFile(displayName) ?: return false - if (!file.exists()) return true + val file = folder?.gotoDirectory(relativePath)?.findFile(displayName) ?: return false + if (file.exists() == false) return true return try { file.delete() } catch (e: Exception) { logError(e) - (context.contentResolver?.delete(file.uri, null, null) ?: return false) > 0 + (context.contentResolver?.delete(file.uri() ?: return true, null, null) + ?: return false) > 0 } - } + }*/ private fun deleteFile(context: Context, id: Int): Boolean { val info = @@ -1795,8 +1728,7 @@ object VideoDownloadManager { downloadProgressEvent.invoke(Triple(id, 0, 0)) downloadStatusEvent.invoke(id to DownloadType.IsStopped) downloadDeleteEvent.invoke(id) - val base = basePathToFile(context, info.basePath) - return deleteFile(context, base, info.relativePath, info.displayName) + return info.toFile(context)?.delete() ?: false } fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt new file mode 100644 index 00000000..83c66b8b --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt @@ -0,0 +1,369 @@ +package com.lagradost.cloudstream3.utils.storage + +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import androidx.annotation.RequiresApi +import com.hippo.unifile.UniRandomAccessFile +import com.lagradost.cloudstream3.mvvm.logError +import okhttp3.internal.closeQuietly +import java.io.File +import java.io.InputStream +import java.io.OutputStream + + +enum class MediaFileContentType { + Downloads, + Audio, + Video, + Images, +} + +// https://developer.android.com/training/data-storage/shared/media +fun MediaFileContentType.toPath(): String { + return when (this) { + MediaFileContentType.Downloads -> Environment.DIRECTORY_DOWNLOADS + MediaFileContentType.Audio -> Environment.DIRECTORY_MUSIC + MediaFileContentType.Video -> Environment.DIRECTORY_MOVIES + MediaFileContentType.Images -> Environment.DIRECTORY_DCIM + } +} + +fun MediaFileContentType.defaultPrefix(): String { + return Environment.getExternalStorageDirectory().absolutePath +} + +fun MediaFileContentType.toAbsolutePath(): String { + return defaultPrefix() + File.separator + + this.toPath() +} + +fun replaceDuplicateFileSeparators(path: String): String { + return path.replace(Regex("${File.separator}+"), File.separator) +} + +@RequiresApi(Build.VERSION_CODES.Q) +fun MediaFileContentType.toUri(external: Boolean): Uri { + val volume = if (external) MediaStore.VOLUME_EXTERNAL_PRIMARY else MediaStore.VOLUME_INTERNAL + return when (this) { + MediaFileContentType.Downloads -> MediaStore.Downloads.getContentUri(volume) + MediaFileContentType.Audio -> MediaStore.Audio.Media.getContentUri(volume) + MediaFileContentType.Video -> MediaStore.Video.Media.getContentUri(volume) + MediaFileContentType.Images -> MediaStore.Images.Media.getContentUri(volume) + } +} + +@RequiresApi(Build.VERSION_CODES.Q) +class MediaFile( + private val context: Context, + private val folderType: MediaFileContentType, + private val external: Boolean = true, + absolutePath: String, +) : SafeFile { + // this is the path relative to the download directory so "/hello/text.txt" = "hello/text.txt" is in fact "Download/hello/text.txt" + private val sanitizedAbsolutePath: String = + replaceDuplicateFileSeparators(absolutePath) + + // this is only a directory if the filepath ends with a / + private val isDir: Boolean = sanitizedAbsolutePath.endsWith(File.separator) + private val isFile: Boolean = !isDir + + // this is the relative path including the Download directory, so "/hello/text.txt" => "Download/hello" + private val relativePath: String = + replaceDuplicateFileSeparators(folderType.toPath() + File.separator + sanitizedAbsolutePath).substringBeforeLast( + File.separator + ) + + // "/hello/text.txt" => "text.txt" + private val namePath: String = sanitizedAbsolutePath.substringAfterLast(File.separator) + private val baseUri = folderType.toUri(external) + private val contentResolver: ContentResolver = context.contentResolver + + init { + // some standard asserts that should always be hold or else this class wont work + assert(!relativePath.endsWith(File.separator)) + assert(!(isDir && isFile)) + assert(!relativePath.contains(File.separator + File.separator)) + assert(!namePath.contains(File.separator)) + + if (isDir) { + assert(namePath.isBlank()) + } else { + assert(namePath.isNotBlank()) + } + } + + companion object { + private fun splitFilenameExt(name: String): Pair { + val split = name.indexOfLast { it == '.' } + if (split <= 0) return name to null + val ext = name.substring(split + 1 until name.length) + if (ext.isBlank()) return name to null + + return name.substring(0 until split) to ext + } + + private fun splitFilenameMime(name: String): Pair { + val (display, ext) = splitFilenameExt(name) + val mimeType = when (ext) { + + // Absolutely ridiculous, if text/vtt is used as mimetype scoped storage prevents + // downloading to /Downloads yet it works with null + + "vtt" -> null // "text/vtt" + "mp4" -> "video/mp4" + "srt" -> null // "application/x-subrip"//"text/plain" + else -> null + } + return display to mimeType + } + } + + private fun appendRelativePath(path: String, folder: Boolean): MediaFile? { + if (isFile) return null + + // VideoDownloadManager.sanitizeFilename(path.replace(File.separator, "")) + + val newPath = + sanitizedAbsolutePath + path + if (folder) File.separator else "" + + return MediaFile( + context = context, + folderType = folderType, + external = external, + absolutePath = newPath + ) + } + + private fun createUri(displayName: String? = namePath): Uri? { + if (displayName == null) return null + if (isFile) return null + val (name, mime) = splitFilenameMime(displayName) + + val newFile = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) + put(MediaStore.MediaColumns.TITLE, name) + if (mime != null) + put(MediaStore.MediaColumns.MIME_TYPE, mime) + put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) + } + return contentResolver.insert(baseUri, newFile) + } + + override fun createFile(displayName: String?): SafeFile? { + if (isFile || displayName == null) return null + query(displayName)?.uri ?: createUri(displayName) ?: return null + return appendRelativePath(displayName, false) //SafeFile.fromUri(context, ?: return null) + } + + override fun createDirectory(directoryName: String?): SafeFile? { + if (directoryName == null) return null + // we don't create a dir here tbh, just fake create it + return appendRelativePath(directoryName, true) + } + + private data class QueryResult( + val uri: Uri, + val lastModified: Long, + val length: Long, + ) + + @RequiresApi(Build.VERSION_CODES.Q) + private fun query(displayName: String = namePath): QueryResult? { + try { + //val (name, mime) = splitFilenameMime(fullName) + + val projection = arrayOf( + MediaStore.MediaColumns._ID, + MediaStore.MediaColumns.DATE_MODIFIED, + MediaStore.MediaColumns.SIZE, + ) + + val selection = + "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}' AND ${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'" + + contentResolver.query( + baseUri, + projection, selection, null, null + )?.use { cursor -> + while (cursor.moveToNext()) { + val id = + cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) + + return QueryResult( + uri = ContentUris.withAppendedId( + baseUri, id + ), + lastModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)), + length = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)), + ) + } + } + } catch (t: Throwable) { + logError(t) + } + + return null + } + + override fun uri(): Uri? { + return query()?.uri + } + + override fun name(): String? { + if (isDir) return null + return namePath + } + + override fun type(): String? { + TODO("Not yet implemented") + } + + override fun filePath(): String { + return replaceDuplicateFileSeparators(relativePath + File.separator + namePath) + } + + override fun isDirectory(): Boolean { + return isDir + } + + override fun isFile(): Boolean { + return isFile + } + + override fun lastModified(): Long? { + if (isDir) return null + return query()?.lastModified + } + + override fun length(): Long? { + if (isDir) return null + val length = query()?.length ?: return null + if(length <= 0) { + val inputStream : InputStream = openInputStream() ?: return null + return try { + inputStream.available().toLong() + } catch (t : Throwable) { + null + } finally { + inputStream.closeQuietly() + } + } + return length + } + + override fun canRead(): Boolean { + TODO("Not yet implemented") + } + + override fun canWrite(): Boolean { + TODO("Not yet implemented") + } + + private fun delete(uri: Uri): Boolean { + return contentResolver.delete(uri, null, null) > 0 + } + + override fun delete(): Boolean { + return if (isDir) { + (listFiles() ?: return false).all { + it.delete() + } + } else { + delete(uri() ?: return false) + } + } + + override fun exists(): Boolean { + if (isDir) return true + return query() != null + } + + override fun listFiles(): List? { + if (isFile) return null + try { + val projection = arrayOf( + MediaStore.MediaColumns.DISPLAY_NAME + ) + + val selection = + "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}'" + contentResolver.query( + baseUri, + projection, selection, null, null + )?.use { cursor -> + val out = ArrayList(cursor.count) + while (cursor.moveToNext()) { + val nameIdx = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) + if (nameIdx == -1) continue + val name = cursor.getString(nameIdx) + + appendRelativePath(name, false)?.let { new -> + out.add(new) + } + } + + out + } + } catch (t: Throwable) { + logError(t) + } + return null + } + + override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? { + if (isFile || displayName == null) return null + + val new = appendRelativePath(displayName, false) ?: return null + if (new.exists()) { + return new + } + + return null//SafeFile.fromUri(context, query(displayName ?: return null)?.uri ?: return null) + } + + override fun renameTo(name: String?): Boolean { + TODO("Not yet implemented") + } + + override fun openOutputStream(append: Boolean): OutputStream? { + try { + // use current file + uri()?.let { + return contentResolver.openOutputStream( + it, + if (append) "wa" else "wt" + ) + } + + // create a new file if current is not found, + // as we know it is new only write access is needed + createUri()?.let { + return contentResolver.openOutputStream( + it, + "w" + ) + } + return null + } catch (t: Throwable) { + return null + } + } + + override fun openInputStream(): InputStream? { + try { + return contentResolver.openInputStream(uri() ?: return null) + } catch (t: Throwable) { + return null + } + } + + override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt new file mode 100644 index 00000000..9ba0ef88 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt @@ -0,0 +1,244 @@ +package com.lagradost.cloudstream3.utils.storage + +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import com.hippo.unifile.UniFile +import com.hippo.unifile.UniRandomAccessFile + +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +interface SafeFile { + companion object { + fun fromUri(context: Context, uri: Uri): SafeFile? { + return UniFileWrapper(UniFile.fromUri(context, uri) ?: return null) + } + + fun fromFile(context: Context, file: File?): SafeFile? { + if (file == null) return null + // because UniFile sucks balls on Media we have to do this + val absPath = file.absolutePath.removePrefix(File.separator) + for (value in MediaFileContentType.values()) { + val prefixes = listOf(value.toAbsolutePath(), value.toPath()) + for (prefix in prefixes) { + if (!absPath.startsWith(prefix)) continue + return fromMedia( + context, + value, + absPath.removePrefix(prefix).ifBlank { File.separator } + ) + } + } + + return UniFileWrapper(UniFile.fromFile(file) ?: return null) + } + + fun fromAsset( + context: Context, + filename: String? + ): SafeFile? { + return UniFileWrapper( + UniFile.fromAsset(context.assets, filename ?: return null) ?: return null + ) + } + + fun fromResource( + context: Context, + id: Int + ): SafeFile? { + return UniFileWrapper( + UniFile.fromResource(context, id) ?: return null + ) + } + + fun fromMedia( + context: Context, + folderType: MediaFileContentType, + path: String = File.separator, + external: Boolean = true, + ): SafeFile? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + //fromUri(context, folderType.toUri(external))?.findFile(folderType.toPath())?.gotoDirectory(path) + + return MediaFile( + context = context, + folderType = folderType, + external = external, + absolutePath = path + ) + } else { + fromFile( + context, + File( + (Environment.getExternalStorageDirectory().absolutePath + File.separator + + folderType.toPath() + File.separator + folderType).replace( + File.separator + File.separator, + File.separator + ) + ) + ) + } + + } + } + + /*val uri: Uri? get() = getUri() + val name: String? get() = getName() + val type: String? get() = getType() + val filePath: String? get() = getFilePath() + val isFile: Boolean? get() = isFile() + val isDirectory: Boolean? get() = isDirectory() + val length: Long? get() = length() + val canRead: Boolean get() = canRead() + val canWrite: Boolean get() = canWrite() + val lastModified: Long? get() = lastModified()*/ + + @Throws(IOException::class) + fun isFileOrThrow(): Boolean { + return isFile() ?: throw IOException("Unable to get if file is a file or directory") + } + + @Throws(IOException::class) + fun lengthOrThrow(): Long { + return length() ?: throw IOException("Unable to get file length") + } + + @Throws(IOException::class) + fun isDirectoryOrThrow(): Boolean { + return isDirectory() ?: throw IOException("Unable to get if file is a directory") + } + + @Throws(IOException::class) + fun filePathOrThrow(): String { + return filePath() ?: throw IOException("Unable to get file path") + } + + @Throws(IOException::class) + fun uriOrThrow(): Uri { + return uri() ?: throw IOException("Unable to get uri") + } + + @Throws(IOException::class) + fun renameOrThrow(name: String?) { + if (!renameTo(name)) { + throw IOException("Unable to rename to $name") + } + } + + @Throws(IOException::class) + fun openOutputStreamOrThrow(append: Boolean = false): OutputStream { + return openOutputStream(append) ?: throw IOException("Unable to open output stream") + } + + @Throws(IOException::class) + fun openInputStreamOrThrow(): InputStream { + return openInputStream() ?: throw IOException("Unable to open input stream") + } + + @Throws(IOException::class) + fun existsOrThrow(): Boolean { + return exists() ?: throw IOException("Unable get if file exists") + } + + @Throws(IOException::class) + fun findFileOrThrow(displayName: String?, ignoreCase: Boolean = false): SafeFile { + return findFile(displayName, ignoreCase) ?: throw IOException("Unable find file") + } + + @Throws(IOException::class) + fun gotoDirectoryOrThrow( + directoryName: String?, + createMissingDirectories: Boolean = true + ): SafeFile { + return gotoDirectory(directoryName, createMissingDirectories) + ?: throw IOException("Unable to go to directory $directoryName") + } + + @Throws(IOException::class) + fun listFilesOrThrow(): List { + return listFiles() ?: throw IOException("Unable to get files") + } + + + @Throws(IOException::class) + fun createFileOrThrow(displayName: String?): SafeFile { + return createFile(displayName) ?: throw IOException("Unable to create file $displayName") + } + + @Throws(IOException::class) + fun createDirectoryOrThrow(directoryName: String?): SafeFile { + return createDirectory( + directoryName ?: throw IOException("Unable to create file with invalid name") + ) + ?: throw IOException("Unable to create directory $directoryName") + } + + @Throws(IOException::class) + fun deleteOrThrow() { + if (!delete()) { + throw IOException("Unable to delete file") + } + } + + /** file.gotoDirectory("a/b/c") -> "file/a/b/c/" where a null or blank directoryName + * returns itself. createMissingDirectories specifies if the dirs should be created + * when travelling or break at a dir not found */ + fun gotoDirectory( + directoryName: String?, + createMissingDirectories: Boolean = true + ): SafeFile? { + if (directoryName == null) return this + + return directoryName.split(File.separatorChar).filter { it.isNotBlank() } + .fold(this) { file: SafeFile?, directory -> + // as MediaFile does not actually create a directory we can do this + if (createMissingDirectories || this is MediaFile) { + file?.createDirectory(directory) + } else { + val next = file?.findFile(directory) + + // we require the file to be a directory + if (next?.isDirectory() != true) { + null + } else { + next + } + } + } + } + + + fun createFile(displayName: String?): SafeFile? + fun createDirectory(directoryName: String?): SafeFile? + fun uri(): Uri? + fun name(): String? + fun type(): String? + fun filePath(): String? + fun isDirectory(): Boolean? + fun isFile(): Boolean? + fun lastModified(): Long? + fun length(): Long? + fun canRead(): Boolean + fun canWrite(): Boolean + fun delete(): Boolean + fun exists(): Boolean? + fun listFiles(): List? + + // fun listFiles(filter: FilenameFilter?): Array? + fun findFile(displayName: String?, ignoreCase: Boolean = false): SafeFile? + + fun renameTo(name: String?): Boolean + + /** Open a stream on to the content associated with the file */ + fun openOutputStream(append: Boolean = false): OutputStream? + + /** Open a stream on to the content associated with the file */ + fun openInputStream(): InputStream? + + /** Get a random access stuff of the UniFile, "r" or "rw" */ + fun createRandomAccessFile(mode: String?): UniRandomAccessFile? +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt new file mode 100644 index 00000000..f1592169 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt @@ -0,0 +1,116 @@ +package com.lagradost.cloudstream3.utils.storage + +import android.net.Uri +import com.hippo.unifile.UniFile +import com.hippo.unifile.UniRandomAccessFile +import com.lagradost.cloudstream3.mvvm.logError +import okhttp3.internal.closeQuietly +import java.io.InputStream +import java.io.OutputStream + +private fun UniFile.toFile(): SafeFile { + return UniFileWrapper(this) +} + +fun safe(apiCall: () -> T): T? { + return try { + apiCall.invoke() + } catch (throwable: Throwable) { + logError(throwable) + null + } +} + +class UniFileWrapper(val file: UniFile) : SafeFile { + override fun createFile(displayName: String?): SafeFile? { + return file.createFile(displayName)?.toFile() + } + + override fun createDirectory(directoryName: String?): SafeFile? { + return file.createDirectory(directoryName)?.toFile() + } + + override fun uri(): Uri? { + return safe { file.uri } + } + + override fun name(): String? { + return safe { file.name } + } + + override fun type(): String? { + return safe { file.type } + } + + override fun filePath(): String? { + return safe { file.filePath } + } + + override fun isDirectory(): Boolean? { + return safe { file.isDirectory } + } + + override fun isFile(): Boolean? { + return safe { file.isFile } + } + + override fun lastModified(): Long? { + return safe { file.lastModified() } + } + + override fun length(): Long? { + return safe { + val len = file.length() + if (len <= 1) { + val inputStream = this.openInputStream() ?: return@safe null + try { + inputStream.available().toLong() + } finally { + inputStream.closeQuietly() + } + } else { + len + } + } + } + + override fun canRead(): Boolean { + return safe { file.canRead() } ?: false + } + + override fun canWrite(): Boolean { + return safe { file.canWrite() } ?: false + } + + override fun delete(): Boolean { + return safe { file.delete() } ?: false + } + + override fun exists(): Boolean? { + return safe { file.exists() } + } + + override fun listFiles(): List? { + return safe { file.listFiles()?.mapNotNull { it?.toFile() } } + } + + override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? { + return safe { file.findFile(displayName, ignoreCase)?.toFile() } + } + + override fun renameTo(name: String?): Boolean { + return safe { file.renameTo(name) } ?: return false + } + + override fun openOutputStream(append: Boolean): OutputStream? { + return safe { file.openOutputStream(append) } + } + + override fun openInputStream(): InputStream? { + return safe { file.openInputStream() } + } + + override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? { + return safe { file.createRandomAccessFile(mode) } + } +} \ No newline at end of file From d436171a2f89f58107d5bc48d2a6be2fbb8845eb Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 06:36:43 +0200 Subject: [PATCH 062/441] removed possible duplicate download queue --- .../lagradost/cloudstream3/utils/VideoDownloadManager.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 37c02be4..442fa32f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -1598,9 +1598,8 @@ object VideoDownloadManager { val item = pkg.item val id = item.ep.id if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT - downloadEvent.invoke(Pair(id, DownloadActionType.Resume)) - /** ID needs to be returned to the work-manager to properly await notification */ - // return id + downloadEvent.invoke(id to DownloadActionType.Resume) + return } currentDownloads.add(id) @@ -1741,14 +1740,14 @@ object VideoDownloadManager { notificationCallback: (Int, Notification) -> Unit, setKey: Boolean = true ) { - if (!currentDownloads.any { it == pkg.item.ep.id }) { + if (!currentDownloads.any { it == pkg.item.ep.id } && !downloadQueue.any { it.item.ep.id == pkg.item.ep.id }) { downloadQueue.addLast(pkg) downloadCheck(context, notificationCallback) if (setKey) saveQueue() //ret } else { downloadEvent( - Pair(pkg.item.ep.id, DownloadActionType.Resume) + pkg.item.ep.id to DownloadActionType.Resume ) //null } From bac2ee980551d194b70866fc1580ecf40455e024 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 17:08:26 +0200 Subject: [PATCH 063/441] fixed div by zero --- .../com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index eb8cb9b3..91e97dfc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -44,6 +44,8 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { private fun fixPlayerSize() { playerWidthHeight?.let { (w, h) -> + if(w <= 0 || h <= 0) return@let + val orientation = context?.resources?.configuration?.orientation ?: return val sw = if (orientation == Configuration.ORIENTATION_LANDSCAPE) { From e2502de02cdf226ab4c37cbc71743c68896f5745 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 18:43:55 +0200 Subject: [PATCH 064/441] bump acra --- app/build.gradle.kts | 2 +- .../main/java/com/lagradost/cloudstream3/AcraApplication.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 50125aa3..178b49c2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,7 +58,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.1.6" + versionName = "4.1.7" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 32702657..c14780d8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -43,9 +43,9 @@ class CustomReportSender : ReportSender { override fun send(context: Context, errorContent: CrashReportData) { println("Sending report") val url = - "https://docs.google.com/forms/d/e/1FAIpQLSdOlbgCx7NeaxjvEGyEQlqdh2nCvwjm2vwpP1VwW7REj9Ri3Q/formResponse" + "https://docs.google.com/forms/d/e/1FAIpQLSfO4r353BJ79TTY_-t5KWSIJT2xfqcQWY81xjAA1-1N0U2eSg/formResponse" val data = mapOf( - "entry.753293084" to errorContent.toJSON() + "entry.1993829403" to errorContent.toJSON() ) thread { // to not run it on main thread From 5bad6aca352506c722749fa6c1b5123ae722a071 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 23 Aug 2023 23:57:54 +0200 Subject: [PATCH 065/441] fixed native crash handle --- .../com/lagradost/cloudstream3/AcraApplication.kt | 11 ++++++++--- .../com/lagradost/cloudstream3/NativeCrashHandler.kt | 11 ++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index c14780d8..4b4747ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -104,13 +104,17 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) : } class AcraApplication : Application() { + override fun onCreate() { super.onCreate() NativeCrashHandler.initCrashHandler() - Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) { + ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) - }) + }.also { + exceptionHandler = it + Thread.setDefaultUncaughtExceptionHandler(it) + } } override fun attachBaseContext(base: Context?) { @@ -138,6 +142,8 @@ class AcraApplication : Application() { } companion object { + var exceptionHandler: ExceptionHandler? = null + /** Use to get activity from Context */ tailrec fun Context.getActivity(): Activity? = this as? Activity ?: (this as? ContextWrapper)?.baseContext?.getActivity() @@ -212,6 +218,5 @@ class AcraApplication : Application() { activity?.supportFragmentManager?.fragments?.lastOrNull() ) } - } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt index e5cb2702..1fe00748 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt @@ -14,6 +14,12 @@ object NativeCrashHandler { private external fun getSignalStatus(): Int private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch { + + //launch { + // delay(10000) + // triggerNativeCrash() + //} + while (true) { delay(10_000) val signal = getSignalStatus() @@ -24,7 +30,10 @@ object NativeCrashHandler { if (lastError != null) continue if (checkSafeModeFile()) continue - throw RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n") + AcraApplication.exceptionHandler?.uncaughtException( + Thread.currentThread(), + RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n") + ) } } From 823ffd87080f12791a72d54508bb2393f4444d3c Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 00:25:05 +0200 Subject: [PATCH 066/441] reverted low api crash handle crashing --- app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 4b4747ae..5f3162b4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -107,7 +107,7 @@ class AcraApplication : Application() { override fun onCreate() { super.onCreate() - NativeCrashHandler.initCrashHandler() + //NativeCrashHandler.initCrashHandler() ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) From 9a1358e295cf9761d3e8c9bba04ef214dcfc96ca Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 24 Aug 2023 14:16:33 +0000 Subject: [PATCH 067/441] Lower targetSdk to get all installed packages (#571) --- app/build.gradle.kts | 2 +- app/src/main/AndroidManifest.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 178b49c2..dfd2c173 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -55,7 +55,7 @@ android { defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 33 + targetSdk = 29 versionCode = 59 versionName = "4.1.7" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 563c82f8..0e716034 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ - + Date: Thu, 24 Aug 2023 16:39:50 +0200 Subject: [PATCH 068/441] fixed removal of predownloaded files --- .../java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt index 9ba0ef88..85a74963 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt @@ -23,7 +23,7 @@ interface SafeFile { // because UniFile sucks balls on Media we have to do this val absPath = file.absolutePath.removePrefix(File.separator) for (value in MediaFileContentType.values()) { - val prefixes = listOf(value.toAbsolutePath(), value.toPath()) + val prefixes = listOf(value.toAbsolutePath(), value.toPath()).map { it.removePrefix(File.separator) } for (prefix in prefixes) { if (!absPath.startsWith(prefix)) continue return fromMedia( From c92ac3e8b3502f26ed812647134dc0977218831e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:13:42 +0200 Subject: [PATCH 069/441] fixed removal of predownloaded files 2 + permission --- app/src/main/AndroidManifest.xml | 2 +- .../java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0e716034..15767d7b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,7 @@ - + Download + if(relativePath == path) return this + val newPath = sanitizedAbsolutePath + path + if (folder) File.separator else "" From 9b4701fe91858c71fa32ecec98c3fb05451dfc6c Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:14:54 +0200 Subject: [PATCH 070/441] dont remove keys while this is tested --- .../com/lagradost/cloudstream3/utils/VideoDownloadManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 442fa32f..948d7b8a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -1682,7 +1682,7 @@ object VideoDownloadManager { // only delete the key if the file is not found if (file == null || !file.existsOrThrow()) { - if (removeKeys) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) + //if (removeKeys) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) // TODO READD return null } From 1a4cbcaea048e9d057f9c70e37a7a46330a0203c Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 21:17:42 +0200 Subject: [PATCH 071/441] small fix --- .../ui/result/ResultViewModel2.kt | 4 +-- .../cloudstream3/utils/storage/MediaFile.kt | 27 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) 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 bdd27091..82d9a8fe 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 @@ -591,7 +591,7 @@ class ResultViewModel2 : ViewModel() { link, "$fileName ${link.name}", folder, - if (link.url.contains(".srt")) ".srt" else "vtt", + if (link.url.contains(".srt")) "srt" else "vtt", false, null, createNotificationCallback = {} ) @@ -719,7 +719,7 @@ class ResultViewModel2 : ViewModel() { ) ) } - .map { ExtractorSubtitleLink(it.name, it.url, "") } + .map { ExtractorSubtitleLink(it.name, it.url, "") }.take(3) .forEach { link -> val fileName = VideoDownloadManager.getFileName(context, meta) downloadSubtitle(context, link, fileName, folder) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt index 526d31ca..51b8adfe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt @@ -13,6 +13,7 @@ import com.hippo.unifile.UniRandomAccessFile import com.lagradost.cloudstream3.mvvm.logError import okhttp3.internal.closeQuietly import java.io.File +import java.io.FileNotFoundException import java.io.InputStream import java.io.OutputStream @@ -65,6 +66,10 @@ class MediaFile( private val external: Boolean = true, absolutePath: String, ) : SafeFile { + override fun toString(): String { + return sanitizedAbsolutePath + } + // this is the path relative to the download directory so "/hello/text.txt" = "hello/text.txt" is in fact "Download/hello/text.txt" private val sanitizedAbsolutePath: String = replaceDuplicateFileSeparators(absolutePath) @@ -130,7 +135,7 @@ class MediaFile( // VideoDownloadManager.sanitizeFilename(path.replace(File.separator, "")) // in case of duplicate path, aka Download -> Download - if(relativePath == path) return this + if (relativePath == path) return this val newPath = sanitizedAbsolutePath + path + if (folder) File.separator else "" @@ -246,12 +251,24 @@ class MediaFile( override fun length(): Long? { if (isDir) return null - val length = query()?.length ?: return null - if(length <= 0) { - val inputStream : InputStream = openInputStream() ?: return null + val query = query() + val length = query?.length ?: return null + if (length <= 0) { + try { + contentResolver.openFileDescriptor(query.uri, "r") + .use { + it?.statSize + }?.let { + return it + } + } catch (e: FileNotFoundException) { + return null + } + + val inputStream: InputStream = openInputStream() ?: return null return try { inputStream.available().toLong() - } catch (t : Throwable) { + } catch (t: Throwable) { null } finally { inputStream.closeQuietly() From b38a9b1ff5d6e1c5de21851af089232fca7c985b Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 24 Aug 2023 21:39:05 +0200 Subject: [PATCH 072/441] fuck android --- .../com/lagradost/cloudstream3/utils/VideoDownloadManager.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 948d7b8a..7bd863ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -62,6 +62,7 @@ const val DOWNLOAD_CHANNEL_DESCRIPT = "The download notification channel" object VideoDownloadManager { var maxConcurrentDownloads = 3 + var maxConcurrentConnections = 3 private var currentDownloads = mutableListOf() private const val USER_AGENT = @@ -1568,7 +1569,7 @@ object VideoDownloadManager { folder ?: "", ep.id, startIndex, - callback + callback, parallelConnections = maxConcurrentConnections ) } else { return downloadThing( @@ -1579,7 +1580,7 @@ object VideoDownloadManager { "mp4", tryResume, ep.id, - callback + callback, parallelConnections = maxConcurrentConnections ) } } catch (t: Throwable) { From 2d82480398c2dd5b0dfa5b0c0db7c626658aafd2 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Fri, 25 Aug 2023 15:58:58 +0700 Subject: [PATCH 073/441] fix Rabbitstream (#573) Co-authored-by: Sofie99 --- .../com/lagradost/cloudstream3/extractors/Rabbitstream.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt index b686f7d8..0154b4e8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt @@ -36,7 +36,6 @@ open class Rabbitstream : ExtractorApi() { override val requiresReferer = false open val embed = "ajax/embed-4" open val key = "https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt" - private var rawKey: String? = null override suspend fun getUrl( url: String, @@ -82,9 +81,10 @@ open class Rabbitstream : ExtractorApi() { ) } + } - private suspend fun getRawKey(): String = rawKey ?: app.get(key).text.also { rawKey = it } + private suspend fun getRawKey(): String = app.get(key).text private fun extractRealKey(originalString: String?, stops: String): Pair { val table = parseJson>>(stops) From d0c03321b90b13142dce2ab5931c13db48e4a90d Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 25 Aug 2023 10:59:18 +0200 Subject: [PATCH 074/441] Translations update from Hosted Weblate (#568) Co-authored-by: Carlos Luiz Co-authored-by: Joel Brink Co-authored-by: Julian Co-authored-by: Mubarek Seyd Juhar Co-authored-by: Sam Cooper Co-authored-by: Skrripy Co-authored-by: mbottari Co-authored-by: tabtomi8 --- app/src/main/res/values-ajp/strings.xml | 2 + app/src/main/res/values-am/strings.xml | 5 + app/src/main/res/values-ars/strings.xml | 203 +++++++++++++++++- app/src/main/res/values-bp/strings.xml | 69 +++++- app/src/main/res/values-de/strings.xml | 12 +- app/src/main/res/values-hu/strings.xml | 38 ++-- app/src/main/res/values-ti/strings.xml | 6 + app/src/main/res/values-uk/strings.xml | 4 +- .../metadata/android/ar-SA/changelogs/2.txt | 1 + .../android/ar-SA/full_description.txt | 10 +- .../android/ar-SA/short_description.txt | 2 +- fastlane/metadata/android/ar-SA/title.txt | 2 +- .../android/de-DE/full_description.txt | 6 +- 13 files changed, 321 insertions(+), 39 deletions(-) create mode 100644 app/src/main/res/values-ajp/strings.xml create mode 100644 app/src/main/res/values-am/strings.xml create mode 100644 app/src/main/res/values-ti/strings.xml create mode 100644 fastlane/metadata/android/ar-SA/changelogs/2.txt diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-ajp/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml new file mode 100644 index 00000000..98eb0e0d --- /dev/null +++ b/app/src/main/res/values-am/strings.xml @@ -0,0 +1,5 @@ + + + %s ክፍል %d + ተዋናዮች: %s + \ No newline at end of file diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index 42eba3cc..12d558ad 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -1,2 +1,203 @@ - + + لافتة + تغيير مزود + جارى التحميل + بث%s + ملء + تخطي التحميل + تحميل… + ترجمات + إعادة محاولة الاتصال … + %sييبي%d + الحلقة%dسيتم نشرها في + %dي%dس%dد + %dس%dد + %dد + لافتة الحلقة + اللافتة الاساسية + اذهب للخالف + معاينة الخلفية + سرعة(%.2fx) + فتح مع كلاودستريم + الصفحة الاساسية + ...%sابحث + لايوجد بيانات + المزيد من الخيارات + فتح في المتصفح + المتصفح + شاهد الفلم + دفق التورنت + بدأ التنزيل + عشوائي قادم + تشغيل المقطع الدعائي + الأنواع + توقف التنزيل + خطط للمشاهدة + لا يوجد + إعادة المشاهدة + !تم العثور على تحديث جديد +\n%s->%s + %.1f:قدر + %dاقل + كلاودستريم + بحث + التحميلات + اعدادات + ...بحث + الحلقة القادمة + شارك + مشاهدة + في التوقف + مكتمل + توقف + تشغيل البث المباشر + مصادر + تشغيل الحلقة + تم إلغاء التنزيل + تم التنزيل + تنززل + تحميل + عُد + التحميل فشل + استخدم سطوع النظام في مشغل التطبيق بدلاً من التراكب الداكن + تم تحميل ملف النسخ الاحتياطي + البحث المتقدم + إزالة الحدود السوداء + ترجمات + يضيف خيار السرعة في المشغل + انقر نقرا مزدوجا للبحث + انقر نقرًا مزدوجًا للإيقاف المؤقت + اللاعب يبحث عن المبلغ (بالثواني) + اسحب من جانب إلى آخر للتحكم بموقعك في الفيديو + ابدأ الحلقة التالية عندما تنتهي الحلقة الحالية + استخدام سطوع النظام + تحديث مراقبة التقدم + قم بمزامنة تقدم الحلقة الحالية تلقائيًا + اسحب لتغيير الإعدادات + استعادة البيانات من النسخة الاحتياطية + فشل في استعادة البيانات من الملف %s + انقر مرتين على الجانب الأيمن أو الأيسر للبحث للأمام أو للخلف + البيانات المخزنة + اضغط مرتين في المنتصف للتوقف مؤقتًا + أذونات التخزين مفقودة. حاول مرة اخرى. + حدث خطأ أثناء النسخ الاحتياطي %s + بحث + مكتبة + معلومات + التحديثات والنسخ الاحتياطي + يعطيك نتائج البحث مفصولة حسب المزود + يرسل فقط البيانات عن الأعطال + عرض المقطورات + عرض الملصقات من كيتسو + حسابات + لا يرسل أي بيانات + عرض حلقة حشو للأنمي + إخفاء جودة الفيديو المحددة في نتائج البحث + تحديثات البرنامج المساعد التلقائي + البحث تلقائيًا عن التحديثات الجديدة بعد بدء تشغيل التطبيق. + التحديث إلى الإصداراالمسبق + تنزيل المكونات الإضافية تلقائيًا + إعادة عملية الإعداد + ابحث عن تحديثات الإصدار التجريبي بدلاً من الإصدارات الكاملة فقط + حدد الوضع لتصفية تنزيل المكونات الإضافية + قم تلقائيًا بتثبيت جميع المكونات الإضافية التي لم يتم تثبيتها بعد من المستودعات المضافة. + إعدادات ترجمات كرومكاست + وضع إيجينجرافي + انتقد للبحث + نسخ إحتياطي للبيانات + إظهار تحديثات التطبيق + إعدادات ترجمات المشغل + ترجمات كرومكاست + قم بالتمرير لأعلى أو لأسفل على الجانب الأيسر أو الأيمن لتغيير السطوع أو مستوى الصوت + التشغيل التلقائي للحلقة القادمة + تطبيق رواية خفيفة من نفس المطورين + أعط بينيني للمطورين + جيتهب + تطبيق انيمي من نفس المطورين + لغة التطبيق + انضم إلى الديسكورد + بنيني معطا + بعض الهواتف لا تدعم مثبت الحزمة الجديد. جرب الخيار القديم إذا لم يتم تثبيت التحديثات. + مثبت تتبيق + اجتاز + الحلقات + موسم + تم نسخ الرابط إلى الحافظة + مسح + وقف + جارٍ تنزيل تحديث التطبيق… + إعادة التعيين إلى القيمة العادية + س + %d%s + لا يتمتع هذا المزود بدعم كرومكاست + لم يتم العثور على أي روابط + تشغيل الحلقة + عذرًا، تعطل التطبيق. سيتم إرسال تقرير خطأ مجهول إلى المطورين + %s%d%s + لا يوجد موسم + حلقة + %d-%d + يي + امسح التاريخ + جارٍ تثبيت تحديث التطبيق… + بدأ + لم يتم العثور على أي حلقات + إظهار تخطي النوافذ المنبثقة للفتح/الإنهاء + الكثير من النص. غير قادر على الحفظ في الحافظة. + وضع علامة كما شاهدت + إزالة من شاهد + حذف ملف + فشل + اكتمل + -30 + +30 + تاريخ + هل أنت متأكد أنك تريد الخروج؟ + نعم + لا + تعذر تثبيت الإصدار الجديد من التطبيق + إرث + منزل المجموعة + التقييم (من الأقل إلى الأعلى) + تم التحديث (من الجديد إلى القديم) + تم التحديث (القديم إلى الجديد) + أبجديًا (من الألف إلى الياء) + مكتبتك فارغة :( +\nقم بتسجيل الدخول إلى حساب المكتبة أو قم بإضافة العروض إلى مكتبتك المحلية. + !تم العثور على ملف الوضع الآمن +\n.عدم تحميل أي ملحقات عند بدء التشغيل حتى تتم إزالة الملف + ارجع + تحديث العروض المشتركة + الوضع العادي + حرر + ملفات تعريفية + مساعدة + .هنا يمكنك تغيير كيفية ترتيب المصادر. إذا كان للفيديو أولوية أعلى، فسيظهر في مكان أعلى في تحديد المصدر. مجموع أولوية المصدر وأولوية الجودة هو أولوية الفيديو +\n +\nالمصدر أ: 3 +\nالجودة ب: 7 +\nستكون أولوية الفيديو المدمجة .10 +\n +\n!ملاحظة: إذا كان المجموع 10 أو أكثر، فسيقوم اللاعب تلقائيًا بتخطي التحميل عند تحميل هذا الرابط + لقد صوت بالفعل + أبجديًا (ياء إلى ألف) + ترتيب حسب + مشترك + سيتم تحديث التطبيق عند الخروج + رتب + التقييم (من الأعلى إلى الأقل) + حدد المكتبة + افتع مع + .هذه القائمة فارغة. حاول التبديل إلى واحد آخر + %sتم الاشتراك في + %sتم إلغاء الاشتراك من + !%dتم إصدار الحلقة + خلفية الملف الشخصي + %dملف التعريف + واي فاي + بيانات الجوال + استخدم + %sتعذر إنشاء واجهة المستخدم بشكل صحيح، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور + الصفات + \ No newline at end of file diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 425293e4..df95d69f 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -156,7 +156,7 @@ Não enviar nenhum dado Mostrar episódios de Filler em anime Mostrar trailers - Mostrar posters do kitsu + Mostrar posters do Kitsu Esconder qualidades de vídeo selecionadas nos resultados da Pesquisa Atualizações de plugin automáticas Mostrar atualizações do app @@ -183,7 +183,7 @@ S E Nenhum Episódio encontrado - Deletar Arquivo + Apagar Arquivo Deletar Pausar Retomar @@ -410,15 +410,19 @@ Transferido %d %s com sucesso Tudo %s já transferido Transferência em batch - plugin - plugins + Plugin + Plugins Isto irá apagar todos os repositórios de plugins Apagar repositório Transferir lista de sites a usar Transferido: %d Desativado: %d Não transferido: %d - Adicionar um repositório para instalar extensões de sites + CloudStream não tem fontes instaladas por padrão. Você precisa instalar um site de repositórios. +\n +\nPor causa das limitações do DMCA (Digital Millennium Copyright Act ) feito em nome de Sky UK Limited 🤮nós não podemos adicionar site de repositórios no app. +\n +\nEntre no nosso Discord ou pesquise online. Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas @@ -455,7 +459,7 @@ Editar Perfis Exibindo Player - procure na Barra de Progresso - remover dos assitidos + Remover dos assistidos Extensões Alfabética(A => Z) Abrir com @@ -468,7 +472,7 @@ Biblioteca Não Trilhas Sonoras - Votação(Baixa para Alta) + Votação (Baixa para Alta) Atualização iniciada Conteúdo +18 Ajuda @@ -476,7 +480,7 @@ Não pudemos instalar a nova versão do App instalador de pacotes Organizar por - Votação(Alta para Baixa) + Votação (Alta para Baixa) Alfabética(Z => A) Qualidade Perfil de plano de fundo @@ -499,4 +503,51 @@ Atualizando shows inscritos Player oculto - Procure na barra de progresso Conteúdo +18 - + Reiniciar + Parar + Marcar como assistido + Aplicativo precisa ser fechado para atualizar + Mostrar popups pulados para abertura e finalização + %d-%d + Player interno + Tamanho + Abrindo + %s %d%s + %d plugins atualizados + Todos as extensões serão desligadas para ajuda se talvez estejam causando algum bug. + Aplicativo não encontrado + Recapitular + Todas as linguagens + Pula %s + Mistura terminada + Modo seguro ligado + Ranquear: %s + Linguagem + Lista de reprodução HLS + Terminando + %d %s + Adicionado em (antigo para novo) + Introdução + plug-ins não foram encontrados no repositório + Repositório não encontrado, verifique o URL e tente usa uma VPN + Descrição + Versão + Autores + Instale a extensão primeiro + Créditos + Historico + Limpar historico + Tem Muito texto. Não é possível salvar no clipboard. + Player de vídeo preferido + Começar + Suportado + Status + MPV + Abrindo mistura + VLC + Aplicar quando reiniciar + Visualização info de crash + Faixas de áudio + Adicionado em (novo para antigo) + Faixas de video + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 45a6a66c..6892c8fd 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -92,7 +92,7 @@ Abbrechen Kopieren Schließen - Löschen + Leeren Speichern Player-Geschwindigkeit Untertiteleinstellungen @@ -390,7 +390,7 @@ Einrichtung überspringen Aussehen der App passend zu dem des Geräts ändern Absturzmeldung - Was möchtest du anschauen\? + Was möchten Sie sehen\? Fertig Erweiterungen Repository hinzufügen @@ -546,4 +546,10 @@ \nWerden eine kombinierte Videopriorität von 10 haben. \n \nHINWEIS: Wenn die Summe 10 oder mehr beträgt, überspringt der Player automatisch das Laden, wenn der Link geladen wird! - + Filtermodus für Plugin-Downloads auswählen + Es wurde bereits abgestimmt + Keine Plugins im Repository gefunden + Repository nicht gefunden, überprüfe die URL und probiere eine VPN + Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein schwerwiegender Fehler und sollte sofort gemeldet werden. %s + Deaktivieren + \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 46407f76..ac817db0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -190,7 +190,7 @@ Adatok eltárolva Hiba a biztonsági mentés során %s Fiókok - Szolgáltatás szerinti keresés eredmények + Szolgáltató szerint elkülönítve adja meg a keresési eredményeket Nem küld adatokat Poszterek megjelenítése Kitsu-ról Kiválasztott videóminőségek elrejtése keresési eredményekbe @@ -198,7 +198,7 @@ Bővítmények automatikus letöltése Automatikusan telepíti az összes még nem telepített bővítményt a hozzáadott tárolókból. Alkalmazás frissítések megjelenítése - Automatikusan keressen új frissítéseket indításkor + Automatikusan keressen új frissítéseket indításkor. Frissítés az előzetes kiadásokhoz (prerelease) Csak előzetesen kiadott frissítések (prerelease) keresése a teljes kiadások helyett Github @@ -232,30 +232,30 @@ Lejátszás böngészőben Feliratok letöltése Újracsatlakozás… - Swipe balra vagy jobbra a videólejátszóban az idő vezérléséhez + Húzd balra vagy jobbra a videólejátszóban az idő vezérléséhez Csúsztassa ujját a beállítások módosításához - Csúsztassa az újját bal vagy jobb oldalon a fényerő vagy hangerő megváltoztatásához + Csúsztassa felfelé vagy lefelé a bal vagy jobb oldalon a fényerő vagy a hangerő megváltoztatásához Biztonsági mentés 0 Banán a fejlesztőknek - Swipe to seek + Húzás a kereséshez Következő epizód automatikus lejátszása Következő epizód lejátszása amikor az aktuális epizód véget ér - Dupla koppintás to seek + Dupla koppintás a kereséshez Dupla koppintás a szüneteltetéshez - Player seek amount + Lejátszó keresési értéke (Másodpercben) Koppintson kétszer a jobb vagy bal oldalra az előre vagy hátra ugráshoz - Koppintson középre a szüneteltetéshez + Koppintson kétszer középen a szüneteltetéshez Rendszer fényerejének használata Rendszer fényerejének használata az appban a sötét átfedés helyett Előrehaladás frissítése Automatikusan szinkronizálja az aktuális epizód előrehaladását - Adatok visszaállítása a biztonsági mentésből + Adatok visszaállítása biztonsági mentésből Biztonsági mentés betöltve Információ Folytatás -30 Frissítés elkezdődött - Nem sikerült visszaállítani az adatok a fájlból %s + Nem sikerült visszaállítani az adatokat a %s fájlból Tárolási engedélyek hiányoznak. Kérjük próbálja újra. Csak összeomlásokról küld adatokat APK Telepítő @@ -280,7 +280,7 @@ DNS HTTPS-en keresztül Böngésző Android TV - kézmozdulatok + Kézmozdulatok frissítés kihagyása Alkalmazásfrissítések Szolgáltatók @@ -496,4 +496,18 @@ HQ %d letöltve Start - + Emulátor elrendezés + Nyomkövetés hozzáadása + Telefon elrendezés + Poszter cím helye + Tegye a címet a poszter alá + Az átugrás mértéke, amikor a lejátszó el van rejtve + Jogi nyilatkozat + Lejátszó megjelenítve - Ugrási Érték + Lejátszó elrejtve - Ugrási Érték + Klónozott oldal + Egy meglévő webhely klónjának hozzáadása, más URL-címmel + TV elrendezés + Automatikus + Az átugrás mértéke, amikor a lejátszó látható + \ No newline at end of file diff --git a/app/src/main/res/values-ti/strings.xml b/app/src/main/res/values-ti/strings.xml new file mode 100644 index 00000000..0f64858d --- /dev/null +++ b/app/src/main/res/values-ti/strings.xml @@ -0,0 +1,6 @@ + + + %s ክፋል %d + ክፋል %d በ ላይ ይወጣል + ተዋሳእቲ፡ %s + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index e0db1c0e..8b1b6c39 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -418,7 +418,7 @@ Почалося завантаження %d %s… Завантажено %d %s Всі %s вже завантажено - Пакетне завантаження + Завантажити пакети плагін плагіни Видалити репозиторій @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - + \ No newline at end of file diff --git a/fastlane/metadata/android/ar-SA/changelogs/2.txt b/fastlane/metadata/android/ar-SA/changelogs/2.txt new file mode 100644 index 00000000..cc43acf1 --- /dev/null +++ b/fastlane/metadata/android/ar-SA/changelogs/2.txt @@ -0,0 +1 @@ +تمت إضافة سجل التغيير! diff --git a/fastlane/metadata/android/ar-SA/full_description.txt b/fastlane/metadata/android/ar-SA/full_description.txt index 2107b338..9668a9b1 100644 --- a/fastlane/metadata/android/ar-SA/full_description.txt +++ b/fastlane/metadata/android/ar-SA/full_description.txt @@ -1,14 +1,10 @@ -يتيح لك كلاود ستريم -3 بث وتنزيل الأفلام والمسلسلات التلفزيونية والأنيمي. يأتي التطبيق بدون أي إعلانات وتحليلات. و يدعم العديد من مواقع البث الاولي(التريلر) والأفلام والمزيد. وتشمل الميزات: - +يسمح لك كلاود ستريم -3 ببث وتنزيل الأفلام, المسلسلات التلفزيونية, والأنيمي. +يأتي التطبيق بدون أي إعلانات وتحليلات و + يدعم العديد من مواقع البث الاولي(التريلر) ,والأفلام, والمزيد. إشارات مرجعية - -قم بتنزيل ودفق الأفلام والبرامج التلفزيونية والأنيمي - - تنزيلات الترجمة - دعم كروم كاست diff --git a/fastlane/metadata/android/ar-SA/short_description.txt b/fastlane/metadata/android/ar-SA/short_description.txt index f396ff81..7ccd9743 100644 --- a/fastlane/metadata/android/ar-SA/short_description.txt +++ b/fastlane/metadata/android/ar-SA/short_description.txt @@ -1 +1 @@ -بث وتحميل الأفلام والأنمي والمسلسلات التلفزيونية. +بث وتحميل الأفلام, الأنمي, والمسلسلات التلفزيونية. diff --git a/fastlane/metadata/android/ar-SA/title.txt b/fastlane/metadata/android/ar-SA/title.txt index 635e1390..7977b290 100644 --- a/fastlane/metadata/android/ar-SA/title.txt +++ b/fastlane/metadata/android/ar-SA/title.txt @@ -1 +1 @@ -كلاود ستريم +كلاودستريم diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt index df314372..ea2a8750 100644 --- a/fastlane/metadata/android/de-DE/full_description.txt +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -1,11 +1,11 @@ -Mit CloudStream-3 kannst du Filme, TV-Serien und Anime streamen und herunterladen. +Mit CloudStream-3 kannst du Filme, TV-Serien und Anime streamen und herunterladen. Die App kommt ganz ohne Werbung und Analytik aus. -Sie unterstützt mehrere Trailer-, Filmseiten und vieles mehr. Integrierte Features: +Sie unterstützt zahlreiche Trailer, Filmseiten und vieles mehr, unter anderem: Lesezeichen -Herunterladen und Streamen von Filmen, Fernsehsendungen und Animes +Herunterladen und Streaming von Filmen, Fernsehsendungen und Animes Downloads von Untertiteln From 557003895b65a7b6e1631489523e1225d56cdc34 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Fri, 25 Aug 2023 08:59:37 +0000 Subject: [PATCH 075/441] chore(locales): fix locale issues --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 3 +++ app/src/main/res/values-ajp/strings.xml | 2 +- app/src/main/res/values-am/strings.xml | 2 +- app/src/main/res/values-ars/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-ti/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- 9 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index f46aac9b..1bd9778e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -54,6 +54,8 @@ fun getCurrentLocale(context: Context): String { // https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes leave blank for auto val appLanguages = arrayListOf( /* begin language list */ + Triple("", "ajp", "ajp"), + Triple("", "አማርኛ", "am"), Triple("", "العربية", "ar"), Triple("", "ars", "ars"), Triple("", "български", "bg"), @@ -96,6 +98,7 @@ val appLanguages = arrayListOf( Triple("", "Soomaaliga", "so"), Triple("", "svenska", "sv"), Triple("", "தமிழ்", "ta"), + Triple("", "ትግርኛ", "ti"), Triple("", "Tagalog", "tl"), Triple("", "Türkçe", "tr"), Triple("", "українська", "uk"), diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index a6b3daec..42eba3cc 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml index 98eb0e0d..5a799eb4 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -2,4 +2,4 @@ %s ክፍል %d ተዋናዮች: %s - \ No newline at end of file + diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index 12d558ad..ea8aa05c 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -200,4 +200,4 @@ استخدم %sتعذر إنشاء واجهة المستخدم بشكل صحيح، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور الصفات - \ No newline at end of file + diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index df95d69f..b70eec12 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -550,4 +550,4 @@ Faixas de áudio Adicionado em (novo para antigo) Faixas de video - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6892c8fd..6739465a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -552,4 +552,4 @@ Repository nicht gefunden, überprüfe die URL und probiere eine VPN Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein schwerwiegender Fehler und sollte sofort gemeldet werden. %s Deaktivieren - \ No newline at end of file + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index ac817db0..05a7f0a7 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -510,4 +510,4 @@ TV elrendezés Automatikus Az átugrás mértéke, amikor a lejátszó látható - \ No newline at end of file + diff --git a/app/src/main/res/values-ti/strings.xml b/app/src/main/res/values-ti/strings.xml index 0f64858d..a9079ed5 100644 --- a/app/src/main/res/values-ti/strings.xml +++ b/app/src/main/res/values-ti/strings.xml @@ -3,4 +3,4 @@ %s ክፋል %d ክፋል %d በ ላይ ይወጣል ተዋሳእቲ፡ %s - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8b1b6c39..4866ecd4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - \ No newline at end of file + From 8193e39b3046192dfb6970e5ff49f30d629d033a Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Fri, 25 Aug 2023 23:16:34 +0200 Subject: [PATCH 076/441] bump + refactor --- app/build.gradle.kts | 15 +- .../lagradost/cloudstream3/MainActivity.kt | 18 +- .../cloudstream3/NativeCrashHandler.kt | 4 +- .../ui/player/DownloadedPlayerActivity.kt | 8 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 6 +- .../ui/settings/SettingsGeneral.kt | 4 +- .../cloudstream3/utils/BackupUtils.kt | 1 + .../utils/VideoDownloadManager.kt | 14 +- .../cloudstream3/utils/storage/MediaFile.kt | 389 ------------------ .../cloudstream3/utils/storage/SafeFile.kt | 244 ----------- .../utils/storage/UniFileWrapper.kt | 116 ------ 11 files changed, 40 insertions(+), 779 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dfd2c173..333fbfb8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,11 +32,12 @@ android { enable = true } - externalNativeBuild { - cmake { - path("CMakeLists.txt") - } - } + // disable this for now + //externalNativeBuild { + // cmake { + // path("CMakeLists.txt") + // } + //} signingConfigs { create("prerelease") { @@ -58,7 +59,7 @@ android { targetSdk = 29 versionCode = 59 - versionName = "4.1.7" + versionName = "4.1.8" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") @@ -232,7 +233,7 @@ dependencies { // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 - implementation("com.github.tachiyomiorg:unifile:17bec43") + implementation("com.github.LagradOst:SafeFile:0.0.2") // API because cba maintaining it myself implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 15b16078..fbad4fce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -144,6 +144,7 @@ import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser +import com.lagradost.safefile.SafeFile import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.io.File @@ -279,6 +280,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { companion object { const val TAG = "MAINACT" var lastError: String? = null + /** * Setting this will automatically enter the query in the search * next time the search fragment is opened. @@ -366,7 +368,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { nextSearchQuery = try { URLDecoder.decode(query, "UTF-8") - } catch (t : Throwable) { + } catch (t: Throwable) { logError(t) query } @@ -859,7 +861,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { RecyclerView::class.java.declaredMethods.firstOrNull { it.name == "scrollStep" }?.also { it.isAccessible = true } - } catch (t : Throwable) { + } catch (t: Throwable) { null } } @@ -906,11 +908,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (dx > 0) dx else 0 } - if(!NO_MOVE_LIST) { + if (!NO_MOVE_LIST) { parent.smoothScrollBy(rdx, 0) - }else { + } else { val smoothScroll = reflectedScroll - if(smoothScroll == null) { + if (smoothScroll == null) { parent.smoothScrollBy(rdx, 0) } else { try { @@ -920,12 +922,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val out = IntArray(2) smoothScroll.invoke(parent, rdx, 0, out) val scrolledX = out[0] - if(abs(scrolledX) <= 0) { // newFocus.measuredWidth*2 + if (abs(scrolledX) <= 0) { // newFocus.measuredWidth*2 smoothScroll.invoke(parent, -rdx, 0, out) parent.smoothScrollBy(scrolledX, 0) if (NO_MOVE_LIST) targetDx = scrolledX } - } catch (t : Throwable) { + } catch (t: Throwable) { parent.smoothScrollBy(rdx, 0) } } @@ -1131,10 +1133,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { snackbar.show() } } - } } + ioSafe { SafeFile.check(this@MainActivity) } if (PluginManager.checkSafeModeFile()) { normalSafeApiCall { diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt index 1fe00748..7be90440 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.launch object NativeCrashHandler { // external fun triggerNativeCrash() - private external fun initNativeCrashHandler() + /*private external fun initNativeCrashHandler() private external fun getSignalStatus(): Int private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch { @@ -49,5 +49,5 @@ object NativeCrashHandler { } initSignalPolling() - } + }*/ } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 03405faf..d181e175 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.player +import android.content.ContentUris import android.content.Intent import android.net.Uri import android.os.Bundle @@ -10,7 +11,7 @@ import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.cloudstream3.utils.storage.SafeFile +import com.lagradost.safefile.SafeFile const val DTAG = "PlayerActivity" @@ -57,7 +58,10 @@ class DownloadedPlayerActivity : AppCompatActivity() { listOf( ExtractorUri( uri = uri, - name = name ?: getString(R.string.downloaded_file) + name = name ?: getString(R.string.downloaded_file), + // well not the same as a normal id, but we take it as users may want to + // play downloaded files and save the location + id = kotlin.runCatching { ContentUris.parseId(uri) }.getOrNull()?.hashCode() ) ) ) 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 341b4ad3..2b9304b6 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 @@ -21,13 +21,11 @@ import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import androidx.media3.common.Format.NO_VALUE import androidx.media3.common.MimeTypes -import com.hippo.unifile.UniFile import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding -import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding import com.lagradost.cloudstream3.mvvm.* @@ -52,7 +50,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx -import com.lagradost.cloudstream3.utils.storage.SafeFile +import com.lagradost.safefile.SafeFile import kotlinx.coroutines.Job import java.util.* import kotlin.math.abs @@ -136,7 +134,7 @@ class GeneratorPlayer : FullScreenPlayer() { return durPos.position } - var currentVerifyLink: Job? = null + private var currentVerifyLink: Job? = null private fun loadExtractorJob(extractorLink: ExtractorLink?) { currentVerifyLink?.cancel() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index f46aac9b..49f678c4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -38,7 +38,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath -import com.lagradost.cloudstream3.utils.storage.SafeFile +import com.lagradost.safefile.SafeFile fun getCurrentLocale(context: Context): String { val res = context.resources @@ -335,7 +335,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { val currentDir = settingsManager.getString(getString(R.string.download_path_pref), null) - ?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx).toString() } + ?: context?.let { ctx -> VideoDownloadManager.getDefaultDir(ctx)?.filePath() } activity?.showBottomDialog( dirs + listOf("Custom"), 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 2da54678..41326eb8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -158,6 +158,7 @@ object BackupUtils { val displayName = "CS3_Backup_${date}" val backupFile = getBackup() val stream = setupStream(this, displayName, null, ext, false) + fileStream = stream.openNew() printStream = PrintWriter(fileStream) printStream.print(mapper.writeValueAsString(backupFile)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 7bd863ae..6425ba66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -35,8 +35,8 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -import com.lagradost.cloudstream3.utils.storage.MediaFileContentType -import com.lagradost.cloudstream3.utils.storage.SafeFile +import com.lagradost.safefile.MediaFileContentType +import com.lagradost.safefile.SafeFile import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -554,9 +554,8 @@ object VideoDownloadManager { extension: String, tryResume: Boolean, ): StreamData { - val (base, _) = context.getBasePath() return setupStream( - base ?: throw IOException("Bad config"), + context.getBasePath().first ?: getDefaultDir(context) ?: throw IOException("Bad config"), name, folder, extension, @@ -1401,7 +1400,12 @@ object VideoDownloadManager { metadata.type = DownloadType.IsFailed } } finally { - fileMutex.unlock() + try { + // may cause java.lang.IllegalStateException: Mutex is not locked because of cancelling + fileMutex.unlock() + } catch (t : Throwable) { + logError(t) + } } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt deleted file mode 100644 index 51b8adfe..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/MediaFile.kt +++ /dev/null @@ -1,389 +0,0 @@ -package com.lagradost.cloudstream3.utils.storage - -import android.content.ContentResolver -import android.content.ContentUris -import android.content.ContentValues -import android.content.Context -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.provider.MediaStore -import androidx.annotation.RequiresApi -import com.hippo.unifile.UniRandomAccessFile -import com.lagradost.cloudstream3.mvvm.logError -import okhttp3.internal.closeQuietly -import java.io.File -import java.io.FileNotFoundException -import java.io.InputStream -import java.io.OutputStream - - -enum class MediaFileContentType { - Downloads, - Audio, - Video, - Images, -} - -// https://developer.android.com/training/data-storage/shared/media -fun MediaFileContentType.toPath(): String { - return when (this) { - MediaFileContentType.Downloads -> Environment.DIRECTORY_DOWNLOADS - MediaFileContentType.Audio -> Environment.DIRECTORY_MUSIC - MediaFileContentType.Video -> Environment.DIRECTORY_MOVIES - MediaFileContentType.Images -> Environment.DIRECTORY_DCIM - } -} - -fun MediaFileContentType.defaultPrefix(): String { - return Environment.getExternalStorageDirectory().absolutePath -} - -fun MediaFileContentType.toAbsolutePath(): String { - return defaultPrefix() + File.separator + - this.toPath() -} - -fun replaceDuplicateFileSeparators(path: String): String { - return path.replace(Regex("${File.separator}+"), File.separator) -} - -@RequiresApi(Build.VERSION_CODES.Q) -fun MediaFileContentType.toUri(external: Boolean): Uri { - val volume = if (external) MediaStore.VOLUME_EXTERNAL_PRIMARY else MediaStore.VOLUME_INTERNAL - return when (this) { - MediaFileContentType.Downloads -> MediaStore.Downloads.getContentUri(volume) - MediaFileContentType.Audio -> MediaStore.Audio.Media.getContentUri(volume) - MediaFileContentType.Video -> MediaStore.Video.Media.getContentUri(volume) - MediaFileContentType.Images -> MediaStore.Images.Media.getContentUri(volume) - } -} - -@RequiresApi(Build.VERSION_CODES.Q) -class MediaFile( - private val context: Context, - private val folderType: MediaFileContentType, - private val external: Boolean = true, - absolutePath: String, -) : SafeFile { - override fun toString(): String { - return sanitizedAbsolutePath - } - - // this is the path relative to the download directory so "/hello/text.txt" = "hello/text.txt" is in fact "Download/hello/text.txt" - private val sanitizedAbsolutePath: String = - replaceDuplicateFileSeparators(absolutePath) - - // this is only a directory if the filepath ends with a / - private val isDir: Boolean = sanitizedAbsolutePath.endsWith(File.separator) - private val isFile: Boolean = !isDir - - // this is the relative path including the Download directory, so "/hello/text.txt" => "Download/hello" - private val relativePath: String = - replaceDuplicateFileSeparators(folderType.toPath() + File.separator + sanitizedAbsolutePath).substringBeforeLast( - File.separator - ) - - // "/hello/text.txt" => "text.txt" - private val namePath: String = sanitizedAbsolutePath.substringAfterLast(File.separator) - private val baseUri = folderType.toUri(external) - private val contentResolver: ContentResolver = context.contentResolver - - init { - // some standard asserts that should always be hold or else this class wont work - assert(!relativePath.endsWith(File.separator)) - assert(!(isDir && isFile)) - assert(!relativePath.contains(File.separator + File.separator)) - assert(!namePath.contains(File.separator)) - - if (isDir) { - assert(namePath.isBlank()) - } else { - assert(namePath.isNotBlank()) - } - } - - companion object { - private fun splitFilenameExt(name: String): Pair { - val split = name.indexOfLast { it == '.' } - if (split <= 0) return name to null - val ext = name.substring(split + 1 until name.length) - if (ext.isBlank()) return name to null - - return name.substring(0 until split) to ext - } - - private fun splitFilenameMime(name: String): Pair { - val (display, ext) = splitFilenameExt(name) - val mimeType = when (ext) { - - // Absolutely ridiculous, if text/vtt is used as mimetype scoped storage prevents - // downloading to /Downloads yet it works with null - - "vtt" -> null // "text/vtt" - "mp4" -> "video/mp4" - "srt" -> null // "application/x-subrip"//"text/plain" - else -> null - } - return display to mimeType - } - } - - private fun appendRelativePath(path: String, folder: Boolean): MediaFile? { - if (isFile) return null - - // VideoDownloadManager.sanitizeFilename(path.replace(File.separator, "")) - - // in case of duplicate path, aka Download -> Download - if (relativePath == path) return this - - val newPath = - sanitizedAbsolutePath + path + if (folder) File.separator else "" - - return MediaFile( - context = context, - folderType = folderType, - external = external, - absolutePath = newPath - ) - } - - private fun createUri(displayName: String? = namePath): Uri? { - if (displayName == null) return null - if (isFile) return null - val (name, mime) = splitFilenameMime(displayName) - - val newFile = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) - put(MediaStore.MediaColumns.TITLE, name) - if (mime != null) - put(MediaStore.MediaColumns.MIME_TYPE, mime) - put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) - } - return contentResolver.insert(baseUri, newFile) - } - - override fun createFile(displayName: String?): SafeFile? { - if (isFile || displayName == null) return null - query(displayName)?.uri ?: createUri(displayName) ?: return null - return appendRelativePath(displayName, false) //SafeFile.fromUri(context, ?: return null) - } - - override fun createDirectory(directoryName: String?): SafeFile? { - if (directoryName == null) return null - // we don't create a dir here tbh, just fake create it - return appendRelativePath(directoryName, true) - } - - private data class QueryResult( - val uri: Uri, - val lastModified: Long, - val length: Long, - ) - - @RequiresApi(Build.VERSION_CODES.Q) - private fun query(displayName: String = namePath): QueryResult? { - try { - //val (name, mime) = splitFilenameMime(fullName) - - val projection = arrayOf( - MediaStore.MediaColumns._ID, - MediaStore.MediaColumns.DATE_MODIFIED, - MediaStore.MediaColumns.SIZE, - ) - - val selection = - "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}' AND ${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'" - - contentResolver.query( - baseUri, - projection, selection, null, null - )?.use { cursor -> - while (cursor.moveToNext()) { - val id = - cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) - - return QueryResult( - uri = ContentUris.withAppendedId( - baseUri, id - ), - lastModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)), - length = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)), - ) - } - } - } catch (t: Throwable) { - logError(t) - } - - return null - } - - override fun uri(): Uri? { - return query()?.uri - } - - override fun name(): String? { - if (isDir) return null - return namePath - } - - override fun type(): String? { - TODO("Not yet implemented") - } - - override fun filePath(): String { - return replaceDuplicateFileSeparators(relativePath + File.separator + namePath) - } - - override fun isDirectory(): Boolean { - return isDir - } - - override fun isFile(): Boolean { - return isFile - } - - override fun lastModified(): Long? { - if (isDir) return null - return query()?.lastModified - } - - override fun length(): Long? { - if (isDir) return null - val query = query() - val length = query?.length ?: return null - if (length <= 0) { - try { - contentResolver.openFileDescriptor(query.uri, "r") - .use { - it?.statSize - }?.let { - return it - } - } catch (e: FileNotFoundException) { - return null - } - - val inputStream: InputStream = openInputStream() ?: return null - return try { - inputStream.available().toLong() - } catch (t: Throwable) { - null - } finally { - inputStream.closeQuietly() - } - } - return length - } - - override fun canRead(): Boolean { - TODO("Not yet implemented") - } - - override fun canWrite(): Boolean { - TODO("Not yet implemented") - } - - private fun delete(uri: Uri): Boolean { - return contentResolver.delete(uri, null, null) > 0 - } - - override fun delete(): Boolean { - return if (isDir) { - (listFiles() ?: return false).all { - it.delete() - } - } else { - delete(uri() ?: return false) - } - } - - override fun exists(): Boolean { - if (isDir) return true - return query() != null - } - - override fun listFiles(): List? { - if (isFile) return null - try { - val projection = arrayOf( - MediaStore.MediaColumns.DISPLAY_NAME - ) - - val selection = - "${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath${File.separator}'" - contentResolver.query( - baseUri, - projection, selection, null, null - )?.use { cursor -> - val out = ArrayList(cursor.count) - while (cursor.moveToNext()) { - val nameIdx = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) - if (nameIdx == -1) continue - val name = cursor.getString(nameIdx) - - appendRelativePath(name, false)?.let { new -> - out.add(new) - } - } - - out - } - } catch (t: Throwable) { - logError(t) - } - return null - } - - override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? { - if (isFile || displayName == null) return null - - val new = appendRelativePath(displayName, false) ?: return null - if (new.exists()) { - return new - } - - return null//SafeFile.fromUri(context, query(displayName ?: return null)?.uri ?: return null) - } - - override fun renameTo(name: String?): Boolean { - TODO("Not yet implemented") - } - - override fun openOutputStream(append: Boolean): OutputStream? { - try { - // use current file - uri()?.let { - return contentResolver.openOutputStream( - it, - if (append) "wa" else "wt" - ) - } - - // create a new file if current is not found, - // as we know it is new only write access is needed - createUri()?.let { - return contentResolver.openOutputStream( - it, - "w" - ) - } - return null - } catch (t: Throwable) { - return null - } - } - - override fun openInputStream(): InputStream? { - try { - return contentResolver.openInputStream(uri() ?: return null) - } catch (t: Throwable) { - return null - } - } - - override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? { - TODO("Not yet implemented") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt deleted file mode 100644 index 85a74963..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/SafeFile.kt +++ /dev/null @@ -1,244 +0,0 @@ -package com.lagradost.cloudstream3.utils.storage - -import android.content.Context -import android.net.Uri -import android.os.Build -import android.os.Environment -import com.hippo.unifile.UniFile -import com.hippo.unifile.UniRandomAccessFile - -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream - -interface SafeFile { - companion object { - fun fromUri(context: Context, uri: Uri): SafeFile? { - return UniFileWrapper(UniFile.fromUri(context, uri) ?: return null) - } - - fun fromFile(context: Context, file: File?): SafeFile? { - if (file == null) return null - // because UniFile sucks balls on Media we have to do this - val absPath = file.absolutePath.removePrefix(File.separator) - for (value in MediaFileContentType.values()) { - val prefixes = listOf(value.toAbsolutePath(), value.toPath()).map { it.removePrefix(File.separator) } - for (prefix in prefixes) { - if (!absPath.startsWith(prefix)) continue - return fromMedia( - context, - value, - absPath.removePrefix(prefix).ifBlank { File.separator } - ) - } - } - - return UniFileWrapper(UniFile.fromFile(file) ?: return null) - } - - fun fromAsset( - context: Context, - filename: String? - ): SafeFile? { - return UniFileWrapper( - UniFile.fromAsset(context.assets, filename ?: return null) ?: return null - ) - } - - fun fromResource( - context: Context, - id: Int - ): SafeFile? { - return UniFileWrapper( - UniFile.fromResource(context, id) ?: return null - ) - } - - fun fromMedia( - context: Context, - folderType: MediaFileContentType, - path: String = File.separator, - external: Boolean = true, - ): SafeFile? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - //fromUri(context, folderType.toUri(external))?.findFile(folderType.toPath())?.gotoDirectory(path) - - return MediaFile( - context = context, - folderType = folderType, - external = external, - absolutePath = path - ) - } else { - fromFile( - context, - File( - (Environment.getExternalStorageDirectory().absolutePath + File.separator + - folderType.toPath() + File.separator + folderType).replace( - File.separator + File.separator, - File.separator - ) - ) - ) - } - - } - } - - /*val uri: Uri? get() = getUri() - val name: String? get() = getName() - val type: String? get() = getType() - val filePath: String? get() = getFilePath() - val isFile: Boolean? get() = isFile() - val isDirectory: Boolean? get() = isDirectory() - val length: Long? get() = length() - val canRead: Boolean get() = canRead() - val canWrite: Boolean get() = canWrite() - val lastModified: Long? get() = lastModified()*/ - - @Throws(IOException::class) - fun isFileOrThrow(): Boolean { - return isFile() ?: throw IOException("Unable to get if file is a file or directory") - } - - @Throws(IOException::class) - fun lengthOrThrow(): Long { - return length() ?: throw IOException("Unable to get file length") - } - - @Throws(IOException::class) - fun isDirectoryOrThrow(): Boolean { - return isDirectory() ?: throw IOException("Unable to get if file is a directory") - } - - @Throws(IOException::class) - fun filePathOrThrow(): String { - return filePath() ?: throw IOException("Unable to get file path") - } - - @Throws(IOException::class) - fun uriOrThrow(): Uri { - return uri() ?: throw IOException("Unable to get uri") - } - - @Throws(IOException::class) - fun renameOrThrow(name: String?) { - if (!renameTo(name)) { - throw IOException("Unable to rename to $name") - } - } - - @Throws(IOException::class) - fun openOutputStreamOrThrow(append: Boolean = false): OutputStream { - return openOutputStream(append) ?: throw IOException("Unable to open output stream") - } - - @Throws(IOException::class) - fun openInputStreamOrThrow(): InputStream { - return openInputStream() ?: throw IOException("Unable to open input stream") - } - - @Throws(IOException::class) - fun existsOrThrow(): Boolean { - return exists() ?: throw IOException("Unable get if file exists") - } - - @Throws(IOException::class) - fun findFileOrThrow(displayName: String?, ignoreCase: Boolean = false): SafeFile { - return findFile(displayName, ignoreCase) ?: throw IOException("Unable find file") - } - - @Throws(IOException::class) - fun gotoDirectoryOrThrow( - directoryName: String?, - createMissingDirectories: Boolean = true - ): SafeFile { - return gotoDirectory(directoryName, createMissingDirectories) - ?: throw IOException("Unable to go to directory $directoryName") - } - - @Throws(IOException::class) - fun listFilesOrThrow(): List { - return listFiles() ?: throw IOException("Unable to get files") - } - - - @Throws(IOException::class) - fun createFileOrThrow(displayName: String?): SafeFile { - return createFile(displayName) ?: throw IOException("Unable to create file $displayName") - } - - @Throws(IOException::class) - fun createDirectoryOrThrow(directoryName: String?): SafeFile { - return createDirectory( - directoryName ?: throw IOException("Unable to create file with invalid name") - ) - ?: throw IOException("Unable to create directory $directoryName") - } - - @Throws(IOException::class) - fun deleteOrThrow() { - if (!delete()) { - throw IOException("Unable to delete file") - } - } - - /** file.gotoDirectory("a/b/c") -> "file/a/b/c/" where a null or blank directoryName - * returns itself. createMissingDirectories specifies if the dirs should be created - * when travelling or break at a dir not found */ - fun gotoDirectory( - directoryName: String?, - createMissingDirectories: Boolean = true - ): SafeFile? { - if (directoryName == null) return this - - return directoryName.split(File.separatorChar).filter { it.isNotBlank() } - .fold(this) { file: SafeFile?, directory -> - // as MediaFile does not actually create a directory we can do this - if (createMissingDirectories || this is MediaFile) { - file?.createDirectory(directory) - } else { - val next = file?.findFile(directory) - - // we require the file to be a directory - if (next?.isDirectory() != true) { - null - } else { - next - } - } - } - } - - - fun createFile(displayName: String?): SafeFile? - fun createDirectory(directoryName: String?): SafeFile? - fun uri(): Uri? - fun name(): String? - fun type(): String? - fun filePath(): String? - fun isDirectory(): Boolean? - fun isFile(): Boolean? - fun lastModified(): Long? - fun length(): Long? - fun canRead(): Boolean - fun canWrite(): Boolean - fun delete(): Boolean - fun exists(): Boolean? - fun listFiles(): List? - - // fun listFiles(filter: FilenameFilter?): Array? - fun findFile(displayName: String?, ignoreCase: Boolean = false): SafeFile? - - fun renameTo(name: String?): Boolean - - /** Open a stream on to the content associated with the file */ - fun openOutputStream(append: Boolean = false): OutputStream? - - /** Open a stream on to the content associated with the file */ - fun openInputStream(): InputStream? - - /** Get a random access stuff of the UniFile, "r" or "rw" */ - fun createRandomAccessFile(mode: String?): UniRandomAccessFile? -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt deleted file mode 100644 index f1592169..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/storage/UniFileWrapper.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.lagradost.cloudstream3.utils.storage - -import android.net.Uri -import com.hippo.unifile.UniFile -import com.hippo.unifile.UniRandomAccessFile -import com.lagradost.cloudstream3.mvvm.logError -import okhttp3.internal.closeQuietly -import java.io.InputStream -import java.io.OutputStream - -private fun UniFile.toFile(): SafeFile { - return UniFileWrapper(this) -} - -fun safe(apiCall: () -> T): T? { - return try { - apiCall.invoke() - } catch (throwable: Throwable) { - logError(throwable) - null - } -} - -class UniFileWrapper(val file: UniFile) : SafeFile { - override fun createFile(displayName: String?): SafeFile? { - return file.createFile(displayName)?.toFile() - } - - override fun createDirectory(directoryName: String?): SafeFile? { - return file.createDirectory(directoryName)?.toFile() - } - - override fun uri(): Uri? { - return safe { file.uri } - } - - override fun name(): String? { - return safe { file.name } - } - - override fun type(): String? { - return safe { file.type } - } - - override fun filePath(): String? { - return safe { file.filePath } - } - - override fun isDirectory(): Boolean? { - return safe { file.isDirectory } - } - - override fun isFile(): Boolean? { - return safe { file.isFile } - } - - override fun lastModified(): Long? { - return safe { file.lastModified() } - } - - override fun length(): Long? { - return safe { - val len = file.length() - if (len <= 1) { - val inputStream = this.openInputStream() ?: return@safe null - try { - inputStream.available().toLong() - } finally { - inputStream.closeQuietly() - } - } else { - len - } - } - } - - override fun canRead(): Boolean { - return safe { file.canRead() } ?: false - } - - override fun canWrite(): Boolean { - return safe { file.canWrite() } ?: false - } - - override fun delete(): Boolean { - return safe { file.delete() } ?: false - } - - override fun exists(): Boolean? { - return safe { file.exists() } - } - - override fun listFiles(): List? { - return safe { file.listFiles()?.mapNotNull { it?.toFile() } } - } - - override fun findFile(displayName: String?, ignoreCase: Boolean): SafeFile? { - return safe { file.findFile(displayName, ignoreCase)?.toFile() } - } - - override fun renameTo(name: String?): Boolean { - return safe { file.renameTo(name) } ?: return false - } - - override fun openOutputStream(append: Boolean): OutputStream? { - return safe { file.openOutputStream(append) } - } - - override fun openInputStream(): InputStream? { - return safe { file.openInputStream() } - } - - override fun createRandomAccessFile(mode: String?): UniRandomAccessFile? { - return safe { file.createRandomAccessFile(mode) } - } -} \ No newline at end of file From f01820059b520912d77be56ce8a39c2530f6f886 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 27 Aug 2023 19:07:08 +0200 Subject: [PATCH 077/441] delete resume watching + delete bookmarks buttons. fixed backup crash --- .../cloudstream3/ui/home/HomeFragment.kt | 6 ++- .../ui/home/HomeParentItemAdapterPreview.kt | 31 ++++++++--- .../cloudstream3/ui/home/HomeViewModel.kt | 36 ++++++++++--- .../cloudstream3/utils/BackupUtils.kt | 53 +++---------------- .../cloudstream3/utils/DataStoreHelper.kt | 12 +++-- 5 files changed, 72 insertions(+), 66 deletions(-) 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 fa0b6dfb..b84c619e 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 @@ -658,12 +658,14 @@ class HomeFragment : Fragment() { return@observeNullable } - bottomSheetDialog = activity?.loadHomepageList(item, expandCallback = { + val (items, delete) = item + + bottomSheetDialog = activity?.loadHomepageList(items, expandCallback = { homeViewModel.expandAndReturn(it) }, dismissCallback = { homeViewModel.popup(null) bottomSheetDialog = null - }) + }, deleteCallback = delete) } homeViewModel.reloadStored() 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 13497a99..1d8e1399 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 @@ -246,7 +246,7 @@ class HomeParentItemAdapterPreview( private val previewViewpagerText: ViewGroup = itemView.findViewById(R.id.home_preview_viewpager_text) - // private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) + // private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder) private var resumeRecyclerView: RecyclerView = itemView.findViewById(R.id.home_watch_child_recyclerview) @@ -257,7 +257,7 @@ class HomeParentItemAdapterPreview( private var homeAccount: View? = itemView.findViewById(R.id.home_preview_switch_account) - private var topPadding : View? = itemView.findViewById(R.id.home_padding) + private var topPadding: View? = itemView.findViewById(R.id.home_padding) private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding) @@ -283,7 +283,11 @@ class HomeParentItemAdapterPreview( item.plot ?: "" homePreviewText.text = item.name - populateChips(homePreviewTags,item.tags ?: emptyList(), R.style.ChipFilledSemiTransparent) + populateChips( + homePreviewTags, + item.tags ?: emptyList(), + R.style.ChipFilledSemiTransparent + ) homePreviewTags.isGone = item.tags.isNullOrEmpty() @@ -413,7 +417,7 @@ class HomeParentItemAdapterPreview( Pair(itemView.findViewById(R.id.home_plan_to_watch_btt), WatchType.PLANTOWATCH), ) - private val toggleListHolder : ChipGroup? = itemView.findViewById(R.id.home_type_holder) + private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder) init { previewViewpager.setPageTransformer(HomeScrollTransformer()) @@ -422,8 +426,14 @@ class HomeParentItemAdapterPreview( resumeRecyclerView.adapter = resumeAdapter bookmarkRecyclerView.adapter = bookmarkAdapter - resumeRecyclerView.setLinearListLayout(nextLeft = R.id.nav_rail_view, nextRight = FOCUS_SELF) - bookmarkRecyclerView.setLinearListLayout(nextLeft = R.id.nav_rail_view, nextRight = FOCUS_SELF) + resumeRecyclerView.setLinearListLayout( + nextLeft = R.id.nav_rail_view, + nextRight = FOCUS_SELF + ) + bookmarkRecyclerView.setLinearListLayout( + nextLeft = R.id.nav_rail_view, + nextRight = FOCUS_SELF + ) fixPaddingStatusbarMargin(topPadding) @@ -547,7 +557,10 @@ class HomeParentItemAdapterPreview( resumeWatching, false ), 1, false - ) + ), + deleteCallback = { + viewModel.deleteResumeWatching() + } ) } } @@ -572,7 +585,9 @@ class HomeParentItemAdapterPreview( list, false ), 1, false - ) + ), deleteCallback = { + viewModel.deleteBookmarks(list) + } ) } } 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 e8cf8863..b27223ec 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 @@ -41,6 +41,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllBookmarkedData +import com.lagradost.cloudstream3.utils.DataStoreHelper.deleteAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData @@ -92,6 +94,21 @@ class HomeViewModel : ViewModel() { } } + fun deleteResumeWatching() { + deleteAllResumeStateIds() + loadResumeWatching() + } + + fun deleteBookmarks(list: List) { + list.forEach { DataStoreHelper.deleteBookmarkedData(it.id) } + loadStoredData() + } + + fun deleteBookmarks() { + deleteAllBookmarkedData() + loadStoredData() + } + var repo: APIRepository? = null private val _apiName = MutableLiveData() @@ -394,11 +411,14 @@ class HomeViewModel : ViewModel() { } - private val _popup = MutableLiveData(null) - val popup: LiveData = _popup + private val _popup = MutableLiveData Unit)?>?>(null) + val popup: LiveData Unit)?>?> = _popup - fun popup(list: ExpandableHomepageList?) { - _popup.postValue(list) + fun popup(list: ExpandableHomepageList?, deleteCallback: (() -> Unit)? = null) { + if (list == null) + _popup.postValue(null) + else + _popup.postValue(list to deleteCallback) } private fun bookmarksUpdated(unused: Boolean) { @@ -436,8 +456,7 @@ class HomeViewModel : ViewModel() { // do nothing } - fun reloadStored() { - loadResumeWatching() + fun loadStoredData() { val list = EnumSet.noneOf(WatchType::class.java) getKey(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let { list.addAll(it) @@ -445,6 +464,11 @@ class HomeViewModel : ViewModel() { loadStoredData(list) } + fun reloadStored() { + loadResumeWatching() + loadStoredData() + } + fun click(load: LoadClickCallback) { loadResult(load.response.url, load.response.apiName, load.action) } 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 41326eb8..96593769 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -25,6 +25,7 @@ import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_T import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi.Companion.OPEN_SUBTITLES_USER_KEY +import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs @@ -143,66 +144,26 @@ object BackupUtils { } @SuppressLint("SimpleDateFormat") - fun FragmentActivity.backup() { + fun FragmentActivity.backup() = ioSafe { var fileStream: OutputStream? = null var printStream: PrintWriter? = null try { if (!checkWrite()) { - showToast(getString(R.string.backup_failed), Toast.LENGTH_LONG) + showToast(R.string.backup_failed, Toast.LENGTH_LONG) requestRW() - return + return@ioSafe } val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) - val ext = "json" + val ext = "txt" val displayName = "CS3_Backup_${date}" val backupFile = getBackup() - val stream = setupStream(this, displayName, null, ext, false) + val stream = setupStream(this@backup, displayName, null, ext, false) fileStream = stream.openNew() printStream = PrintWriter(fileStream) printStream.print(mapper.writeValueAsString(backupFile)) - /*val steam = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q - && subDir?.isDownloadDir() == true - ) { - val cr = this.contentResolver - val contentUri = - MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI - //val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - - val newFile = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) - put(MediaStore.MediaColumns.TITLE, displayName) - // While it a json file we store as txt because not - // all file managers support mimetype json - put(MediaStore.MediaColumns.MIME_TYPE, "text/plain") - //put(MediaStore.MediaColumns.RELATIVE_PATH, folder) - } - - val newFileUri = cr.insert( - contentUri, - newFile - ) ?: throw IOException("Error creating file uri") - cr.openOutputStream(newFileUri, "w") - ?: throw IOException("Error opening stream") - } else { - val fileName = "$displayName.$ext" - val rFile = subDir?.findFile(fileName) - if (rFile?.exists() == true) { - rFile.delete() - } - val file = - subDir?.createFile(fileName) - ?: throw IOException("Error creating file") - if (!file.exists()) throw IOException("File does not exist") - file.openOutputStream() - } - - val printStream = PrintWriter(steam) - printStream.print(mapper.writeValueAsString(backupFile)) - printStream.close()*/ - showToast( R.string.backup_success, Toast.LENGTH_LONG @@ -211,7 +172,7 @@ object BackupUtils { logError(e) try { showToast( - getString(R.string.backup_failed_error_format).format(e.toString()), + txt(R.string.backup_failed_error_format, e.toString()), Toast.LENGTH_LONG ) } catch (e: Exception) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 991651dc..2eb2ab01 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -353,6 +353,12 @@ object DataStoreHelper { removeKeys(folder2) } + fun deleteBookmarkedData(id : Int?) { + if (id == null) return + removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString()) + removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) + } + fun getAllResumeStateIds(): List? { val folder = "$currentAccount/$RESULT_RESUME_WATCHING" return getKeys(folder)?.mapNotNull { @@ -519,12 +525,10 @@ object DataStoreHelper { fun setResultWatchState(id: Int?, status: Int) { if (id == null) return - val folder = "$currentAccount/$RESULT_WATCH_STATE" if (status == WatchType.NONE.internalId) { - removeKey(folder, id.toString()) - removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) + deleteBookmarkedData(id) } else { - setKey(folder, id.toString(), status) + setKey("$currentAccount/$RESULT_WATCH_STATE", id.toString(), status) } } From ce1f48978be3aada3e6b68d8726539375c3f14f1 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 28 Aug 2023 20:56:58 +0200 Subject: [PATCH 078/441] fixed download error --- app/build.gradle.kts | 2 +- .../cloudstream3/extractors/SpeedoStream.kt | 13 +++++++++---- .../lagradost/cloudstream3/utils/ExtractorApi.kt | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 333fbfb8..3927d081 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -233,7 +233,7 @@ dependencies { // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 - implementation("com.github.LagradOst:SafeFile:0.0.2") + implementation("com.github.LagradOst:SafeFile:0.0.3") // API because cba maintaining it myself implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt index 3f6fff2f..213ecdf3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt @@ -7,15 +7,22 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +class SpeedoStream2 : SpeedoStream() { + override val mainUrl = "https://speedostream.mom" +} + class SpeedoStream1 : SpeedoStream() { override val mainUrl = "https://speedostream.pm" } open class SpeedoStream : ExtractorApi() { override val name = "SpeedoStream" - override val mainUrl = "https://speedostream.mom" + override val mainUrl = "https://speedostream.bond" override val requiresReferer = true + // .bond, .pm, .mom redirect to .bond + private val hostUrl = "https://speedostream.bond" + override suspend fun getUrl(url: String, referer: String?): List { val sources = mutableListOf() app.get(url, referer = referer).document.select("script").map { script -> @@ -26,7 +33,7 @@ open class SpeedoStream : ExtractorApi() { M3u8Helper.generateM3u8( name, it.file, - "$mainUrl/", + "$hostUrl/", ).forEach { m3uData -> sources.add(m3uData) } } } @@ -37,6 +44,4 @@ open class SpeedoStream : ExtractorApi() { private data class File( @JsonProperty("file") val file: String, ) - - } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 83c61542..ffda32d7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -389,6 +389,7 @@ val extractorApis: MutableList = arrayListOf( Acefile(), SpeedoStream(), SpeedoStream1(), + SpeedoStream2(), Zorofile(), Embedgram(), Mvidoo(), From 6089cbc48493d746a110bba8d9997c3d2209b82e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 30 Aug 2023 00:52:34 +0200 Subject: [PATCH 079/441] fixed subs on downloads --- app/build.gradle.kts | 2 +- .../ui/player/DownloadFileGenerator.kt | 73 +++++++++++-------- .../utils/VideoDownloadManager.kt | 2 +- 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3927d081..55d0f7ae 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -233,7 +233,7 @@ dependencies { // To fix SSL fuckery on android 9 implementation("org.conscrypt:conscrypt-android:2.2.1") // Util to skip the URI file fuckery 🙏 - implementation("com.github.LagradOst:SafeFile:0.0.3") + implementation("com.github.LagradOst:SafeFile:0.0.5") // API because cba maintaining it myself implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index baf7ed52..1b618e45 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -50,6 +50,10 @@ class DownloadFileGenerator( return null } + fun cleanDisplayName(name: String): String { + return name.substringBeforeLast('.').trim() + } + override suspend fun generateLinks( clearCache: Boolean, isCasting: Boolean, @@ -58,39 +62,48 @@ class DownloadFileGenerator( offset: Int, ): Boolean { val meta = episodes[currentIndex + offset] - callback(Pair(null, meta)) + callback(null to meta) - context?.let { ctx -> - val relative = meta.relativePath - val display = meta.displayName + val ctx = context ?: return true + val relative = meta.relativePath ?: return true + val display = meta.displayName ?: return true - if (display == null || relative == null) { - return@let + val cleanDisplay = cleanDisplayName(display) + + VideoDownloadManager.getFolder(ctx, relative, meta.basePath) + ?.forEach { (name, uri) -> + // only these files are allowed, so no videos as subtitles + if (listOf( + ".vtt", + ".srt", + ".txt", + ".ass", + ".ttml", + ".sbv", + ".dfxp" + ).none { name.contains(it, true) } + ) return@forEach + + // cant have the exact same file as a subtitle + if (name.equals(display, true)) return@forEach + + val cleanName = cleanDisplayName(name) + + // we only want files with the approx same name + if (!cleanName.startsWith(cleanDisplay, true)) return@forEach + + val realName = cleanName.removePrefix(cleanDisplay) + + subtitleCallback( + SubtitleData( + realName.ifBlank { ctx.getString(R.string.default_subtitles) }, + uri.toString(), + SubtitleOrigin.DOWNLOADED_FILE, + name.toSubtitleMimeType(), + emptyMap() + ) + ) } - VideoDownloadManager.getFolder(ctx, relative, meta.basePath) - ?.forEach { file -> - val name = display.removeSuffix(".mp4") - if (file.first != meta.displayName && file.first.startsWith(name)) { - val realName = file.first.removePrefix(name) - .removeSuffix(".vtt") - .removeSuffix(".srt") - .removeSuffix(".txt") - .trim() - .removePrefix("(") - .removeSuffix(")") - - subtitleCallback( - SubtitleData( - realName.ifBlank { ctx.getString(R.string.default_subtitles) }, - file.second.toString(), - SubtitleOrigin.DOWNLOADED_FILE, - name.toSubtitleMimeType(), - emptyMap() - ) - ) - } - } - } return true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 6425ba66..72eb002a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -505,7 +505,7 @@ object VideoDownloadManager { ): List>? { val base = basePathToFile(context, basePath) val folder = base?.gotoDirectory(relativePath, false) ?: return null - if (folder.isDirectory() != false) return null + //if (folder.isDirectory() != false) return null return folder.listFiles() ?.mapNotNull { (it.name() ?: "") to (it.uri() ?: return@mapNotNull null) } From 9c991f2abd53bdbf50f7ae52f3fe64221d341e57 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Sat, 2 Sep 2023 05:32:18 +0700 Subject: [PATCH 080/441] extractor: fix chillx (#583) * Extractor: added Rabbitstream * Extractor: added Rabbitstream * extractor: fix Chillx * comply --------- Co-authored-by: Sofie99 --- .../cloudstream3/extractors/Chillx.kt | 58 +---------- .../cloudstream3/extractors/Gdriveplayer.kt | 88 +---------------- .../extractors/helper/AesHelper.kt | 95 +++++++++++++++++++ 3 files changed, 102 insertions(+), 139 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index b4f3d897..bcf8848c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -2,15 +2,12 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.extractors.helper.* +import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Qualities -import javax.crypto.Cipher -import javax.crypto.SecretKeyFactory -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.PBEKeySpec -import javax.crypto.spec.SecretKeySpec class Moviesapi : Chillx() { override val name = "Moviesapi" @@ -32,7 +29,7 @@ open class Chillx : ExtractorApi() { override val requiresReferer = true companion object { - private const val KEY = "11x&W5UBrcqn\$9Yl" + private const val KEY = "m4H6D9%0\$N&F6rQ&" } override suspend fun getUrl( @@ -47,8 +44,7 @@ open class Chillx : ExtractorApi() { referer = referer ).text )?.groupValues?.get(1) - val encData = AppUtils.tryParseJson(base64Decode(master ?: return)) - val decrypt = cryptoAESHandler(encData ?: return, KEY, false) + val decrypt = cryptoAESHandler(master ?: return, KEY.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1) @@ -86,52 +82,6 @@ open class Chillx : ExtractorApi() { } } - private fun cryptoAESHandler( - data: AESData, - pass: String, - encrypt: Boolean = true - ): String { - val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512") - val spec = PBEKeySpec( - pass.toCharArray(), - data.salt?.hexToByteArray(), - data.iterations?.toIntOrNull() ?: 1, - 256 - ) - val key = factory.generateSecret(spec) - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") - return if (!encrypt) { - cipher.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(key.encoded, "AES"), - IvParameterSpec(data.iv?.hexToByteArray()) - ) - String(cipher.doFinal(base64DecodeArray(data.ciphertext.toString()))) - } else { - cipher.init( - Cipher.ENCRYPT_MODE, - SecretKeySpec(key.encoded, "AES"), - IvParameterSpec(data.iv?.hexToByteArray()) - ) - base64Encode(cipher.doFinal(data.ciphertext?.toByteArray())) - } - } - - private fun String.hexToByteArray(): ByteArray { - check(length % 2 == 0) { "Must have an even length" } - return chunked(2) - .map { it.toInt(16).toByte() } - - .toByteArray() - } - - data class AESData( - @JsonProperty("ciphertext") val ciphertext: String? = null, - @JsonProperty("iv") val iv: String? = null, - @JsonProperty("salt") val salt: String? = null, - @JsonProperty("iterations") val iterations: String? = null, - ) - data class Tracks( @JsonProperty("file") val file: String? = null, @JsonProperty("label") val label: String? = null, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt index df9c74a4..8d1a4d07 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt @@ -2,14 +2,10 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import org.jsoup.nodes.Element -import java.security.DigestException -import java.security.MessageDigest -import javax.crypto.Cipher -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec class DatabaseGdrive2 : Gdriveplayer() { override var mainUrl = "https://databasegdriveplayer.co" @@ -65,78 +61,6 @@ open class Gdriveplayer : ExtractorApi() { ?.data()?.let { getAndUnpack(it) } } - private fun String.decodeHex(): ByteArray { - check(length % 2 == 0) { "Must have an even length" } - return chunked(2) - .map { it.toInt(16).toByte() } - .toByteArray() - } - - // https://stackoverflow.com/a/41434590/8166854 - private fun GenerateKeyAndIv( - password: ByteArray, - salt: ByteArray, - hashAlgorithm: String = "MD5", - keyLength: Int = 32, - ivLength: Int = 16, - iterations: Int = 1 - ): List? { - - val md = MessageDigest.getInstance(hashAlgorithm) - val digestLength = md.digestLength - val targetKeySize = keyLength + ivLength - val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength - val generatedData = ByteArray(requiredLength) - var generatedLength = 0 - - try { - md.reset() - - while (generatedLength < targetKeySize) { - if (generatedLength > 0) - md.update( - generatedData, - generatedLength - digestLength, - digestLength - ) - - md.update(password) - md.update(salt, 0, 8) - md.digest(generatedData, generatedLength, digestLength) - - for (i in 1 until iterations) { - md.update(generatedData, generatedLength, digestLength) - md.digest(generatedData, generatedLength, digestLength) - } - - generatedLength += digestLength - } - return listOf( - generatedData.copyOfRange(0, keyLength), - generatedData.copyOfRange(keyLength, targetKeySize) - ) - } catch (e: DigestException) { - return null - } - } - - private fun cryptoAESHandler( - data: AesData, - pass: ByteArray, - encrypt: Boolean = true - ): String? { - val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null - val cipher = Cipher.getInstance("AES/CBC/NoPadding") - return if (!encrypt) { - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) - String(cipher.doFinal(base64DecodeArray(data.ct))) - } else { - cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) - base64Encode(cipher.doFinal(data.ct.toByteArray())) - - } - } - private fun Regex.first(str: String): String? { return find(str)?.groupValues?.getOrNull(1) } @@ -154,14 +78,14 @@ open class Gdriveplayer : ExtractorApi() { val document = app.get(url).document val eval = unpackJs(document)?.replace("\\", "") ?: return - val data = tryParseJson(Regex("data='(\\S+?)'").first(eval)) ?: return + val data = Regex("data='(\\S+?)'").first(eval) ?: return val password = Regex("null,['|\"](\\w+)['|\"]").first(eval) ?.split(Regex("\\D+")) ?.joinToString("") { Char(it.toInt()).toString() }.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() } ?: throw ErrorLoadingException("can't find password") - val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "") + val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "") val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],") val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],") @@ -194,12 +118,6 @@ open class Gdriveplayer : ExtractorApi() { } - data class AesData( - @JsonProperty("ct") val ct: String, - @JsonProperty("iv") val iv: String, - @JsonProperty("s") val s: String - ) - data class Tracks( @JsonProperty("file") val file: String, @JsonProperty("kind") val kind: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt new file mode 100644 index 00000000..b41eae52 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt @@ -0,0 +1,95 @@ +package com.lagradost.cloudstream3.extractors.helper + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.base64DecodeArray +import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.utils.AppUtils +import java.security.DigestException +import java.security.MessageDigest +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +object AesHelper { + + private const val HASH = "AES/CBC/PKCS5PADDING" + private const val KDF = "MD5" + + fun cryptoAESHandler( + data: String, + pass: ByteArray, + encrypt: Boolean = true, + padding: String = HASH, + ): String? { + val parse = AppUtils.tryParseJson(data) ?: return null + val (key, iv) = generateKeyAndIv(pass, parse.s.hexToByteArray()) ?: return null + val cipher = Cipher.getInstance(padding) + return if (!encrypt) { + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + String(cipher.doFinal(base64DecodeArray(parse.ct))) + } else { + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) + base64Encode(cipher.doFinal(parse.ct.toByteArray())) + } + } + + // https://stackoverflow.com/a/41434590/8166854 + fun generateKeyAndIv( + password: ByteArray, + salt: ByteArray, + hashAlgorithm: String = KDF, + keyLength: Int = 32, + ivLength: Int = 16, + iterations: Int = 1 + ): Pair? { + + val md = MessageDigest.getInstance(hashAlgorithm) + val digestLength = md.digestLength + val targetKeySize = keyLength + ivLength + val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength + val generatedData = ByteArray(requiredLength) + var generatedLength = 0 + + try { + md.reset() + + while (generatedLength < targetKeySize) { + if (generatedLength > 0) + md.update( + generatedData, + generatedLength - digestLength, + digestLength + ) + + md.update(password) + md.update(salt, 0, 8) + md.digest(generatedData, generatedLength, digestLength) + + for (i in 1 until iterations) { + md.update(generatedData, generatedLength, digestLength) + md.digest(generatedData, generatedLength, digestLength) + } + + generatedLength += digestLength + } + return generatedData.copyOfRange(0, keyLength) to generatedData.copyOfRange(keyLength, targetKeySize) + } catch (e: DigestException) { + return null + } + } + + fun String.hexToByteArray(): ByteArray { + check(length % 2 == 0) { "Must have an even length" } + return chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() + } + + private data class AesData( + @JsonProperty("ct") val ct: String, + @JsonProperty("iv") val iv: String, + @JsonProperty("s") val s: String + ) + +} \ No newline at end of file From 6211b02e85b0dcd4ab5a2954623917e8c27ba552 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 3 Sep 2023 23:32:43 +0200 Subject: [PATCH 081/441] switched from isM3u8 to ExtractorLinkType --- .../cloudstream3/extractors/Pelisplus.kt | 3 +- .../cloudstream3/extractors/Vidstream.kt | 3 +- .../cloudstream3/extractors/WcoStream.kt | 4 +- .../cloudstream3/extractors/Wibufile.kt | 6 +- .../cloudstream3/ui/APIRepository.kt | 14 ++- .../cloudstream3/ui/ControllerActivity.kt | 4 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 12 +- .../ui/player/DownloadFileGenerator.kt | 4 +- .../ui/player/ExtractorLinkGenerator.kt | 7 +- .../cloudstream3/ui/player/IGenerator.kt | 43 ++++++- .../cloudstream3/ui/player/LinkGenerator.kt | 2 +- .../ui/player/PlayerGeneratorViewModel.kt | 12 +- .../ui/player/RepoLinkGenerator.kt | 21 ++-- .../ui/result/ResultViewModel2.kt | 40 +++--- .../cloudstream3/utils/CastHelper.kt | 6 +- .../cloudstream3/utils/ExtractorApi.kt | 119 +++++++++++++++--- .../utils/VideoDownloadManager.kt | 78 +++++++----- 17 files changed, 269 insertions(+), 109 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt index 45ec4c2f..4163cd94 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt @@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall 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 @@ -66,7 +67,7 @@ open class Pelisplus(val mainUrl: String) { href, page.url, getQualityFromName(qual), - element.attr("href").contains(".m3u8") + type = INFER_TYPE ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt index 7eb7fbac..c6493dbe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt @@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.argamap 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 @@ -70,7 +71,7 @@ class Vidstream(val mainUrl: String) { href, page.url, getQualityFromName(qual), - element.attr("href").contains(".m3u8") + type = INFER_TYPE ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt index 6cc486cd..659d7804 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt @@ -7,6 +7,7 @@ import com.lagradost.cloudstream3.extractors.helper.NineAnimeHelper.encrypt 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.Qualities class Vidstreamz : WcoStream() { @@ -126,8 +127,7 @@ open class WcoStream : ExtractorApi() { if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server") return response.parsed().data.media.sources.map { - ExtractorLink(name, it.file,it.file,host,Qualities.Unknown.value,it.file.contains(".m3u8")) + ExtractorLink(name, it.file, it.file, host, Qualities.Unknown.value, type = INFER_TYPE) } - } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt index ae1e872a..c69f0938 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt @@ -4,8 +4,8 @@ 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.Qualities -import java.net.URI open class Wibufile : ExtractorApi() { override val name: String = "Wibufile" @@ -28,10 +28,8 @@ open class Wibufile : ExtractorApi() { video ?: return, "$mainUrl/", Qualities.Unknown.value, - URI(url).path.endsWith(".m3u8") + type = INFER_TYPE ) ) - } - } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index 4ab2e8e2..a075cc2e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -1,16 +1,24 @@ package com.lagradost.cloudstream3.ui -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.DubStatus +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.HomePageResponse +import com.lagradost.cloudstream3.LoadResponse +import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent +import com.lagradost.cloudstream3.MainPageRequest +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.fixUrl import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.ExtractorLink import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope.coroutineContext import kotlinx.coroutines.async import kotlinx.coroutines.delay @@ -174,7 +182,7 @@ class APIRepository(val api: MainAPI) { data: String, isCasting: Boolean, subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit + callback: (ExtractorLink) -> Unit, ): Boolean { if (isInvalidData(data)) return false // this makes providers cleaner return try { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 46ddce09..6c0e7796 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -25,6 +25,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.sortSubs import com.lagradost.cloudstream3.sortUrls +import com.lagradost.cloudstream3.ui.player.LoadType import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultEpisode @@ -294,7 +295,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi val generator = RepoLinkGenerator(listOf(epData)) val isSuccessful = safeApiCall { - generator.generateLinks(clearCache = false, isCasting = true, + generator.generateLinks( + clearCache = false, type = LoadType.Chromecast, callback = { it.first?.let { link -> currentLinks.add(link) 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 2067eb04..fd1da5ca 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 @@ -53,9 +53,11 @@ import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData 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.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File +import java.lang.IllegalArgumentException import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession @@ -1257,10 +1259,12 @@ class CS3IPlayer : IPlayer { HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) } - val mime = when { - link.isM3u8 -> MimeTypes.APPLICATION_M3U8 - link.isDash -> MimeTypes.APPLICATION_MPD - else -> MimeTypes.VIDEO_MP4 + val mime = when(link.type) { + ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8 + ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD + ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4 + ExtractorLinkType.TORRENT -> throw IllegalArgumentException("No torrent support") + ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support") } val mediaItems = if (link is ExtractorLinkPlayList) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index 1b618e45..b0223bb5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -56,10 +56,10 @@ class DownloadFileGenerator( override suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, - offset: Int, + offset: Int ): Boolean { val meta = episodes[currentIndex + offset] callback(null to meta) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt index 7c19e97d..d8d2d537 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt @@ -37,14 +37,17 @@ class ExtractorLinkGenerator( override suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, offset: Int ): Boolean { subtitles.forEach(subtitleCallback) + val allowedTypes = type.toSet() links.forEach { - callback.invoke(it to null) + if(allowedTypes.contains(it.type)) { + callback.invoke(it to null) + } } return true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index a1287e6a..af74cb57 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -1,8 +1,43 @@ package com.lagradost.cloudstream3.ui.player import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.ExtractorUri +enum class LoadType { + Unknown, + InApp, + InAppDownload, + ExternalApp, + Browser, + Chromecast +} + +fun LoadType.toSet() : Set { + return when(this) { + LoadType.InApp -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) + LoadType.Browser -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) + LoadType.InAppDownload -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.M3U8 + ) + LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet() + LoadType.Chromecast -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) + } +} + interface IGenerator { val hasCache: Boolean @@ -13,15 +48,15 @@ interface IGenerator { fun goto(index: Int) fun getCurrentId(): Int? // this is used to save data or read data about this id - fun getCurrent(offset : Int = 0): Any? // this is used to get metadata about the current playing, can return null - fun getAll() : List? // this us used to get the metadata about all entries, not needed + fun getCurrent(offset: Int = 0): Any? // this is used to get metadata about the current playing, can return null + fun getAll(): List? // this us used to get the metadata about all entries, not needed /* not safe, must use try catch */ suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, - offset : Int = 0, + offset: Int = 0, ): Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index 0b560857..ba2cdb40 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -48,7 +48,7 @@ class LinkGenerator( override suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, offset: Int 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 1b13b519..42659f8d 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 @@ -78,10 +78,10 @@ class PlayerGeneratorViewModel : ViewModel() { if (generator?.hasCache == true && generator?.hasNext() == true) { safeApiCall { generator?.generateLinks( + type = LoadType.InApp, clearCache = false, - isCasting = false, - {}, - {}, + callback = {}, + subtitleCallback = {}, offset = 1 ) } @@ -147,7 +147,7 @@ class PlayerGeneratorViewModel : ViewModel() { } } - fun loadLinks(clearCache: Boolean = false, isCasting: Boolean = false) { + fun loadLinks(clearCache: Boolean = false, type: LoadType = LoadType.InApp) { Log.i(TAG, "loadLinks") currentJob?.cancel() @@ -162,14 +162,14 @@ class PlayerGeneratorViewModel : ViewModel() { // load more data _loadingLinks.postValue(Resource.Loading()) val loadingState = safeApiCall { - generator?.generateLinks(clearCache = clearCache, isCasting = isCasting, { + generator?.generateLinks(type = type,clearCache = clearCache, callback = { currentLinks.add(it) // Clone to prevent ConcurrentModificationException normalSafeApiCall { // Extra normalSafeApiCall since .toSet() iterates. _currentLinks.postValue(currentLinks.toSet()) } - }, { + }, subtitleCallback = { currentSubs.add(it) normalSafeApiCall { _currentSubs.postValue(currentSubs.toSet()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt index 2ce53ea5..d55da57c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt @@ -67,18 +67,19 @@ class RepoLinkGenerator( override suspend fun generateLinks( clearCache: Boolean, - isCasting: Boolean, + type: LoadType, callback: (Pair) -> Unit, subtitleCallback: (SubtitleData) -> Unit, - offset: Int, + offset: Int ): Boolean { + val allowedTypes = type.toSet() val index = currentIndex val current = episodes.getOrNull(index + offset) ?: return false val (currentLinkCache, currentSubsCache) = if (clearCache) { Pair(mutableSetOf(), mutableSetOf()) } else { - cache[Pair(current.apiName, current.id)] ?: Pair(mutableSetOf(), mutableSetOf()) + cache[current.apiName to current.id] ?: Pair(mutableSetOf(), mutableSetOf()) } //val currentLinkCache = if (clearCache) mutableSetOf() else linkCache[index].toMutableSet() @@ -88,9 +89,9 @@ class RepoLinkGenerator( val currentSubsUrls = mutableSetOf() // makes all subs urls unique val currentSubsNames = mutableSetOf() // makes all subs names unique - currentLinkCache.forEach { link -> + currentLinkCache.filter { allowedTypes.contains(it.type) }.forEach { link -> currentLinks.add(link.url) - callback(Pair(link, null)) + callback(link to null) } currentSubsCache.forEach { sub -> @@ -108,8 +109,8 @@ class RepoLinkGenerator( val result = APIRepository( getApiFromNameNull(current.apiName) ?: throw Exception("This provider does not exist") ).loadLinks(current.data, - isCasting, - { file -> + isCasting = LoadType.Chromecast == type, + subtitleCallback = { file -> val correctFile = PlayerSubtitleHelper.getSubtitleData(file) if (!currentSubsUrls.contains(correctFile.url)) { currentSubsUrls.add(correctFile.url) @@ -132,12 +133,14 @@ class RepoLinkGenerator( } } }, - { link -> + callback = { link -> Log.d(TAG, "Loaded ExtractorLink: $link") if (!currentLinks.contains(link.url)) { if (!currentLinkCache.contains(link)) { currentLinks.add(link.url) - callback(Pair(link, null)) + if (allowedTypes.contains(link.type)) { + callback(Pair(link, null)) + } currentLinkCache.add(link) //linkCache[index] = currentLinkCache } 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 82d9a8fe..b2c57137 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 @@ -36,6 +36,7 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.IGenerator +import com.lagradost.cloudstream3.ui.player.LoadType import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction @@ -745,7 +746,7 @@ class ResultViewModel2 : ViewModel() { val generator = RepoLinkGenerator(listOf(episode)) val currentLinks = mutableSetOf() val currentSubs = mutableSetOf() - generator.generateLinks(clearCache = false, isCasting = false, callback = { + generator.generateLinks(clearCache = false, LoadType.Chromecast, callback = { it.first?.let { link -> currentLinks.add(link) } @@ -825,7 +826,7 @@ class ResultViewModel2 : ViewModel() { isVisible: Boolean = true ) { if (activity == null) return - loadLinks(result, isVisible = isVisible, isCasting = true) { data -> + loadLinks(result, isVisible = isVisible, LoadType.Chromecast) { data -> startChromecast(activity, result, data.links, data.subs, 0) } } @@ -936,7 +937,7 @@ class ResultViewModel2 : ViewModel() { private fun loadLinks( result: ResultEpisode, isVisible: Boolean, - isCasting: Boolean, + type: LoadType, clearCache: Boolean = false, work: suspend (CoroutineScope.(LinkLoadingResult) -> Unit) ) { @@ -945,7 +946,7 @@ class ResultViewModel2 : ViewModel() { val links = loadLinks( result, isVisible = isVisible, - isCasting = isCasting, + type = type, clearCache = clearCache ) if (!this.isActive) return@ioSafe @@ -956,11 +957,11 @@ class ResultViewModel2 : ViewModel() { private var currentLoadLinkJob: Job? = null private fun acquireSingleLink( result: ResultEpisode, - isCasting: Boolean, + type: LoadType, text: UiText, callback: (Pair) -> Unit, ) { - loadLinks(result, isVisible = true, isCasting = isCasting) { links -> + loadLinks(result, isVisible = true, type) { links -> postPopup( text, links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { @@ -971,11 +972,10 @@ class ResultViewModel2 : ViewModel() { private fun acquireSingleSubtitle( result: ResultEpisode, - isCasting: Boolean, text: UiText, callback: (Pair) -> Unit, ) { - loadLinks(result, isVisible = true, isCasting = isCasting) { links -> + loadLinks(result, isVisible = true, type = LoadType.Unknown) { links -> postPopup( text, links.subs.map { txt(it.name) }) @@ -988,7 +988,7 @@ class ResultViewModel2 : ViewModel() { private suspend fun CoroutineScope.loadLinks( result: ResultEpisode, isVisible: Boolean, - isCasting: Boolean, + type: LoadType, clearCache: Boolean = false, ): LinkLoadingResult { val tempGenerator = RepoLinkGenerator(listOf(result)) @@ -1002,7 +1002,7 @@ class ResultViewModel2 : ViewModel() { } try { updatePage() - tempGenerator.generateLinks(clearCache, isCasting, { (link, _) -> + tempGenerator.generateLinks(clearCache, type, { (link, _) -> if (link != null) { links += link updatePage() @@ -1272,7 +1272,6 @@ class ResultViewModel2 : ViewModel() { acquireSingleSubtitle( click.data, - false, txt(R.string.episode_action_download_subtitle) ) { (links, index) -> downloadSubtitle( @@ -1317,7 +1316,7 @@ class ResultViewModel2 : ViewModel() { val response = currentResponse ?: return acquireSingleLink( click.data, - false, + LoadType.InAppDownload, txt(R.string.episode_action_download_mirror) ) { (result, index) -> ioSafe { @@ -1347,7 +1346,7 @@ class ResultViewModel2 : ViewModel() { loadLinks( click.data, isVisible = false, - isCasting = false, + type = LoadType.InApp, clearCache = true ) } @@ -1356,7 +1355,7 @@ class ResultViewModel2 : ViewModel() { ACTION_CHROME_CAST_MIRROR -> { acquireSingleLink( click.data, - isCasting = true, + LoadType.Chromecast, txt(R.string.episode_action_chromecast_mirror) ) { (result, index) -> startChromecast(activity, click.data, result.links, result.subs, index) @@ -1365,7 +1364,7 @@ class ResultViewModel2 : ViewModel() { ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( click.data, - isCasting = true, + LoadType.Browser, txt(R.string.episode_action_play_in_browser) ) { (result, index) -> try { @@ -1380,7 +1379,7 @@ class ResultViewModel2 : ViewModel() { ACTION_COPY_LINK -> { acquireSingleLink( click.data, - isCasting = true, + LoadType.ExternalApp, txt(R.string.episode_action_copy_link) ) { (result, index) -> val act = activity ?: return@acquireSingleLink @@ -1399,7 +1398,7 @@ class ResultViewModel2 : ViewModel() { } ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> { - loadLinks(click.data, isVisible = true, isCasting = true) { links -> + loadLinks(click.data, isVisible = true, LoadType.ExternalApp) { links -> if (links.links.isEmpty()) { showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) return@loadLinks @@ -1415,7 +1414,7 @@ class ResultViewModel2 : ViewModel() { ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink( click.data, - isCasting = true, + LoadType.Chromecast, txt( R.string.episode_action_play_in_format, txt(R.string.player_settings_play_in_web) @@ -1432,7 +1431,7 @@ class ResultViewModel2 : ViewModel() { ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink( click.data, - isCasting = true, + LoadType.Chromecast, txt( R.string.episode_action_play_in_format, txt(R.string.player_settings_play_in_mpv) @@ -1461,7 +1460,6 @@ class ResultViewModel2 : ViewModel() { if (index >= 0) it.goto(index) } - } ?: return, list ) ) @@ -2173,7 +2171,7 @@ class ResultViewModel2 : ViewModel() { trailerData.extractorUrl, trailerData.referer ?: "", Qualities.Unknown.value, - trailerData.extractorUrl.contains(".m3u8") + type = INFER_TYPE ) ) to arrayListOf() } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt index 6b5e9ec2..d8373165 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/CastHelper.kt @@ -55,7 +55,11 @@ object CastHelper { val builder = MediaInfo.Builder(link.url) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setContentType(if (link.isM3u8) MimeTypes.APPLICATION_M3U8 else MimeTypes.VIDEO_MP4) + .setContentType(when(link.type) { + ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8 + ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD + else -> MimeTypes.VIDEO_MP4 + }) .setMetadata(movieMetadata) .setMediaTracks(tracks) data?.let { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index ffda32d7..2a539f0d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -4,8 +4,10 @@ import android.net.Uri import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.extractors.* +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay import org.jsoup.Jsoup +import java.net.URL import kotlin.collections.MutableList /** @@ -35,35 +37,101 @@ data class ExtractorLinkPlayList( val playlist: List, override val referer: String, override val quality: Int, - override val isM3u8: Boolean = false, + val isM3u8: Boolean = false, override val headers: Map = mapOf(), /** Used for getExtractorVerifierJob() */ override val extractorData: String? = null, + override val type: ExtractorLinkType, ) : ExtractorLink( - source, - name, - // Blank as un-used - "", - referer, - quality, - isM3u8, - headers, - extractorData -) + source = source, + name = name, + url = "", + referer = referer, + quality = quality, + headers = headers, + extractorData = extractorData, + type = type +) { + constructor( + source: String, + name: String, + playlist: List, + referer: String, + quality: Int, + isM3u8: Boolean = false, + headers: Map = mapOf(), + extractorData: String? = null, + ) : this( + source = source, + name = name, + playlist = playlist, + referer = referer, + quality = quality, + type = if (isM3u8) ExtractorLinkType.M3U8 else ExtractorLinkType.VIDEO, + headers = headers, + extractorData = extractorData, + ) +} +/** Metadata about the file type used for downloads and exoplayer hint, + * if you respond with the wrong one the file will fail to download or be played */ +enum class ExtractorLinkType { + /** Single stream of bytes no matter the actual file type */ + VIDEO, + /** Split into several .ts files, has support for encrypted m3u8s */ + M3U8, + /** Like m3u8 but uses xml, currently no download support */ + DASH, + /** No support at the moment */ + TORRENT, + /** No support at the moment */ + MAGNET, +} +private fun inferTypeFromUrl(url: String): ExtractorLinkType { + val path = normalSafeApiCall { URL(url).path } + return when { + path?.endsWith(".m3u8") == true -> ExtractorLinkType.M3U8 + path?.endsWith(".mpd") == true -> ExtractorLinkType.DASH + path?.endsWith(".torrent") == true -> ExtractorLinkType.TORRENT + url.startsWith("magnet:") -> ExtractorLinkType.MAGNET + else -> ExtractorLinkType.VIDEO + } +} +val INFER_TYPE : ExtractorLinkType? = null open class ExtractorLink constructor( open val source: String, open val name: String, override val url: String, override val referer: String, open val quality: Int, - open val isM3u8: Boolean = false, override val headers: Map = mapOf(), /** Used for getExtractorVerifierJob() */ open val extractorData: String? = null, - open val isDash: Boolean = false, + open val type: ExtractorLinkType, ) : VideoDownloadManager.IDownloadableMinimum { + constructor( + source: String, + name: String, + url: String, + referer: String, + quality: Int, + /** the type of the media, use INFER_TYPE if you want to auto infer the type from the url */ + type: ExtractorLinkType?, + headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + extractorData: String? = null, + ) : this( + source = source, + name = name, + url = url, + referer = referer, + quality = quality, + headers = headers, + extractorData = extractorData, + type = type ?: inferTypeFromUrl(url) + ) + /** * Old constructor without isDash, allows for backwards compatibility with extensions. * Should be removed after all extensions have updated their cloudstream.jar @@ -80,8 +148,30 @@ open class ExtractorLink constructor( extractorData: String? = null ) : this(source, name, url, referer, quality, isM3u8, headers, extractorData, false) + constructor( + source: String, + name: String, + url: String, + referer: String, + quality: Int, + isM3u8: Boolean = false, + headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + extractorData: String? = null, + isDash: Boolean, + ) : this( + source = source, + name = name, + url = url, + referer = referer, + quality = quality, + headers = headers, + extractorData = extractorData, + type = if (isDash) ExtractorLinkType.DASH else if (isM3u8) ExtractorLinkType.M3U8 else ExtractorLinkType.VIDEO + ) + override fun toString(): String { - return "ExtractorLink(name=$name, url=$url, referer=$referer, isM3u8=$isM3u8)" + return "ExtractorLink(name=$name, url=$url, referer=$referer, type=$type)" } } @@ -135,6 +225,7 @@ enum class Qualities(var value: Int, val defaultPriority: Int) { else -> "${qual}p" } } + fun getStringByIntFull(quality: Int): String { return when (quality) { 0 -> "Auto" diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 72eb002a..d108daed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -53,7 +53,7 @@ import java.io.Closeable import java.io.File import java.io.IOException import java.io.OutputStream -import java.net.URL +import java.lang.IllegalArgumentException import java.util.* const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" @@ -951,7 +951,10 @@ object VideoDownloadManager { /** how many bytes every connection should be, by default it is 10 MiB */ chuckSize: Long = (1 shl 20) * 10, /** maximum bytes in the buffer that responds */ - bufferSize: Int = DEFAULT_BUFFER_SIZE + bufferSize: Int = DEFAULT_BUFFER_SIZE, + /** how many bytes bytes it should require to use the parallel downloader instead, + * if we download a very small file we don't want it parallel */ + maximumSmallSize : Long = chuckSize * 2 ): LazyStreamDownloadData { // we don't want to make a separate connection for every 1kb require(chuckSize > 1000) @@ -963,7 +966,7 @@ object VideoDownloadManager { var downloadLength: Long? = null var totalLength: Long? = null - val ranges = if (contentLength == null) { + val ranges = if (contentLength == null || contentLength < maximumSmallSize) { // is the equivalent of [startByte..EOF] as we don't know the size we can only do one // connection LongArray(1) { startByte } @@ -1024,6 +1027,7 @@ object VideoDownloadManager { } } + /** download a file that consist of a single stream of data*/ suspend fun downloadThing( context: Context, link: IDownloadableMinimum, @@ -1035,8 +1039,7 @@ object VideoDownloadManager { createNotificationCallback: (CreateNotificationMetadata) -> Unit, parallelConnections: Int = 3 ): DownloadStatus = withContext(Dispatchers.IO) { - // we cant download torrents with this implementation, aria2c might be used in the future - if (link.url.startsWith("magnet") || link.url.endsWith(".torrent") || parallelConnections < 1) { + if (parallelConnections < 1) { return@withContext DOWNLOAD_INVALID_INPUT } @@ -1529,6 +1532,11 @@ object VideoDownloadManager { notificationCallback: (Int, Notification) -> Unit, tryResume: Boolean = false, ): DownloadStatus { + // no support for these file formats + if(link.type == ExtractorLinkType.MAGNET || link.type == ExtractorLinkType.TORRENT || link.type == ExtractorLinkType.DASH) { + return DOWNLOAD_INVALID_INPUT + } + val name = getFileName(context, ep) // Make sure this is cancelled when download is done or cancelled. @@ -1557,35 +1565,39 @@ object VideoDownloadManager { } try { - if (link.isM3u8 || normalSafeApiCall { URL(link.url).path.endsWith(".m3u8") } == true) { - val startIndex = if (tryResume) { - context.getKey( - KEY_DOWNLOAD_INFO, - ep.id.toString(), - null - )?.extraInfo?.toIntOrNull() - } else null + when(link.type) { + ExtractorLinkType.M3U8 -> { + val startIndex = if (tryResume) { + context.getKey( + KEY_DOWNLOAD_INFO, + ep.id.toString(), + null + )?.extraInfo?.toIntOrNull() + } else null - return downloadHLS( - context, - link, - name, - folder ?: "", - ep.id, - startIndex, - callback, parallelConnections = maxConcurrentConnections - ) - } else { - return downloadThing( - context, - link, - name, - folder ?: "", - "mp4", - tryResume, - ep.id, - callback, parallelConnections = maxConcurrentConnections - ) + return downloadHLS( + context, + link, + name, + folder ?: "", + ep.id, + startIndex, + callback, parallelConnections = maxConcurrentConnections + ) + } + ExtractorLinkType.VIDEO -> { + return downloadThing( + context, + link, + name, + folder ?: "", + "mp4", + tryResume, + ep.id, + callback, parallelConnections = maxConcurrentConnections + ) + } + else -> throw IllegalArgumentException("unsuported download type") } } catch (t: Throwable) { return DOWNLOAD_FAILED From 0839775172db1f55b4703d189e1aa7e8f3aed3f3 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Mon, 4 Sep 2023 20:36:36 +0000 Subject: [PATCH 082/441] Upgrade SDK (#590) * Update sdk version * Let's not be too radical --- 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 55d0f7ae..e31de078 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -51,12 +51,12 @@ android { } compileSdk = 33 - buildToolsVersion = "30.0.3" + buildToolsVersion = "34.0.0" defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 29 + targetSdk = 33 versionCode = 59 versionName = "4.1.8" From 3fe247fb193ada9bbb7ff391d38e02a24630c1e0 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 6 Sep 2023 20:53:43 +0200 Subject: [PATCH 083/441] added drm player support --- .../cloudstream3/ui/player/CS3IPlayer.kt | 87 ++++++++++++++++--- .../cloudstream3/utils/ExtractorApi.kt | 55 ++++++++++++ 2 files changed, 132 insertions(+), 10 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 fd1da5ca..c779943b 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 @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.player +import android.annotation.SuppressLint import android.content.Context import android.net.Uri import android.os.Handler @@ -7,6 +8,7 @@ import android.os.Looper import android.util.Log import android.util.Rational import android.widget.FrameLayout +import androidx.media3.common.C import androidx.media3.common.C.* import androidx.media3.common.Format import androidx.media3.common.MediaItem @@ -31,6 +33,10 @@ import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.SeekParameters +import androidx.media3.exoplayer.dash.DashMediaSource +import androidx.media3.exoplayer.drm.DefaultDrmSessionManager +import androidx.media3.exoplayer.drm.FrameworkMediaDrm +import androidx.media3.exoplayer.drm.LocalMediaDrmCallback import androidx.media3.exoplayer.source.ClippingMediaSource import androidx.media3.exoplayer.source.ConcatenatingMediaSource import androidx.media3.exoplayer.source.DefaultMediaSourceFactory @@ -50,6 +56,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.DrmExtractorLink import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList @@ -58,6 +65,7 @@ import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File import java.lang.IllegalArgumentException +import java.util.UUID import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession @@ -104,7 +112,16 @@ class CS3IPlayer : IPlayer { * */ data class MediaItemSlice( val mediaItem: MediaItem, - val durationUs: Long + val durationUs: Long, + val drm: DrmMetadata? = null + ) + + data class DrmMetadata( + val kid: String, + val key: String, + val uuid: UUID, + val kty: String, + val keyRequestParameters: HashMap, ) override fun getDuration(): Long? = exoPlayer?.duration @@ -340,6 +357,7 @@ class CS3IPlayer : IPlayer { }.flatten() } + @SuppressLint("UnsafeOptInUsageError") private fun Tracks.Group.getFormats(): List> { return (0 until this.mediaTrackGroup.length).mapNotNull { i -> if (this.isSupported) @@ -368,6 +386,7 @@ class CS3IPlayer : IPlayer { ) } + @SuppressLint("UnsafeOptInUsageError") override fun getVideoTracks(): CurrentTracks { val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList() val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO } @@ -387,6 +406,7 @@ class CS3IPlayer : IPlayer { /** * @return True if the player should be reloaded * */ + @SuppressLint("UnsafeOptInUsageError") override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { Log.i(TAG, "setPreferredSubtitles init $subtitle") currentSubtitles = subtitle @@ -465,6 +485,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") override fun getAspectRatio(): Rational? { return exoPlayer?.videoFormat?.let { format -> Rational(format.width, format.height) @@ -475,6 +496,7 @@ class CS3IPlayer : IPlayer { subtitleHelper.setSubStyle(style) } + @SuppressLint("UnsafeOptInUsageError") override fun saveData() { Log.i(TAG, "saveData") updatedTime() @@ -548,6 +570,7 @@ class CS3IPlayer : IPlayer { var requestSubtitleUpdate: (() -> Unit)? = null + @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(headers: Map): HttpDataSource.Factory { val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT) return source.apply { @@ -555,6 +578,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory { val provider = getApiFromNameNull(link.source) val interceptor = provider?.getVideoInterceptor(link) @@ -632,6 +656,7 @@ class CS3IPlayer : IPlayer { return Pair(subSources, activeSubtitles) }*/ + @SuppressLint("UnsafeOptInUsageError") private fun getCache(context: Context, cacheSize: Long): SimpleCache? { return try { val databaseProvider = StandaloneDatabaseProvider(context) @@ -663,6 +688,7 @@ class CS3IPlayer : IPlayer { return getMediaItemBuilder(mimeType).setUri(url).build() } + @SuppressLint("UnsafeOptInUsageError") private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector { val trackSelector = DefaultTrackSelector(context) trackSelector.parameters = trackSelector.buildUponParameters() @@ -676,6 +702,7 @@ class CS3IPlayer : IPlayer { var currentTextRenderer: CustomTextRenderer? = null + @SuppressLint("UnsafeOptInUsageError") private fun buildExoPlayer( context: Context, mediaItemSlices: List, @@ -760,15 +787,33 @@ class CS3IPlayer : IPlayer { // If there is only one item then treat it as normal, if multiple: concatenate the items. val videoMediaSource = if (mediaItemSlices.size == 1) { - factory.createMediaSource(mediaItemSlices.first().mediaItem) + val item = mediaItemSlices.first() + + item.drm?.let { drm -> + val drmCallback = + LocalMediaDrmCallback("{\"keys\":[{\"kty\":\"${drm.kty}\",\"k\":\"${drm.key}\",\"kid\":\"${drm.kid}\"}],\"type\":\"temporary\"}".toByteArray()) + val manager = DefaultDrmSessionManager.Builder() + .setPlayClearSamplesWithoutKeys(true) + .setMultiSession(false) + .setKeyRequestParameters(drm.keyRequestParameters) + .setUuidAndExoMediaDrmProvider(drm.uuid, FrameworkMediaDrm.DEFAULT_PROVIDER) + .build(drmCallback) + val manifestDataSourceFactory = DefaultHttpDataSource.Factory() + + DashMediaSource.Factory(manifestDataSourceFactory) + .setDrmSessionManagerProvider { manager } + .createMediaSource(item.mediaItem) + } ?: run { + factory.createMediaSource(item.mediaItem) + } } else { val source = ConcatenatingMediaSource() - mediaItemSlices.map { + mediaItemSlices.map { item -> source.addMediaSource( // The duration MUST be known for it to work properly, see https://github.com/google/ExoPlayer/issues/4727 ClippingMediaSource( - factory.createMediaSource(it.mediaItem), - it.durationUs + factory.createMediaSource(item.mediaItem), + item.durationUs ) ) } @@ -1105,6 +1150,8 @@ class CS3IPlayer : IPlayer { } private var lastTimeStamps: List = emptyList() + + @SuppressLint("UnsafeOptInUsageError") override fun addTimeStamps(timeStamps: List) { lastTimeStamps = timeStamps timeStamps.forEach { timestamp -> @@ -1122,6 +1169,7 @@ class CS3IPlayer : IPlayer { updatedTime() } + @SuppressLint("UnsafeOptInUsageError") fun onRenderFirst() { if (hasUsedFirstRender) { // this insures that we only call this once per player load return @@ -1188,6 +1236,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") private fun getSubSources( onlineSourceFactory: HttpDataSource.Factory?, offlineSourceFactory: DataSource.Factory?, @@ -1243,6 +1292,7 @@ class CS3IPlayer : IPlayer { return exoPlayer != null } + @SuppressLint("UnsafeOptInUsageError") private fun loadOnlinePlayer(context: Context, link: ExtractorLink) { Log.i(TAG, "loadOnlinePlayer $link") try { @@ -1259,7 +1309,7 @@ class CS3IPlayer : IPlayer { HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) } - val mime = when(link.type) { + val mime = when (link.type) { ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8 ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4 @@ -1267,12 +1317,29 @@ class CS3IPlayer : IPlayer { ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support") } - val mediaItems = if (link is ExtractorLinkPlayList) { - link.playlist.map { + + val mediaItems = when (link) { + is ExtractorLinkPlayList -> link.playlist.map { MediaItemSlice(getMediaItem(mime, it.url), it.durationUs) } - } else { - listOf( + + is DrmExtractorLink -> { + listOf( + // Single sliced list with unset length + MediaItemSlice( + getMediaItem(mime, link.url), Long.MIN_VALUE, + drm = DrmMetadata( + kid = link.kid, + key = link.key, + uuid = link.uuid ?: C.CLEARKEY_UUID, + kty = link.kty, + keyRequestParameters = link.keyRequestParameters + ) + ) + ) + } + + else -> listOf( // Single sliced list with unset length MediaItemSlice(getMediaItem(mime, link.url), Long.MIN_VALUE) ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 2a539f0d..85e88819 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -8,6 +8,7 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay import org.jsoup.Jsoup import java.net.URL +import java.util.UUID import kotlin.collections.MutableList /** @@ -99,6 +100,60 @@ private fun inferTypeFromUrl(url: String): ExtractorLinkType { } } val INFER_TYPE : ExtractorLinkType? = null + +open class DrmExtractorLink private constructor( + override val source: String, + override val name: String, + override val url: String, + override val referer: String, + override val quality: Int, + override val headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + override val extractorData: String? = null, + override val type: ExtractorLinkType, + open val kid : String, + open val key : String, + /** if null then it uses the UUID for the ClearKey DRM scheme */ + open val uuid : UUID?, + open val kty : String, + + open val keyRequestParameters : HashMap +) : ExtractorLink( + source, name, url, referer, quality, type, headers, extractorData +) { + constructor( + source: String, + name: String, + url: String, + referer: String, + quality: Int, + /** the type of the media, use INFER_TYPE if you want to auto infer the type from the url */ + type: ExtractorLinkType?, + headers: Map = mapOf(), + /** Used for getExtractorVerifierJob() */ + extractorData: String? = null, + kid : String, + key : String, + uuid : UUID? = null, + kty : String = "oct", + keyRequestParameters : HashMap = hashMapOf(), + ) : this( + source = source, + name = name, + url = url, + referer = referer, + quality = quality, + headers = headers, + extractorData = extractorData, + type = type ?: inferTypeFromUrl(url), + kid = kid, + key = key, + uuid = uuid, + keyRequestParameters = keyRequestParameters, + kty = kty, + ) +} + open class ExtractorLink constructor( open val source: String, open val name: String, From 49731cd6995dc2b784fef17554faeb90a1b895c0 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 6 Sep 2023 22:42:22 +0200 Subject: [PATCH 084/441] changed drm API a bit --- .../cloudstream3/ui/player/CS3IPlayer.kt | 2 +- .../cloudstream3/utils/ExtractorApi.kt | 225 +++++++++++++++++- 2 files changed, 220 insertions(+), 7 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 c779943b..4a88a2e7 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 @@ -1331,7 +1331,7 @@ class CS3IPlayer : IPlayer { drm = DrmMetadata( kid = link.kid, key = link.key, - uuid = link.uuid ?: C.CLEARKEY_UUID, + uuid = link.uuid, kty = link.kty, keyRequestParameters = link.keyRequestParameters ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 85e88819..0a926374 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -1,15 +1,204 @@ package com.lagradost.cloudstream3.utils import android.net.Uri -import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.extractors.AStreamHub +import com.lagradost.cloudstream3.extractors.Acefile +import com.lagradost.cloudstream3.extractors.Ahvsh +import com.lagradost.cloudstream3.extractors.Aico +import com.lagradost.cloudstream3.extractors.AsianLoad +import com.lagradost.cloudstream3.extractors.Bestx +import com.lagradost.cloudstream3.extractors.Blogger +import com.lagradost.cloudstream3.extractors.BullStream +import com.lagradost.cloudstream3.extractors.ByteShare +import com.lagradost.cloudstream3.extractors.Cda +import com.lagradost.cloudstream3.extractors.Cdnplayer +import com.lagradost.cloudstream3.extractors.Chillx +import com.lagradost.cloudstream3.extractors.CineGrabber +import com.lagradost.cloudstream3.extractors.Cinestart +import com.lagradost.cloudstream3.extractors.DBfilm +import com.lagradost.cloudstream3.extractors.Dailymotion +import com.lagradost.cloudstream3.extractors.DatabaseGdrive +import com.lagradost.cloudstream3.extractors.DatabaseGdrive2 +import com.lagradost.cloudstream3.extractors.DesuArcg +import com.lagradost.cloudstream3.extractors.DesuDrive +import com.lagradost.cloudstream3.extractors.DesuOdchan +import com.lagradost.cloudstream3.extractors.DesuOdvip +import com.lagradost.cloudstream3.extractors.Dokicloud +import com.lagradost.cloudstream3.extractors.DoodCxExtractor +import com.lagradost.cloudstream3.extractors.DoodLaExtractor +import com.lagradost.cloudstream3.extractors.DoodPmExtractor +import com.lagradost.cloudstream3.extractors.DoodShExtractor +import com.lagradost.cloudstream3.extractors.DoodSoExtractor +import com.lagradost.cloudstream3.extractors.DoodToExtractor +import com.lagradost.cloudstream3.extractors.DoodWatchExtractor +import com.lagradost.cloudstream3.extractors.DoodWfExtractor +import com.lagradost.cloudstream3.extractors.DoodWsExtractor +import com.lagradost.cloudstream3.extractors.DoodYtExtractor +import com.lagradost.cloudstream3.extractors.Dooood +import com.lagradost.cloudstream3.extractors.Embedgram +import com.lagradost.cloudstream3.extractors.Evoload +import com.lagradost.cloudstream3.extractors.Evoload1 +import com.lagradost.cloudstream3.extractors.FEmbed +import com.lagradost.cloudstream3.extractors.FEnet +import com.lagradost.cloudstream3.extractors.Fastream +import com.lagradost.cloudstream3.extractors.FeHD +import com.lagradost.cloudstream3.extractors.Fembed9hd +import com.lagradost.cloudstream3.extractors.FileMoon +import com.lagradost.cloudstream3.extractors.FileMoonIn +import com.lagradost.cloudstream3.extractors.FileMoonSx +import com.lagradost.cloudstream3.extractors.Filesim +import com.lagradost.cloudstream3.extractors.Fplayer +import com.lagradost.cloudstream3.extractors.GMPlayer +import com.lagradost.cloudstream3.extractors.Gdriveplayer +import com.lagradost.cloudstream3.extractors.Gdriveplayerapi +import com.lagradost.cloudstream3.extractors.Gdriveplayerapp +import com.lagradost.cloudstream3.extractors.Gdriveplayerbiz +import com.lagradost.cloudstream3.extractors.Gdriveplayerco +import com.lagradost.cloudstream3.extractors.Gdriveplayerfun +import com.lagradost.cloudstream3.extractors.Gdriveplayerio +import com.lagradost.cloudstream3.extractors.Gdriveplayerme +import com.lagradost.cloudstream3.extractors.Gdriveplayerorg +import com.lagradost.cloudstream3.extractors.Gdriveplayerus +import com.lagradost.cloudstream3.extractors.Gofile +import com.lagradost.cloudstream3.extractors.GuardareStream +import com.lagradost.cloudstream3.extractors.Guccihide +import com.lagradost.cloudstream3.extractors.Hxfile +import com.lagradost.cloudstream3.extractors.JWPlayer +import com.lagradost.cloudstream3.extractors.Jawcloud +import com.lagradost.cloudstream3.extractors.Jeniusplay +import com.lagradost.cloudstream3.extractors.Keephealth +import com.lagradost.cloudstream3.extractors.KotakAnimeid +import com.lagradost.cloudstream3.extractors.Kotakajair +import com.lagradost.cloudstream3.extractors.Krakenfiles +import com.lagradost.cloudstream3.extractors.LayarKaca +import com.lagradost.cloudstream3.extractors.Linkbox +import com.lagradost.cloudstream3.extractors.Luxubu +import com.lagradost.cloudstream3.extractors.Lvturbo +import com.lagradost.cloudstream3.extractors.Maxstream +import com.lagradost.cloudstream3.extractors.Mcloud +import com.lagradost.cloudstream3.extractors.Megacloud +import com.lagradost.cloudstream3.extractors.Meownime +import com.lagradost.cloudstream3.extractors.MixDrop +import com.lagradost.cloudstream3.extractors.MixDropBz +import com.lagradost.cloudstream3.extractors.MixDropCh +import com.lagradost.cloudstream3.extractors.MixDropTo +import com.lagradost.cloudstream3.extractors.Movhide +import com.lagradost.cloudstream3.extractors.Moviehab +import com.lagradost.cloudstream3.extractors.MoviehabNet +import com.lagradost.cloudstream3.extractors.Moviesapi +import com.lagradost.cloudstream3.extractors.Moviesm4u +import com.lagradost.cloudstream3.extractors.Mp4Upload +import com.lagradost.cloudstream3.extractors.Mvidoo +import com.lagradost.cloudstream3.extractors.MwvnVizcloudInfo +import com.lagradost.cloudstream3.extractors.Neonime7n +import com.lagradost.cloudstream3.extractors.Neonime8n +import com.lagradost.cloudstream3.extractors.OkRu +import com.lagradost.cloudstream3.extractors.OkRuHttps +import com.lagradost.cloudstream3.extractors.Okrulink +import com.lagradost.cloudstream3.extractors.Pixeldrain +import com.lagradost.cloudstream3.extractors.PlayLtXyz +import com.lagradost.cloudstream3.extractors.PlayerVoxzer +import com.lagradost.cloudstream3.extractors.Rabbitstream +import com.lagradost.cloudstream3.extractors.Rasacintaku +import com.lagradost.cloudstream3.extractors.SBfull +import com.lagradost.cloudstream3.extractors.Sbasian +import com.lagradost.cloudstream3.extractors.Sbface +import com.lagradost.cloudstream3.extractors.Sbflix +import com.lagradost.cloudstream3.extractors.Sblona +import com.lagradost.cloudstream3.extractors.Sblongvu +import com.lagradost.cloudstream3.extractors.Sbnet +import com.lagradost.cloudstream3.extractors.Sbrapid +import com.lagradost.cloudstream3.extractors.Sbsonic +import com.lagradost.cloudstream3.extractors.Sbspeed +import com.lagradost.cloudstream3.extractors.Sbthe +import com.lagradost.cloudstream3.extractors.Sendvid +import com.lagradost.cloudstream3.extractors.ShaveTape +import com.lagradost.cloudstream3.extractors.Solidfiles +import com.lagradost.cloudstream3.extractors.SpeedoStream +import com.lagradost.cloudstream3.extractors.SpeedoStream1 +import com.lagradost.cloudstream3.extractors.SpeedoStream2 +import com.lagradost.cloudstream3.extractors.Ssbstream +import com.lagradost.cloudstream3.extractors.StreamM4u +import com.lagradost.cloudstream3.extractors.StreamSB +import com.lagradost.cloudstream3.extractors.StreamSB1 +import com.lagradost.cloudstream3.extractors.StreamSB10 +import com.lagradost.cloudstream3.extractors.StreamSB11 +import com.lagradost.cloudstream3.extractors.StreamSB2 +import com.lagradost.cloudstream3.extractors.StreamSB3 +import com.lagradost.cloudstream3.extractors.StreamSB4 +import com.lagradost.cloudstream3.extractors.StreamSB5 +import com.lagradost.cloudstream3.extractors.StreamSB6 +import com.lagradost.cloudstream3.extractors.StreamSB7 +import com.lagradost.cloudstream3.extractors.StreamSB8 +import com.lagradost.cloudstream3.extractors.StreamSB9 +import com.lagradost.cloudstream3.extractors.StreamTape +import com.lagradost.cloudstream3.extractors.StreamTapeNet +import com.lagradost.cloudstream3.extractors.StreamhideCom +import com.lagradost.cloudstream3.extractors.StreamhideTo +import com.lagradost.cloudstream3.extractors.Streamhub2 +import com.lagradost.cloudstream3.extractors.Streamlare +import com.lagradost.cloudstream3.extractors.StreamoUpload +import com.lagradost.cloudstream3.extractors.Streamplay +import com.lagradost.cloudstream3.extractors.Streamsss +import com.lagradost.cloudstream3.extractors.Supervideo +import com.lagradost.cloudstream3.extractors.Tantifilm +import com.lagradost.cloudstream3.extractors.Tomatomatela +import com.lagradost.cloudstream3.extractors.TomatomatelalClub +import com.lagradost.cloudstream3.extractors.Tubeless +import com.lagradost.cloudstream3.extractors.Upstream +import com.lagradost.cloudstream3.extractors.UpstreamExtractor +import com.lagradost.cloudstream3.extractors.Uqload +import com.lagradost.cloudstream3.extractors.Uqload1 +import com.lagradost.cloudstream3.extractors.Uqload2 +import com.lagradost.cloudstream3.extractors.Userload +import com.lagradost.cloudstream3.extractors.Userscloud +import com.lagradost.cloudstream3.extractors.Uservideo +import com.lagradost.cloudstream3.extractors.Vanfem +import com.lagradost.cloudstream3.extractors.Vicloud +import com.lagradost.cloudstream3.extractors.VidSrcExtractor +import com.lagradost.cloudstream3.extractors.VidSrcExtractor2 +import com.lagradost.cloudstream3.extractors.VideoVard +import com.lagradost.cloudstream3.extractors.VideovardSX +import com.lagradost.cloudstream3.extractors.Vidgomunime +import com.lagradost.cloudstream3.extractors.Vidgomunimesb +import com.lagradost.cloudstream3.extractors.Vidmoly +import com.lagradost.cloudstream3.extractors.Vidmolyme +import com.lagradost.cloudstream3.extractors.Vido +import com.lagradost.cloudstream3.extractors.Vidstreamz +import com.lagradost.cloudstream3.extractors.Vizcloud +import com.lagradost.cloudstream3.extractors.Vizcloud2 +import com.lagradost.cloudstream3.extractors.VizcloudCloud +import com.lagradost.cloudstream3.extractors.VizcloudDigital +import com.lagradost.cloudstream3.extractors.VizcloudInfo +import com.lagradost.cloudstream3.extractors.VizcloudLive +import com.lagradost.cloudstream3.extractors.VizcloudOnline +import com.lagradost.cloudstream3.extractors.VizcloudSite +import com.lagradost.cloudstream3.extractors.VizcloudXyz +import com.lagradost.cloudstream3.extractors.Voe +import com.lagradost.cloudstream3.extractors.Watchx +import com.lagradost.cloudstream3.extractors.WcoStream +import com.lagradost.cloudstream3.extractors.Wibufile +import com.lagradost.cloudstream3.extractors.XStreamCdn +import com.lagradost.cloudstream3.extractors.YourUpload +import com.lagradost.cloudstream3.extractors.YoutubeExtractor +import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor +import com.lagradost.cloudstream3.extractors.YoutubeNoCookieExtractor +import com.lagradost.cloudstream3.extractors.YoutubeShortLinkExtractor +import com.lagradost.cloudstream3.extractors.Yufiles +import com.lagradost.cloudstream3.extractors.Zorofile +import com.lagradost.cloudstream3.extractors.Zplayer +import com.lagradost.cloudstream3.extractors.ZplayerV2 +import com.lagradost.cloudstream3.extractors.Ztreamhub import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.extractors.* import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay import org.jsoup.Jsoup import java.net.URL import java.util.UUID -import kotlin.collections.MutableList /** * For use in the ConcatenatingMediaSource. @@ -101,6 +290,31 @@ private fun inferTypeFromUrl(url: String): ExtractorLinkType { } val INFER_TYPE : ExtractorLinkType? = null +/** + * UUID for the ClearKey DRM scheme. + * + * + * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up. + */ +val CLEARKEY_UUID = UUID(-0x1d8e62a7567a4c37L, 0x781AB030AF78D30EL) + +/** + * UUID for the Widevine DRM scheme. + * + * + * Widevine is supported on Android devices running Android 4.3 (API Level 18) and up. + */ +val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L) + +/** + * UUID for the PlayReady DRM scheme. + * + * + * PlayReady is supported on all AndroidTV devices. Note that most other Android devices do not + * provide PlayReady support. + */ +val PLAYREADY_UUID = UUID(-0x65fb0f8667bfbd7aL, -0x546d19a41f77a06bL) + open class DrmExtractorLink private constructor( override val source: String, override val name: String, @@ -113,8 +327,7 @@ open class DrmExtractorLink private constructor( override val type: ExtractorLinkType, open val kid : String, open val key : String, - /** if null then it uses the UUID for the ClearKey DRM scheme */ - open val uuid : UUID?, + open val uuid : UUID, open val kty : String, open val keyRequestParameters : HashMap @@ -134,7 +347,7 @@ open class DrmExtractorLink private constructor( extractorData: String? = null, kid : String, key : String, - uuid : UUID? = null, + uuid : UUID = CLEARKEY_UUID, kty : String = "oct", keyRequestParameters : HashMap = hashMapOf(), ) : this( From 4ddd78ebb6892742543b082a6395553d9642dc8e Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:30:00 +0530 Subject: [PATCH 085/441] fook jitpack (#595) --- 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 e31de078..f52d6e5e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -250,9 +250,9 @@ dependencies { // used for subtitle decoding https://github.com/albfernandez/juniversalchardet implementation("com.github.albfernandez:juniversalchardet:2.4.0") - // newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204 + // newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev // this should be updated frequently to avoid trailer fu*kery - implementation("com.github.TeamNewPipe:NewPipeExtractor:1f08d28") + implementation("com.github.teamnewpipe:NewPipeExtractor:1f08d28") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") // Library/extensions searching with Levenshtein distance From f05c65cf5c62964c73b9756c4f5c2dedb7cfb919 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Fri, 8 Sep 2023 10:01:11 +0200 Subject: [PATCH 086/441] Translated using Weblate (Odia) (#574) Currently translated at 39.5% (249 of 630 strings) Translated using Weblate (Odia) Currently translated at 25.0% (1 of 4 strings) Translated using Weblate (Odia) Currently translated at 38.8% (245 of 630 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Tamil) Currently translated at 20.9% (132 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Ukrainian) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (630 of 630 strings) Translated using Weblate (Croatian) Currently translated at 99.8% (629 of 630 strings) Translated using Weblate (Croatian) Currently translated at 99.3% (626 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Arabic (Najdi)) Currently translated at 54.9% (346 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Ukrainian) Currently translated at 100.0% (630 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (French) Currently translated at 100.0% (630 of 630 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Hungarian) Currently translated at 85.7% (540 of 630 strings) Translated using Weblate (Romanian) Currently translated at 95.7% (603 of 630 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 50.1% (316 of 630 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (630 of 630 strings) Translated using Weblate (Portuguese (Brazil)) Currently translated at 96.1% (606 of 630 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' Translated using Weblate (Arabic (Najdi)) Currently translated at 44.9% (283 of 630 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 39.6% (250 of 630 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 39.5% (249 of 630 strings) Translated using Weblate (German) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (German) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 33.4% (211 of 630 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (630 of 630 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 strings) Translated using Weblate (German) Currently translated at 99.8% (629 of 630 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' Update translation files Updated by "Remove blank strings" hook in Weblate. Translated using Weblate (Arabic (Najdi)) Currently translated at 32.0% (202 of 630 strings) Translated using Weblate (Portuguese (Brazil)) Currently translated at 91.5% (577 of 630 strings) Translated using Weblate (Arabic (Saudi Arabia)) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Arabic (Najdi)) Currently translated at 23.9% (151 of 630 strings) Added translation using Weblate (Arabic (South Levantine)) Translated using Weblate (Amharic) Currently translated at 14.9% (94 of 630 strings) Translated using Weblate (Tigrinya) Currently translated at 15.0% (95 of 630 strings) Added translation using Weblate (Amharic) Added translation using Weblate (Tigrinya) Merge remote-tracking branch 'origin/master' Translated using Weblate (Hungarian) Currently translated at 85.2% (537 of 630 strings) Translated using Weblate (Hungarian) Currently translated at 81.4% (513 of 630 strings) Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/am/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ars/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/or/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ro/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ti/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ar_SA/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/de/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/or/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/uk/ Translation: Cloudstream/App Translation: Cloudstream/Fastlane Co-authored-by: Alexandru Co-authored-by: Anarchydr Co-authored-by: Cait Martin Newnham <85128509+helloiamcait@users.noreply.github.com> Co-authored-by: Carlos Luiz Co-authored-by: Chi Uma Co-authored-by: GobinathAL Co-authored-by: Gyuri Bajzik Co-authored-by: Joel Brink Co-authored-by: Julian Co-authored-by: Mubarek Seyd Juhar Co-authored-by: Sam Cooper Co-authored-by: Skrripy Co-authored-by: Subham Jena Co-authored-by: mbottari Co-authored-by: pedrolinharesmoreira Co-authored-by: tabtomi8 --- app/src/main/res/values-ars/strings.xml | 67 +++++++++++++- app/src/main/res/values-bp/strings.xml | 43 ++++++--- app/src/main/res/values-de/strings.xml | 92 +++++++++---------- app/src/main/res/values-fr/strings.xml | 6 +- app/src/main/res/values-hr/strings.xml | 13 ++- app/src/main/res/values-hu/strings.xml | 7 +- app/src/main/res/values-or/strings.xml | 7 +- app/src/main/res/values-ro/strings.xml | 3 +- app/src/main/res/values-ta/strings.xml | 26 ++++-- app/src/main/res/values-uk/strings.xml | 38 ++++---- fastlane/metadata/android/or/changelogs/2.txt | 1 + fastlane/metadata/android/uk/changelogs/2.txt | 2 +- 12 files changed, 209 insertions(+), 96 deletions(-) create mode 100644 fastlane/metadata/android/or/changelogs/2.txt diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index ea8aa05c..5135c97e 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -200,4 +200,69 @@ استخدم %sتعذر إنشاء واجهة المستخدم بشكل صحيح، وهذا خطأ كبير ويجب الإبلاغ عنه على الفور الصفات - + نوع الحافة + العب + حدث خطأ أثناء تحميل الروابط + التخزين الداخلي + الترجمة + استئناف تحميل + معلومات + وقفة التحميل + الغي + احفظ + إعدادات الترجمة + لون الخط + لون المخطط التفصيلي + اقفل + امسح + سرعة اللاعب + لون الخلفية + لون النافذة + ارتفاع الترجمة + حذف ملف + تعطيل الإبلاغ التلقائي عن الأخطاء + بدأ التحديث + انسخ + بث + ملف اللعب + مزيد من المعلومات + تصفية الإشارات المرجعية + إشارات مرجعية + زيل + ضبط حالة المشاهدة + مدبلجة + اخفي + قدم + وصف + يستمر التشغيل في مشغل مصغر فوق التطبيقات الأخرى + نهائيا %sسيؤدي هذا الى حذف +\nهل أنت متأكد؟ + الخط + حجم الخط + زيل + هذا المزود عبارة عن تورنت، ويوصى باستخدام فيبيان + لا يتم توفير البيانات الوصفية بواسطة الموقع، وسيفشل تحميل الفيديو إذا لم يكن موجودًا في الموقع. + جاري التنفيذ + مكتمل + حالة + التحديد التلقائي للغة + زر تغيير حجم المشغل + مواصلة المشاهدة + مزيد من المعلومات + البحث باستخدام مقدمي الخدمات + البحث باستخدام الأنواع + بنيني الى المطورين %d تم منح + لم يتم تقديم بنيني + تحميل اللغات + لغة الترجمة + اضغط لإعادة التعيين إلى الوضع الافتراضي + %s قم باستيراد الخطوط بوضعها في + قد تكون هناك حاجة إلى فيبيان حتى يعمل هذا المزود بشكل صحيح + لم يتم العثور على قطعة أرض + لم يتم العثور على وصف + 🐈عرض لوجكات + سجل + صور في صور + %d +\nباقي + \ No newline at end of file diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index b70eec12..daa352a7 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -157,7 +157,7 @@ Mostrar episódios de Filler em anime Mostrar trailers Mostrar posters do Kitsu - Esconder qualidades de vídeo selecionadas nos resultados da Pesquisa + Esconder qualidades de vídeo selecionadas nos resultados da pesquisa Atualizações de plugin automáticas Mostrar atualizações do app Automaticamente procurar por novas atualizações ao abrir @@ -222,7 +222,7 @@ Filme Série Desenho Animado - @string/anime + Anime @string/ova Torrent Documentário @@ -265,14 +265,14 @@ Cache do vídeo em disco Limpar cache de vídeo e imagem Causará travamentos aleatórios se definido muito alto. Não mude caso tiver pouca memória RAM, como um Android TV ou um telefone antigo - Pode causar problemas em sistemas com pouco espaço de armazenamento se definido muito alto, como em dispositivos Android TV + Causa problemas em sistemas com pouco espaço de armazenamento se definido muito alto, como em dispositivos Android TV. DNS sobre HTTPS Útil para burlar bloqueios de provedores de internet Clonar site Remover site Adiciona um clone de um site existente, com uma URL diferente Caminho para Download - Url do servidor Nginx + URL do servidor NGINX Mostrar Anime Dublado/Legendado Ajustar para a Tela Esticar @@ -338,7 +338,7 @@ Sombreado Em Relevo Sincronizar legendas - 1000ms + 1000 ms Atraso de legenda Use isto se as legendas forem mostradas %dms adiantadas Use isto se as legendas forem mostradas %dms atrasadas @@ -382,9 +382,9 @@ Resolução e título Título Resolução - Id invalida + ID inválido Dado invalido - URL invalido + URL inválida Erro Remover legendas ocultas(CC) das legendas Remover bloat das legendas @@ -406,8 +406,8 @@ Plugin Carregado Plugin Apagado Falha ao carregar %s - Iniciada a transferência %d %s - Transferido %d %s com sucesso + Iniciada a transferência %d %s… + Transferido %d %s Tudo %s já transferido Transferência em batch Plugin @@ -444,7 +444,7 @@ Navegador Copia de Segurança A Barra de Progresso pode ser usada quando o player estiver oculto - Inscrever + Inscrito Essa lista está vazia. Tente mudar para outra. Reproduzir Livestream Log do Teste @@ -493,10 +493,10 @@ \nNOTA: Se a soma for 10 ou mais, o Player pulará automaticamente o carregamento quando o link for carregado! Arquivo de modo de segurança encontrado! \nNão carregar nenhuma extensão na inicialização até que o arquivo seja removido. - Inscrevel em %d + Inscrito em %s Episódio %d Lançado Selecionar padrão - Disinscrevel em %d + Desinscrito de %s Alguns aparelhos não possuem suporte para este pacote de instalação. Tente a opção legada se a atualização não instalar. Dados móveis Perfil %d @@ -550,4 +550,21 @@ Faixas de áudio Adicionado em (novo para antigo) Faixas de video - + Legendas + Navegador + 18+ + Links + Funcionalidades do Player + Instalador APK + Aparência + Desativar + Usar + Link da stream + Gestos + Plugin baixado + Não foi possível se conectar ao GitHub. Ativando proxy jsDelivr… + Cache + Vídeo + Android TV + Wi-Fi + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6739465a..233e38e4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -5,19 +5,19 @@ Episode %d wird veröffentlicht in Vorschaubild Vorschaubild - Halten, um auf die Standardeinstellungen zurückzusetzen + Halten, um auf Standardeinstellungen zurückzusetzen Wiederherstellung der Daten aus der Datei %s fehlgeschlagen Daten erfolgreich gesichert Fehler beim Sichern von %s Dieser Anbieter hat keine Chromecast-Unterstützung Chromecast-Mirror In App wiedergeben - Vermischte Openings + Gemischte Openings Abspann Intro Verlauf löschen Verlauf - Überspringen Knopf für Openings/Endings anzeigen + Button zum Überspringen für Openings/Endings anzeigen Zu viel Text. Kann nicht in der Zwischenablage gespeichert werden. Episodenvorschaubild Medienvorschaubild @@ -34,7 +34,7 @@ CloudStream Mit CloudStream abspielen Startseite - Suchen + Suche Downloads Einstellungen Suchen… @@ -44,8 +44,8 @@ Nächste Episode Genres Teilen - In Browser öffnen - Puffern überspringen + Im Browser öffnen + Laden überspringen Lädt… Am schauen Pausiert @@ -79,7 +79,7 @@ Datei abspielen Download fortsetzen Download pausieren - Automatische Fehlerberichterstattung deaktivieren + Automatische Fehlerberichtserstattung deaktivieren Mehr Infos Verstecken Abspielen @@ -106,8 +106,8 @@ Schriftgröße Suche anhand Anbietern Suche anhand Typen - %d Benenes an die Devs verteilt - Noch keine Benenes verteilt + %d Benenes an die Devs geschenkt + Noch keine Benenes verschenkt Sprache automatisch wählen Sprachen herunterladen Untertitelsprache @@ -117,8 +117,8 @@ Mehr Infos @string/home_play Damit dieser Anbieter korrekt funktioniert, ist möglicherweise ein VPN erforderlich - Dieser Anbieter bietet Torrents an, ein VPN wird dringend empfohlen - Metadaten werden nicht von der Website bereitgestellt, das Laden des Videos schlägt fehl, wenn sie auf der Website nicht vorhanden sind. + Dieser Anbieter bietet Torrents an, ein VPN wird deswegen dringend empfohlen + Metadaten werden nicht von der Website bereitgestellt, das Laden des Videos schlägt fehl, wenn sie nicht auf der Website vorhanden sind. Beschreibung Keine Handlung gefunden Keine Beschreibung gefunden @@ -143,7 +143,7 @@ Doppeltippen zum Pausieren Zeit für vor- und zurückspulen im Player (Sekunden) Zweimal auf die rechte oder linke Seite tippen, um vor- oder zurückzuspulen - Doppelt in die Mitte tippen, um zu pausieren + Zweimal in die Mitte tippen, um zu pausieren Systemhelligkeit verwenden Systemhelligkeit anstelle eines dunklen Overlay im Player verwenden Episodenfortschritt aktualisieren @@ -163,7 +163,7 @@ Füller-Episoden für Animes anzeigen Trailer anzeigen Vorschaubilder von Kitsu anzeigen - Ausgewählte Videoqualität bei Suchergebnissen ausblenden + Ausgewählte Videoqualität in den Suchergebnissen ausblenden Automatische Plugin-Updates App-Updates anzeigen Automatisches Suchen nach neuen Updates nach dem Start. @@ -172,11 +172,11 @@ Github Light Novel App von denselben Entwicklern Anime App von denselben Entwicklern - Discord beitreten - Eine Benene an die Devs verteilen - Verteilte Benenes + Trete dem Discord Server bei + Eine Benene an die Devs schenken + Geschenkte Benenes App-Sprache - Keine Verlinkung gefunden + Keine Links gefunden Link in die Zwischenablage kopiert Episode abspielen Auf Standardwert zurücksetzen @@ -240,7 +240,7 @@ Remote-Fehler Renderfehler Unerwarteter Playerfehler - Downloadfehler, Speicherberechtigungen prüfen + Downloadfehler, bitte überprüfen sie die Speicherberechtigungen Chromecast-Episode In %s wiedergeben In Browser wiedergeben @@ -255,7 +255,7 @@ Titel UI-Elemente auf Vorschaubild umschalten Kein Update gefunden - Auf Update prüfen + Auf Updates prüfen Sperren Skalieren Quelle @@ -270,16 +270,16 @@ Videopufferlänge Video-Cache in Speicher Video- und Bild-Cache leeren - Verursacht Abstürze, wenn zu hoch eingestellt. Nicht ändern, wenn wenig Arbeitsspeicher verfügbar ist, wie z.B. ein Android TV oder ein altes Telefon. - Kann auf Systemen mit geringem Speicherplatz, wie z. B. Android TV-Geräten, zu Problemen führen, wenn der Wert zu hoch eingestellt ist. + Verursacht Abstürze, wenn zu hoch eingestellt. Nicht ändern, wenn wenig Arbeitsspeicher verfügbar ist, wie z.B. auf einem Android TV oder auf einem alten Smartphone. + Kann auf Systemen mit geringem Speicherplatz, wie z. B. auf Android TV-Geräten, zu Problemen führen, wenn der Wert zu hoch eingestellt ist. DNS über HTTPS - Nützlich für die Umgehung von ISP-Sperren + Nützlich zur Umgehung von ISP-Sperren Website klonen Website entfernen Einen Klon einer bestehenden Website mit einer anderen URL hinzufügen Downloadpfad Nginx-Server-URL - Dubbed/Subbed Anime anzeigen (Synchronisiert/Untertitelt) + Dubbed/Subbed Anime anzeigen An Bildschirm anpassen Strecken Vergrößern @@ -308,7 +308,7 @@ 127.0.0.1 MeineCooleSeite example.com - Sprachcode (en) + Sprachencode (en) %s %s Account Ausloggen @@ -317,13 +317,13 @@ Account hinzufügen Account erstellen Synchronisation hinzufügen - Hinzugefügt %s + %s hinzugefügt Sync Bewertung %d / 10 /\?\? /%d - Authentifiziert %s + %s authentifiziert Die Authentifizierung bei %s ist fehlgeschlagen Keine Normal @@ -335,10 +335,10 @@ Schatten Erhöht Untertitel synchronisieren - 1000ms + 1000 ms Untertitelverzögerung - Verwenden, wenn die Untertitel %dms zu früh angezeigt werden - Verwenden, wenn die Untertitel %dms zu spät angezeigt werden + Verwenden, wenn die Untertitel %d ms zu früh angezeigt werden + Verwenden, wenn die Untertitel %d ms zu spät angezeigt werden Keine Untertitelverzögerung Vogel Quax zwickt Johnys Pferd Bim Empfohlen @@ -359,7 +359,7 @@ HD TS TC - BlueRay + Blue-ray WP DVD 4K @@ -408,7 +408,7 @@ Plugins Dadurch werden auch alle Repository-Plugins gelöscht Repository löschen - Lade eine Liste der Websiten herunter, welche du verwenden möchtest + Lade eine Liste der Websites herunter, welche du verwenden möchtest Heruntergeladen: %d Deaktiviert: %d Nicht heruntergeladen: %d @@ -416,7 +416,7 @@ \n \nAufgrund eines hirnlosen DMCA-Takedowns durch Sky UK Limited 🤮 können wir die Repository-Site nicht in der App verlinken. \n -\nTrete unserem Discord bei oder suche online. +\nTrete unserem Discord Server bei oder suche online. Community-Repositories anzeigen Öffentliche Liste Alle Untertitel in Großbuchstaben @@ -427,7 +427,7 @@ Videospuren Bei Neustart anwenden Abgesicherter Modus aktiviert - Alle Erweiterungen wurden aufgrund eines Absturzes deaktiviert, damit Sie diejenige finden können, die Probleme verursacht. + Alle Erweiterungen wurden aufgrund eines Absturzes deaktiviert, damit Sie diejenige finden können, welche Probleme verursacht. Absturzinfo ansehen Bewertung: %s Beschreibung @@ -460,7 +460,7 @@ Automatische Installation aller noch nicht installierten Plugins aus hinzugefügten Repositories. Einrichtungsvorgang wiederholen APK-Installer - Einige Telefone unterstützen den neuen Package-Installer nicht. Benutze die Legacy-Option, wenn sich die Updates nicht installieren lassen. + Einige Smartphones unterstützen den neuen Package-Installer nicht. Benutze die Legacy-Option, wenn sich die Updates nicht installieren lassen. %s %d%s Links App-Updates @@ -482,7 +482,7 @@ Nein App-Update wird heruntergeladen… App-Update wird installiert… - Konnte die neue Version der App nicht installieren + Die neue Version der App konnte nicht installieren werden Legacy PackageInstaller Aktualisierung gestartet @@ -493,18 +493,18 @@ Browser Sortieren nach Sortieren - Bewertung (gut bis schlecht) - Bewertung (schlecht bis gut) - Aktualisiert (neu bis alt) - Aktualisiert (alt bis neu) - Alphabetisch (A bis Z) - Alphabetisch (Z bis A) + Bewertung (gut zu schlecht) + Bewertung (schlecht zu gut) + Aktualisiert (neu zu alt) + Aktualisiert (alt zu neu) + Alphabetisch (A zu Z) + Alphabetisch (Z zu A) Bibliothek auswählen Öffnen mit Deine Bibliothek ist leer :( \nMelde dich mit einem Bibliothekskonto an oder füge Titel zu deiner lokalen Bibliothek hinzu. - Diese Liste ist leer. Versuche zu einer anderen Liste zu wechseln. - Datei für abgesicherten Modus gefunden! + Diese Liste ist leer. Versuch zu einer anderen Liste zu wechseln. + Datei für den abgesicherten Modus gefunden! \nBeim Start werden keine Erweiterungen geladen, bis die Datei entfernt wird. Player ausgeblendet - Betrag zum vor- und zurückspulen Der Betrag, welcher verwendet wird, wenn der Player eingeblendet ist @@ -549,7 +549,7 @@ Filtermodus für Plugin-Downloads auswählen Es wurde bereits abgestimmt Keine Plugins im Repository gefunden - Repository nicht gefunden, überprüfe die URL und probiere eine VPN - Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein schwerwiegender Fehler und sollte sofort gemeldet werden. %s + Repository nicht gefunden, überprüf die URL und versuch ein VPN + Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein SCHWERWIEGENDER FEHLER und sollte sofort gemeldet werden. %s Deaktivieren - + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 208e6140..2849b744 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -540,7 +540,7 @@ \n \nREMARQUE : Si la somme est de 10 ou plus, le joueur sautera automatiquement le chargement lorsque ce lien est chargé ! Aucun plugin trouvé dans ce dossier - Dossier non trouvé, vérifiez l\'url et essayé un VPN + Dépôt introuvable, vérifiez l\'URL et essayez avec un VPN Données mobiles Définir par défaut Utiliser @@ -552,4 +552,6 @@ Qualités L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s Sélectionnez le mode pour filtrer le téléchargement des plugins - + Fond de profil + \@string/default_subtitles + \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 35df36ac..a0bf44ca 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -566,4 +566,15 @@ Pozadina profila Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s Odaberi modus za filtriranje preuzimanja dodataka - + Onemogući + \@string/default_subtitles + U repozitoriju nisu pronađeni dodaci + Repozitorij nije pronađen, provjerite URL i pokušajte koristiti VPN + Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je video prioritet. +\n +\nIzvor A: 3 +\nKvaliteta B: 7 +\nImat će kombinirani prioritet videozapisa od 10. +\n +\nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! + \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 05a7f0a7..4c30caaf 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -219,7 +219,7 @@ Folyamatban levő Év Webhely - Szinopszis + Összegzés Nincsenek feliratok Távoli hiba Render hiba @@ -237,7 +237,7 @@ Csúsztassa felfelé vagy lefelé a bal vagy jobb oldalon a fényerő vagy a hangerő megváltoztatásához Biztonsági mentés 0 Banán a fejlesztőknek - Húzás a kereséshez + Húzd el, hogy beless Következő epizód automatikus lejátszása Következő epizód lejátszása amikor az aktuális epizód véget ér Dupla koppintás a kereséshez @@ -510,4 +510,5 @@ TV elrendezés Automatikus Az átugrás mértéke, amikor a lejátszó látható - + Válassza ki a módot a pluginek letöltésének szűréséhez + \ No newline at end of file diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 9b9385c2..e5044571 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -153,4 +153,9 @@ ଆପ୍ ଅଦ୍ୟତନ ଦେଖାଇବା ଅଦ୍ୟତନ ଆରମ୍ଭ ହୋଇଛି ସନ୍ଧାନ କରିବା… - + ସଂକ୍ଷିପ୍ତବୃତ୍ତି + ଚଳଚ୍ଚିତ୍ର ଚଲାଅ + %s ସନ୍ଧାନ କରିବା… + ପରବର୍ତ୍ତୀ ଅଧ୍ୟାୟ + କୌଣସି ତଥ୍ୟ ନାହିଁ + \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 1f288d2a..3db9accb 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -570,4 +570,5 @@ UI nu a putut fi creată corect, acesta este un BUG MAJOR și trebuie raportat imediat %s Selectați modul de filtrare a descărcării plugin-urilor @string/default_subtitles - + Ați votat deja + \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index affb04bf..41cfa846 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -6,12 +6,12 @@ முகப்பு தேடு பதிவிறக்கம் - தகவல் எதுவும் இல்லை + தரவு இல்லை மேலும் விருப்பங்கள் - அடுத்த எபிசோட் + அடுத்த அத்தியாயம் வகைகள் பகிர் - Browser இல் திற + உலாவியில் திற ஏற்றுவதைத் தவிர் பார்த்து கொண்டிருப்பது நிறுத்தி வைக்கப்பட்டுள்ளது @@ -21,9 +21,9 @@ ஸ்ட்ரீம் டோரண்ட் வசன வரிகள் பின் செல் - எபிசோடை இயக்கு + அத்தியாயத்தை இயக்கு எபிசோட் பதிவிற்கான அனுமதி கொடுக்கவும் - பதிவிறக்கம் செய்யப்பட்டது + பதிவிறக்கப்பட்டது பதிவிறக்குகிறது பதிவிறக்கம் இடைநிறுத்தப்பட்டது பதிவிறக்கம் தொடங்கியது @@ -67,10 +67,10 @@ ஏற்றுகிறது… கைவிடப்பட்டது பதிவிறக்கம் முடிந்தது - இணைப்பை மீண்டும் முயற்சிக்கவும்… + இணைப்பை மீண்டும் முயலவும்… திரைப்படத்தை இயக்கு லைவ்ஸ்ட்ரீம் இயக்கு - டிரெய்லரை இயக்கவும் + டிரெய்லரை இயக்கு மூலம் இணைப்புகளை ஏற்றுவதில் பிழை இயக்கு @@ -107,4 +107,14 @@ இடைநிறுத்துவதற்கு இருமுறை தட்டவும் Chromecast வசன அமைப்புகள் இருண்ட மேலடுக்குக்குப் பதிலாக ஆப் பிளேயரில் சிஸ்டம் பிரகாசத்தைப் பயன்படுத்தவும் - + அத்தியாயம் %d-இன் வெளியீட்டு நேரம் + %dம %dநி + %dநி + அடுத்து ஏதாவது + உலாவி + %d நிமி + CloudStream-உடன் இயக்கு + புதிய புதுப்பிப்பு உள்ளது +\n%s->%s + நிரப்பி + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 4866ecd4..8e0dd88e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -18,7 +18,7 @@ Попередній перегляд фону Швидкість (%.2fx) Знайдено нове оновлення! -\n%s -> %s +\n%s –> %s Пошук Завантаження %d хв @@ -37,7 +37,7 @@ Покинуто Переглянути фільм Переглянути трейлер - Трансляція через торрент + Трансляція через торент Повторити підключення… Назад Переглянути епізод @@ -75,7 +75,7 @@ Продовжити перегляд Вилучити Детальніше - Цей постачальник є торрентом, рекомендується VPN + Цей постачальник є торентом, рекомендується використовувати VPN Опис Сюжет не знайдено Опис не знайдено @@ -86,9 +86,9 @@ Субтитри Chromecast Налаштування субтитрів Chromecast Режим Eigengravy - Проведіть пальцем, щоб змінити налаштування + Проведіть, щоб змінити налаштування Проведіть вгору або вниз з лівого або правого боку, щоб змінити яскравість чи гучність - Відтворювати наступний епізод після закінчення поточного + Відтворює наступний епізод після закінчення поточного Головна CloudStream Філер @@ -130,7 +130,7 @@ Картинка в картинці Налаштування субтитрів плеєра Додає опцію керування швидкістю в плеєрі - Проведіть пальцем, щоб перемотати + Проведіть, щоб перемотати Двічі торкніться, щоб перемотати Двічі торкніться для паузи Крок перемотки (секунди) @@ -224,7 +224,7 @@ Двічі торкніться праворуч або ліворуч, щоб перемотати відео вперед або назад Використовуйте системну яскравість у плеєрі замість темної накладки Завантажено файл резервної копії - Торренти + Торенти Автоматична синхронізація прогресу поточного епізоду Відсутні дозволи на зберігання. Будь ласка, спробуйте ще раз. Показувати постери від Kitsu @@ -256,7 +256,7 @@ NSFW Фільм OVA - Торрент + Торент Мітка якості NSFW Переглянути в браузері @@ -294,9 +294,9 @@ Основний колір Тема застосунку Розташування назви постера - Розмістіть назву під постером + Розмістити назву під постером Пароль123 - Моє круте ім\'я + Моє круте ім’я hello@world.com Мій крутий сайт Код мови (uk) @@ -348,9 +348,9 @@ Роздільна здатність відеоплеєра Довжина буфера відео Очистити кеш відео та зображень - Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об\'ємом пам\'яті, наприклад Android TV. + Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об’ємом пам’яті, наприклад Android TV. Корисно для обходу блокувань провайдера - Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об\'ємом вільної пам\'яті, наприклад Android TV. + Спричиняє збої, якщо встановлено занадто високе значення на пристроях із малим об’ємом вільної пам’яті, наприклад Android TV. DNS через HTTPS Шлях завантаження Додайте клон існуючого сайту, з іншою URL-адресою @@ -374,10 +374,10 @@ Оцінений Завантажити з файлу Макс. - Щастям б\'єш жук їх глицю в фон й ґедзь пріч + Щастям б’єш жук їх глицю в фон й ґедзь пріч 1000 мс - Використовуйте цей параметр, якщо субтитри з\'являються на %d мс занадто рано - Використовуйте це, якщо субтитри з\'являються із запізненням на %d мс + Використовуйте цей параметр, якщо субтитри з’являються на %d мс занадто рано + Використовуйте це, якщо субтитри з’являються із запізненням на %d мс Завантажено %s Підтримка Фон @@ -507,8 +507,8 @@ Файл безпечного режиму знайдено! \nРозширеня не завантажуються під час запуску, доки файл не буде видалено. Android TV - Плеєр сховано - обсяг перемотки - Плеєр показано - обсяг перемотки + Плеєр сховано – обсяг перемотки + Плеєр показано – обсяг перемотки Обсяг перемотки, який використовується, коли плеєр видимий Обсяг перемотки, який використовується, коли плеєр прихований Тест провалено @@ -532,7 +532,7 @@ Встановити за замовчуванням Профілі Допомога - Тут ви можете змінити порядок джерел. Якщо відео має вищий пріоритет, воно з\'явиться вище у списку джерел. Сума пріоритету джерела та пріоритету якості є пріоритетом відео. + Тут ви можете змінити порядок джерел. Якщо відео має вищий пріоритет, воно з’явиться вище у списку джерел. Сума пріоритету джерела та пріоритету якості є пріоритетом відео. \n \nДжерело A: 3 \nЯкість B: 7 @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - + \ No newline at end of file diff --git a/fastlane/metadata/android/or/changelogs/2.txt b/fastlane/metadata/android/or/changelogs/2.txt new file mode 100644 index 00000000..e8b23e5f --- /dev/null +++ b/fastlane/metadata/android/or/changelogs/2.txt @@ -0,0 +1 @@ +- ପରିବର୍ତ୍ତନ ପୋଥି ଯୋଡ଼ାଗଲା! diff --git a/fastlane/metadata/android/uk/changelogs/2.txt b/fastlane/metadata/android/uk/changelogs/2.txt index 2c8d9f7e..97e84fa8 100644 --- a/fastlane/metadata/android/uk/changelogs/2.txt +++ b/fastlane/metadata/android/uk/changelogs/2.txt @@ -1 +1 @@ -- Додано журнал змін! +– Додано журнал змін! From 1629db2fc9c617d6bbe9617035213f3064a822ba Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Fri, 8 Sep 2023 08:01:34 +0000 Subject: [PATCH 087/441] chore(locales): fix locale issues --- app/src/main/res/values-ars/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 4 ++-- app/src/main/res/values-hr/strings.xml | 4 ++-- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-or/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 2 +- app/src/main/res/values-ta/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index 5135c97e..a1042b7e 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -265,4 +265,4 @@ صور في صور %d \nباقي - \ No newline at end of file + diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index daa352a7..016fbe43 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -567,4 +567,4 @@ Vídeo Android TV Wi-Fi - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 233e38e4..3efc4072 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -552,4 +552,4 @@ Repository nicht gefunden, überprüf die URL und versuch ein VPN Die Benutzeroberfläche konnte nicht korrekt erstellt werden. Dies ist ein SCHWERWIEGENDER FEHLER und sollte sofort gemeldet werden. %s Deaktivieren - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2849b744..63d03a6b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -553,5 +553,5 @@ L\'interface utilisateur n\'a pas pu être créée correctement. Il s\'agit d\'un bogue majeur qui doit être signalé immédiatement %s Sélectionnez le mode pour filtrer le téléchargement des plugins Fond de profil - \@string/default_subtitles - \ No newline at end of file + @string/default_subtitles + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index a0bf44ca..477ab92b 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -567,7 +567,7 @@ Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s Odaberi modus za filtriranje preuzimanja dodataka Onemogući - \@string/default_subtitles + @string/default_subtitles U repozitoriju nisu pronađeni dodaci Repozitorij nije pronađen, provjerite URL i pokušajte koristiti VPN Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je video prioritet. @@ -577,4 +577,4 @@ \nImat će kombinirani prioritet videozapisa od 10. \n \nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! - \ No newline at end of file + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 4c30caaf..677beaf8 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -511,4 +511,4 @@ Automatikus Az átugrás mértéke, amikor a lejátszó látható Válassza ki a módot a pluginek letöltésének szűréséhez - \ No newline at end of file + diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index e5044571..177f7ea1 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -158,4 +158,4 @@ %s ସନ୍ଧାନ କରିବା… ପରବର୍ତ୍ତୀ ଅଧ୍ୟାୟ କୌଣସି ତଥ୍ୟ ନାହିଁ - \ No newline at end of file + diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 3db9accb..b6971c37 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -571,4 +571,4 @@ Selectați modul de filtrare a descărcării plugin-urilor @string/default_subtitles Ați votat deja - \ No newline at end of file + diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 41cfa846..3f4134e5 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -117,4 +117,4 @@ புதிய புதுப்பிப்பு உள்ளது \n%s->%s நிரப்பி - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 8e0dd88e..f9dccfc4 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -553,4 +553,4 @@ Репозиторій не знайдено, перевірте URL-адресу та спробуйте VPN Не знайдено жодних плагінів у репозиторії Ви вже проголосували - \ No newline at end of file + From 130cc16e258a08c1a3a2ba75e5669d9ffee6d024 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Fri, 8 Sep 2023 22:13:04 +0000 Subject: [PATCH 088/441] Simkl API optimizations (#581) * Fix episode removal in simkl * Simkl API optimizations --- .../syncproviders/providers/SimklApi.kt | 570 ++++++++++++------ 1 file changed, 377 insertions(+), 193 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index b4a9d789..cd1df562 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -5,7 +5,9 @@ import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -13,6 +15,7 @@ import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mapper import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.logError @@ -33,6 +36,9 @@ import java.text.SimpleDateFormat import java.time.Instant import java.util.Date import java.util.TimeZone +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration class SimklApi(index: Int) : AccountManager(index), SyncAPI { override var name = "Simkl" @@ -59,6 +65,80 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { */ private var lastScoreTime = -1L + private object SimklCache { + private const val SIMKL_CACHE_KEY = "SIMKL_API_CACHE" + + enum class CacheTimes(val value: String) { + OneMonth("30d"), + ThirtyMinutes("30m") + } + + private class SimklCacheWrapper( + @JsonProperty("obj") val obj: T?, + @JsonProperty("validUntil") val validUntil: Long, + @JsonProperty("cacheTime") val cacheTime: Long = unixTime, + ) { + /** Returns true if cache is newer than cacheDays */ + fun isFresh(): Boolean { + return validUntil > unixTime + } + + fun remainingTime(): Duration { + val unixTime = unixTime + return if (validUntil > unixTime) { + (validUntil - unixTime).toDuration(DurationUnit.SECONDS) + } else { + Duration.ZERO + } + } + } + + fun cleanOldCache() { + getKeys(SIMKL_CACHE_KEY)?.forEach { + val isOld = AcraApplication.getKey>(it)?.isFresh() == false + if (isOld) { + removeKey(it) + } + } + } + + fun setKey(path: String, value: T, cacheTime: Duration) { + debugPrint { "Set cache: $SIMKL_CACHE_KEY/$path for ${cacheTime.inWholeDays} days or ${cacheTime.inWholeSeconds} seconds." } + setKey( + SIMKL_CACHE_KEY, + path, + // Storing as plain sting is required to make generics work. + SimklCacheWrapper(value, unixTime + cacheTime.inWholeSeconds).toJson() + ) + } + + /** + * Gets cached object, if object is not fresh returns null and removes it from cache + */ + inline fun getKey(path: String): T? { + // Required for generic otherwise "LinkedHashMap cannot be cast to MediaObject" + val type = mapper.typeFactory.constructParametricType( + SimklCacheWrapper::class.java, + T::class.java + ) + val cache = getKey(SIMKL_CACHE_KEY, path)?.let { + mapper.readValue>(it, type) + } + + return if (cache?.isFresh() == true) { + debugPrint { + "Cache hit at: $SIMKL_CACHE_KEY/$path. " + + "Remains fresh for ${cache.remainingTime().inWholeDays} days or ${cache.remainingTime().inWholeSeconds} seconds." + } + cache.obj + } else { + debugPrint { "Cache miss at: $SIMKL_CACHE_KEY/$path" } + removeKey(SIMKL_CACHE_KEY, path) + null + } + } + } + companion object { private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET @@ -210,18 +290,18 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("img") val img: String? ) { companion object { - fun convertToEpisodes(list: List?): List { + fun convertToEpisodes(list: List?): List? { return list?.map { MediaObject.Season.Episode(it.episode) - } ?: emptyList() + } } - fun convertToSeasons(list: List?): List { + fun convertToSeasons(list: List?): List? { return list?.filter { it.season != null }?.groupBy { it.season - }?.map { (season, episodes) -> - MediaObject.Season(season!!, convertToEpisodes(episodes)) - } ?: emptyList() + }?.mapNotNull { (season, episodes) -> + convertToEpisodes(episodes)?.let { MediaObject.Season(season!!, it) } + }?.ifEmpty { null } } } } @@ -235,11 +315,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("title") val title: String?, @JsonProperty("year") val year: Int?, @JsonProperty("ids") val ids: Ids?, + @JsonProperty("total_episodes") val total_episodes: Int? = null, + @JsonProperty("status") val status: String? = null, @JsonProperty("poster") val poster: String? = null, @JsonProperty("type") val type: String? = null, @JsonProperty("seasons") val seasons: List? = null, @JsonProperty("episodes") val episodes: List? = null ) { + fun hasEnded(): Boolean { + return status == "released" || status == "ended" + } + @JsonInclude(JsonInclude.Include.NON_EMPTY) data class Season( @JsonProperty("number") val number: Int, @@ -281,6 +367,194 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } } + class SimklScoreBuilder private constructor() { + data class Builder( + private var url: String? = null, + private var interceptor: Interceptor? = null, + private var ids: MediaObject.Ids? = null, + private var score: Int? = null, + private var status: Int? = null, + private var addEpisodes: Pair?, List?>? = null, + private var removeEpisodes: Pair?, List?>? = null, + ) { + fun interceptor(interceptor: Interceptor) = apply { this.interceptor = interceptor } + fun apiUrl(url: String) = apply { this.url = url } + fun ids(ids: MediaObject.Ids) = apply { this.ids = ids } + fun score(score: Int?, oldScore: Int?) = apply { + if (score != oldScore) { + this.score = score + } + } + + fun status(newStatus: Int?, oldStatus: Int?) = apply { + // Only set status if its new + if (newStatus != oldStatus) { + this.status = newStatus + } else { + this.status = null + } + } + + fun episodes( + allEpisodes: List?, + newEpisodes: Int?, + oldEpisodes: Int?, + ) = apply { + if (allEpisodes == null || newEpisodes == null) return@apply + + fun getEpisodes(rawEpisodes: List) = + if (rawEpisodes.any { it.season != null }) { + EpisodeMetadata.convertToSeasons(rawEpisodes) to null + } else { + null to EpisodeMetadata.convertToEpisodes(rawEpisodes) + } + + // Do not add episodes if there is no change + if (newEpisodes > (oldEpisodes ?: 0)) { + this.addEpisodes = getEpisodes(allEpisodes.take(newEpisodes)) + } + if ((oldEpisodes ?: 0) > newEpisodes) { + this.removeEpisodes = getEpisodes(allEpisodes.drop(newEpisodes)) + } + } + + suspend fun execute(): Boolean { + val time = getDateTime(unixTime) + + return if (this.status == SimklListStatusType.None.value) { + app.post( + "$url/sync/history/remove", + json = StatusRequest( + shows = listOf(HistoryMediaObject(ids = ids)), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } else { + val episodeRemovalResponse = removeEpisodes?.let { (seasons, episodes) -> + app.post( + "${this.url}/sync/history/remove", + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + ids = ids, + seasons = seasons, + episodes = episodes + ) + ), + movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } ?: true + + val historyResponse = + // Only post if there are episodes or score to upload + if (addEpisodes != null || score != null) { + app.post( + "${this.url}/sync/history", + json = StatusRequest( + shows = listOf( + HistoryMediaObject( + null, + null, + ids, + addEpisodes?.first, + addEpisodes?.second, + score, + score?.let { time }, + ) + ), movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } else { + true + } + + val statusResponse = status?.let { setStatus -> + val newStatus = + SimklListStatusType.values() + .firstOrNull { it.value == setStatus }?.originalName + ?: SimklListStatusType.Watching.originalName!! + + app.post( + "${this.url}/sync/add-to-list", + json = StatusRequest( + shows = listOf( + StatusMediaObject( + null, + null, + ids, + newStatus, + ) + ), movies = emptyList() + ), + interceptor = interceptor + ).isSuccessful + } ?: true + + statusResponse && episodeRemovalResponse && historyResponse + } + } + } + } + + suspend fun getEpisodes( + simklId: Int?, + type: String?, + episodes: Int?, + hasEnded: Boolean? + ): Array? { + if (simklId == null) return null + + val cacheKey = "Episodes/$simklId" + val cache = SimklCache.getKey>(cacheKey) + + // Return cached result if its higher or equal the amount of episodes. + if (cache != null && cache.size >= (episodes ?: 0)) { + return cache + } + + // There is always one season in Anime -> no request necessary + if (type == "anime" && episodes != null) { + return episodes.takeIf { it > 0 }?.let { + (1..it).map { episode -> + EpisodeMetadata( + null, null, null, episode, null + ) + }.toTypedArray() + } + } + val url = when (type) { + "anime" -> "https://api.simkl.com/anime/episodes/$simklId" + "tv" -> "https://api.simkl.com/tv/episodes/$simklId" + "movie" -> return null + else -> return null + } + + debugPrint { "Requesting episodes from $url" } + return app.get(url, params = mapOf("client_id" to clientId)) + .parsedSafe>()?.also { + val cacheTime = + if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value + + // 1 Month cache + SimklCache.setKey(cacheKey, it, Duration.parse(cacheTime)) + } + } + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + class HistoryMediaObject( + @JsonProperty("title") title: String? = null, + @JsonProperty("year") year: Int? = null, + @JsonProperty("ids") ids: Ids? = null, + @JsonProperty("seasons") seasons: List? = null, + @JsonProperty("episodes") episodes: List? = null, + @JsonProperty("rating") val rating: Int? = null, + @JsonProperty("rated_at") val rated_at: String? = null, + ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) + @JsonInclude(JsonInclude.Include.NON_EMPTY) class RatingMediaObject( @JsonProperty("title") title: String?, @@ -299,15 +573,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime) ) : MediaObject(title, year, ids) - @JsonInclude(JsonInclude.Include.NON_EMPTY) - class HistoryMediaObject( - @JsonProperty("title") title: String?, - @JsonProperty("year") year: Int?, - @JsonProperty("ids") ids: Ids?, - @JsonProperty("seasons") seasons: List?, - @JsonProperty("episodes") episodes: List?, - ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) - @JsonInclude(JsonInclude.Include.NON_EMPTY) data class StatusRequest( @JsonProperty("movies") val movies: List, @@ -404,13 +669,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } data class ShowMetadata( - override val last_watched_at: String?, - override val status: String, - override val user_rating: Int?, - override val last_watched: String?, - override val watched_episodes_count: Int?, - override val total_episodes_count: Int?, - val show: Show + @JsonProperty("last_watched_at") override val last_watched_at: String?, + @JsonProperty("status") override val status: String, + @JsonProperty("user_rating") override val user_rating: Int?, + @JsonProperty("last_watched") override val last_watched: String?, + @JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?, + @JsonProperty("total_episodes_count") override val total_episodes_count: Int?, + @JsonProperty("show") val show: Show ) : Metadata { override fun getIds(): Show.Ids { return this.show.ids @@ -435,23 +700,23 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } data class Show( - val title: String, - val poster: String?, - val year: Int?, - val ids: Ids, + @JsonProperty("title") val title: String, + @JsonProperty("poster") val poster: String?, + @JsonProperty("year") val year: Int?, + @JsonProperty("ids") val ids: Ids, ) { data class Ids( - val simkl: Int, - val slug: String?, - val imdb: String?, - val zap2it: String?, - val tmdb: String?, - val offen: String?, - val tvdb: String?, - val mal: String?, - val anidb: String?, - val anilist: String?, - val traktslug: String? + @JsonProperty("simkl") val simkl: Int, + @JsonProperty("slug") val slug: String?, + @JsonProperty("imdb") val imdb: String?, + @JsonProperty("zap2it") val zap2it: String?, + @JsonProperty("tmdb") val tmdb: String?, + @JsonProperty("offen") val offen: String?, + @JsonProperty("tvdb") val tvdb: String?, + @JsonProperty("mal") val mal: String?, + @JsonProperty("anidb") val anidb: String?, + @JsonProperty("anilist") val anilist: String?, + @JsonProperty("traktslug") val traktslug: String? ) { fun matchesId(database: SyncServices, id: String): Boolean { return when (database) { @@ -491,20 +756,58 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } } + /** + * Useful to get episodes on demand to prevent unnecessary requests. + */ + class SimklEpisodeConstructor( + private val simklId: Int?, + private val type: String?, + private val totalEpisodeCount: Int?, + private val hasEnded: Boolean? + ) { + suspend fun getEpisodes(): Array? { + return getEpisodes(simklId, type, totalEpisodeCount, hasEnded) + } + } + class SimklSyncStatus( override var status: Int, override var score: Int?, + val oldScore: Int?, override var watchedEpisodes: Int?, - val episodes: Array?, + val episodeConstructor: SimklEpisodeConstructor, override var isFavorite: Boolean? = null, override var maxEpisodes: Int? = null, /** Save seen episodes separately to know the change from old to new. * Required to remove seen episodes if count decreases */ val oldEpisodes: Int, + val oldStatus: String? ) : SyncAPI.AbstractSyncStatus() override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { val realIds = readIdFromString(id) + + // Key which assumes all ids are the same each time :/ + // This could be some sort of reference system to make multiple IDs + // point to the same key. + val idKey = + realIds.toList().map { "${it.first.originalName}=${it.second}" }.sorted().joinToString() + + val cachedObject = SimklCache.getKey(idKey) + val searchResult: MediaObject = cachedObject + ?: (searchByIds(realIds)?.firstOrNull()?.also { result -> + val cacheTime = + if (result.hasEnded()) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value + SimklCache.setKey(idKey, result, Duration.parse(cacheTime)) + }) ?: return null + + val episodeConstructor = SimklEpisodeConstructor( + searchResult.ids?.simkl, + searchResult.type, + searchResult.total_episodes, + searchResult.hasEnded() + ) + val foundItem = getSyncListSmart()?.let { list -> listOf(list.shows, list.anime, list.movies).flatten().firstOrNull { show -> realIds.any { (database, id) -> @@ -513,172 +816,63 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } } - // Search to get episodes - val searchResult = searchByIds(realIds)?.firstOrNull() - val episodes = getEpisodes(searchResult?.ids?.simkl, searchResult?.type) - if (foundItem != null) { return SimklSyncStatus( status = foundItem.status?.let { SimklListStatusType.fromString(it)?.value } ?: return null, score = foundItem.user_rating, watchedEpisodes = foundItem.watched_episodes_count, - maxEpisodes = foundItem.total_episodes_count, - episodes = episodes, + maxEpisodes = searchResult.total_episodes, + episodeConstructor = episodeConstructor, oldEpisodes = foundItem.watched_episodes_count ?: 0, + oldScore = foundItem.user_rating, + oldStatus = foundItem.status ) } else { - return if (searchResult != null) { - SimklSyncStatus( - status = SimklListStatusType.None.value, - score = 0, - watchedEpisodes = 0, - maxEpisodes = if (searchResult.type == "movie") 0 else null, - episodes = episodes, - oldEpisodes = 0, - ) - } else { - null - } + return SimklSyncStatus( + status = SimklListStatusType.None.value, + score = 0, + watchedEpisodes = 0, + maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes, + episodeConstructor = episodeConstructor, + oldEpisodes = 0, + oldStatus = null, + oldScore = null + ) } } override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean { val parsedId = readIdFromString(id) lastScoreTime = unixTime - - if (status.status == SimklListStatusType.None.value) { - return app.post( - "$mainUrl/sync/history/remove", - json = StatusRequest( - shows = listOf( - HistoryMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - emptyList(), - emptyList() - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ).isSuccessful - } - - val realScore = status.score - val ratingResponseSuccess = if (realScore != null) { - // Remove rating if score is 0 - val ratingsSuffix = if (realScore == 0) "/remove" else "" - debugPrint { "Rate ${this.name} item: rating=$realScore" } - app.post( - "$mainUrl/sync/ratings$ratingsSuffix", - json = StatusRequest( - // Not possible to know if TV or Movie - shows = listOf( - RatingMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - realScore - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ).isSuccessful - } else { - true - } - val simklStatus = status as? SimklSyncStatus + + val builder = SimklScoreBuilder.Builder() + .apiUrl(this.mainUrl) + .score(status.score, simklStatus?.oldScore) + .status(status.status, (status as? SimklSyncStatus)?.oldStatus?.let { oldStatus -> + SimklListStatusType.values().firstOrNull { + it.originalName == oldStatus + }?.value + }) + .interceptor(interceptor) + .ids(MediaObject.Ids.fromMap(parsedId)) + + + // Get episodes only when required + val episodes = simklStatus?.episodeConstructor?.getEpisodes() + // All episodes if marked as completed val watchedEpisodes = if (status.status == SimklListStatusType.Completed.value) { - simklStatus?.episodes?.size + episodes?.size } else { status.watchedEpisodes } - // Only post episodes if available episodes and the status is correct - val episodeResponseSuccess = - if (simklStatus != null && watchedEpisodes != null && !simklStatus.episodes.isNullOrEmpty() && listOf( - SimklListStatusType.Paused.value, - SimklListStatusType.Dropped.value, - SimklListStatusType.Watching.value, - SimklListStatusType.Completed.value, - SimklListStatusType.ReWatching.value - ).contains(status.status) - ) { - suspend fun postEpisodes( - url: String, - rawEpisodes: List - ): Boolean { - val (seasons, episodes) = if (rawEpisodes.any { it.season != null }) { - EpisodeMetadata.convertToSeasons(rawEpisodes) to null - } else { - null to EpisodeMetadata.convertToEpisodes(rawEpisodes) - } - debugPrint { "Synced history using $url: seasons=${seasons?.toList()}, episodes=${episodes?.toList()}" } - return app.post( - url, - json = StatusRequest( - shows = listOf( - HistoryMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - seasons, - episodes - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ).isSuccessful - } + builder.episodes(episodes?.toList(), watchedEpisodes, simklStatus?.oldEpisodes) - // If episodes decrease: remove all episodes beyond watched episodes. - val removeResponse = if (simklStatus.oldEpisodes > watchedEpisodes) { - val removeEpisodes = simklStatus.episodes - .drop(watchedEpisodes) - postEpisodes("$mainUrl/sync/history/remove", removeEpisodes) - } else { - true - } - val cutEpisodes = simklStatus.episodes.take(watchedEpisodes) - val addResponse = postEpisodes("$mainUrl/sync/history/", cutEpisodes) - - removeResponse && addResponse - } else true - - val newStatus = - SimklListStatusType.values().firstOrNull { it.value == status.status }?.originalName - ?: SimklListStatusType.Watching.originalName - - val statusResponseSuccess = if (newStatus != null) { - debugPrint { "Add to ${this.name} list: status=$newStatus" } - app.post( - "$mainUrl/sync/add-to-list", - json = StatusRequest( - shows = listOf( - StatusMediaObject( - null, - null, - MediaObject.Ids.fromMap(parsedId), - newStatus - ) - ), - movies = emptyList() - ), - interceptor = interceptor - ).isSuccessful - } else { - true - } - - debugPrint { "All scoring complete: rating=$ratingResponseSuccess, status=$statusResponseSuccess, episode=$episodeResponseSuccess" } requireLibraryRefresh = true - return ratingResponseSuccess && statusResponseSuccess && episodeResponseSuccess + return builder.execute() } @@ -694,17 +888,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ).parsedSafe() } - suspend fun getEpisodes(simklId: Int?, type: String?): Array? { - if (simklId == null) return null - val url = when (type) { - "anime" -> "https://api.simkl.com/anime/episodes/$simklId" - "tv" -> "https://api.simkl.com/tv/episodes/$simklId" - "movie" -> return null - else -> return null - } - return app.get(url, params = mapOf("client_id" to clientId)).parsedSafe() - } - override suspend fun search(name: String): List? { return app.get( "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name) @@ -737,16 +920,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { return null } - private suspend fun getSyncListSince(since: Long?): AllItemsResponse { + private suspend fun getSyncListSince(since: Long?): AllItemsResponse? { val params = getDateTime(since)?.let { mapOf("date_from" to it) } ?: emptyMap() + // Can return null on no change. return app.get( "$mainUrl/sync/all-items/", params = params, interceptor = interceptor - ).parsed() + ).parsedSafe() } private suspend fun getActivities(): ActivitiesResponse? { From b6e99d7358ee5d13127be79869fa162a505f3a09 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 9 Sep 2023 23:18:21 +0200 Subject: [PATCH 089/441] backend change for events in player --- .../ui/player/AbstractPlayerFragment.kt | 95 ++++++-- .../cloudstream3/ui/player/CS3IPlayer.kt | 215 +++++++----------- .../ui/player/FullScreenPlayer.kt | 4 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 11 +- .../cloudstream3/ui/player/IPlayer.kt | 136 +++++++++-- .../ui/result/ResultFragmentPhone.kt | 4 +- .../ui/result/ResultTrailerPlayer.kt | 10 +- 7 files changed, 289 insertions(+), 186 deletions(-) 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 53ee5e12..c6f02f1a 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 @@ -92,11 +92,11 @@ abstract class AbstractPlayerFragment( throw NotImplementedError() } - open fun playerPositionChanged(posDur: Pair) { + open fun playerPositionChanged(position: Long, duration : Long) { throw NotImplementedError() } - open fun playerDimensionsLoaded(widthHeight: Pair) { + open fun playerDimensionsLoaded(width: Int, height : Int) { throw NotImplementedError() } @@ -132,8 +132,8 @@ abstract class AbstractPlayerFragment( } } - private fun updateIsPlaying(playing: Pair) { - val (wasPlaying, isPlaying) = playing + private fun updateIsPlaying(wasPlaying : CSPlayerLoading, + isPlaying : CSPlayerLoading) { val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying @@ -206,7 +206,7 @@ abstract class AbstractPlayerFragment( CSPlayerEvent.values()[intent.getIntExtra( EXTRA_CONTROL_TYPE, 0 - )] + )], source = PlayerEventSource.UI ) } } @@ -216,7 +216,7 @@ abstract class AbstractPlayerFragment( val isPlaying = player.getIsPlaying() val isPlayingValue = if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused - updateIsPlaying(Pair(isPlayingValue, isPlayingValue)) + updateIsPlaying(isPlayingValue, isPlayingValue) } else { // Restore the full-screen UI. piphide?.isVisible = true @@ -249,7 +249,7 @@ abstract class AbstractPlayerFragment( } } - open fun playerError(exception: Exception) { + open fun playerError(exception: Throwable) { fun showToast(message: String, gotoNext: Boolean = false) { if (gotoNext && hasNextMirror()) { showToast( @@ -326,6 +326,7 @@ abstract class AbstractPlayerFragment( } } + @SuppressLint("UnsafeOptInUsageError") private fun playerUpdated(player: Any?) { if (player is ExoPlayer) { context?.let { ctx -> @@ -366,6 +367,71 @@ abstract class AbstractPlayerFragment( // } //} + /** This receives the events from the player, if you want to append functionality you do it here, + * do note that this only receives events for UI changes, + * and returning early WONT stop it from changing in eg the player time or pause status */ + open fun mainCallback(event : PlayerEvent) { + when(event) { + is ResizedEvent -> { + playerDimensionsLoaded(event.width, event.height) + } + is PlayerAttachedEvent -> { + playerUpdated(event.player) + } + is SubtitlesUpdatedEvent -> { + subtitlesChanged() + } + is TimestampSkippedEvent -> { + onTimestampSkipped(event.timestamp) + } + is TimestampInvokedEvent -> { + onTimestamp(event.timestamp) + } + is TracksChangedEvent -> { + onTracksInfoChanged() + } + is EmbeddedSubtitlesFetchedEvent -> { + embeddedSubtitlesFetched(event.tracks) + } + is ErrorEvent -> { + playerError(event.error) + } + is RequestAudioFocusEvent -> { + requestAudioFocus() + } + is EpisodeSeekEvent -> { + when(event.offset) { + -1 -> prevEpisode() + 1 -> nextEpisode() + else -> {} + } + } + is StatusEvent -> { + updateIsPlaying(wasPlaying = event.wasPlaying, isPlaying = event.isPlaying) + } + is PositionEvent -> { + playerPositionChanged(position = event.toMs, duration = event.durationMs) + } + is VideoEndedEvent -> { + context?.let { ctx -> + // Only play next episode if autoplay is on (default) + if (PreferenceManager.getDefaultSharedPreferences(ctx) + ?.getBoolean( + ctx.getString(R.string.autoplay_next_key), + true + ) == true + ) { + player.handleEvent( + CSPlayerEvent.NextEpisode, + source = PlayerEventSource.Player + ) + } + } + } + is PauseEvent -> Unit + is PlayEvent -> Unit + } + } @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -374,25 +440,13 @@ abstract class AbstractPlayerFragment( player.releaseCallbacks() player.initCallbacks( - playerUpdated = ::playerUpdated, - updateIsPlaying = ::updateIsPlaying, - playerError = ::playerError, - requestAutoFocus = ::requestAudioFocus, - nextEpisode = ::nextEpisode, - prevEpisode = ::prevEpisode, - playerPositionChanged = ::playerPositionChanged, - playerDimensionsLoaded = ::playerDimensionsLoaded, + eventHandler = ::mainCallback, requestedListeningPercentages = listOf( SKIP_OP_VIDEO_PERCENTAGE, PRELOAD_NEXT_EPISODE_PERCENTAGE, NEXT_WATCH_EPISODE_PERCENTAGE, UPDATE_SYNC_PROGRESS_PERCENTAGE, ), - subtitlesUpdates = ::subtitlesChanged, - embeddedSubtitlesFetched = ::embeddedSubtitlesFetched, - onTracksInfoChanged = ::onTracksInfoChanged, - onTimestampInvoked = ::onTimestamp, - onTimestampSkipped = ::onTimestampSkipped ) if (player is CS3IPlayer) { @@ -461,6 +515,7 @@ abstract class AbstractPlayerFragment( resize(PlayerResize.values()[resize], showToast) } + @SuppressLint("UnsafeOptInUsageError") fun resize(resize: PlayerResize, showToast: Boolean) { setKey(RESIZE_MODE_KEY, resize.ordinal) val type = when (resize) { 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 4a88a2e7..fe4e3423 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 @@ -8,7 +8,6 @@ import android.os.Looper import android.util.Log import android.util.Rational import android.widget.FrameLayout -import androidx.media3.common.C import androidx.media3.common.C.* import androidx.media3.common.Format import androidx.media3.common.MediaItem @@ -135,80 +134,24 @@ class CS3IPlayer : IPlayer { * Boolean = if it's active * */ private var playerSelectedSubtitleTracks = listOf>() - - /** isPlaying */ - private var updateIsPlaying: ((Pair) -> Unit)? = null - private var requestAutoFocus: (() -> Unit)? = null - private var playerError: ((Exception) -> Unit)? = null - private var subtitlesUpdates: (() -> Unit)? = null - - /** width x height */ - private var playerDimensionsLoaded: ((Pair) -> Unit)? = null - - /** used for playerPositionChanged */ private var requestedListeningPercentages: List? = null - /** Fired when seeking the player or on requestedListeningPercentages, - * used to make things appear on que - * position, duration */ - private var playerPositionChanged: ((Pair) -> Unit)? = null + private var eventHandler: ((PlayerEvent) -> Unit)? = null - private var nextEpisode: (() -> Unit)? = null - private var prevEpisode: (() -> Unit)? = null - - private var playerUpdated: ((Any?) -> Unit)? = null - private var embeddedSubtitlesFetched: ((List) -> Unit)? = null - private var onTracksInfoChanged: (() -> Unit)? = null - private var onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)? = null - private var onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)? = null + fun event(event: PlayerEvent) { + eventHandler?.invoke(event) + } override fun releaseCallbacks() { - playerUpdated = null - updateIsPlaying = null - requestAutoFocus = null - playerError = null - playerDimensionsLoaded = null - requestedListeningPercentages = null - playerPositionChanged = null - nextEpisode = null - prevEpisode = null - subtitlesUpdates = null - onTracksInfoChanged = null - onTimestampInvoked = null - requestSubtitleUpdate = null - onTimestampSkipped = null + eventHandler = null } override fun initCallbacks( - playerUpdated: (Any?) -> Unit, - updateIsPlaying: ((Pair) -> Unit)?, - requestAutoFocus: (() -> Unit)?, - playerError: ((Exception) -> Unit)?, - playerDimensionsLoaded: ((Pair) -> Unit)?, + eventHandler: ((PlayerEvent) -> Unit), requestedListeningPercentages: List?, - playerPositionChanged: ((Pair) -> Unit)?, - nextEpisode: (() -> Unit)?, - prevEpisode: (() -> Unit)?, - subtitlesUpdates: (() -> Unit)?, - embeddedSubtitlesFetched: ((List) -> Unit)?, - onTracksInfoChanged: (() -> Unit)?, - onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)?, - onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)?, ) { - this.playerUpdated = playerUpdated - this.updateIsPlaying = updateIsPlaying - this.requestAutoFocus = requestAutoFocus - this.playerError = playerError - this.playerDimensionsLoaded = playerDimensionsLoaded this.requestedListeningPercentages = requestedListeningPercentages - this.playerPositionChanged = playerPositionChanged - this.nextEpisode = nextEpisode - this.prevEpisode = prevEpisode - this.subtitlesUpdates = subtitlesUpdates - this.embeddedSubtitlesFetched = embeddedSubtitlesFetched - this.onTracksInfoChanged = onTracksInfoChanged - this.onTimestampInvoked = onTimestampInvoked - this.onTimestampSkipped = onTimestampSkipped + this.eventHandler = eventHandler } // I know, this is not a perfect solution, however it works for fixing subs @@ -217,7 +160,7 @@ class CS3IPlayer : IPlayer { try { Handler(it).post { try { - seekTime(1L) + seekTime(1L, source = PlayerEventSource.Player) } catch (e: Exception) { logError(e) } @@ -271,8 +214,9 @@ class CS3IPlayer : IPlayer { subtitleHelper.setAllSubtitles(subtitles) } - var currentSubtitles: SubtitleData? = null + private var currentSubtitles: SubtitleData? = null + @SuppressLint("UnsafeOptInUsageError") private fun List.getTrack(id: String?): Pair? { if (id == null) return null // This beast of an expression does: @@ -526,14 +470,14 @@ class CS3IPlayer : IPlayer { Log.i(TAG, "onStop") saveData() - exoPlayer?.pause() + handleEvent(CSPlayerEvent.Pause, PlayerEventSource.Player) //releasePlayer() } override fun onPause() { Log.i(TAG, "onPause") saveData() - exoPlayer?.pause() + handleEvent(CSPlayerEvent.Pause, PlayerEventSource.Player) //releasePlayer() } @@ -611,6 +555,7 @@ class CS3IPlayer : IPlayer { } } + @SuppressLint("UnsafeOptInUsageError") private fun Context.createOfflineSource(): DataSource.Factory { return DefaultDataSourceFactory(this, USER_AGENT) } @@ -846,43 +791,55 @@ class CS3IPlayer : IPlayer { return null } - fun updatedTime(writePosition: Long? = null) { + fun updatedTime( + writePosition: Long? = null, + source: PlayerEventSource = PlayerEventSource.Player + ) { val position = writePosition ?: exoPlayer?.currentPosition getCurrentTimestamp(position)?.let { timestamp -> - onTimestampInvoked?.invoke(timestamp) + event(TimestampInvokedEvent(timestamp, source)) } val duration = exoPlayer?.contentDuration if (duration != null && position != null) { - playerPositionChanged?.invoke(Pair(position, duration)) + event( + PositionEvent( + source, + fromMs = exoPlayer?.currentPosition ?: 0, + position, + duration + ) + ) } } - override fun seekTime(time: Long) { - exoPlayer?.seekTime(time) + override fun seekTime(time: Long, source: PlayerEventSource) { + exoPlayer?.seekTime(time, source) } - override fun seekTo(time: Long) { - updatedTime(time) + override fun seekTo(time: Long, source: PlayerEventSource) { + updatedTime(time, source) exoPlayer?.seekTo(time) } - private fun ExoPlayer.seekTime(time: Long) { - updatedTime(currentPosition + time) + private fun ExoPlayer.seekTime(time: Long, source: PlayerEventSource) { + updatedTime(currentPosition + time, source) seekTo(currentPosition + time) } - override fun handleEvent(event: CSPlayerEvent) { + override fun handleEvent(event: CSPlayerEvent, source: PlayerEventSource) { Log.i(TAG, "handleEvent ${event.name}") try { exoPlayer?.apply { when (event) { CSPlayerEvent.Play -> { + event(PlayEvent(source)) play() } CSPlayerEvent.Pause -> { + event(PauseEvent(source)) pause() } @@ -899,32 +856,32 @@ class CS3IPlayer : IPlayer { CSPlayerEvent.PlayPauseToggle -> { if (isPlaying) { - pause() + handleEvent(CSPlayerEvent.Pause, source) } else { - play() + handleEvent(CSPlayerEvent.Play, source) } } - CSPlayerEvent.SeekForward -> seekTime(seekActionTime) - CSPlayerEvent.SeekBack -> seekTime(-seekActionTime) - CSPlayerEvent.NextEpisode -> nextEpisode?.invoke() - CSPlayerEvent.PrevEpisode -> prevEpisode?.invoke() + CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source) + CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source) + CSPlayerEvent.NextEpisode -> event(EpisodeSeekEvent(offset = 1, source = source)) + CSPlayerEvent.PrevEpisode -> event(EpisodeSeekEvent(offset = -1, source = source)) CSPlayerEvent.SkipCurrentChapter -> { //val dur = this@CS3IPlayer.getDuration() ?: return@apply getCurrentTimestamp()?.let { lastTimeStamp -> if (lastTimeStamp.skipToNextEpisode) { - handleEvent(CSPlayerEvent.NextEpisode) + handleEvent(CSPlayerEvent.NextEpisode, source) } else { seekTo(lastTimeStamp.endMs + 1L) } - onTimestampSkipped?.invoke(lastTimeStamp) + event(TimestampSkippedEvent(timestamp = lastTimeStamp, source = source)) } } } } - } catch (e: Exception) { - Log.e(TAG, "handleEvent error", e) - playerError?.invoke(e) + } catch (t: Throwable) { + Log.e(TAG, "handleEvent error", t) + event(ErrorEvent(t)) } } @@ -963,18 +920,14 @@ class CS3IPlayer : IPlayer { requestSubtitleUpdate = ::reloadSubs - playerUpdated?.invoke(exoPlayer) + event(PlayerAttachedEvent(exoPlayer)) exoPlayer?.prepare() exoPlayer?.let { exo -> - updateIsPlaying?.invoke( - Pair( - CSPlayerLoading.IsBuffering, - CSPlayerLoading.IsBuffering - ) - ) + event(StatusEvent(CSPlayerLoading.IsBuffering, CSPlayerLoading.IsBuffering)) isPlaying = exo.isPlaying } + exoPlayer?.addListener(object : Player.Listener { override fun onTracksChanged(tracks: Tracks) { normalSafeApiCall { @@ -1008,18 +961,19 @@ class CS3IPlayer : IPlayer { ) } - embeddedSubtitlesFetched?.invoke(exoPlayerReportedTracks) - onTracksInfoChanged?.invoke() - subtitlesUpdates?.invoke() + event(EmbeddedSubtitlesFetchedEvent(tracks = exoPlayerReportedTracks)) + event(TracksChangedEvent()) + event(SubtitlesUpdatedEvent()) } } + @SuppressLint("UnsafeOptInUsageError") override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { exoPlayer?.let { exo -> - updateIsPlaying?.invoke( - Pair( - if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused, - if (playbackState == Player.STATE_BUFFERING) CSPlayerLoading.IsBuffering else if (exo.isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused + event( + StatusEvent( + wasPlaying = if (isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused, + isPlaying = if (playbackState == Player.STATE_BUFFERING) CSPlayerLoading.IsBuffering else if (exo.isPlaying) CSPlayerLoading.IsPlaying else CSPlayerLoading.IsPaused ) ) isPlaying = exo.isPlaying @@ -1041,23 +995,15 @@ class CS3IPlayer : IPlayer { } Player.STATE_ENDED -> { - // Only play next episode if autoplay is on (default) - if (PreferenceManager.getDefaultSharedPreferences(context) - ?.getBoolean( - context.getString(com.lagradost.cloudstream3.R.string.autoplay_next_key), - true - ) == true - ) { - handleEvent(CSPlayerEvent.NextEpisode) - } + event(VideoEndedEvent()) } Player.STATE_BUFFERING -> { - updatedTime() + updatedTime(source = PlayerEventSource.Player) } Player.STATE_IDLE -> { - // IDLE + } else -> Unit @@ -1082,7 +1028,7 @@ class CS3IPlayer : IPlayer { } else -> { - playerError?.invoke(error) + event(ErrorEvent(error)) } } @@ -1096,7 +1042,7 @@ class CS3IPlayer : IPlayer { override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) if (isPlaying) { - requestAutoFocus?.invoke() + event(RequestAudioFocusEvent()) onRenderFirst() } } @@ -1116,12 +1062,15 @@ class CS3IPlayer : IPlayer { true ) == true ) { - handleEvent(CSPlayerEvent.NextEpisode) + handleEvent( + CSPlayerEvent.NextEpisode, + source = PlayerEventSource.Player + ) } } Player.STATE_BUFFERING -> { - updatedTime() + updatedTime(source = PlayerEventSource.Player) } Player.STATE_IDLE -> { @@ -1134,18 +1083,18 @@ class CS3IPlayer : IPlayer { override fun onVideoSizeChanged(videoSize: VideoSize) { super.onVideoSizeChanged(videoSize) - playerDimensionsLoaded?.invoke(Pair(videoSize.width, videoSize.height)) + event(ResizedEvent(height = videoSize.height, width = videoSize.width)) } override fun onRenderedFirstFrame() { super.onRenderedFirstFrame() onRenderFirst() - updatedTime() + updatedTime(source = PlayerEventSource.Player) } }) - } catch (e: Exception) { - Log.e(TAG, "loadExo error", e) - playerError?.invoke(e) + } catch (t: Throwable) { + Log.e(TAG, "loadExo error", t) + event(ErrorEvent(t)) } } @@ -1156,7 +1105,7 @@ class CS3IPlayer : IPlayer { lastTimeStamps = timeStamps timeStamps.forEach { timestamp -> exoPlayer?.createMessage { _, _ -> - updatedTime() + updatedTime(source = PlayerEventSource.Player) //if (payload is EpisodeSkip.SkipStamp) // this should always be true // onTimestampInvoked?.invoke(payload) } @@ -1166,7 +1115,7 @@ class CS3IPlayer : IPlayer { ?.setDeleteAfterDelivery(false) ?.send() } - updatedTime() + updatedTime(source = PlayerEventSource.Player) } @SuppressLint("UnsafeOptInUsageError") @@ -1187,7 +1136,7 @@ class CS3IPlayer : IPlayer { if (invalid) { releasePlayer(saveTime = false) - playerError?.invoke(InvalidFileException("Too short playback")) + event(ErrorEvent(InvalidFileException("Too short playback"))) return } @@ -1196,7 +1145,7 @@ class CS3IPlayer : IPlayer { val width = format?.width val height = format?.height if (height != null && width != null) { - playerDimensionsLoaded?.invoke(Pair(width, height)) + event(ResizedEvent(width = width, height = height)) updatedTime() exoPlayer?.apply { requestedListeningPercentages?.forEach { percentage -> @@ -1230,9 +1179,9 @@ class CS3IPlayer : IPlayer { subtitleHelper.setActiveSubtitles(activeSubtitles.toSet()) loadExo(context, listOf(MediaItemSlice(mediaItem, Long.MIN_VALUE)), subSources) - } catch (e: Exception) { - Log.e(TAG, "loadOfflinePlayer error", e) - playerError?.invoke(e) + } catch (t: Throwable) { + Log.e(TAG, "loadOfflinePlayer error", t) + event(ErrorEvent(t)) } } @@ -1365,9 +1314,9 @@ class CS3IPlayer : IPlayer { } loadExo(context, mediaItems, subSources, cacheFactory) - } catch (e: Exception) { - Log.e(TAG, "loadOnlinePlayer error", e) - playerError?.invoke(e) + } catch (t: Throwable) { + Log.e(TAG, "loadOnlinePlayer error", t) + event(ErrorEvent(t)) } } 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 0f3c189d..6dabb5b7 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 @@ -874,7 +874,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { currentTouch )?.let { seekTo -> if (abs(seekTo - startTime) > MINIMUM_SEEK_TIME) { - player.seekTo(seekTo) + player.seekTo(seekTo, PlayerEventSource.UI) } } } @@ -909,7 +909,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } else -> { - player.handleEvent(CSPlayerEvent.PlayPauseToggle) + player.handleEvent(CSPlayerEvent.PlayPauseToggle, PlayerEventSource.UI) } } } else if (doubleTapEnabled && isFullScreenPlayer) { 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 2b9304b6..b2542ffa 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 @@ -551,7 +551,7 @@ class GeneratorPlayer : FullScreenPlayer() { //println("CURRENT SELECTED :$currentSelectedSubtitles of $currentSubs") context?.let { ctx -> val isPlaying = player.getIsPlaying() - player.handleEvent(CSPlayerEvent.Pause) + player.handleEvent(CSPlayerEvent.Pause, PlayerEventSource.UI) val currentSubtitles = sortSubs(currentSubs) val sourceDialog = Dialog(ctx, R.style.AlertDialogCustomBlack) @@ -883,7 +883,7 @@ class GeneratorPlayer : FullScreenPlayer() { } - override fun playerError(exception: Exception) { + override fun playerError(exception: Throwable) { Log.i(TAG, "playerError = $currentSelectedLink") super.playerError(exception) } @@ -945,14 +945,13 @@ class GeneratorPlayer : FullScreenPlayer() { var maxEpisodeSet: Int? = null var hasRequestedStamps: Boolean = false - override fun playerPositionChanged(posDur: Pair) { + override fun playerPositionChanged(position: Long, duration : Long) { // Don't save livestream data if ((currentMeta as? ResultEpisode)?.tvType?.isLiveStream() == true) return // Don't save NSFW data if ((currentMeta as? ResultEpisode)?.tvType == TvType.NSFW) return - val (position, duration) = posDur if (duration <= 0L) return // idk how you achieved this, but div by zero crash if (!hasRequestedStamps) { hasRequestedStamps = true @@ -1209,8 +1208,8 @@ class GeneratorPlayer : FullScreenPlayer() { } } - override fun playerDimensionsLoaded(widthHeight: Pair) { - setPlayerDimen(widthHeight) + override fun playerDimensionsLoaded(width: Int, height : Int) { + setPlayerDimen(width to height) } private fun unwrapBundle(savedInstanceState: Bundle?) { 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 3038cb8d..ec006234 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 @@ -45,9 +45,120 @@ enum class CSPlayerLoading { IsPaused, IsPlaying, IsBuffering, - //IsDone, } +enum class PlayerEventSource { + /** This event was invoked from the user pressing some button or selecting something */ + UI, + + /** This event was invoked automatically */ + Player, + + /** This event was invoked from a external sync tool like WatchTogether */ + Sync, +} + +abstract class PlayerEvent { + abstract val source: PlayerEventSource +} + +/** this is used to update UI based of the current time, + * using requestedListeningPercentages as well as saving time */ +data class PositionEvent( + override val source: PlayerEventSource, + val fromMs: Long, + val toMs: Long, + /** duration of the entire video */ + val durationMs: Long, +) : PlayerEvent() { + /** how many ms (+-) we have skipped */ + val seekMs : Long get() = toMs - fromMs +} + +/** player error when rendering or misc, used to display toast or log */ +data class ErrorEvent( + val error: Throwable, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() + +/** Event when timestamps appear, null when it should disappear */ +data class TimestampInvokedEvent( + val timestamp: EpisodeSkip.SkipStamp, + 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, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() + +/** this is used by the player to load the next or prev episode */ +data class EpisodeSeekEvent( + /** -1 = prev, 1 = next, will never be 0, atm the user cant seek more than +-1 */ + val offset: Int, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() { + init { + assert(offset != 0) + } +} + +/** Event when the video is resized aka changed resolution or mirror */ +data class ResizedEvent( + val height: Int, + val width: Int, + override val source: PlayerEventSource = PlayerEventSource.Player, +) : PlayerEvent() + +/** Event when the player status update, along with the previous status (for animation)*/ +data class StatusEvent( + val wasPlaying: CSPlayerLoading, + val isPlaying: CSPlayerLoading, + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Event when tracks are changed, used for UI changes */ +data class TracksChangedEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Event from player to give all embedded subtitles */ +data class EmbeddedSubtitlesFetchedEvent( + val tracks: List, + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** on attach player to view */ +data class PlayerAttachedEvent( + val player: Any?, + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Event from player to inform that subtitles have updated in some way */ +data class SubtitlesUpdatedEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** current player starts, asking for all other programs to shut the fuck up */ +data class RequestAudioFocusEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Pause event, separate from StatusEvent */ +data class PauseEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Play event, separate from StatusEvent */ +data class PlayEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + +/** Event when the player video has ended, up to the settings on what to do when that happens */ +data class VideoEndedEvent( + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() interface Track { /** @@ -108,27 +219,16 @@ interface IPlayer { fun getDuration(): Long? fun getPosition(): Long? - fun seekTime(time: Long) - fun seekTo(time: Long) + fun seekTime(time: Long, source: PlayerEventSource = PlayerEventSource.UI) + fun seekTo(time: Long, source: PlayerEventSource = PlayerEventSource.UI) fun getSubtitleOffset(): Long // in ms fun setSubtitleOffset(offset: Long) // in ms fun initCallbacks( - playerUpdated: (Any?) -> Unit, // attach player to view - updateIsPlaying: ((Pair) -> Unit)? = null, // (wasPlaying, isPlaying) - requestAutoFocus: (() -> Unit)? = null, // current player starts, asking for all other programs to shut the fuck up - playerError: ((Exception) -> Unit)? = null, // player error when rendering or misc, used to display toast or log - playerDimensionsLoaded: ((Pair) -> Unit)? = null, // (with, height), for UI - requestedListeningPercentages: List? = null, // this is used to request when the player should report back view percentage - playerPositionChanged: ((Pair) -> Unit)? = null,// (position, duration) this is used to update UI based of the current time - nextEpisode: (() -> Unit)? = null, // this is used by the player to load the next episode - prevEpisode: (() -> Unit)? = null, // this is used by the player to load the previous episode - subtitlesUpdates: (() -> Unit)? = null, // callback from player to inform that subtitles have updated in some way - embeddedSubtitlesFetched: ((List) -> Unit)? = null, // callback from player to give all embedded subtitles - onTracksInfoChanged: (() -> Unit)? = null, // Callback when tracks are changed, used for UI changes - onTimestampInvoked: ((EpisodeSkip.SkipStamp?) -> Unit)? = null, // Callback when timestamps appear, null when it should disappear - onTimestampSkipped: ((EpisodeSkip.SkipStamp) -> Unit)? = null, // callback for when a chapter is skipped, aka when event is handled (or for future use when skip automatically ads/sponsor) + eventHandler: ((PlayerEvent) -> Unit), + /** this is used to request when the player should report back view percentage */ + requestedListeningPercentages: List? = null, ) fun releaseCallbacks() @@ -155,7 +255,7 @@ interface IPlayer { fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean // returns true if the player requires a reload, null for nothing fun getCurrentPreferredSubtitle(): SubtitleData? - fun handleEvent(event: CSPlayerEvent) + fun handleEvent(event: CSPlayerEvent, source: PlayerEventSource = PlayerEventSource.UI) fun onStop() fun onPause() 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 a932a57c..ef2ed0df 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 @@ -130,8 +130,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { return currentTrailerIndex + 1 < currentTrailers.size } - override fun playerError(exception: Exception) { - if (player.getIsPlaying()) { // because we dont want random toasts in player + override fun playerError(exception: Throwable) { + if (player.getIsPlaying()) { // because we don't want random toasts in player super.playerError(exception) } else { nextMirror() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 91e97dfc..5208e4a5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.ui.result import android.animation.ValueAnimator import android.content.Context import android.content.res.Configuration -import android.graphics.Rect import android.os.Bundle import android.view.View import android.view.ViewGroup @@ -12,6 +11,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent +import com.lagradost.cloudstream3.ui.player.PlayerEventSource import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.utils.IOnBackPressed @@ -32,7 +32,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { override fun prevEpisode() {} - override fun playerPositionChanged(posDur: Pair) {} + override fun playerPositionChanged(position: Long, duration : Long) {} override fun nextMirror() {} @@ -99,8 +99,8 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { } } - override fun playerDimensionsLoaded(widthHeight: Pair) { - playerWidthHeight = widthHeight + override fun playerDimensionsLoaded(width: Int, height : Int) { + playerWidthHeight = width to height fixPlayerSize() } @@ -164,7 +164,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed { playerBinding?.playerIntroPlay?.setOnClickListener { playerBinding?.playerIntroPlay?.isGone = true - player.handleEvent(CSPlayerEvent.Play) + player.handleEvent(CSPlayerEvent.Play, PlayerEventSource.UI) updateUIVisibility() fixPlayerSize() } From 85c4c74222188ef1b998bdef62eb011cea8efedc Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 9 Sep 2023 23:53:35 +0200 Subject: [PATCH 090/441] fixed shitty external extractor code --- .../main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 0a926374..62217a0b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -378,6 +378,9 @@ open class ExtractorLink constructor( open val extractorData: String? = null, open val type: ExtractorLinkType, ) : VideoDownloadManager.IDownloadableMinimum { + val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 + val isDash : Boolean get() = type == ExtractorLinkType.DASH + constructor( source: String, name: String, From 0afbc90cd2a57c0cd5bb6d0709dcd44c0fc85b26 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 9 Sep 2023 23:57:18 +0200 Subject: [PATCH 091/441] fixed last fix --- .../main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 62217a0b..5edff7a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -227,7 +227,6 @@ data class ExtractorLinkPlayList( val playlist: List, override val referer: String, override val quality: Int, - val isM3u8: Boolean = false, override val headers: Map = mapOf(), /** Used for getExtractorVerifierJob() */ override val extractorData: String? = null, From f6b0ea8dfa4253446e9532689531a0aba6505f54 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sun, 10 Sep 2023 16:31:01 +0300 Subject: [PATCH 092/441] Stream button type switch support (#597) * Stream button type switch support * Hide Hls playlist switch --- .../com/lagradost/cloudstream3/ui/player/LinkGenerator.kt | 5 ++--- app/src/main/res/layout/stream_input.xml | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index ba2cdb40..ca2d9c81 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -67,9 +67,8 @@ class LinkGenerator( link.name ?: link.url, unshortenLinkSafe(link.url), // unshorten because it might be a raw link referer ?: "", - Qualities.Unknown.value, isM3u8 ?: normalSafeApiCall { - URI(link.url).path?.substringAfterLast(".")?.contains("m3u") - } ?: false + Qualities.Unknown.value, + type = INFER_TYPE, ) to null ) } diff --git a/app/src/main/res/layout/stream_input.xml b/app/src/main/res/layout/stream_input.xml index 20a91b4a..83605ce3 100644 --- a/app/src/main/res/layout/stream_input.xml +++ b/app/src/main/res/layout/stream_input.xml @@ -49,7 +49,8 @@ android:layout_marginTop="10dp" android:text="@string/hls_playlist" android:textColor="?attr/textColor" - android:textSize="16sp" /> + android:textSize="16sp" + android:visibility="invisible" /> From 7f7c81828a294a6f77cbc563dcf1b77b96173e6e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:37:02 +0200 Subject: [PATCH 093/441] added UI event for seekbar --- .../ui/player/AbstractPlayerFragment.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 c6f02f1a..4316bbc6 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 @@ -7,6 +7,7 @@ import android.graphics.drawable.AnimatedVectorDrawable import android.media.metrics.PlaybackErrorEvent import android.os.Build import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -25,8 +26,10 @@ import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.DefaultTimeBar import androidx.media3.ui.PlayerView import androidx.media3.ui.SubtitleView +import androidx.media3.ui.TimeBar import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode @@ -371,6 +374,7 @@ abstract class AbstractPlayerFragment( * do note that this only receives events for UI changes, * and returning early WONT stop it from changing in eg the player time or pause status */ open fun mainCallback(event : PlayerEvent) { + Log.i(TAG, "Handle event: $event") when(event) { is ResizedEvent -> { playerDimensionsLoaded(event.width, event.height) @@ -433,7 +437,7 @@ abstract class AbstractPlayerFragment( } } - @SuppressLint("SetTextI18n") + @SuppressLint("SetTextI18n", "UnsafeOptInUsageError") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { resizeMode = getKey(RESIZE_MODE_KEY) ?: 0 resize(resizeMode, false) @@ -454,6 +458,19 @@ abstract class AbstractPlayerFragment( subStyle = SubtitlesFragment.getCurrentSavedStyle() player.initSubtitles(subView, subtitleHolder, subStyle) + /** this might seam a bit fucky and that is because it is, the seek event is captured twice, once by the player + * and once by the UI even if it should only be registered once by the UI */ + playerView?.findViewById(R.id.exo_progress)?.addListener(object : TimeBar.OnScrubListener { + override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit + override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit + override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) { + if (canceled) return + val playerDuration = player.getDuration() ?: return + val playerPosition = player.getPosition() ?: return + mainCallback(PositionEvent(source = PlayerEventSource.UI, durationMs = playerDuration, fromMs = playerPosition, toMs = position)) + } + }) + SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged try { From 10bc688eaf00d34bd41da06d53f1142fc7888f09 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:29:30 +0200 Subject: [PATCH 094/441] fixed tracker on dub --- .../lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 b2c57137..b398b54e 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 @@ -1538,7 +1538,11 @@ class ResultViewModel2 : ViewModel() { this.name, this.japName ).filter { it.length > 2 } - .distinct(), // the reason why we filter is due to not wanting smth like " " or "?" + .distinct().map { + // this actually would be nice if we improved a bit as 3rd season == season 3 == III ect + // right now it just removes the dubbed status + it.lowercase().replace(Regex("""\(?[ds]ub(bed)?\)?(\s|$)""") , "").trim() + }, TrackerType.getTypes(this.type), this.year ) From 01e7acdeace32f20e6c23f5cb187977bd6131d45 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Mon, 11 Sep 2023 19:31:11 +0700 Subject: [PATCH 095/441] getTracker: switched to anilist api (#593) * getTracker: switched to anilist api --------- Co-authored-by: Sofie99 --- .../com/lagradost/cloudstream3/MainAPI.kt | 119 +++++++++++++----- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 80332445..0175e0d0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -22,8 +22,10 @@ import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf +import com.lagradost.nicehttp.RequestBodyTypes import okhttp3.Interceptor -import org.mozilla.javascript.Scriptable +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody import java.text.SimpleDateFormat import java.util.* import kotlin.math.absoluteValue @@ -180,7 +182,7 @@ object APIHolder { /** * Get anime tracker information based on title, year and type. * Both titles are attempted to be matched with both Romaji and English title. - * Uses the consumet api. + * Uses the anilist api. * * @param titles uses first index to search, but if you have multiple titles and want extra guarantee to match you can also have that * @param types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes() @@ -189,7 +191,8 @@ object APIHolder { suspend fun getTracker( titles: List, types: Set?, - year: Int? + year: Int?, + lessAccurate: Boolean = false ): Tracker? { return try { require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" } @@ -197,30 +200,70 @@ object APIHolder { val mainTitle = titles[0] val search = trackerCache[mainTitle] - ?: app.get("https://api.consumet.org/meta/anilist/$mainTitle") - .parsedSafe()?.also { - trackerCache[mainTitle] = it - } ?: return null + ?: searchAnilist(mainTitle)?.also { + trackerCache[mainTitle] = it + } ?: return null - val res = search.results?.find { media -> - val matchingYears = year == null || media.releaseDate == year + val res = search.data?.page?.media?.find { media -> + val matchingYears = year == null || media.seasonYear == year val matchingTitles = media.title?.let { title -> titles.any { userTitle -> title.isMatchingTitles(userTitle) } } ?: false - val matchingTypes = types?.any { it.name.equals(media.type, true) } == true - matchingTitles && matchingTypes && matchingYears + val matchingTypes = types?.any { it.name.equals(media.format, true) } == true + if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears } ?: return null - Tracker(res.malId, res.aniId, res.image, res.cover) + Tracker(res.idMal, res.id.toString(), res.coverImage?.extraLarge ?: res.coverImage?.large, res.bannerImage) } catch (t: Throwable) { logError(t) null } } + private suspend fun searchAnilist( + title: String?, + ): AniSearch? { + val query = """ + query ( + ${'$'}page: Int = 1 + ${'$'}search: String + ${'$'}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC] + ${'$'}type: MediaType + ) { + Page(page: ${'$'}page, perPage: 20) { + media( + search: ${'$'}search + sort: ${'$'}sort + type: ${'$'}type + ) { + id + idMal + title { romaji english } + coverImage { extraLarge large } + bannerImage + seasonYear + format + } + } + } + """.trimIndent().trim() + + val data = mapOf( + "query" to query, + "variables" to mapOf( + "search" to title, + "sort" to "SEARCH_MATCH", + "type" to "ANIME", + ) + ).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull()) + + return app.post("https://graphql.anilist.co", requestBody = data) + .parsedSafe() + } + fun Context.getApiSettings(): HashSet { //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) @@ -1730,30 +1773,42 @@ data class Tracker( val cover: String? = null, ) -data class Title( - @JsonProperty("romaji") val romaji: String? = null, - @JsonProperty("english") val english: String? = null, +data class AniSearch( + @JsonProperty("data") var data: Data? = Data() ) { - fun isMatchingTitles(title: String?): Boolean { - if (title == null) return false - return english.equals(title, true) || romaji.equals(title, true) + data class Data( + @JsonProperty("Page") var page: Page? = Page() + ) { + data class Page( + @JsonProperty("media") var media: ArrayList = arrayListOf() + ) { + data class Media( + @JsonProperty("title") var title: Title? = null, + @JsonProperty("id") var id: Int? = null, + @JsonProperty("idMal") var idMal: Int? = null, + @JsonProperty("seasonYear") var seasonYear: Int? = null, + @JsonProperty("format") var format: String? = null, + @JsonProperty("coverImage") var coverImage: CoverImage? = null, + @JsonProperty("bannerImage") var bannerImage: String? = null, + ) { + data class CoverImage( + @JsonProperty("extraLarge") var extraLarge: String? = null, + @JsonProperty("large") var large: String? = null, + ) + data class Title( + @JsonProperty("romaji") var romaji: String? = null, + @JsonProperty("english") var english: String? = null, + ) { + fun isMatchingTitles(title: String?): Boolean { + if (title == null) return false + return english.equals(title, true) || romaji.equals(title, true) + } + } + } + } } } -data class Results( - @JsonProperty("id") val aniId: String? = null, - @JsonProperty("malId") val malId: Int? = null, - @JsonProperty("title") val title: Title? = null, - @JsonProperty("releaseDate") val releaseDate: Int? = null, - @JsonProperty("type") val type: String? = null, - @JsonProperty("image") val image: String? = null, - @JsonProperty("cover") val cover: String? = null, -) - -data class AniSearch( - @JsonProperty("results") val results: ArrayList? = arrayListOf() -) - /** * used for the getTracker() method **/ From 2baa75496ec16ba5e8cf2eba13641563bcf6f8fc Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Mon, 11 Sep 2023 18:13:42 +0000 Subject: [PATCH 096/441] Fix opensubtitles (#598) * Fix OpenSubtitles --- .../providers/OpenSubtitlesApi.kt | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index 3e372c2d..4030649d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -2,12 +2,13 @@ package com.lagradost.cloudstream3.syncproviders.providers import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty -import com.google.common.collect.BiMap -import com.google.common.collect.HashBiMap -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.subtitles.AbstractSubApi import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities @@ -15,8 +16,8 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager import com.lagradost.cloudstream3.utils.AppUtils -import java.net.URLEncoder -import java.nio.charset.StandardCharsets +import okhttp3.Interceptor +import okhttp3.Response class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi { override val idPrefix = "opensubtitles" @@ -36,6 +37,23 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi var currentSession: SubtitleOAuthEntity? = null } + private val headerInterceptor = OpenSubtitleInterceptor() + + /** Automatically adds required api headers */ + private class OpenSubtitleInterceptor : Interceptor { + /** Required user agent! */ + private val userAgent = "Cloudstream3 v0.1" + override fun intercept(chain: Interceptor.Chain): Response { + return chain.proceed( + chain.request().newBuilder() + .removeHeader("user-agent") + .addHeader("user-agent", userAgent) + .addHeader("Api-Key", apiKey) + .build() + ) + } + } + private fun canDoRequest(): Boolean { return unixTimeMs > currentCoolDown } @@ -98,13 +116,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val response = app.post( url = "$host/login", headers = mapOf( - "Api-Key" to apiKey, - "Content-Type" to "application/json" + "Content-Type" to "application/json", ), data = mapOf( "username" to username, "password" to password - ) + ), + interceptor = headerInterceptor ) //Log.i(TAG, "Responsecode = ${response.code}") //Log.i(TAG, "Result => ${response.text}") @@ -149,11 +167,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi // "pt" to "pt-PT", // "pt" to "pt-BR" ) - private fun fixLanguage(language: String?) : String? { + + private fun fixLanguage(language: String?): String? { return languageExceptions[language] ?: language } + // O(n) but good enough, BiMap did not want to work properly - private fun fixLanguageReverse(language: String?) : String? { + private fun fixLanguageReverse(language: String?): String? { return languageExceptions.entries.firstOrNull { it.value == language }?.key ?: language } @@ -183,9 +203,9 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val req = app.get( url = searchQueryUrl, headers = mapOf( - Pair("Api-Key", apiKey), Pair("Content-Type", "application/json") - ) + ), + interceptor = headerInterceptor ) Log.i(TAG, "Search Req => ${req.text}") if (!req.isSuccessful) { @@ -207,7 +227,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi //Use any valid name/title in hierarchy val name = filename ?: featureDetails?.movieName ?: featureDetails?.title ?: featureDetails?.parentTitle ?: attr.release ?: query.query - val lang = fixLanguageReverse(attr.language)?: "" + val lang = fixLanguageReverse(attr.language) ?: "" val resEpNum = featureDetails?.episodeNumber ?: query.epNumber val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber val year = featureDetails?.year ?: query.year @@ -251,13 +271,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi "Authorization", "Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}" ), - Pair("Api-Key", apiKey), Pair("Content-Type", "application/json"), Pair("Accept", "*/*") ), data = mapOf( Pair("file_id", data.data) - ) + ), + interceptor = headerInterceptor ) Log.i(TAG, "Request result => (${req.code}) ${req.text}") //Log.i(TAG, "Request headers => ${req.headers}") From 7d6ba8c7a4feed2636ab7fd386d1c76ffa73f2de Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 11 Sep 2023 23:05:10 +0200 Subject: [PATCH 097/441] tv changes for better centering + bigger text + better contrast --- .../lagradost/cloudstream3/CommonActivity.kt | 37 ++++++++++++-- .../lagradost/cloudstream3/MainActivity.kt | 50 ++++++++++++++++--- .../ui/player/FullScreenPlayer.kt | 15 +----- .../ui/result/ResultTrailerPlayer.kt | 2 + .../main/res/layout/fragment_result_tv.xml | 40 +++++++++------ 5 files changed, 107 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 0bcd4152..a7d899b6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -7,9 +7,14 @@ import android.content.Context import android.content.pm.PackageManager import android.content.res.Resources import android.os.Build +import android.util.DisplayMetrics import android.util.Log -import android.view.* +import android.view.Gravity +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View import android.view.View.NO_ID +import android.view.ViewGroup import android.widget.TextView import android.widget.Toast import androidx.activity.ComponentActivity @@ -40,7 +45,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode import com.lagradost.cloudstream3.utils.UIHelper.toPx import org.schabi.newpipe.extractor.NewPipe import java.lang.ref.WeakReference -import java.util.* +import java.util.Locale +import kotlin.math.max +import kotlin.math.min enum class FocusDirection { Start, @@ -63,6 +70,19 @@ object CommonActivity { return (this as MainActivity?)?.mSessionManager?.currentCastSession } + val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics + + // screenWidth and screenHeight does always + // refer to the screen while in landscape mode + val screenWidth: Int + get() { + return max(displayMetrics.widthPixels, displayMetrics.heightPixels) + } + val screenHeight: Int + get() { + return min(displayMetrics.widthPixels, displayMetrics.heightPixels) + } + var canEnterPipMode: Boolean = false var canShowPipMode: Boolean = false @@ -328,6 +348,14 @@ object CommonActivity { currentLook = currentLook.parent as? View ?: break }*/ + private fun View.hasContent() : Boolean { + return isShown && when(this) { + //is RecyclerView -> this.childCount > 0 + is ViewGroup -> this.childCount > 0 + else -> true + } + } + /** skips the initial stage of searching for an id using the view, see getNextFocus for specification */ fun continueGetNextFocus( root: Any?, @@ -348,16 +376,17 @@ object CommonActivity { } ?: return null next = localLook(view, nextId) ?: next + val shown = next.hasContent() // if cant focus but visible then break and let android decide // the exception if is the view is a parent and has children that wants focus val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent -> parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0 } ?: false - if (!next.isFocusable && next.isShown && !hasChildrenThatWantsFocus) return null + if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null // if not shown then continue because we will "skip" over views to get to a replacement - if (!next.isShown) { + if (!shown) { // we don't want a while true loop, so we let android decide if we find a recursive view if (next == view) return null return getNextFocus(root, next, direction, depth + 1) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index fbad4fce..a07ae2c2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.content.res.Configuration +import android.graphics.Rect import android.net.Uri import android.os.Build import android.os.Bundle @@ -52,6 +53,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.snackbar.Snackbar +import com.google.common.collect.Comparators.min import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.apis @@ -64,13 +66,13 @@ import com.lagradost.cloudstream3.CommonActivity.loadThemes import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint +import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale import com.lagradost.cloudstream3.databinding.ActivityMainBinding import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding import com.lagradost.cloudstream3.mvvm.Resource -import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observeNullable @@ -832,6 +834,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { focusOutline.get()?.isVisible = false } } + /*private val scrollListener = object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + current = current.copy(x = current.x + dx, y = current.y + dy) + setTargetPosition(current) + } + }*/ private fun setTargetPosition(target: FocusTarget) { focusOutline.get()?.apply { @@ -874,7 +883,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (!exactlyTheSame) { lastView?.removeOnLayoutChangeListener(layoutListener) lastView?.removeOnAttachStateChangeListener(attachListener) - (lastView?.parent as? RecyclerView)?.removeOnLayoutChangeListener(layoutListener) + (lastView?.parent as? RecyclerView)?.apply { + removeOnLayoutChangeListener(layoutListener) + //removeOnScrollListener(scrollListener) + } } val wasGone = focusOutline.isGone @@ -952,7 +964,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { focusOutline.isVisible = false } if (!exactlyTheSame) { - (newFocus.parent as? RecyclerView)?.addOnLayoutChangeListener(layoutListener) + (newFocus.parent as? RecyclerView)?.apply { + addOnLayoutChangeListener(layoutListener) + //addOnScrollListener(scrollListener) + } newFocus.addOnLayoutChangeListener(layoutListener) newFocus.addOnAttachStateChangeListener(attachListener) } @@ -970,8 +985,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { ) // if they are the same within then snap, aka scrolling - val deltaMin = 50.toPx - if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMin && (start.y - end.y).absoluteValue < deltaMin) { + val deltaMinX = min(end.width / 2, 60.toPx) + val deltaMinY = min(end.height / 2, 60.toPx) + if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMinX && (start.y - end.y).absoluteValue < deltaMinY) { animator?.cancel() last = start current = end @@ -1000,7 +1016,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // animate between a and b animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { startDelay = 0 - duration = 100 + duration = 200 addUpdateListener { animation -> val animatedValue = animation.animatedValue as Float val target = FocusTarget.lerp(last, current, minOf(animatedValue, 1.0f)) @@ -1095,7 +1111,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> // println("refocus $oldFocus -> $newFocus") + try { + val r = Rect(0,0,0,0) + newFocus.getDrawingRect(r) + val x = r.centerX() + val y = r.centerY() + val dx = 0 //screenWidth / 2 + val dy = screenHeight / 2 + val r2 = Rect(x-dx,y-dy,x+dx,y+dy) + newFocus.requestRectangleOnScreen(r2, false) + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_ : Throwable) { } TvFocus.updateFocusView(newFocus) + /*var focus = newFocus + + while(focus != null) { + if(focus is ScrollingView && focus.canScrollVertically()) { + focus.scrollBy() + } + when(focus.parent) { + is View -> focus = newFocus + else -> break + } + }*/ } newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) 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 6dabb5b7..e698191d 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 @@ -38,6 +38,8 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener +import com.lagradost.cloudstream3.CommonActivity.screenHeight +import com.lagradost.cloudstream3.CommonActivity.screenWidth import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding @@ -126,19 +128,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected var useTrueSystemBrightness = true private val fullscreenNotch = true //TODO SETTING - protected val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics - - // screenWidth and screenHeight does always - // refer to the screen while in landscape mode - protected val screenWidth: Int - get() { - return max(displayMetrics.widthPixels, displayMetrics.heightPixels) - } - protected val screenHeight: Int - get() { - return min(displayMetrics.widthPixels, displayMetrics.heightPixels) - } - private var statusBarHeight: Int? = null private var navigationBarHeight: Int? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 5208e4a5..c30e70e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -9,6 +9,8 @@ import android.view.ViewGroup import android.widget.FrameLayout import androidx.core.view.isGone import androidx.core.view.isVisible +import com.lagradost.cloudstream3.CommonActivity.screenHeight +import com.lagradost.cloudstream3.CommonActivity.screenWidth import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.PlayerEventSource diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 1fde999c..4d236d78 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -128,7 +128,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit - - @@ -411,7 +409,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:padding="5dp" android:requiresFadingEdge="vertical" android:textColor="?attr/textColor" - android:textSize="12sp" + android:textSize="16sp" tools:text="Ryan Quicksave Romano is an eccentric adventurer with a strange power: he can create a save-point in time and redo his life whenever he dies. Arriving in New Rome, the glitzy capital of sin of a rebuilding Europe, he finds the city torn between mega-corporations, sponsored heroes, superpowered criminals, and true monsters. It's a time of chaos, where potions can grant the power to rule the world and dangers lurk everywhere. " /> - + android:layout_height="match_parent"> + + + + Date: Thu, 14 Sep 2023 10:53:35 +0000 Subject: [PATCH 098/441] More robust player release (#601) --- .../ui/player/AbstractPlayerFragment.kt | 1 + .../cloudstream3/ui/player/CS3IPlayer.kt | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) 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 4316bbc6..8388e58f 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 @@ -517,6 +517,7 @@ abstract class AbstractPlayerFragment( canEnterPipMode = false mMediaSession?.release() mMediaSession = null + playerView?.player = null SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged keepScreenOn(false) 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 fe4e3423..331cfb73 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 @@ -51,6 +51,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle @@ -85,6 +86,12 @@ const val toleranceAfterUs = 300_000L class CS3IPlayer : IPlayer { private var isPlaying = false private var exoPlayer: ExoPlayer? = null + set(value) { + // If the old value is not null then the player has not been properly released. + debugAssert({ field != null && value != null }, { "Previous player instance should be released!" }) + field = value + } + var cacheSize = 0L var simpleCacheSize = 0L var videoBufferMs = 0L @@ -682,13 +689,13 @@ class CS3IPlayer : IPlayer { metadataRendererOutput ).map { if (it is TextRenderer) { - currentTextRenderer = CustomTextRenderer( + val currentTextRenderer = CustomTextRenderer( subtitleOffset, textRendererOutput, eventHandler.looper, CustomSubtitleDecoderFactory() - ) - currentTextRenderer!! + ).also { this.currentTextRenderer = it } + currentTextRenderer } else it }.toTypedArray() } @@ -1323,7 +1330,7 @@ class CS3IPlayer : IPlayer { override fun reloadPlayer(context: Context) { Log.i(TAG, "reloadPlayer") - exoPlayer?.release() + releasePlayer(false) currentLink?.let { loadOnlinePlayer(context, it) } ?: currentDownloadedFile?.let { From 2bed79b1f18e7b7ae145377865f427a781197e11 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:53:54 +0700 Subject: [PATCH 099/441] Update Gofile.kt (#600) --- .../main/java/com/lagradost/cloudstream3/extractors/Gofile.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt index d76b0e11..eaf9c65f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt @@ -19,7 +19,7 @@ open class Gofile : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z]+)").find(url)?.groupValues?.get(1) + val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1) val token = app.get("$mainApi/createAccount").parsedSafe()?.data?.get("token") val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let { Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1) @@ -59,4 +59,4 @@ open class Gofile : ExtractorApi() { @JsonProperty("data") val data: Data? = null, ) -} \ No newline at end of file +} From 6957a8f95dc7144c5b6c454930950d533c276f95 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 14 Sep 2023 20:30:44 +0200 Subject: [PATCH 100/441] bump --- app/build.gradle.kts | 4 ++-- app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f52d6e5e..66ba16c6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,8 +58,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 59 - versionName = "4.1.8" + versionCode = 60 + versionName = "4.1.9" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 0175e0d0..5b674c4c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -179,6 +179,13 @@ object APIHolder { private var trackerCache: HashMap = hashMapOf() + /** backwards compatibility, use getTracker4 instead */ + suspend fun getTracker( + titles: List, + types: Set?, + year: Int?, + ): Tracker? = getTracker(titles, types, year, false) + /** * Get anime tracker information based on title, year and type. * Both titles are attempted to be matched with both Romaji and English title. @@ -192,7 +199,7 @@ object APIHolder { titles: List, types: Set?, year: Int?, - lessAccurate: Boolean = false + lessAccurate: Boolean ): Tracker? { return try { require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" } From 24977a8d628a3418051e9bbbead8d4a88f7df035 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Fri, 15 Sep 2023 20:47:59 +0000 Subject: [PATCH 101/441] Potential fix for crash loops (#608) --- .../com/lagradost/cloudstream3/MainActivity.kt | 12 ++++++++---- .../cloudstream3/plugins/PluginManager.kt | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a07ae2c2..82a52a2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1092,15 +1092,19 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { updateTv() // backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting? - try { + normalSafeApiCall { val appVer = BuildConfig.VERSION_NAME val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: "" if (appVer != lastAppAutoBackup) { setKey("VERSION_NAME", BuildConfig.VERSION_NAME) - backup() + normalSafeApiCall { + backup() + } + normalSafeApiCall { + // Recompile oat on new version + PluginManager.deleteAllOatFiles(this) + } } - } catch (t: Throwable) { - logError(t) } // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH 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 87b0ba3b..5bb96ed1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -137,6 +137,20 @@ object PluginManager { } } + /** + * Deletes all generated oat files which will force Android to recompile the dex extensions. + * This might fix unrecoverable SIGSEGV exceptions when old oat files are loaded in a new app update. + */ + fun deleteAllOatFiles(context: Context) { + File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}").listFiles()?.forEach { repo -> + repo.listFiles { file -> file.name == "oat" && file.isDirectory }?.forEach { file -> + val success = file.deleteRecursively() + Log.i(TAG, "Deleted oat directory: ${file.absolutePath} Success=$success") + } + } + } + + fun getPluginsOnline(): Array { return getKey(PLUGINS_KEY) ?: emptyArray() } From 627c1bb223a3cc4b8a917807e91100211f30a859 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Fri, 15 Sep 2023 22:30:34 +0000 Subject: [PATCH 102/441] Many UI fixes (#606) * Lower targetSdk to get all installed packages * Update sdk version * Let's not be too radical * Many fixes * Revert targetSdk * Make account homepage persistent --- .../lagradost/cloudstream3/MainActivity.kt | 25 +- ...RecyclerView.kt => CustomRecyclerViews.kt} | 39 ++- .../cloudstream3/ui/home/HomeFragment.kt | 3 +- .../cloudstream3/ui/home/HomeViewModel.kt | 17 +- .../ui/result/ResultFragmentTv.kt | 4 +- .../ui/result/ResultViewModel2.kt | 3 +- .../ui/settings/SettingsProviders.kt | 4 +- .../ui/setup/SetupFragmentMedia.kt | 4 +- .../cloudstream3/utils/DataStoreHelper.kt | 33 ++- app/src/main/res/drawable/episodes_shadow.xml | 6 +- .../main/res/layout/fragment_result_tv.xml | 239 ++++++++++-------- 11 files changed, 239 insertions(+), 138 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/ui/{AutofitRecyclerView.kt => CustomRecyclerViews.kt} (79%) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 82a52a2a..263a40f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -128,6 +128,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.IOnBackPressed @@ -305,6 +306,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // kinda shitty solution, but cant com main->home otherwise for popups val bookmarksUpdatedEvent = Event() + /** + * Used by data store helper to fully reload home when switching accounts + */ + val reloadHomeEvent = Event() /** @@ -539,6 +544,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val isTrueTv = isTrueTvSettings() navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + + // Hide downloads on TV + navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv } } @@ -1116,16 +1125,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> // println("refocus $oldFocus -> $newFocus") try { - val r = Rect(0,0,0,0) + val r = Rect(0, 0, 0, 0) newFocus.getDrawingRect(r) val x = r.centerX() val y = r.centerY() val dx = 0 //screenWidth / 2 val dy = screenHeight / 2 - val r2 = Rect(x-dx,y-dy,x+dx,y+dy) + val r2 = Rect(x - dx, y - dy, x + dx, y + dy) newFocus.requestRectangleOnScreen(r2, false) - // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) - } catch (_ : Throwable) { } + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_: Throwable) { + } TvFocus.updateFocusView(newFocus) /*var focus = newFocus @@ -1186,7 +1196,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } else if (lastError == null) { ioSafe { - getKey(USER_SELECTED_HOMEPAGE_API)?.let { homeApi -> + DataStoreHelper.currentHomePage?.let { homeApi -> mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi)) } ?: run { mainPluginsLoadedEvent.invoke(false) @@ -1547,6 +1557,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { migrateResumeWatching() } + getKey(USER_SELECTED_HOMEPAGE_API)?.let { homepage -> + DataStoreHelper.currentHomePage = homepage + removeKey(USER_SELECTED_HOMEPAGE_API) + } + try { if (getKey(HAS_DONE_SETUP_KEY, false) != true) { navController.navigate(R.id.navigation_setup_language) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt similarity index 79% rename from app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt rename to app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt index 28ced48c..1a9549e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui import android.content.Context import android.util.AttributeSet import android.view.View +import androidx.core.view.children import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs @@ -70,8 +71,8 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : val orientation = this.orientation // fixes arabic by inverting left and right layout focus - val correctDirection = if(this.isLayoutRTL) { - when(direction) { + val correctDirection = if (this.isLayoutRTL) { + when (direction) { View.FOCUS_RIGHT -> View.FOCUS_LEFT View.FOCUS_LEFT -> View.FOCUS_RIGHT else -> direction @@ -83,12 +84,15 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : View.FOCUS_DOWN -> { return spanCount } + View.FOCUS_UP -> { return -spanCount } + View.FOCUS_RIGHT -> { return 1 } + View.FOCUS_LEFT -> { return -1 } @@ -98,12 +102,15 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : View.FOCUS_DOWN -> { return 1 } + View.FOCUS_UP -> { return -1 } + View.FOCUS_RIGHT -> { return spanCount } + View.FOCUS_LEFT -> { return -spanCount } @@ -155,4 +162,32 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att layoutManager = manager } +} + +/** + * Recyclerview wherein the max item width or height is set by the biggest view to prevent inconsistent view sizes. + */ +class MaxRecyclerView(ctx: Context, attrs: AttributeSet) : RecyclerView(ctx, attrs) { + private var biggestObserved: Int = 0 + private val orientation = LayoutManager.getProperties(context, attrs, 0, 0).orientation + private val isHorizontal = orientation == HORIZONTAL + private fun View.updateMaxSize() { + if (isHorizontal) { + this.minimumHeight = biggestObserved + } else { + this.minimumWidth = biggestObserved + } + } + + override fun onChildAttachedToWindow(child: View) { + child.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + val observed = if (isHorizontal) child.measuredHeight else child.measuredWidth + if (observed > biggestObserved) { + biggestObserved = observed + children.forEach { it.updateMaxSize() } + } else { + child.updateMaxSize() + } + super.onChildAttachedToWindow(child) + } } \ No newline at end of file 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 b84c619e..0797e9a0 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 @@ -69,7 +69,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import java.util.* @@ -669,7 +668,7 @@ class HomeFragment : Fragment() { } homeViewModel.reloadStored() - homeViewModel.loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) + homeViewModel.loadAndCancel(DataStoreHelper.currentHomePage, false) //loadHomePage(false) // nice profile pic on homepage 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 b27223ec..13d34b59 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 @@ -49,7 +49,6 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.cloudstream3.utils.VideoDownloadHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -426,23 +425,29 @@ class HomeViewModel : ViewModel() { } private fun afterPluginsLoaded(forceReload: Boolean) { - loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), forceReload) + loadAndCancel(DataStoreHelper.currentHomePage, forceReload) } private fun afterMainPluginsLoaded(unused: Boolean = false) { - loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) + loadAndCancel(DataStoreHelper.currentHomePage, false) + } + + private fun reloadHome(unused: Boolean = false) { + loadAndCancel(DataStoreHelper.currentHomePage, true) } init { MainActivity.bookmarksUpdatedEvent += ::bookmarksUpdated MainActivity.afterPluginsLoadedEvent += ::afterPluginsLoaded MainActivity.mainPluginsLoadedEvent += ::afterMainPluginsLoaded + MainActivity.reloadHomeEvent += ::reloadHome } override fun onCleared() { MainActivity.bookmarksUpdatedEvent -= ::bookmarksUpdated MainActivity.afterPluginsLoadedEvent -= ::afterPluginsLoaded MainActivity.mainPluginsLoadedEvent -= ::afterMainPluginsLoaded + MainActivity.reloadHomeEvent -= ::reloadHome super.onCleared() } @@ -495,7 +500,7 @@ class HomeViewModel : ViewModel() { val api = getApiFromNameNull(preferredApiName) if (preferredApiName == noneApi.name) { // just set to random - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) + if (fromUI) DataStoreHelper.currentHomePage = noneApi.name loadAndCancel(noneApi) } else if (preferredApiName == randomApi.name) { // randomize the api, if none exist like if not loaded or not installed @@ -506,7 +511,7 @@ class HomeViewModel : ViewModel() { } else { val apiRandom = validAPIs.random() loadAndCancel(apiRandom) - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) + if (fromUI) DataStoreHelper.currentHomePage = apiRandom.name } } else if (api == null) { // API is not found aka not loaded or removed, post the loading @@ -520,7 +525,7 @@ class HomeViewModel : ViewModel() { } } else { // if the api is found, then set it to it and save key - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, api.name) + if (fromUI) DataStoreHelper.currentHomePage = api.name loadAndCancel(api) } } 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 be3de52b..c40d995b 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 @@ -177,7 +177,7 @@ class ResultFragmentTv : Fragment() { isVisible = true } - this.animate().alpha(if (turnVisible) 1.0f else 0.0f).apply { + this.animate().alpha(if (turnVisible) 0.97f else 0.0f).apply { duration = 200 interpolator = DecelerateInterpolator() setListener(object : Animator.AnimatorListener { @@ -294,9 +294,9 @@ class ResultFragmentTv : Fragment() { toggleEpisodes(true) binding?.apply { val views = listOf( + resultDubSelection, resultSeasonSelection, resultRangeSelection, - resultDubSelection, resultEpisodes, resultPlayTrailer, ) 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 b398b54e..6acf476a 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 @@ -518,7 +518,8 @@ class ResultViewModel2 : ViewModel() { val episodeNumber = episodes[currentIndex].episode if (episodeNumber < currentMin) { currentMin = episodeNumber - } else if (episodeNumber > currentMax) { + } + if (episodeNumber > currentMax) { currentMax = episodeNumber } ++currentIndex diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 0bef5e9a..7e57fc5b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard @@ -96,7 +96,7 @@ class SettingsProviders : PreferenceFragmentCompat() { this.getString(R.string.prefer_media_type_key), selectedList.map { it.toString() }.toMutableSet() ).apply() - removeKey(USER_SELECTED_HOMEPAGE_API) + DataStoreHelper.currentHomePage = null //(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index 6916cafe..f9197213 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -15,8 +15,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API class SetupFragmentMedia : Fragment() { @@ -77,7 +77,7 @@ class SetupFragmentMedia : Fragment() { .apply() // Regenerate set homepage - removeKey(USER_SELECTED_HOMEPAGE_API) + DataStoreHelper.currentHomePage = null } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 2eb2ab01..7bce1b6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -77,10 +77,28 @@ object DataStoreHelper { var selectedKeyIndex by PreferenceDelegate("$TAG/account_key_index", 0) val currentAccount: String get() = selectedKeyIndex.toString() - private fun setAccount(account: Account) { + /** + * Get or set the current account homepage. + * Setting this does not automatically reload the homepage. + */ + var currentHomePage: String? + get() = getKey("$currentAccount/$USER_SELECTED_HOMEPAGE_API") + set(value) { + val key = "$currentAccount/$USER_SELECTED_HOMEPAGE_API" + if (value == null) { + removeKey(key) + } else { + setKey(key, value) + } + } + + private fun setAccount(account: Account, refreshHomePage: Boolean) { selectedKeyIndex = account.keyIndex showToast(account.name) MainActivity.bookmarksUpdatedEvent(true) + if (refreshHomePage) { + MainActivity.reloadHomeEvent(true) + } } private fun editAccount(context: Context, account: Account, isNewAccount: Boolean) { @@ -112,7 +130,7 @@ object DataStoreHelper { accounts = currentAccounts.toTypedArray() // update UI - setAccount(getDefaultAccount(context)) + setAccount(getDefaultAccount(context), true) MainActivity.bookmarksUpdatedEvent(true) dialog?.dismissSafe() } @@ -161,8 +179,13 @@ object DataStoreHelper { currentAccounts.add(currentEditAccount) } + // Save the current homepage for new accounts + val currentHomePage = DataStoreHelper.currentHomePage + // set the new default account as well as add the key for the new account - setAccount(currentEditAccount) + setAccount(currentEditAccount, false) + DataStoreHelper.currentHomePage = currentHomePage + accounts = currentAccounts.toTypedArray() dialog.dismissSafe() @@ -204,7 +227,7 @@ object DataStoreHelper { ) binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter( selectCallBack = { account -> - setAccount(account) + setAccount(account, true) builder.dismissSafe() }, addAccountCallback = { @@ -353,7 +376,7 @@ object DataStoreHelper { removeKeys(folder2) } - fun deleteBookmarkedData(id : Int?) { + fun deleteBookmarkedData(id: Int?) { if (id == null) return removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString()) removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) diff --git a/app/src/main/res/drawable/episodes_shadow.xml b/app/src/main/res/drawable/episodes_shadow.xml index b4cdd382..a77cbf25 100644 --- a/app/src/main/res/drawable/episodes_shadow.xml +++ b/app/src/main/res/drawable/episodes_shadow.xml @@ -1,6 +1,8 @@ + android:centerColor="?attr/primaryBlackBackground" + android:centerX="0.2" + android:endColor="?attr/primaryBlackBackground" + android:startColor="@color/transparent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 4d236d78..a143fbda 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -535,129 +535,150 @@ https://developer.android.com/design/ui/tv/samples/jet-fit - - - - - - - - - - - - - + tools:visibility="visible"> - + - style="@style/Widget.AppCompat.ProgressBar" - android:layout_gravity="center" - android:layout_width="50dp" - android:layout_height="50dp" />--> + + - - - - + android:focusableInTouchMode="false" + android:importantForAccessibility="no" + android:src="@drawable/episodes_shadow" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/shadow_space_2" + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + - Date: Sun, 17 Sep 2023 20:35:01 +0200 Subject: [PATCH 103/441] fix --- .../ui/player/PlayerGeneratorViewModel.kt | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) 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 42659f8d..3179cb9f 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 @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.ui.result.ResultEpisode @@ -15,6 +16,7 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorUri import kotlinx.coroutines.Job +import kotlinx.coroutines.launch class PlayerGeneratorViewModel : ViewModel() { companion object { @@ -38,6 +40,11 @@ class PlayerGeneratorViewModel : ViewModel() { private val _currentSubtitleYear = MutableLiveData(null) val currentSubtitleYear: LiveData = _currentSubtitleYear + /** + * Save the Episode ID to prevent starting multiple link loading Jobs when preloading links. + */ + private var currentLoadingEpisodeId: Int? = null + fun setSubtitleYear(year: Int?) { _currentSubtitleYear.postValue(year) } @@ -72,18 +79,32 @@ class PlayerGeneratorViewModel : ViewModel() { } fun preLoadNextLinks() { + val id = getId() + // Do not preload if already loading + if (id == currentLoadingEpisodeId) return + Log.i(TAG, "preLoadNextLinks") currentJob?.cancel() - currentJob = viewModelScope.launchSafe { - if (generator?.hasCache == true && generator?.hasNext() == true) { - safeApiCall { - generator?.generateLinks( - type = LoadType.InApp, - clearCache = false, - callback = {}, - subtitleCallback = {}, - offset = 1 - ) + currentLoadingEpisodeId = id + + currentJob = viewModelScope.launch { + try { + if (generator?.hasCache == true && generator?.hasNext() == true) { + safeApiCall { + generator?.generateLinks( + type = LoadType.InApp, + clearCache = false, + callback = {}, + subtitleCallback = {}, + offset = 1 + ) + } + } + } catch (t: Throwable) { + logError(t) + } finally { + if (currentLoadingEpisodeId == id) { + currentLoadingEpisodeId = null } } } @@ -162,14 +183,14 @@ class PlayerGeneratorViewModel : ViewModel() { // load more data _loadingLinks.postValue(Resource.Loading()) val loadingState = safeApiCall { - generator?.generateLinks(type = type,clearCache = clearCache, callback = { + generator?.generateLinks(type = type, clearCache = clearCache, callback = { currentLinks.add(it) // Clone to prevent ConcurrentModificationException normalSafeApiCall { // Extra normalSafeApiCall since .toSet() iterates. _currentLinks.postValue(currentLinks.toSet()) } - }, subtitleCallback = { + }, subtitleCallback = { currentSubs.add(it) normalSafeApiCall { _currentSubs.postValue(currentSubs.toSet()) From 0d2a19b350021427922d7419f485ef1c46550366 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 17 Sep 2023 20:38:59 +0200 Subject: [PATCH 104/441] bump --- 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 66ba16c6..ca99894d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,8 +58,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 60 - versionName = "4.1.9" + versionCode = 61 + versionName = "4.1.10" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") From 2ae5b6cefbaa85fffaa12838a87d5623e15e73ca Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 18 Sep 2023 22:28:26 +0200 Subject: [PATCH 105/441] fixed the fucking updater :skull: --- app/build.gradle.kts | 4 ++-- .../lagradost/cloudstream3/utils/InAppUpdater.kt | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ca99894d..0f815f8b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,8 +58,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 61 - versionName = "4.1.10" + versionCode = 62 + versionName = "4.2.0" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") 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 b2c4aa5c..0dce0b2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -109,18 +109,19 @@ class InAppUpdater { releases.sortedWith(compareBy { versionRegex.find(it.name)?.groupValues?.get(2) }).toList().lastOrNull()*/ - val found = + val foundList = response.filter { rel -> !rel.prerelease }.sortedWith(compareBy { release -> - release.assets.filter { it.content_type == "application/vnd.android.package-archive" } - .getOrNull(0)?.name?.let { it1 -> + release.assets.firstOrNull { it.content_type == "application/vnd.android.package-archive" }?.name?.let { it1 -> versionRegex.find( it1 - )?.groupValues?.get(2) + )?.groupValues?.let { + it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() + } } - }).toList().lastOrNull() - + }).toList() + val found = foundList.lastOrNull() val foundAsset = found?.assets?.getOrNull(0) val currentVersion = packageName?.let { packageManager.getPackageInfo( From 15333123cd298357baf85fa2c5f044ada60481d6 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Mon, 18 Sep 2023 21:22:39 +0000 Subject: [PATCH 106/441] TV UI fixes (#612) * TV UI fixes --- .../cloudstream3/ui/result/ActorAdaptor.kt | 26 +++++++++++--- .../ui/result/ResultFragmentTv.kt | 35 +++++++++++++------ .../ui/settings/SettingsFragment.kt | 5 +++ .../main/res/layout/fragment_search_tv.xml | 2 +- 4 files changed, 51 insertions(+), 17 deletions(-) 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 531cb5d2..7b743388 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 @@ -3,6 +3,8 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.IdRes +import androidx.annotation.LayoutRes import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -12,7 +14,10 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.CastItemBinding import com.lagradost.cloudstream3.utils.UIHelper.setImage -class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerView.Adapter() { +class ActorAdaptor( + private var nextFocusUpId: Int? = null, + private val focusCallback: (View?) -> Unit = {} +) : RecyclerView.Adapter() { data class ActorMetaData( var isInverted: Boolean, val actor: ActorData, @@ -22,7 +27,8 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return CardViewHolder( - CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), focusCallback + CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + focusCallback ) } @@ -64,10 +70,10 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV } } - private class CardViewHolder + private inner class CardViewHolder constructor( val binding: CastItemBinding, - private val focusCallback : (View?) -> Unit = {} + private val focusCallback: (View?) -> Unit = {} ) : RecyclerView.ViewHolder(binding.root) { @@ -78,8 +84,18 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV Pair(actor.voiceActor?.image, actor.actor.image) } + // Fix tv focus escaping the recyclerview + if (position == 0) { + itemView.nextFocusLeftId = R.id.result_cast_items + } else if ((position - 1) == itemCount) { + itemView.nextFocusRightId = R.id.result_cast_items + } + nextFocusUpId?.let { + itemView.nextFocusUpId = it + } + itemView.setOnFocusChangeListener { v, hasFocus -> - if(hasFocus) { + if (hasFocus) { focusCallback(v) } } 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 c40d995b..4503cb88 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 @@ -114,10 +114,20 @@ class ResultFragmentTv : Fragment() { } } - private fun hasNoFocus(): Boolean { - val focus = activity?.currentFocus - if (focus == null || !focus.isVisible) return true - return focus == binding?.resultRoot +// private fun hasNoFocus(): Boolean { +// val focus = activity?.currentFocus +// if (focus == null || !focus.isVisible) return true +// return focus == binding?.resultRoot +// } + + /** + * Force focus any play button. + * Note that this will steal any focus if the episode loading is too slow (unlikely). + */ + private fun focusPlayButton() { + binding?.resultPlayMovie?.requestFocus() + binding?.resultPlaySeries?.requestFocus() + binding?.resultResumeSeries?.requestFocus() } private fun setRecommendations(rec: List?, validApiName: String?) { @@ -413,7 +423,13 @@ class ResultFragmentTv : Fragment() { setHorizontal() } - resultCastItems.adapter = ActorAdaptor { + val aboveCast = listOf( + binding?.resultEpisodesShow, + binding?.resultBookmarkButton, + ).firstOrNull { + it?.isVisible == true + } + resultCastItems.adapter = ActorAdaptor(aboveCast?.id) { toggleEpisodes(false) } } @@ -454,9 +470,7 @@ class ResultFragmentTv : Fragment() { resultPlaySeries.isVisible = false resultResumeSeries.isVisible = true - if (hasNoFocus()) { - resultResumeSeries.requestFocus() - } + focusPlayButton() resultResumeSeries.text = if (resume.isMovie) context?.getString(R.string.play_movie_button) else context?.getNameFull( @@ -539,9 +553,7 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } - if (hasNoFocus()) { - resultPlayMovie.requestFocus() - } + focusPlayButton() } } } @@ -663,6 +675,7 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } + focusPlayButton() } /* 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 e53fa91a..4895b0d2 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 @@ -205,6 +205,11 @@ class SettingsFragment : Fragment() { } } } + + // Default focus on TV + if (isTrueTv) { + settingsGeneral.requestFocus() + } } } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index 4c4af404..5fec8c6a 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -99,7 +99,7 @@ android:id="@+id/search_autofit_results" android:layout_width="match_parent" android:layout_height="match_parent" - + android:layout_marginStart="@dimen/navbar_width" android:background="?attr/primaryBlackBackground" android:clipToPadding="false" android:descendantFocusability="afterDescendants" From 527a6388a96a4fa961e1544e6b54acdf6a6f5ee5 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 21 Sep 2023 22:46:23 +0200 Subject: [PATCH 107/441] small fix --- .../lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 4503cb88..7c784a43 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 @@ -648,6 +648,9 @@ class ResultFragmentTv : Fragment() { .show() } } + + // Used to request focus the first time the episodes are loaded. + var hasLoadedEpisodesOnce = false observeNullable(viewModel.episodes) { episodes -> binding?.apply { resultEpisodes.isVisible = episodes is Resource.Success @@ -675,7 +678,10 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } - focusPlayButton() + if (!hasLoadedEpisodesOnce) { + hasLoadedEpisodesOnce = true + focusPlayButton() + } } /* From d4fff7cee67d8f7b5e610caf98c5d1816cabac83 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:50:31 +0000 Subject: [PATCH 108/441] Add homepage search on TV (#618) * Add search button on homepage for TV --- .../lagradost/cloudstream3/MainActivity.kt | 1 + .../ui/home/HomeParentItemAdapterPreview.kt | 5 ++++ .../ui/quicksearch/QuickSearchFragment.kt | 5 ++++ app/src/main/res/layout/fragment_home.xml | 16 +++++++++++++ .../main/res/layout/fragment_home_head_tv.xml | 20 ++++++++++++++-- app/src/main/res/layout/fragment_home_tv.xml | 24 +++++++++++++++++-- 6 files changed, 67 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 263a40f0..627893c3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -497,6 +497,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { R.id.navigation_results_phone, R.id.navigation_results_tv, R.id.navigation_player, + R.id.navigation_quick_search, ).contains(destination.id) binding?.navHostFragment?.apply { 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 1d8e1399..d7956f39 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 @@ -461,6 +461,11 @@ class HomeParentItemAdapterPreview( } } + homePreviewSearchButton.setOnClickListener { _ -> + // Open blank screen. + viewModel.queryTextSubmit("") + } + // This makes the hidden next buttons only available when on the info button // Otherwise you might be able to go to the next item without being at the info button homePreviewInfoBtt.setOnFocusChangeListener { _, hasFocus -> 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 89a09ae2..53c7c2fa 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 @@ -33,6 +33,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.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar @@ -269,6 +270,10 @@ class QuickSearchFragment : Fragment() { activity?.popCurrentPage() } + if (isTrueTvSettings()) { + binding?.quickSearch?.requestFocus() + } + arguments?.getString(AUTOSEARCH_KEY)?.let { binding?.quickSearch?.setQuery(it, true) arguments?.remove(AUTOSEARCH_KEY) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index ac660ccd..36cb5f42 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -114,6 +114,22 @@ android:nextFocusRight="@id/home_switch_account" /> + + + + + android:nextFocusRight="@id/home_preview_search_button" + android:nextFocusDown="@id/home_preview_play_btt" /> + + Date: Mon, 25 Sep 2023 09:40:58 +0000 Subject: [PATCH 109/441] Fix episode layout when using RTL language (#631) * Fix episode layout when using RTL language --- .../lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 6 ++---- app/src/main/res/layout/fragment_result_tv.xml | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) 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 7c784a43..5e4869cc 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 @@ -216,11 +216,9 @@ class ResultFragmentTv : Fragment() { episodesShadow.fade(show) episodeHolderTv.fade(show) if (episodesShadow.isRtl()) { - episodesShadow.scaleX = -1.0f - episodesShadow.scaleY = -1.0f + episodesShadowBackground.scaleX = -1f } else { - episodesShadow.scaleX = 1.0f - episodesShadow.scaleY = 1.0f + episodesShadowBackground.scaleX = 1f } } } diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index a143fbda..feaf6fbc 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -572,6 +572,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit app:layout_constraintTop_toTopOf="@+id/shadow_space_1" /> Date: Mon, 25 Sep 2023 17:18:05 +0530 Subject: [PATCH 110/441] refactor: speedostream and newpipe, tmdb update (#632) * migrate speedostream (yomovies) * update tmdb and newpipe --- app/build.gradle.kts | 14 ++++++-------- .../{SpeedoStream.kt => Minoplres.kt} | 17 ++++------------- .../cloudstream3/utils/ExtractorApi.kt | 8 ++------ 3 files changed, 12 insertions(+), 27 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/extractors/{SpeedoStream.kt => Minoplres.kt} (74%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0f815f8b..f13095fb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -171,7 +171,7 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test:core") - //implementation("io.karn:khttp-android:0.1.2") //okhttp instead + // implementation("io.karn:khttp-android:0.1.2") //okhttp instead // implementation("org.jsoup:jsoup:1.13.1") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") @@ -199,8 +199,6 @@ dependencies { // Custom ffmpeg extension for audio codecs implementation("com.github.recloudstream:media-ffmpeg:1.1.0") - //implementation("com.google.android.exoplayer:extension-leanback:2.14.0") - // Bug reports implementation("ch.acra:acra-core:5.11.0") implementation("ch.acra:acra-toast:5.11.0") @@ -214,13 +212,13 @@ dependencies { // subtitle color picker implementation("com.jaredrummler:colorpicker:1.1.0") - //run JS + // run JS // do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not // available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown implementation("org.mozilla:rhino:1.7.13") // TorrentStream - //implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0") + // implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0") // Downloading implementation("androidx.work:work-runtime:2.8.1") @@ -236,11 +234,11 @@ dependencies { implementation("com.github.LagradOst:SafeFile:0.0.5") // API because cba maintaining it myself - implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") + implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0") implementation("com.github.discord:OverlappingPanels:0.1.5") // debugImplementation because LeakCanary should only run in debug builds. - //debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") + // debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") // for shimmer when loading implementation("com.facebook.shimmer:shimmer:0.5.0") @@ -252,7 +250,7 @@ dependencies { // newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev // this should be updated frequently to avoid trailer fu*kery - implementation("com.github.teamnewpipe:NewPipeExtractor:1f08d28") + implementation("com.github.teamnewpipe:NewPipeExtractor:917554a") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") // Library/extensions searching with Levenshtein distance diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt similarity index 74% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt index 213ecdf3..702501a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt @@ -7,21 +7,12 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper -class SpeedoStream2 : SpeedoStream() { - override val mainUrl = "https://speedostream.mom" -} +open class Minoplres : ExtractorApi() { -class SpeedoStream1 : SpeedoStream() { - override val mainUrl = "https://speedostream.pm" -} - -open class SpeedoStream : ExtractorApi() { - override val name = "SpeedoStream" - override val mainUrl = "https://speedostream.bond" + override val name = "Minoplres" // formerly SpeedoStream override val requiresReferer = true - - // .bond, .pm, .mom redirect to .bond - private val hostUrl = "https://speedostream.bond" + 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() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 5edff7a1..9db62dc8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -82,6 +82,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream import com.lagradost.cloudstream3.extractors.Mcloud import com.lagradost.cloudstream3.extractors.Megacloud import com.lagradost.cloudstream3.extractors.Meownime +import com.lagradost.cloudstream3.extractors.Minoplres import com.lagradost.cloudstream3.extractors.MixDrop import com.lagradost.cloudstream3.extractors.MixDropBz import com.lagradost.cloudstream3.extractors.MixDropCh @@ -118,9 +119,6 @@ import com.lagradost.cloudstream3.extractors.Sbthe import com.lagradost.cloudstream3.extractors.Sendvid import com.lagradost.cloudstream3.extractors.ShaveTape import com.lagradost.cloudstream3.extractors.Solidfiles -import com.lagradost.cloudstream3.extractors.SpeedoStream -import com.lagradost.cloudstream3.extractors.SpeedoStream1 -import com.lagradost.cloudstream3.extractors.SpeedoStream2 import com.lagradost.cloudstream3.extractors.Ssbstream import com.lagradost.cloudstream3.extractors.StreamM4u import com.lagradost.cloudstream3.extractors.StreamSB @@ -748,9 +746,7 @@ val extractorApis: MutableList = arrayListOf( Vido(), Linkbox(), Acefile(), - SpeedoStream(), - SpeedoStream1(), - SpeedoStream2(), + Minoplres(), // formerly SpeedoStream Zorofile(), Embedgram(), Mvidoo(), From 74060e7da32e0f8af23051149b383c92a6b1efb2 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Mon, 25 Sep 2023 18:48:35 +0700 Subject: [PATCH 111/441] Chillx: fix key (#628) --- .../main/java/com/lagradost/cloudstream3/extractors/Chillx.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index bcf8848c..a9fafc39 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -29,7 +29,7 @@ open class Chillx : ExtractorApi() { override val requiresReferer = true companion object { - private const val KEY = "m4H6D9%0\$N&F6rQ&" + private const val KEY = "eN0^>\$^#M08uFv%c" } override suspend fun getUrl( From 0351053d809ef9e86c5faabf29702c2c50d9409c Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Mon, 25 Sep 2023 22:57:18 +0200 Subject: [PATCH 112/441] Readded downloads to tv --- app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 627893c3..d5187029 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -547,8 +547,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv // Hide downloads on TV - navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv - navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + //navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + //navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv } } From 194678c419343cc8c441bf7946906b039d6e47a6 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 28 Sep 2023 13:21:03 +0300 Subject: [PATCH 113/441] Player Source & Subs navigation change (#633) --- app/src/main/res/layout/player_select_source_and_subs.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/player_select_source_and_subs.xml b/app/src/main/res/layout/player_select_source_and_subs.xml index 7351a41f..f5eab1e7 100644 --- a/app/src/main/res/layout/player_select_source_and_subs.xml +++ b/app/src/main/res/layout/player_select_source_and_subs.xml @@ -66,8 +66,8 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" - android:nextFocusLeft="@id/sort_subtitles" - android:nextFocusRight="@id/apply_btt" + android:nextFocusRight="@id/sort_subtitles" + android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" tools:layout_height="100dp" tools:listitem="@layout/sort_bottom_single_choice" /> @@ -140,7 +140,8 @@ android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" android:nextFocusLeft="@id/sort_providers" - android:nextFocusRight="@id/cancel_btt" + android:nextFocusRight="@id/apply_btt" + android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" tools:layout_height="200dp" tools:listfooter="@layout/sort_bottom_footer_add_choice" From 16c2290090a1becb4edab44bf85a54837d6007f0 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 28 Sep 2023 13:22:51 +0300 Subject: [PATCH 114/441] Amazon FireTV focus fixes (#635) * Fix quick search button focus * Switch profile button focus * Cast & Recommendations focus * Player: Profiles settings focus * Player: Subtitles encoding settings focus * profile selection: card item focus * Search history item selectable & deleteable * Search: search filter button next focus fix --- app/src/main/res/layout/cast_item.xml | 2 +- app/src/main/res/layout/fragment_home_head_tv.xml | 2 ++ app/src/main/res/layout/fragment_search_tv.xml | 2 +- app/src/main/res/layout/player_quality_profile_item.xml | 1 + app/src/main/res/layout/player_select_source_and_subs.xml | 4 ++++ app/src/main/res/layout/search_history_item.xml | 4 ++-- 6 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/cast_item.xml b/app/src/main/res/layout/cast_item.xml index 54df59a8..c09cecfa 100644 --- a/app/src/main/res/layout/cast_item.xml +++ b/app/src/main/res/layout/cast_item.xml @@ -34,7 +34,7 @@ android:id="@+id/actor_image" android:layout_width="match_parent" - + android:focusable="true" android:layout_height="match_parent" android:contentDescription="@string/episode_poster_img_des" android:scaleType="centerCrop" diff --git a/app/src/main/res/layout/fragment_home_head_tv.xml b/app/src/main/res/layout/fragment_home_head_tv.xml index f9ea6974..05cb3a41 100644 --- a/app/src/main/res/layout/fragment_home_head_tv.xml +++ b/app/src/main/res/layout/fragment_home_head_tv.xml @@ -55,6 +55,7 @@ android:layout_gravity="end" android:background="@drawable/player_button_tv_attr_no_bg" android:contentDescription="@string/search" + android:focusable="true" android:nextFocusLeft="@id/home_preview_change_api" android:nextFocusRight="@id/home_preview_switch_account" android:nextFocusDown="@id/home_preview_info_btt" @@ -70,6 +71,7 @@ android:layout_gravity="end" android:background="@drawable/player_button_tv_attr_no_bg" android:contentDescription="@string/account" + android:focusable="true" android:nextFocusLeft="@id/home_preview_search_button" android:nextFocusRight="@id/home_preview_switch_account" android:nextFocusDown="@id/home_preview_info_btt" diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index 5fec8c6a..b3f88cd2 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -84,7 +84,7 @@ android:nextFocusLeft="@id/main_search" android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" - android:nextFocusDown="@id/search_autofit_results" + android:nextFocusDown="@id/tvtypes_chips_scroll" android:src="@drawable/ic_baseline_tune_24" app:tint="?attr/textColor" /> diff --git a/app/src/main/res/layout/player_quality_profile_item.xml b/app/src/main/res/layout/player_quality_profile_item.xml index 0eab2407..5178a12f 100644 --- a/app/src/main/res/layout/player_quality_profile_item.xml +++ b/app/src/main/res/layout/player_quality_profile_item.xml @@ -10,6 +10,7 @@ android:id="@+id/card_view" android:layout_width="0dp" android:layout_height="0dp" + android:focusable="true" app:layout_constraintDimensionRatio="1" android:layout_marginStart="10dp" android:animateLayoutChanges="true" diff --git a/app/src/main/res/layout/player_select_source_and_subs.xml b/app/src/main/res/layout/player_select_source_and_subs.xml index f5eab1e7..6bf8006b 100644 --- a/app/src/main/res/layout/player_select_source_and_subs.xml +++ b/app/src/main/res/layout/player_select_source_and_subs.xml @@ -27,6 +27,7 @@ android:layout_height="wrap_content" android:layout_rowWeight="1" android:layout_marginTop="10dp" + android:focusable="true" android:foreground="@drawable/outline_drawable_forced" android:gravity="center_vertical" android:orientation="horizontal"> @@ -66,6 +67,7 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" + android:nextFocusUp="@id/profiles_click_settings" android:nextFocusRight="@id/sort_subtitles" android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" @@ -94,6 +96,7 @@ android:layout_height="wrap_content" android:layout_rowWeight="1" android:layout_marginTop="10dp" + android:focusable="true" android:foreground="@drawable/outline_drawable_forced" android:orientation="horizontal" android:paddingTop="10dp" @@ -139,6 +142,7 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" + android:nextFocusUp="@id/subtitles_click_settings" android:nextFocusLeft="@id/sort_providers" android:nextFocusRight="@id/apply_btt" android:nextFocusDown="@id/apply_btt" diff --git a/app/src/main/res/layout/search_history_item.xml b/app/src/main/res/layout/search_history_item.xml index 3e9ee833..4c50d0c0 100644 --- a/app/src/main/res/layout/search_history_item.xml +++ b/app/src/main/res/layout/search_history_item.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/outline_drawable_less" - + android:focusable="true" android:nextFocusRight="@id/home_history_remove" android:orientation="horizontal"> diff --git a/app/src/main/res/layout/player_select_source_priority.xml b/app/src/main/res/layout/player_select_source_priority.xml index 86a8a756..2af3c339 100644 --- a/app/src/main/res/layout/player_select_source_priority.xml +++ b/app/src/main/res/layout/player_select_source_priority.xml @@ -42,8 +42,8 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_less" - android:nextFocusLeft="@id/sort_subtitles" - android:nextFocusRight="@id/apply_btt" + android:nextFocusRight="@id/sort_subtitles" + android:nextFocusDown="@id/profile_text_editable" android:requiresFadingEdge="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:layout_height="100dp" @@ -92,6 +92,8 @@ android:layout_height="50dp" android:background="?attr/selectableItemBackgroundBorderless" android:padding="12dp" + android:focusable="true" + android:nextFocusLeft="@id/sort_sources" android:src="@drawable/baseline_help_outline_24" android:contentDescription="@string/help" /> @@ -115,8 +117,10 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_less" - android:nextFocusLeft="@id/sort_providers" - android:nextFocusRight="@id/cancel_btt" + android:nextFocusLeft="@id/sort_sources" + android:nextFocusRight="@id/apply_btt" + android:nextFocusUp="@id/help_btt" + android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:layout_height="200dp" diff --git a/app/src/main/res/layout/subtitle_offset.xml b/app/src/main/res/layout/subtitle_offset.xml index a98fafef..c17c5eff 100644 --- a/app/src/main/res/layout/subtitle_offset.xml +++ b/app/src/main/res/layout/subtitle_offset.xml @@ -35,6 +35,7 @@ android:layout_gravity="center" android:layout_weight="1" android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" android:nextFocusRight="@id/subtitle_offset_subtract" android:padding="10dp" android:src="@drawable/ic_baseline_keyboard_arrow_left_24" @@ -48,6 +49,7 @@ android:layout_gravity="center" android:layout_weight="1" android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" android:nextFocusLeft="@id/subtitle_offset_subtract_more" android:padding="10dp" android:src="@drawable/baseline_remove_24" @@ -70,6 +72,7 @@ android:layout_gravity="center" android:layout_weight="1" android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" android:nextFocusRight="@id/subtitle_offset_add_more" android:padding="10dp" android:src="@drawable/ic_baseline_add_24" @@ -83,7 +86,9 @@ android:layout_gravity="center" android:layout_weight="1" android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" android:nextFocusLeft="@id/subtitle_offset_add" + android:nextFocusDown="@id/apply_btt" android:padding="10dp" android:src="@drawable/ic_baseline_keyboard_arrow_right_24" app:tint="?attr/white" diff --git a/app/src/main/res/layout/who_is_watching_account.xml b/app/src/main/res/layout/who_is_watching_account.xml index afa1a2a7..4970d004 100644 --- a/app/src/main/res/layout/who_is_watching_account.xml +++ b/app/src/main/res/layout/who_is_watching_account.xml @@ -11,6 +11,7 @@ android:foreground="?attr/selectableItemBackgroundBorderless" app:cardCornerRadius="@dimen/rounded_image_radius" android:layout_margin="5dp" + android:focusable="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="1" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/who_is_watching_account_add.xml b/app/src/main/res/layout/who_is_watching_account_add.xml index ed67e144..91c7e419 100644 --- a/app/src/main/res/layout/who_is_watching_account_add.xml +++ b/app/src/main/res/layout/who_is_watching_account_add.xml @@ -11,6 +11,7 @@ android:foreground="?attr/selectableItemBackgroundBorderless" app:cardCornerRadius="@dimen/rounded_image_radius" android:layout_margin="5dp" + android:focusable="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="1" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/who_is_watching_account_edit.xml b/app/src/main/res/layout/who_is_watching_account_edit.xml index 74553517..cec37a4f 100644 --- a/app/src/main/res/layout/who_is_watching_account_edit.xml +++ b/app/src/main/res/layout/who_is_watching_account_edit.xml @@ -88,6 +88,7 @@ android:layout_width="match_parent" android:layout_height="60dp" android:layout_gravity="center" + android:focusable="true" android:contentDescription="@string/preview_background_img_des" android:scaleType="centerCrop" android:src="@drawable/profile_bg_blue" /> From bd05a67f260263f1e4d49580cd40fc587b354fd0 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 2 Oct 2023 17:44:06 +0200 Subject: [PATCH 116/441] preview seekbar --- app/build.gradle.kts | 2 + .../ui/player/AbstractPlayerFragment.kt | 42 ++++- .../cloudstream3/ui/player/CS3IPlayer.kt | 53 ++++++- .../cloudstream3/ui/player/GeneratorPlayer.kt | 1 + .../cloudstream3/ui/player/IPlayer.kt | 7 +- .../ui/player/PreviewGenerator.kt | 147 ++++++++++++++++++ .../ui/result/ResultFragmentPhone.kt | 3 +- app/src/main/res/drawable/video_frame.xml | 10 ++ .../main/res/layout/player_custom_layout.xml | 63 ++++++-- .../res/layout/player_custom_layout_tv.xml | 62 ++++++-- .../main/res/layout/trailer_custom_layout.xml | 78 +++++++--- app/src/main/res/values/dimens.xml | 2 +- 12 files changed, 413 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt create mode 100644 app/src/main/res/drawable/video_frame.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f13095fb..9f484c48 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -258,6 +258,8 @@ dependencies { // color palette for images -> colors implementation("androidx.palette:palette-ktx:1.0.0") + // seekbar https://github.com/rubensousa/PreviewSeekBar + implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") } tasks.register("androidSourcesJar", Jar::class) { 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 8388e58f..862504a1 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 @@ -18,10 +18,9 @@ import android.widget.ProgressBar import android.widget.Toast import androidx.annotation.LayoutRes import androidx.annotation.StringRes +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.preference.PreferenceManager -import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession @@ -30,6 +29,10 @@ import androidx.media3.ui.DefaultTimeBar import androidx.media3.ui.PlayerView import androidx.media3.ui.SubtitleView import androidx.media3.ui.TimeBar +import androidx.preference.PreferenceManager +import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat +import com.github.rubensousa.previewseekbar.PreviewBar +import com.github.rubensousa.previewseekbar.media3.PreviewTimeBar import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode @@ -454,6 +457,41 @@ abstract class AbstractPlayerFragment( ) if (player is CS3IPlayer) { + // preview bar + val progressBar : PreviewTimeBar? = playerView?.findViewById(R.id.exo_progress) + val previewImageView : ImageView? = playerView?.findViewById(R.id.previewImageView) + val previewFrameLayout : FrameLayout? = playerView?.findViewById(R.id.previewFrameLayout) + if(progressBar != null && previewImageView != null && previewFrameLayout != null) { + var resume = false + progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener { + override fun onScrubStart(previewBar: PreviewBar?) { + progressBar.isPreviewEnabled = player.hasPreview() + resume = player.getIsPlaying() + if (resume) player.handleEvent( + CSPlayerEvent.Pause, + PlayerEventSource.Player + ) + } + + override fun onScrubMove( + previewBar: PreviewBar?, + progress: Int, + fromUser: Boolean + ) { + } + + override fun onScrubStop(previewBar: PreviewBar?) { + if (resume) player.handleEvent(CSPlayerEvent.Play, PlayerEventSource.Player) + } + }) + progressBar.attachPreviewView(previewFrameLayout) + progressBar.setPreviewLoader { currentPosition, max -> + val bitmap = player.getPreview(currentPosition.toFloat().div(max.toFloat())) + previewImageView.isGone = bitmap == null + previewImageView.setImageBitmap(bitmap) + } + } + subView = playerView?.findViewById(R.id.exo_subtitles) subStyle = SubtitlesFragment.getCurrentSavedStyle() player.initSubtitles(subView, subtitleHolder, subStyle) 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 331cfb73..946743a3 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 @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player import android.annotation.SuppressLint import android.content.Context +import android.graphics.Bitmap import android.net.Uri import android.os.Handler import android.os.Looper @@ -88,7 +89,9 @@ class CS3IPlayer : IPlayer { private var exoPlayer: ExoPlayer? = null set(value) { // If the old value is not null then the player has not been properly released. - debugAssert({ field != null && value != null }, { "Previous player instance should be released!" }) + debugAssert( + { field != null && value != null }, + { "Previous player instance should be released!" }) field = value } @@ -96,6 +99,8 @@ class CS3IPlayer : IPlayer { var simpleCacheSize = 0L var videoBufferMs = 0L + private val imageGenerator = PreviewGenerator() + private val seekActionTime = 30000L private var ignoreSSL: Boolean = true @@ -182,6 +187,14 @@ class CS3IPlayer : IPlayer { subtitleHelper.initSubtitles(subView, subHolder, style) } + override fun getPreview(fraction: Float): Bitmap? { + return imageGenerator.getPreviewImage(fraction) + } + + override fun hasPreview(): Boolean { + return imageGenerator.hasPreview() + } + override fun loadPlayer( context: Context, sameEpisode: Boolean, @@ -190,7 +203,8 @@ class CS3IPlayer : IPlayer { startPosition: Long?, subtitles: Set, subtitle: SubtitleData?, - autoPlay: Boolean? + autoPlay: Boolean?, + preview : Boolean, ) { Log.i(TAG, "loadPlayer") if (sameEpisode) { @@ -210,9 +224,27 @@ class CS3IPlayer : IPlayer { // release the current exoplayer and cache releasePlayer() if (link != null) { + // only video support atm + if (link.type == ExtractorLinkType.VIDEO && preview) { + val headers = if (link.referer.isBlank()) { + link.headers + } else { + mapOf("referer" to link.referer) + link.headers + } + imageGenerator.load(sameEpisode, link.url, headers) + } else { + imageGenerator.clear(sameEpisode) + } loadOnlinePlayer(context, link) } else if (data != null) { + if (preview) { + imageGenerator.load(sameEpisode, context, data.uri) + } else { + imageGenerator.clear(sameEpisode) + } loadOfflinePlayer(context, data) + } else { + throw IllegalArgumentException("Requires link or uri") } } @@ -494,6 +526,7 @@ class CS3IPlayer : IPlayer { } override fun release() { + imageGenerator.release() releasePlayer() } @@ -871,8 +904,20 @@ class CS3IPlayer : IPlayer { CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source) CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source) - CSPlayerEvent.NextEpisode -> event(EpisodeSeekEvent(offset = 1, source = source)) - CSPlayerEvent.PrevEpisode -> event(EpisodeSeekEvent(offset = -1, source = source)) + CSPlayerEvent.NextEpisode -> event( + EpisodeSeekEvent( + offset = 1, + source = source + ) + ) + + CSPlayerEvent.PrevEpisode -> event( + EpisodeSeekEvent( + offset = -1, + source = source + ) + ) + CSPlayerEvent.SkipCurrentChapter -> { //val dur = this@CS3IPlayer.getDuration() ?: return@apply getCurrentTimestamp()?.let { lastTimeStamp -> 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 b2542ffa..1c751897 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 @@ -180,6 +180,7 @@ class GeneratorPlayer : FullScreenPlayer() { (if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle( currentSubs, settings = true, downloads = true ), + preview = isFullScreenPlayer ) } 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 ec006234..a08360ae 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 @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.player 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 @@ -246,11 +247,15 @@ interface IPlayer { startPosition: Long? = null, subtitles: Set, subtitle: SubtitleData?, - autoPlay: Boolean? = true + autoPlay: Boolean? = true, + preview : Boolean = true, ) fun reloadPlayer(context: Context) + fun getPreview(fraction : Float) : Bitmap? + fun hasPreview() : Boolean + fun setActiveSubtitles(subtitles: Set) fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean // returns true if the player requires a reload, null for nothing fun getCurrentPreferredSubtitle(): SubtitleData? diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt new file mode 100644 index 00000000..0f47d009 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -0,0 +1,147 @@ +package com.lagradost.cloudstream3.ui.player + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.util.Log +import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlin.math.absoluteValue +import kotlin.math.ceil +import kotlin.math.log2 + +const val MAX_LOD = 6 +const val MIN_LOD = 3 + +class PreviewGenerator { + // lod = level of detail where the number indicates how many ones there is + // 2^(lod-1) = images + private var loadedLod = 0 + private var loadedImages = 0 + private var images = Array((1 shl MAX_LOD) - 1) { + null + } + + fun hasPreview(): Boolean { + synchronized(images) { + return loadedLod >= MIN_LOD + } + } + + val TAG = "PreviewImg" + + fun getPreviewImage(fraction: Float): Bitmap? { + synchronized(images) { + if (loadedLod < MIN_LOD) { + Log.i(TAG, "Requesting preview for $fraction but $loadedLod < $MIN_LOD") + return null + } + Log.i(TAG, "Requesting preview for $fraction") + + var bestIdx = 0 + var bestDiff = 0.5f.minus(fraction).absoluteValue + + // this should be done mathematically, but for now we just loop all images + for (l in 1..loadedLod + 1) { + val items = (1 shl (l - 1)) + for (i in 0 until items) { + val idx = items - 1 + i + if (idx > loadedImages) { + break + } + val currentFraction = + (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) + val diff = currentFraction.minus(fraction).absoluteValue + if (diff < bestDiff) { + bestDiff = diff + bestIdx = idx + } + } + } + Log.i(TAG, "Best diff found at ${bestDiff * 100}% diff (${bestIdx})") + return images[bestIdx] + } + } + + // also check out https://github.com/wseemann/FFmpegMediaMetadataRetriever + private val retriever: MediaMetadataRetriever = MediaMetadataRetriever() + + fun clear(keepCache: Boolean = false) { + if (keepCache) return + synchronized(images) { + loadedLod = 0 + loadedImages = 0 + images.fill(null) + } + } + + private var currentJob: Job? = null + fun load(keepCache: Boolean, url: String, headers: Map) { + currentJob?.cancel() + currentJob = ioSafe { + Log.i(TAG, "Loading with url = $url headers = $headers") + clear(keepCache) + retriever.setDataSource(url, headers) + start(this) + } + } + + fun load(keepCache: Boolean, context: Context, uri: Uri) { + currentJob?.cancel() + currentJob = ioSafe { + Log.i(TAG, "Loading with uri = $uri") + clear(keepCache) + retriever.setDataSource(context, uri) + start(this) + } + } + + fun release() { + currentJob?.cancel() + clear(false) + } + + @Throws + @WorkerThread + private fun start(scope: CoroutineScope) { + Log.i(TAG, "Started loading preview") + + val durationMs = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() + ?: throw IllegalArgumentException("Bad video duration") + val durationUs = (durationMs * 1000L).toFloat() + //val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: throw IllegalArgumentException("Bad video width") + //val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: throw IllegalArgumentException("Bad video height") + + // log2 # 10s durations in the video ~= how many segments we have + val maxLod = ceil(log2((durationMs / 10_000).toFloat())).toInt().coerceIn(MIN_LOD, MAX_LOD) + + for (l in 1..maxLod) { + val items = (1 shl (l - 1)) + for (i in 0 until items) { + val idx = items - 1 + i // as sum(prev) = cur-1 + // frame = 100 / 2^lod + i * 100 / 2^(lod-1) = duration % where lod is one indexed + val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) + Log.i(TAG, "Generating preview for ${fraction * 100}%") + val frame = durationUs * fraction + val img = retriever.getFrameAtTime( + frame.toLong(), + MediaMetadataRetriever.OPTION_CLOSEST_SYNC + ) + if (!scope.isActive) return + synchronized(images) { + images[idx] = img + loadedImages = maxOf(loadedImages,idx) + } + } + + synchronized(images) { + loadedLod = maxOf(loadedLod, l) + } + } + } +} \ No newline at end of file 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 ef2ed0df..e5f16dd5 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 @@ -151,7 +151,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { startPosition = 0L, subtitles = emptySet(), subtitle = null, - autoPlay = false + autoPlay = false, + preview = false ) true } ?: run { diff --git a/app/src/main/res/drawable/video_frame.xml b/app/src/main/res/drawable/video_frame.xml new file mode 100644 index 00000000..19fcf26d --- /dev/null +++ b/app/src/main/res/drawable/video_frame.xml @@ -0,0 +1,10 @@ + + + + + + + \ 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 5592f3a6..0f76e4dd 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -86,7 +86,6 @@ + + android:importantForAccessibility="no" + android:visibility="gone" /> + + + - - - + + + + + - - - + - - + tools:ignore="ContentDescription" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" /> - - + + + + + - + - - + - - - + + + + + - - + + - 62dp 50dp - + 1dp \ No newline at end of file From 1d90858f64bd113df71dc68fa8e03f9be640c542 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:04:40 -0600 Subject: [PATCH 117/441] Make search history account specific (#638) * Make search history account specific * Update for clear history --- .../com/lagradost/cloudstream3/ui/search/SearchFragment.kt | 7 ++++--- .../lagradost/cloudstream3/ui/search/SearchViewModel.kt | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) 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 bdf82377..845c36ef 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 @@ -55,6 +55,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar @@ -398,7 +399,7 @@ class SearchFragment : Fragment() { DialogInterface.OnClickListener { _, which -> when (which) { DialogInterface.BUTTON_POSITIVE -> { - removeKeys(SEARCH_HISTORY_KEY) + removeKeys("$currentAccount/$SEARCH_HISTORY_KEY") searchViewModel.updateHistory() } DialogInterface.BUTTON_NEGATIVE -> { @@ -510,7 +511,7 @@ class SearchFragment : Fragment() { binding?.mainSearch?.setQuery(searchItem.searchText, true) } SEARCH_HISTORY_REMOVE -> { - removeKey(SEARCH_HISTORY_KEY, searchItem.key) + removeKey("$currentAccount/$SEARCH_HISTORY_KEY", searchItem.key) searchViewModel.updateHistory() } else -> { @@ -559,4 +560,4 @@ class SearchFragment : Fragment() { .commit()*/ } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index 320687f8..839b9d3f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -14,6 +14,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -64,7 +65,7 @@ class SearchViewModel : ViewModel() { fun updateHistory() = viewModelScope.launch { ioSafe { - val items = getKeys(SEARCH_HISTORY_KEY)?.mapNotNull { + val items = getKeys("$currentAccount/$SEARCH_HISTORY_KEY")?.mapNotNull { getKey(it) }?.sortedByDescending { it.searchedAt } ?: emptyList() _currentHistory.postValue(items) @@ -87,7 +88,7 @@ class SearchViewModel : ViewModel() { if (!isQuickSearch) { val key = query.hashCode().toString() setKey( - SEARCH_HISTORY_KEY, + "$currentAccount/$SEARCH_HISTORY_KEY", key, SearchHistoryItem( searchedAt = System.currentTimeMillis(), @@ -140,4 +141,4 @@ class SearchViewModel : ViewModel() { _searchResponse.postValue(Resource.Success(list)) } } -} \ No newline at end of file +} From 08060314ad94054ed8c42bd38824122bfb24565e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:50:34 +0200 Subject: [PATCH 118/441] preview seekbar m3u8 --- .../cloudstream3/ui/player/CS3IPlayer.kt | 11 +- .../ui/player/PreviewGenerator.kt | 263 +++++++++++++++++- .../cloudstream3/utils/ExtractorApi.kt | 9 + .../cloudstream3/utils/M3u8Helper.kt | 42 ++- 4 files changed, 298 insertions(+), 27 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 946743a3..6256bef6 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 @@ -225,20 +225,15 @@ class CS3IPlayer : IPlayer { releasePlayer() if (link != null) { // only video support atm - if (link.type == ExtractorLinkType.VIDEO && preview) { - val headers = if (link.referer.isBlank()) { - link.headers - } else { - mapOf("referer" to link.referer) + link.headers - } - imageGenerator.load(sameEpisode, link.url, headers) + if (preview) { + imageGenerator.load(link, sameEpisode) } else { imageGenerator.clear(sameEpisode) } loadOnlinePlayer(context, link) } else if (data != null) { if (preview) { - imageGenerator.load(sameEpisode, context, data.uri) + imageGenerator.load(context, data, sameEpisode) } else { imageGenerator.clear(sameEpisode) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index 0f47d009..53699782 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -6,10 +6,18 @@ import android.media.MediaMetadataRetriever import android.net.Uri import android.util.Log import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkType +import com.lagradost.cloudstream3.utils.ExtractorUri +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.M3u8Helper2 import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext import kotlin.math.absoluteValue import kotlin.math.ceil import kotlin.math.log2 @@ -17,7 +25,248 @@ import kotlin.math.log2 const val MAX_LOD = 6 const val MIN_LOD = 3 -class PreviewGenerator { +interface IPreviewGenerator { + fun hasPreview(): Boolean + fun getPreviewImage(fraction: Float): Bitmap? + fun clear(keepCache: Boolean = false) + fun release() +} + +class PreviewGenerator : IPreviewGenerator { + private var currentGenerator: IPreviewGenerator = NoPreviewGenerator() + override fun hasPreview(): Boolean { + return currentGenerator.hasPreview() + } + + override fun getPreviewImage(fraction: Float): Bitmap? { + return try { + currentGenerator.getPreviewImage(fraction) + } catch (t: Throwable) { + logError(t) + null + } + } + + override fun clear(keepCache: Boolean) { + currentGenerator.clear(keepCache) + } + + override fun release() { + currentGenerator.release() + } + + fun load(link: ExtractorLink, keepCache: Boolean) { + val gen = currentGenerator + when (link.type) { + ExtractorLinkType.M3U8 -> { + if (gen is M3u8PreviewGenerator) { + gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) + } else { + currentGenerator.release() + currentGenerator = M3u8PreviewGenerator().apply { + load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) + } + } + } + + ExtractorLinkType.VIDEO -> { + if (gen is Mp4PreviewGenerator) { + gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) + } else { + currentGenerator.release() + currentGenerator = Mp4PreviewGenerator().apply { + load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) + } + } + } + + else -> { + currentGenerator.clear(keepCache) + } + } + } + + fun load(context: Context, link: ExtractorUri, keepCache: Boolean) { + val gen = currentGenerator + if (gen is Mp4PreviewGenerator) { + gen.load(keepCache = keepCache, context = context, uri = link.uri) + } else { + currentGenerator.release() + currentGenerator = Mp4PreviewGenerator().apply { + load(keepCache = keepCache, context = context, uri = link.uri) + } + } + } +} + +class NoPreviewGenerator : IPreviewGenerator { + override fun hasPreview(): Boolean = false + override fun getPreviewImage(fraction: Float): Bitmap? = null + override fun clear(keepCache: Boolean) = Unit + override fun release() = Unit +} + +class M3u8PreviewGenerator : IPreviewGenerator { + // generated images 1:1 to idx of hsl + private var images: Array = arrayOf() + + private val TAG = "PreviewImgM3u8" + + // prefixSum[i] = sum(hsl.ts[0..i].time) + // where [0] = 0, [1] = hsl.ts[0].time aka time at start of segment, do [b] - [a] for range a,b + private var prefixSum: Array = arrayOf() + + // how many images has been generated + private var loadedImages: Int = 0 + + // how many images we can generate in total, == hsl.size ?: 0 + private var totalImages: Int = 0 + + override fun hasPreview(): Boolean { + return totalImages > 0 && loadedImages >= minOf(totalImages, 4) + } + + override fun getPreviewImage(fraction: Float): Bitmap? { + var bestIdx = -1 + var bestDiff = Double.MAX_VALUE + synchronized(images) { + // just find the best one in a for loop, we don't care about bin searching rn + for (i in 0..images.size) { + val diff = prefixSum[i].minus(fraction).absoluteValue + if (diff > bestDiff) { + break + } + if (images[i] != null) { + bestIdx = i + bestDiff = diff + } + } + return images.getOrNull(bestIdx) + } + /* + val targetIndex = prefixSum.binarySearch(target) + var ret = images[targetIndex] + if (ret != null) { + return ret + } + for (i in 0..images.size) { + ret = images.getOrNull(i+targetIndex) ?: + }*/ + } + + override fun clear(keepCache: Boolean) { + synchronized(images) { + currentJob?.cancel() + images = arrayOf() + prefixSum = arrayOf() + loadedImages = 0 + totalImages = 0 + } + } + + override fun release() { + clear() + images = arrayOf() + } + + private var currentJob: Job? = null + fun load(keepCache: Boolean, url: String, headers: Map) { + clear(keepCache) + currentJob?.cancel() + currentJob = ioSafe { + withContext(Dispatchers.IO) { + Log.i(TAG, "Loading with url = $url headers = $headers") + //tmpFile = + // File.createTempFile("video", ".ts", context.cacheDir).apply { + // deleteOnExit() + // } + val retriever = MediaMetadataRetriever() + val hsl = M3u8Helper2.hslLazy( + listOf( + M3u8Helper.M3u8Stream( + streamUrl = url, + headers = headers + ) + ), + selectBest = false + ) + + // no support for encryption atm + if (hsl.isEncrypted) { + totalImages = 0 + return@withContext + } + + // total duration of the entire m3u8 in seconds + val duration = hsl.allTsLinks.sumOf { it.time ?: 0.0 } + val durationInv = 1.0 / duration + + // if the total duration is less then 10s then something is very wrong or + // too short playback to matter + if (duration <= 10.0) { + totalImages = 0 + return@withContext + } + + totalImages = hsl.allTsLinks.size + + // we cant init directly as it is no guarantee of in order + prefixSum = Array(hsl.allTsLinks.size + 1) { 0.0 } + var runningSum = 0.0 + for (i in hsl.allTsLinks.indices) { + runningSum += (hsl.allTsLinks[i].time ?: 0.0) + prefixSum[i + 1] = runningSum * durationInv + } + synchronized(images) { + images = Array(hsl.size) { null } + loadedImages = 0 + } + + val maxLod = ceil(log2(duration)).toInt().coerceIn(MIN_LOD, MAX_LOD) + val count = hsl.allTsLinks.size + for (l in 1..maxLod) { + val items = (1 shl (l - 1)) + for (i in 0 until items) { + val index = (count.div(1 shl l) + (i * count) / items).coerceIn(0, hsl.size) + if (synchronized(images) { images[index] } != null) { + continue + } + Log.i(TAG, "Generating preview for $index") + + val ts = hsl.allTsLinks[index] + try { + retriever.setDataSource(ts.url, hsl.headers) + if (!isActive) { + return@withContext + } + val frame = retriever.getFrameAtTime(0) + if (!isActive) { + return@withContext + } + synchronized(images) { + images[index] = frame + loadedImages += 1 + } + } catch (t: Throwable) { + logError(t) + continue + } + + /* + val buffer = hsl.resolveLinkSafe(index) ?: continue + tmpFile?.writeBytes(buffer) + val buff = FileOutputStream(tmpFile) + retriever.setDataSource(buff.fd) + val frame = retriever.getFrameAtTime(0L)*/ + } + } + + } + } + } +} + +class Mp4PreviewGenerator : IPreviewGenerator { // lod = level of detail where the number indicates how many ones there is // 2^(lod-1) = images private var loadedLod = 0 @@ -26,15 +275,15 @@ class PreviewGenerator { null } - fun hasPreview(): Boolean { + override fun hasPreview(): Boolean { synchronized(images) { return loadedLod >= MIN_LOD } } - val TAG = "PreviewImg" + val TAG = "PreviewImgMp4" - fun getPreviewImage(fraction: Float): Bitmap? { + override fun getPreviewImage(fraction: Float): Bitmap? { synchronized(images) { if (loadedLod < MIN_LOD) { Log.i(TAG, "Requesting preview for $fraction but $loadedLod < $MIN_LOD") @@ -70,7 +319,7 @@ class PreviewGenerator { // also check out https://github.com/wseemann/FFmpegMediaMetadataRetriever private val retriever: MediaMetadataRetriever = MediaMetadataRetriever() - fun clear(keepCache: Boolean = false) { + override fun clear(keepCache: Boolean) { if (keepCache) return synchronized(images) { loadedLod = 0 @@ -100,7 +349,7 @@ class PreviewGenerator { } } - fun release() { + override fun release() { currentJob?.cancel() clear(false) } @@ -135,7 +384,7 @@ class PreviewGenerator { if (!scope.isActive) return synchronized(images) { images[idx] = img - loadedImages = maxOf(loadedImages,idx) + loadedImages = maxOf(loadedImages, idx) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 9db62dc8..d89e67fa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -378,6 +378,15 @@ open class ExtractorLink constructor( val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 val isDash : Boolean get() = type == ExtractorLinkType.DASH + fun getAllHeaders() : Map { + if (referer.isBlank()) { + return headers + } else if (headers.keys.none { it.equals("referer", ignoreCase = true) }) { + return headers + mapOf("referer" to referer) + } + return headers + } + constructor( source: String, name: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 11dfa441..d3fe7162 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -71,7 +71,7 @@ object M3u8Helper2 { private val QUALITY_REGEX = Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""") private val TS_EXTENSION_REGEX = - Regex("""#EXTINF:.*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways + Regex("""#EXTINF:(([0-9]*[.])?[0-9]+|).*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways //Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts private fun absoluteExtensionDetermination(url: String): String? { @@ -122,6 +122,15 @@ object M3u8Helper2 { return result.lastOrNull() } + private fun selectWorst(qualities: List): M3u8Helper.M3u8Stream? { + val result = qualities.sortedBy { + if (it.quality != null && it.quality <= 1080) it.quality else 0 + }.filter { + listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl)) + } + return result.firstOrNull() + } + private fun getParentLink(uri: String): String { val split = uri.split("/").toMutableList() split.removeLast() @@ -173,14 +182,20 @@ object M3u8Helper2 { return list } + data class TsLink( + val url : String, + val time : Double?, + ) + data class LazyHlsDownloadData( private val encryptionData: ByteArray, private val encryptionIv: ByteArray, - private val isEncrypted: Boolean, - private val allTsLinks: List, - private val relativeUrl: String, - private val headers: Map, + val isEncrypted: Boolean, + val allTsLinks: List, + val relativeUrl: String, + val headers: Map, ) { + val size get() = allTsLinks.size suspend fun resolveLinkWhileSafe( @@ -228,9 +243,9 @@ object M3u8Helper2 { @Throws suspend fun resolveLink(index: Int): ByteArray { if (index < 0 || index >= size) throw IllegalArgumentException("index must be in the bounds of the ts") - val url = allTsLinks[index] + val ts = allTsLinks[index] - val tsResponse = app.get(url, headers = headers, verify = false) + val tsResponse = app.get(ts.url, headers = headers, verify = false) val tsData = tsResponse.body.bytes() if (tsData.isEmpty()) throw ErrorLoadingException("no data") @@ -244,15 +259,16 @@ object M3u8Helper2 { @Throws suspend fun hslLazy( - qualities: List + qualities: List, selectBest : Boolean = true ): LazyHlsDownloadData { if (qualities.isEmpty()) throw IllegalArgumentException("qualities must be non empty") - val selected = selectBest(qualities) ?: qualities.first() + val selected = if(selectBest) { selectBest(qualities) } else { selectWorst(qualities) } ?: qualities.first() val headers = selected.headers val streams = qualities.map { m3u8Generation(it, false) }.flatten() // this selects the best quality of the qualities offered, // due to the recursive nature of m3u8, we only go 2 depth - val secondSelection = selectBest(streams.ifEmpty { listOf(selected) }) + val innerStreams = streams.ifEmpty { listOf(selected) } + val secondSelection = if(selectBest) { selectBest(innerStreams) } else { selectWorst(innerStreams) } ?: throw IllegalArgumentException("qualities has no streams") val m3u8Response = @@ -285,12 +301,14 @@ object M3u8Helper2 { } val relativeUrl = getParentLink(secondSelection.streamUrl) val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts -> - val value = ts.groupValues[1] - if (isNotCompleteUrl(value)) { + val time = ts.groupValues[1] + val value = ts.groupValues[3] + val url = if (isNotCompleteUrl(value)) { "$relativeUrl/${value}" } else { value } + TsLink(url = url, time = time.toDoubleOrNull()) }.toList() if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty") From 462073bd747c62ab995ce200b5a55ddce18811ad Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 3 Oct 2023 08:56:38 -0600 Subject: [PATCH 119/441] Make search prefs account specific (#640) --- .../cloudstream3/ui/search/SearchFragment.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 845c36ef..bad78624 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 @@ -194,7 +194,7 @@ class SearchFragment : Fragment() { validAPIs.flatMap { api -> api.supportedTypes }.distinct() ) { list -> if (selectedSearchTypes.toSet() != list.toSet()) { - setKey(SEARCH_PREF_TAGS, selectedSearchTypes) + setKey("$currentAccount/$SEARCH_PREF_TAGS", selectedSearchTypes) selectedSearchTypes.clear() selectedSearchTypes.addAll(list) search(binding?.mainSearch?.query?.toString()) @@ -236,7 +236,7 @@ class SearchFragment : Fragment() { context?.let { ctx -> val validAPIs = ctx.filterProviderByPreferredMedia() selectedApis = ctx.getKey( - SEARCH_PREF_PROVIDERS, + "$currentAccount/$SEARCH_PREF_PROVIDERS", defVal = validAPIs.map { it.name } )!!.toMutableSet() } @@ -287,7 +287,7 @@ class SearchFragment : Fragment() { } fun updateList(types: List) { - setKey(SEARCH_PREF_TAGS, types.map { it.name }) + setKey("$currentAccount/$SEARCH_PREF_TAGS", types.map { it.name }) arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> @@ -312,7 +312,7 @@ class SearchFragment : Fragment() { arrayAdapter.notifyDataSetChanged() } - val selectedSearchTypes = getKey>(SEARCH_PREF_TAGS) + val selectedSearchTypes = getKey>("$currentAccount/$SEARCH_PREF_TAGS") ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } @@ -343,7 +343,7 @@ class SearchFragment : Fragment() { } dialog.setOnDismissListener { - context?.setKey(SEARCH_PREF_PROVIDERS, currentSelectedApis.toList()) + context?.setKey("$currentAccount/$SEARCH_PREF_PROVIDERS", currentSelectedApis.toList()) selectedApis = currentSelectedApis } updateList(selectedSearchTypes.toList()) @@ -354,7 +354,7 @@ class SearchFragment : Fragment() { val settingsManager = context?.let { PreferenceManager.getDefaultSharedPreferences(it) } val isAdvancedSearch = settingsManager?.getBoolean("advanced_search", true) ?: true - selectedSearchTypes = context?.getKey>(SEARCH_PREF_TAGS) + selectedSearchTypes = context?.getKey>("$currentAccount/$SEARCH_PREF_TAGS") ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } ?.toMutableList() ?: mutableListOf(TvType.Movie, TvType.TvSeries) From cc00e73e16459365bb7d412ee07422a354767ad0 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:25:31 -0600 Subject: [PATCH 120/441] Make homepage preferences account specific (#647) * Make homepage preferences account specific * Fix accidentally removed whitespace * Fix in setkey --- .../java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt | 4 ++-- .../com/lagradost/cloudstream3/ui/home/HomeViewModel.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) 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 0797e9a0..ebbb245c 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 @@ -377,7 +377,7 @@ class HomeFragment : Fragment() { var currentApiName = selectedApiName var currentValidApis: MutableList = mutableListOf() - val preSelectedTypes = this.getKey>(HOME_PREF_HOMEPAGE) + val preSelectedTypes = this.getKey>("${DataStoreHelper.currentAccount}/$HOME_PREF_HOMEPAGE") ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } ?.toMutableList() ?: mutableListOf(TvType.Movie, TvType.TvSeries) @@ -408,7 +408,7 @@ class HomeFragment : Fragment() { } fun updateList() { - this.setKey(HOME_PREF_HOMEPAGE, preSelectedTypes) + this.setKey("${DataStoreHelper.currentAccount}/$HOME_PREF_HOMEPAGE", preSelectedTypes) arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> 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 13d34b59..a5ef2bb4 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 @@ -171,7 +171,7 @@ class HomeViewModel : ViewModel() { if (currentWatchTypes.size <= 0) { setKey( - HOME_BOOKMARK_VALUE_LIST, + "${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST", intArrayOf() ) _availableWatchStatusTypes.postValue(setOf() to setOf()) @@ -182,7 +182,7 @@ class HomeViewModel : ViewModel() { val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first()) //if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first()) setKey( - HOME_BOOKMARK_VALUE_LIST, + "${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST", watchPrefNotNull.map { it.internalId }.toIntArray() ) _availableWatchStatusTypes.postValue( @@ -463,7 +463,7 @@ class HomeViewModel : ViewModel() { fun loadStoredData() { val list = EnumSet.noneOf(WatchType::class.java) - getKey(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let { + getKey("${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST")?.map { WatchType.fromInternalId(it) }?.let { list.addAll(it) } loadStoredData(list) From b5d4c3bd27cc70edaa461dd0a29c4c5828be8fcf Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:26:56 -0600 Subject: [PATCH 121/441] Make player preferences account specific (#646) --- .../cloudstream3/ui/player/AbstractPlayerFragment.kt | 5 +++-- .../java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 5 +++-- .../com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) 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 862504a1..52974ff7 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 @@ -48,6 +48,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI @@ -442,7 +443,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("SetTextI18n", "UnsafeOptInUsageError") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - resizeMode = getKey(RESIZE_MODE_KEY) ?: 0 + resizeMode = getKey("$currentAccount/$RESIZE_MODE_KEY") ?: 0 resize(resizeMode, false) player.releaseCallbacks() @@ -573,7 +574,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("UnsafeOptInUsageError") fun resize(resize: PlayerResize, showToast: Boolean) { - setKey(RESIZE_MODE_KEY, resize.ordinal) + setKey("$currentAccount/$RESIZE_MODE_KEY", resize.ordinal) val type = when (resize) { PlayerResize.Fill -> AspectRatioFrameLayout.RESIZE_MODE_FILL PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT 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 6256bef6..49904f6a 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 @@ -57,6 +57,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +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 @@ -536,12 +537,12 @@ class CS3IPlayer : IPlayer { **/ var preferredAudioTrackLanguage: String? = null get() { - return field ?: getKey(PREFERRED_AUDIO_LANGUAGE_KEY, field)?.also { + return field ?: getKey("$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", field)?.also { field = it } } set(value) { - setKey(PREFERRED_AUDIO_LANGUAGE_KEY, value) + setKey("$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", value) field = value } 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 e698191d..43e8aa0b 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 @@ -49,6 +49,7 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -356,7 +357,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun setPlayBackSpeed(speed: Float) { try { - setKey(PLAYBACK_SPEED_KEY, speed) + setKey("$currentAccount/$PLAYBACK_SPEED_KEY", speed) playerBinding?.playerSpeedBtt?.text = getString(R.string.player_speed_text_format).format(speed) .replace(".0x", "x") @@ -1194,7 +1195,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // init variables - setPlayBackSpeed(getKey(PLAYBACK_SPEED_KEY) ?: 1.0f) + setPlayBackSpeed(getKey("$currentAccount/$PLAYBACK_SPEED_KEY") ?: 1.0f) savedInstanceState?.getLong(SUBTITLE_DELAY_BUNDLE_KEY)?.let { subtitleDelay = it } From 3f5119525c3a17a13a3dce42bef6a9caffd5c86e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:59:26 -0600 Subject: [PATCH 122/441] Make library preferences account specific (#649) --- .../lagradost/cloudstream3/ui/library/LibraryFragment.kt | 9 +++++---- .../cloudstream3/ui/library/LibraryViewModel.kt | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index 04ef3d96..d5fdc1aa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -35,6 +35,7 @@ import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount @@ -180,7 +181,7 @@ class LibraryFragment : Fragment() { val items = baseOptions.map { txt(it.stringRes).asString(this) } + availableProviders - val savedSelection = getKey(LIBRARY_FOLDER, key) + val savedSelection = getKey("$currentAccount/$LIBRARY_FOLDER", key) val selectedIndex = when { savedSelection == null -> 0 @@ -215,7 +216,7 @@ class LibraryFragment : Fragment() { } setKey( - LIBRARY_FOLDER, + "$currentAccount/$LIBRARY_FOLDER", key, savedData, ) @@ -262,8 +263,8 @@ class LibraryFragment : Fragment() { // This basically first selects the individual opener and if that is default then // selects the whole list opener val savedListSelection = - getKey(LIBRARY_FOLDER, syncName.name) - val savedSelection = getKey(LIBRARY_FOLDER, syncId).takeIf { + getKey("$currentAccount/$LIBRARY_FOLDER", syncName.name) + val savedSelection = getKey("$currentAccount/$LIBRARY_FOLDER", syncId).takeIf { it?.openType != LibraryOpenerType.Default } ?: savedListSelection diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index 14d31356..25a5a0f8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -11,6 +11,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount enum class ListSorting(@StringRes val stringRes: Int) { Query(R.string.none), @@ -35,12 +36,12 @@ class LibraryViewModel : ViewModel() { get() = SyncApis.filter { it.hasAccount() } var currentSyncApi = availableSyncApis.let { allApis -> - val lastSelection = getKey(LAST_SYNC_API_KEY) + val lastSelection = getKey("$currentAccount/$LAST_SYNC_API_KEY") availableSyncApis.firstOrNull { it.name == lastSelection } ?: allApis.firstOrNull() } private set(value) { field = value - setKey(LAST_SYNC_API_KEY, field?.name) + setKey("$currentAccount/$LAST_SYNC_API_KEY", field?.name) } val availableApiNames: List From 177b1e47f3f08110c07e3839a2d1935fe491a4da Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 4 Oct 2023 11:33:55 +0200 Subject: [PATCH 123/441] added extra logging --- .../cloudstream3/ui/player/PreviewGenerator.kt | 11 +++++++++-- .../com/lagradost/cloudstream3/utils/M3u8Helper.kt | 12 ++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index 53699782..946c1d33 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -81,6 +81,7 @@ class PreviewGenerator : IPreviewGenerator { } else -> { + Log.i("PreviewImg", "unsupported format for $link") currentGenerator.clear(keepCache) } } @@ -193,6 +194,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { // no support for encryption atm if (hsl.isEncrypted) { + Log.i(TAG, "m3u8 is encrypted") totalImages = 0 return@withContext } @@ -239,12 +241,13 @@ class M3u8PreviewGenerator : IPreviewGenerator { if (!isActive) { return@withContext } - val frame = retriever.getFrameAtTime(0) + val img = retriever.getFrameAtTime(0) if (!isActive) { return@withContext } + if(img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { - images[index] = frame + images[index] = img loadedImages += 1 } } catch (t: Throwable) { @@ -302,6 +305,9 @@ class Mp4PreviewGenerator : IPreviewGenerator { if (idx > loadedImages) { break } + if(images[idx] == null) { + continue + } val currentFraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) val diff = currentFraction.minus(fraction).absoluteValue @@ -382,6 +388,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { MediaMetadataRetriever.OPTION_CLOSEST_SYNC ) if (!scope.isActive) return + if(img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { images[idx] = img loadedImages = maxOf(loadedImages, idx) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index d3fe7162..298f1601 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -115,19 +115,19 @@ object M3u8Helper2 { private fun selectBest(qualities: List): M3u8Helper.M3u8Stream? { val result = qualities.sortedBy { - if (it.quality != null && it.quality <= 1080) it.quality else 0 - }.filter { + it.quality ?: Qualities.Unknown.value //if (it.quality != null && it.quality <= 1080) else 0 + }/*.filter { listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl)) - } + }*/ return result.lastOrNull() } private fun selectWorst(qualities: List): M3u8Helper.M3u8Stream? { val result = qualities.sortedBy { - if (it.quality != null && it.quality <= 1080) it.quality else 0 - }.filter { + it.quality ?: Qualities.Unknown.value //if (it.quality != null && it.quality <= 1080) else 0 + }/*.filter { listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl)) - } + }*/ return result.firstOrNull() } From 0a327ccbda8d3622127cae2e419a386357250b6e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Fri, 6 Oct 2023 15:50:31 -0600 Subject: [PATCH 124/441] Reload library when reloading home (#656) So that library is reloaded when switching accounts. Fixes #650 --- .../cloudstream3/ui/library/LibraryViewModel.kt | 10 ++++++++++ .../lagradost/cloudstream3/utils/DataStoreHelper.kt | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index 25a5a0f8..c104a7c3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis @@ -101,4 +102,13 @@ class LibraryViewModel : ViewModel() { } } } + + init { + MainActivity.reloadHomeEvent += ::reloadPages + } + + override fun onCleared() { + MainActivity.reloadHomeEvent -= ::reloadPages + super.onCleared() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 7bce1b6c..775cb718 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -131,7 +131,6 @@ object DataStoreHelper { // update UI setAccount(getDefaultAccount(context), true) - MainActivity.bookmarksUpdatedEvent(true) dialog?.dismissSafe() } From 77294dc68e6ba19c21017f4ac17847755572ad18 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 7 Oct 2023 01:39:30 +0200 Subject: [PATCH 125/441] cleanup --- .../lagradost/cloudstream3/MainActivity.kt | 40 +++--- .../cloudstream3/ui/home/HomeFragment.kt | 25 +--- .../cloudstream3/ui/home/HomeViewModel.kt | 22 ++- .../ui/player/AbstractPlayerFragment.kt | 11 +- .../ui/player/FullScreenPlayer.kt | 6 +- .../cloudstream3/ui/player/IPlayer.kt | 9 -- .../ui/player/PreviewGenerator.kt | 134 ++++++++++++------ .../cloudstream3/ui/search/SearchFragment.kt | 43 ++---- .../cloudstream3/utils/DataStoreHelper.kt | 68 +++++++++ .../main/res/layout/player_custom_layout.xml | 4 +- 10 files changed, 215 insertions(+), 147 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index d5187029..17823f7c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1122,23 +1122,25 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (isTvSettings()) { val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) setContentView(newLocalBinding.root) - TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) - newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> - // println("refocus $oldFocus -> $newFocus") - try { - val r = Rect(0, 0, 0, 0) - newFocus.getDrawingRect(r) - val x = r.centerX() - val y = r.centerY() - val dx = 0 //screenWidth / 2 - val dy = screenHeight / 2 - val r2 = Rect(x - dx, y - dy, x + dx, y + dy) - newFocus.requestRectangleOnScreen(r2, false) - // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) - } catch (_: Throwable) { - } - TvFocus.updateFocusView(newFocus) - /*var focus = newFocus + + if(isTrueTvSettings()) { + TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) + newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> + // println("refocus $oldFocus -> $newFocus") + try { + val r = Rect(0, 0, 0, 0) + newFocus.getDrawingRect(r) + val x = r.centerX() + val y = r.centerY() + val dx = 0 //screenWidth / 2 + val dy = screenHeight / 2 + val r2 = Rect(x - dx, y - dy, x + dx, y + dy) + newFocus.requestRectangleOnScreen(r2, false) + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_: Throwable) { + } + TvFocus.updateFocusView(newFocus) + /*var focus = newFocus while(focus != null) { if(focus is ScrollingView && focus.canScrollVertically()) { @@ -1149,7 +1151,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { else -> break } }*/ + } + } else { + newLocalBinding.focusOutline.isVisible = false } + newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) } 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 ebbb245c..4d940123 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 @@ -7,7 +7,6 @@ import android.content.DialogInterface import android.content.Intent import android.content.res.Configuration import android.net.Uri -import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -23,18 +22,12 @@ import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent -import com.lagradost.cloudstream3.MainActivity.Companion.bookmarksUpdatedEvent -import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding @@ -45,37 +38,26 @@ import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi -import com.lagradost.cloudstream3.ui.WatchType -import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.* import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings -import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable -import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.ownHide import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -import com.lagradost.cloudstream3.utils.DataStore.getKey -import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes - import java.util.* -const val HOME_BOOKMARK_VALUE_LIST = "home_bookmarked_last_list" -const val HOME_PREF_HOMEPAGE = "home_pref_homepage" - class HomeFragment : Fragment() { companion object { val configEvent = Event() @@ -377,10 +359,7 @@ class HomeFragment : Fragment() { var currentApiName = selectedApiName var currentValidApis: MutableList = mutableListOf() - val preSelectedTypes = this.getKey>("${DataStoreHelper.currentAccount}/$HOME_PREF_HOMEPAGE") - ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } - ?.toMutableList() - ?: mutableListOf(TvType.Movie, TvType.TvSeries) + val preSelectedTypes = DataStoreHelper.homePreference.toMutableList() binding.cancelBtt.setOnClickListener { dialog.dismissSafe() @@ -408,7 +387,7 @@ class HomeFragment : Fragment() { } fun updateList() { - this.setKey("${DataStoreHelper.currentAccount}/$HOME_PREF_HOMEPAGE", preSelectedTypes) + DataStoreHelper.homePreference = preSelectedTypes arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> 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 a5ef2bb4..ad75aa9d 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 @@ -12,7 +12,6 @@ import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse @@ -170,10 +169,7 @@ class HomeViewModel : ViewModel() { currentWatchTypes.remove(WatchType.NONE) if (currentWatchTypes.size <= 0) { - setKey( - "${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST", - intArrayOf() - ) + DataStoreHelper.homeBookmarkedList = intArrayOf() _availableWatchStatusTypes.postValue(setOf() to setOf()) _bookmarks.postValue(Pair(false, ArrayList())) return@launchSafe @@ -181,16 +177,14 @@ class HomeViewModel : ViewModel() { val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first()) //if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first()) - setKey( - "${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST", - watchPrefNotNull.map { it.internalId }.toIntArray() - ) + + DataStoreHelper.homeBookmarkedList = watchPrefNotNull.map { it.internalId }.toIntArray() _availableWatchStatusTypes.postValue( - Pair( - watchPrefNotNull, - currentWatchTypes, + + watchPrefNotNull to + currentWatchTypes, + ) - ) val list = withContext(Dispatchers.IO) { watchStatusIds.filter { watchPrefNotNull.contains(it.second) } @@ -463,7 +457,7 @@ class HomeViewModel : ViewModel() { fun loadStoredData() { val list = EnumSet.noneOf(WatchType::class.java) - getKey("${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST")?.map { WatchType.fromInternalId(it) }?.let { + DataStoreHelper.homeBookmarkedList.map { WatchType.fromInternalId(it) }.let { list.addAll(it) } loadStoredData(list) 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 52974ff7..431e4fe1 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 @@ -33,8 +33,6 @@ import androidx.preference.PreferenceManager import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.github.rubensousa.previewseekbar.PreviewBar import com.github.rubensousa.previewseekbar.media3.PreviewTimeBar -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode import com.lagradost.cloudstream3.CommonActivity.isInPIPMode import com.lagradost.cloudstream3.CommonActivity.keyEventListener @@ -48,7 +46,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus -import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount +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 @@ -443,7 +441,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("SetTextI18n", "UnsafeOptInUsageError") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - resizeMode = getKey("$currentAccount/$RESIZE_MODE_KEY") ?: 0 + resizeMode = DataStoreHelper.resizeMode resize(resizeMode, false) player.releaseCallbacks() @@ -466,7 +464,8 @@ abstract class AbstractPlayerFragment( var resume = false progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener { override fun onScrubStart(previewBar: PreviewBar?) { - progressBar.isPreviewEnabled = player.hasPreview() + val hasPreview = player.hasPreview() + progressBar.isPreviewEnabled = hasPreview resume = player.getIsPlaying() if (resume) player.handleEvent( CSPlayerEvent.Pause, @@ -574,7 +573,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("UnsafeOptInUsageError") fun resize(resize: PlayerResize, showToast: Boolean) { - setKey("$currentAccount/$RESIZE_MODE_KEY", resize.ordinal) + DataStoreHelper.resizeMode = resize.ordinal val type = when (resize) { PlayerResize.Fill -> AspectRatioFrameLayout.RESIZE_MODE_FILL PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT 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 43e8aa0b..819e50ba 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 @@ -49,7 +49,7 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData -import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -357,7 +357,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun setPlayBackSpeed(speed: Float) { try { - setKey("$currentAccount/$PLAYBACK_SPEED_KEY", speed) + DataStoreHelper.playBackSpeed = speed playerBinding?.playerSpeedBtt?.text = getString(R.string.player_speed_text_format).format(speed) .replace(".0x", "x") @@ -1195,7 +1195,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // init variables - setPlayBackSpeed(getKey("$currentAccount/$PLAYBACK_SPEED_KEY") ?: 1.0f) + setPlayBackSpeed(DataStoreHelper.playBackSpeed) savedInstanceState?.getLong(SUBTITLE_DELAY_BUNDLE_KEY)?.let { subtitleDelay = it } 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 a08360ae..0e54e2cb 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 @@ -199,17 +199,8 @@ data class CurrentTracks( class InvalidFileException(msg: String) : Exception(msg) //http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 -const val STATE_RESUME_WINDOW = "resumeWindow" -const val STATE_RESUME_POSITION = "resumePosition" -const val STATE_PLAYER_FULLSCREEN = "playerFullscreen" -const val STATE_PLAYER_PLAYING = "playerOnPlay" const val ACTION_MEDIA_CONTROL = "media_control" const val EXTRA_CONTROL_TYPE = "control_type" -const val PLAYBACK_SPEED = "playback_speed" -const val RESIZE_MODE_KEY = "resize_mode" // Last used resize mode -const val PLAYBACK_SPEED_KEY = "playback_speed" // Last used playback speed -const val PREFERRED_SUBS_KEY = "preferred_subtitles" // Last used resize mode -//const val PLAYBACK_FASTFORWARD = "playback_fastforward" // Last used resize mode /** Abstract Exoplayer logic, can be expanded to other players */ interface IPlayer { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index 946c1d33..ffb4751f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -28,86 +28,122 @@ const val MIN_LOD = 3 interface IPreviewGenerator { fun hasPreview(): Boolean fun getPreviewImage(fraction: Float): Bitmap? - fun clear(keepCache: Boolean = false) fun release() + + var durationMs: Long + var loadedImages: Int } +/** PreviewGenerator that hides the implementation details of the sub generators that is used, used for source switch cache */ class PreviewGenerator : IPreviewGenerator { + /** the most up to date generator, will always mirror the actual source in the player */ private var currentGenerator: IPreviewGenerator = NoPreviewGenerator() + /** the longest generated preview of the same episode */ + private var lastGenerator: IPreviewGenerator = NoPreviewGenerator() + /** always NoPreviewGenerator, used as a cache for nothing */ + private val dummy: IPreviewGenerator = NoPreviewGenerator() + + /** if the current generator is the same as the last by checking time */ + private fun isSameLength(): Boolean = + currentGenerator.durationMs.minus(lastGenerator.durationMs).absoluteValue < 10_000L + + /** use the backup if the current generator is init or if they have the same length */ + private val backupGenerator: IPreviewGenerator + get() { + if (currentGenerator.durationMs == 0L || isSameLength()) { + return lastGenerator + } + return dummy + } + override fun hasPreview(): Boolean { - return currentGenerator.hasPreview() + return currentGenerator.hasPreview() || backupGenerator.hasPreview() } override fun getPreviewImage(fraction: Float): Bitmap? { return try { - currentGenerator.getPreviewImage(fraction) + currentGenerator.getPreviewImage(fraction) ?: backupGenerator.getPreviewImage(fraction) } catch (t: Throwable) { logError(t) null } } - override fun clear(keepCache: Boolean) { - currentGenerator.clear(keepCache) + override fun release() { + lastGenerator.release() + currentGenerator.release() + lastGenerator = NoPreviewGenerator() + currentGenerator = NoPreviewGenerator() } - override fun release() { - currentGenerator.release() + override var durationMs: Long + get() = currentGenerator.durationMs + set(_) {} + override var loadedImages: Int + get() = currentGenerator.loadedImages + set(_) {} + + fun clear(keepCache: Boolean) { + if (keepCache) { + if (!isSameLength() || currentGenerator.loadedImages >= lastGenerator.loadedImages || lastGenerator.durationMs == 0L) { + // the current generator is better than the last generator, therefore keep the current + // or the lengths are not the same, therefore favoring the more recent selection + + // if they are the same we favor the current generator + lastGenerator.release() + lastGenerator = currentGenerator + } else { + // otherwise just keep the last generator and throw away the current generator + currentGenerator.release() + } + } else { + // we switched the episode, therefore keep nothing + lastGenerator.release() + lastGenerator = NoPreviewGenerator() + currentGenerator.release() + // we assume that we set currentGenerator right after this, so currentGenerator != NoPreviewGenerator + } } fun load(link: ExtractorLink, keepCache: Boolean) { - val gen = currentGenerator + clear(keepCache) + when (link.type) { ExtractorLinkType.M3U8 -> { - if (gen is M3u8PreviewGenerator) { - gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) - } else { - currentGenerator.release() - currentGenerator = M3u8PreviewGenerator().apply { - load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) - } + currentGenerator = M3u8PreviewGenerator().apply { + load(url = link.url, headers = link.getAllHeaders()) } } ExtractorLinkType.VIDEO -> { - if (gen is Mp4PreviewGenerator) { - gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) - } else { - currentGenerator.release() - currentGenerator = Mp4PreviewGenerator().apply { - load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) - } + currentGenerator = Mp4PreviewGenerator().apply { + load(url = link.url, headers = link.getAllHeaders()) } } else -> { Log.i("PreviewImg", "unsupported format for $link") - currentGenerator.clear(keepCache) } } } fun load(context: Context, link: ExtractorUri, keepCache: Boolean) { - val gen = currentGenerator - if (gen is Mp4PreviewGenerator) { - gen.load(keepCache = keepCache, context = context, uri = link.uri) - } else { - currentGenerator.release() - currentGenerator = Mp4PreviewGenerator().apply { - load(keepCache = keepCache, context = context, uri = link.uri) - } + clear(keepCache) + currentGenerator = Mp4PreviewGenerator().apply { + load(keepCache = keepCache, context = context, uri = link.uri) } } } -class NoPreviewGenerator : IPreviewGenerator { +private class NoPreviewGenerator : IPreviewGenerator { override fun hasPreview(): Boolean = false override fun getPreviewImage(fraction: Float): Bitmap? = null - override fun clear(keepCache: Boolean) = Unit override fun release() = Unit + override var durationMs: Long = 0L + override var loadedImages: Int = 0 } -class M3u8PreviewGenerator : IPreviewGenerator { +private class M3u8PreviewGenerator : IPreviewGenerator { // generated images 1:1 to idx of hsl private var images: Array = arrayOf() @@ -118,7 +154,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { private var prefixSum: Array = arrayOf() // how many images has been generated - private var loadedImages: Int = 0 + override var loadedImages: Int = 0 // how many images we can generate in total, == hsl.size ?: 0 private var totalImages: Int = 0 @@ -155,7 +191,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { }*/ } - override fun clear(keepCache: Boolean) { + private fun clear() { synchronized(images) { currentJob?.cancel() images = arrayOf() @@ -170,9 +206,11 @@ class M3u8PreviewGenerator : IPreviewGenerator { images = arrayOf() } + override var durationMs: Long = 0L + private var currentJob: Job? = null - fun load(keepCache: Boolean, url: String, headers: Map) { - clear(keepCache) + fun load(url: String, headers: Map) { + clear() currentJob?.cancel() currentJob = ioSafe { withContext(Dispatchers.IO) { @@ -201,6 +239,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { // total duration of the entire m3u8 in seconds val duration = hsl.allTsLinks.sumOf { it.time ?: 0.0 } + durationMs = (duration * 1000.0).toLong() val durationInv = 1.0 / duration // if the total duration is less then 10s then something is very wrong or @@ -245,7 +284,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { if (!isActive) { return@withContext } - if(img == null || img.width <= 1 || img.height <= 1) continue + if (img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { images[index] = img loadedImages += 1 @@ -269,11 +308,11 @@ class M3u8PreviewGenerator : IPreviewGenerator { } } -class Mp4PreviewGenerator : IPreviewGenerator { +private class Mp4PreviewGenerator : IPreviewGenerator { // lod = level of detail where the number indicates how many ones there is // 2^(lod-1) = images private var loadedLod = 0 - private var loadedImages = 0 + override var loadedImages = 0 private var images = Array((1 shl MAX_LOD) - 1) { null } @@ -305,7 +344,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { if (idx > loadedImages) { break } - if(images[idx] == null) { + if (images[idx] == null) { continue } val currentFraction = @@ -325,7 +364,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { // also check out https://github.com/wseemann/FFmpegMediaMetadataRetriever private val retriever: MediaMetadataRetriever = MediaMetadataRetriever() - override fun clear(keepCache: Boolean) { + private fun clear(keepCache: Boolean) { if (keepCache) return synchronized(images) { loadedLod = 0 @@ -335,11 +374,11 @@ class Mp4PreviewGenerator : IPreviewGenerator { } private var currentJob: Job? = null - fun load(keepCache: Boolean, url: String, headers: Map) { + fun load(url: String, headers: Map) { currentJob?.cancel() currentJob = ioSafe { Log.i(TAG, "Loading with url = $url headers = $headers") - clear(keepCache) + clear(true) retriever.setDataSource(url, headers) start(this) } @@ -360,6 +399,8 @@ class Mp4PreviewGenerator : IPreviewGenerator { clear(false) } + override var durationMs: Long = 0L + @Throws @WorkerThread private fun start(scope: CoroutineScope) { @@ -368,6 +409,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: throw IllegalArgumentException("Bad video duration") + this.durationMs = durationMs val durationUs = (durationMs * 1000L).toFloat() //val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: throw IllegalArgumentException("Bad video width") //val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: throw IllegalArgumentException("Bad video height") @@ -388,7 +430,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { MediaMetadataRetriever.OPTION_CLOSEST_SYNC ) if (!scope.isActive) return - if(img == null || img.width <= 1 || img.height <= 1) continue + if (img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { images[idx] = img loadedImages = maxOf(loadedImages, idx) 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 bad78624..0e994be8 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 @@ -22,17 +22,22 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.getApiSettings -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.AllLanguagesName +import com.lagradost.cloudstream3.AnimeSearchResponse +import com.lagradost.cloudstream3.HomePageList +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSearchBinding import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding import com.lagradost.cloudstream3.mvvm.Resource @@ -53,8 +58,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.ownHide import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.main -import com.lagradost.cloudstream3.utils.DataStore.getKey -import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -63,9 +67,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import java.util.concurrent.locks.ReentrantLock -const val SEARCH_PREF_TAGS = "search_pref_tags" -const val SEARCH_PREF_PROVIDERS = "search_pref_providers" - class SearchFragment : Fragment() { companion object { fun List.filterSearchResponse(): List { @@ -194,7 +195,7 @@ class SearchFragment : Fragment() { validAPIs.flatMap { api -> api.supportedTypes }.distinct() ) { list -> if (selectedSearchTypes.toSet() != list.toSet()) { - setKey("$currentAccount/$SEARCH_PREF_TAGS", selectedSearchTypes) + DataStoreHelper.searchPreferenceTags = list selectedSearchTypes.clear() selectedSearchTypes.addAll(list) search(binding?.mainSearch?.query?.toString()) @@ -233,13 +234,7 @@ class SearchFragment : Fragment() { //searchMagIcon.scaleX = 0.65f //searchMagIcon.scaleY = 0.65f - context?.let { ctx -> - val validAPIs = ctx.filterProviderByPreferredMedia() - selectedApis = ctx.getKey( - "$currentAccount/$SEARCH_PREF_PROVIDERS", - defVal = validAPIs.map { it.name } - )!!.toMutableSet() - } + selectedApis = DataStoreHelper.searchPreferenceProviders.toMutableSet() binding?.searchFilter?.setOnClickListener { searchView -> searchView?.context?.let { ctx -> @@ -287,7 +282,7 @@ class SearchFragment : Fragment() { } fun updateList(types: List) { - setKey("$currentAccount/$SEARCH_PREF_TAGS", types.map { it.name }) + DataStoreHelper.searchPreferenceTags = types arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> @@ -312,12 +307,7 @@ class SearchFragment : Fragment() { arrayAdapter.notifyDataSetChanged() } - val selectedSearchTypes = getKey>("$currentAccount/$SEARCH_PREF_TAGS") - ?.mapNotNull { listName -> - TvType.values().firstOrNull { it.name == listName } - } - ?.toMutableList() - ?: mutableListOf(TvType.Movie, TvType.TvSeries) + val selectedSearchTypes = DataStoreHelper.searchPreferenceTags bindChips( binding.tvtypesChipsScroll.tvtypesChips, @@ -343,7 +333,7 @@ class SearchFragment : Fragment() { } dialog.setOnDismissListener { - context?.setKey("$currentAccount/$SEARCH_PREF_PROVIDERS", currentSelectedApis.toList()) + DataStoreHelper.searchPreferenceProviders = currentSelectedApis.toList() selectedApis = currentSelectedApis } updateList(selectedSearchTypes.toList()) @@ -354,10 +344,7 @@ class SearchFragment : Fragment() { val settingsManager = context?.let { PreferenceManager.getDefaultSharedPreferences(it) } val isAdvancedSearch = settingsManager?.getBoolean("advanced_search", true) ?: true - selectedSearchTypes = context?.getKey>("$currentAccount/$SEARCH_PREF_TAGS") - ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } - ?.toMutableList() - ?: mutableListOf(TvType.Movie, TvType.TvSeries) + selectedSearchTypes = DataStoreHelper.searchPreferenceTags.toMutableList() if (isTrueTvSettings()) { binding?.searchFilter?.isFocusable = true diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 775cb718..10c0546f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -10,7 +10,9 @@ import androidx.core.widget.doOnTextChanged import com.fasterxml.jackson.annotation.JsonProperty import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey @@ -31,6 +33,8 @@ import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe +import kotlin.reflect.KClass +import kotlin.reflect.KProperty const val VIDEO_POS_DUR = "video_pos_dur" const val VIDEO_WATCH_STATE = "video_watch_state" @@ -44,6 +48,28 @@ const val RESULT_EPISODE = "result_episode" const val RESULT_SEASON = "result_season" const val RESULT_DUB = "result_dub" + +class UserPreferenceDelegate( + private val key: String, private val default: T //, private val klass: KClass +) { + private val klass: KClass = default::class + private val realKey get() = "${DataStoreHelper.currentAccount}/$key" + operator fun getValue(self: Any?, property: KProperty<*>) = + AcraApplication.getKeyClass(realKey, klass.java) ?: default + + operator fun setValue( + self: Any?, + property: KProperty<*>, + t: T? + ) { + if (t == null) { + removeKey(realKey) + } else { + AcraApplication.setKeyClass(realKey, t) + } + } +} + object DataStoreHelper { // be aware, don't change the index of these as Account uses the index for the art private val profileImages = arrayOf( @@ -56,6 +82,48 @@ object DataStoreHelper { R.drawable.profile_bg_teal ) + private var searchPreferenceProvidersStrings : List by UserPreferenceDelegate( + /** java moment right here, as listOf()::class.java != List(0) { "" }::class.java */ + "search_pref_providers", List(0) { "" } + ) + + private fun serializeTv(data : List) : List = data.map { it.name } + + private fun deserializeTv(data : List) : List { + return data.mapNotNull { listName -> + TvType.values().firstOrNull { it.name == listName } + } + } + + var searchPreferenceProviders : List + get() { + val ret = searchPreferenceProvidersStrings + return ret.ifEmpty { + context?.filterProviderByPreferredMedia()?.map { it.name } ?: emptyList() + } + } set(value) { + searchPreferenceProvidersStrings = value + } + + private var searchPreferenceTagsStrings : List by UserPreferenceDelegate("search_pref_tags", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) + var searchPreferenceTags : List + get() = deserializeTv(searchPreferenceTagsStrings) + set(value) { + searchPreferenceTagsStrings = serializeTv(value) + } + + + private var homePreferenceStrings : List by UserPreferenceDelegate("home_pref_homepage", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) + var homePreference : List + get() = deserializeTv(homePreferenceStrings) + set(value) { + homePreferenceStrings = serializeTv(value) + } + + var homeBookmarkedList : IntArray by UserPreferenceDelegate("home_bookmarked_last_list", IntArray(0)) + var playBackSpeed : Float by UserPreferenceDelegate("playback_speed", 1.0f) + var resizeMode : Int by UserPreferenceDelegate("resize_mode", 0) + data class Account( @JsonProperty("keyIndex") val keyIndex: Int, diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 0f76e4dd..38df4c5b 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -349,14 +349,15 @@ android:layout_width="150dp" android:layout_height="40dp" android:layout_marginEnd="100dp" + android:layout_marginTop="60dp" android:backgroundTint="@color/skipOpTransparent" android:maxLines="1" android:padding="10dp" android:textColor="@color/white" android:visibility="gone" app:cornerRadius="@dimen/rounded_button_radius" - app:layout_constraintBottom_toTopOf="@+id/bottom_player_bar" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@+id/player_top_holder" app:strokeColor="@color/white" app:strokeWidth="1dp" tools:text="Skip Opening" @@ -435,6 +436,7 @@ android:id="@+id/player_video_bar" android:layout_width="match_parent" android:layout_height="wrap_content" + tools:visibility="visible" android:layoutDirection="ltr" android:orientation="horizontal"> From f14557fe6a3268b07ea1421df109367e7d040da0 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 7 Oct 2023 01:54:34 +0200 Subject: [PATCH 126/441] lib fix --- .../lagradost/cloudstream3/ui/library/LibraryFragment.kt | 8 ++++++++ .../lagradost/cloudstream3/ui/library/LibraryViewModel.kt | 2 ++ 2 files changed, 10 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index d5fdc1aa..a3cc16c9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -15,6 +15,7 @@ import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayoutMediator import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.allProviders @@ -229,6 +230,7 @@ class LibraryFragment : Fragment() { } binding?.viewpager?.setPageTransformer(LibraryScrollTransformer()) + binding?.viewpager?.adapter = binding?.viewpager?.adapter ?: ViewpagerAdapter( mutableListOf(), @@ -357,6 +359,7 @@ class LibraryFragment : Fragment() { 0, viewpager.adapter?.itemCount ?: 0 ) + binding?.viewpager?.setCurrentItem(libraryViewModel.currentPage, false) // Only stop loading after 300ms to hide the fade effect the viewpager produces when updating // Without this there would be a flashing effect: @@ -415,6 +418,11 @@ class LibraryFragment : Fragment() { } } } + binding?.viewpager?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + libraryViewModel.currentPage = position + } + }) } override fun onConfigurationChanged(newConfig: Configuration) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index c104a7c3..e590b151 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -30,6 +30,8 @@ class LibraryViewModel : ViewModel() { private val _pages: MutableLiveData>> = MutableLiveData(null) val pages: LiveData>> = _pages + var currentPage : Int = 0 + private val _currentApiName: MutableLiveData = MutableLiveData("") val currentApiName: LiveData = _currentApiName From 33eb3a3b29a1dd7dd21a0847222bac1e6cc04bf5 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 7 Oct 2023 21:48:24 +0200 Subject: [PATCH 127/441] lib fix2 --- .../ui/library/LibraryViewModel.kt | 32 +++++++++++++------ .../cloudstream3/utils/DataStoreHelper.kt | 2 ++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index e590b151..b44913d9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -12,6 +12,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount enum class ListSorting(@StringRes val stringRes: Int) { @@ -30,7 +31,7 @@ class LibraryViewModel : ViewModel() { private val _pages: MutableLiveData>> = MutableLiveData(null) val pages: LiveData>> = _pages - var currentPage : Int = 0 + var currentPage: Int = 0 private val _currentApiName: MutableLiveData = MutableLiveData("") val currentApiName: LiveData = _currentApiName @@ -62,13 +63,21 @@ class LibraryViewModel : ViewModel() { reloadPages(true) } - fun sort(method: ListSorting, query: String? = null) { - val currentList = pages.value ?: return + fun sort(method: ListSorting, query: String? = null) = ioSafe { + val value = _pages.value ?: return@ioSafe + if (value is Resource.Success) { + sort(method, query, value.value) + } + } + + private fun sort(method: ListSorting, query: String? = null, items: List) { currentSortingMethod = method - (currentList as? Resource.Success)?.value?.forEachIndexed { _, page -> + DataStoreHelper.librarySortingMode = method.ordinal + + items.forEach { page -> page.sort(method, query) } - _pages.postValue(currentList) + _pages.postValue(Resource.Success(items)) } fun reloadPages(forceReload: Boolean) { @@ -89,8 +98,6 @@ class LibraryViewModel : ViewModel() { val library = (libraryResource as? Resource.Success)?.value ?: return@let sortingMethods = library.supportedListSorting.toList() - currentSortingMethod = null - repo.requireLibraryRefresh = false val pages = library.allLibraryLists.map { @@ -100,11 +107,18 @@ class LibraryViewModel : ViewModel() { ) } - _pages.postValue(Resource.Success(pages)) + val desiredSortingMethod = + ListSorting.values().getOrNull(DataStoreHelper.librarySortingMode) + if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) { + sort(desiredSortingMethod, null, pages) + } else { + // null query = no sorting + sort(ListSorting.Query, null, pages) + } } } } - + init { MainActivity.reloadHomeEvent += ::reloadPages } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 10c0546f..952422a4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -26,6 +26,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WhoIsWatchingAdapter +import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState @@ -123,6 +124,7 @@ object DataStoreHelper { var homeBookmarkedList : IntArray by UserPreferenceDelegate("home_bookmarked_last_list", IntArray(0)) var playBackSpeed : Float by UserPreferenceDelegate("playback_speed", 1.0f) var resizeMode : Int by UserPreferenceDelegate("resize_mode", 0) + var librarySortingMode : Int by UserPreferenceDelegate("library_sorting_mode", ListSorting.AlphabeticalA.ordinal) data class Account( @JsonProperty("keyIndex") From d277d8a9aabce833b6384948716a5c3d8c680807 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:56:30 +0200 Subject: [PATCH 128/441] bump upstream --- app/build.gradle.kts | 36 +++++++++++++++--------------- app/src/main/res/values/styles.xml | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f484c48..639932c4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,16 +50,16 @@ android { } } - compileSdk = 33 + compileSdk = 34 buildToolsVersion = "34.0.0" defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 33 + targetSdk = 34 versionCode = 62 - versionName = "4.2.0" + versionName = "4.2.1" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") @@ -154,18 +154,18 @@ repositories { dependencies { implementation("com.google.android.mediahome:video:1.0.0") implementation("androidx.test.ext:junit-ktx:1.1.5") - testImplementation("org.json:json:20180813") + testImplementation("org.json:json:20230618") - implementation("androidx.core:core-ktx:1.10.1") + implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0 // dont change this to 1.6.0 it looks ugly af - implementation("com.google.android.material:material:1.5.0") + implementation("com.google.android.material:material:1.10.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.navigation:navigation-fragment-ktx:2.6.0") - implementation("androidx.navigation:navigation-ui-ktx:2.6.0") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.4") + implementation("androidx.navigation:navigation-ui-ktx:2.7.4") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") @@ -173,9 +173,9 @@ dependencies { // implementation("io.karn:khttp-android:0.1.2") //okhttp instead // implementation("org.jsoup:jsoup:1.13.1") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2") - implementation("androidx.preference:preference-ktx:1.2.0") + implementation("androidx.preference:preference-ktx:1.2.1") implementation("com.github.bumptech.glide:glide:4.13.1") kapt("com.github.bumptech.glide:compiler:4.13.1") @@ -200,14 +200,14 @@ dependencies { implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Bug reports - implementation("ch.acra:acra-core:5.11.0") - implementation("ch.acra:acra-toast:5.11.0") + implementation("ch.acra:acra-core:5.11.2") + implementation("ch.acra:acra-toast:5.11.2") - compileOnly("com.google.auto.service:auto-service-annotations:1.0") + compileOnly("com.google.auto.service:auto-service-annotations:1.1.1") //either for java sources: - annotationProcessor("com.google.auto.service:auto-service:1.0") + annotationProcessor("com.google.auto.service:auto-service:1.1.1") //or for kotlin sources (requires kapt gradle plugin): - kapt("com.google.auto.service:auto-service:1.0") + kapt("com.google.auto.service:auto-service:1.1.1") // subtitle color picker implementation("com.jaredrummler:colorpicker:1.1.0") @@ -229,7 +229,7 @@ dependencies { // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") implementation("com.github.Blatzar:NiceHttp:0.4.3") // To fix SSL fuckery on android 9 - implementation("org.conscrypt:conscrypt-android:2.2.1") + implementation("org.conscrypt:conscrypt-android:2.5.2") // Util to skip the URI file fuckery 🙏 implementation("com.github.LagradOst:SafeFile:0.0.5") diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e2f11221..5f7adea4 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -388,7 +388,7 @@ true @null @null - @color/transparent + ?attr/primaryBlackBackground @drawable/rounded_dialog 512dp From 5b4fd8d77de24dbc012d6c2814e35db26d251fa9 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 10 Oct 2023 09:05:34 -0600 Subject: [PATCH 129/441] Fix issue where DownloadedPlayerActivity interferes with MainActivity (#674) * Fix issue where DownloadedPlayerActivity interferes with MainActivity --- app/src/main/AndroidManifest.xml | 4 ++- .../lagradost/cloudstream3/CommonActivity.kt | 27 ++++++++++++------- .../lagradost/cloudstream3/MainActivity.kt | 2 ++ .../ui/player/DownloadedPlayerActivity.kt | 5 ++++ 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 15767d7b..503cd76b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -61,7 +61,9 @@ android:exported="true" android:resizeableActivity="true" android:screenOrientation="userLandscape" - android:supportsPictureInPicture="true"> + android:supportsPictureInPicture="true" + android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer" + android:launchMode="singleTask"> diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index a7d899b6..16a438b3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -65,6 +65,11 @@ object CommonActivity { _activity = WeakReference(value) } + @MainThread + fun setActivityInstance(newActivity: Activity?) { + activity = newActivity + } + @MainThread fun Activity?.getCastSession(): CastSession? { return (this as MainActivity?)?.mSessionManager?.currentCastSession @@ -203,23 +208,25 @@ object CommonActivity { setLocale(this, localeCode) } - fun init(act: ComponentActivity?) { - if (act == null) return - activity = act + fun init(act: Activity) { + setActivityInstance(act) + + val componentActivity = activity as? ComponentActivity ?: return + //https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission //https://developer.android.com/guide/topics/ui/picture-in-picture canShowPipMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT - act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN - act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS + componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN + componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS - act.updateLocale() - act.updateTv() + componentActivity.updateLocale() + componentActivity.updateTv() NewPipe.init(DownloaderTestImpl.getInstance()) for (resumeApp in resumeApps) { resumeApp.launcher = - act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val resultCode = result.resultCode val data = result.data if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) { @@ -236,11 +243,11 @@ object CommonActivity { // Ask for notification permissions on Android 13 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission( - act, + componentActivity, Manifest.permission.POST_NOTIFICATIONS ) != PackageManager.PERMISSION_GRANTED ) { - val requestPermissionLauncher = act.registerForActivityResult( + val requestPermissionLauncher = componentActivity.registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> Log.d(TAG, "Notification permission: $isGranted") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 17823f7c..5595c377 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -67,6 +67,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint import com.lagradost.cloudstream3.CommonActivity.screenHeight +import com.lagradost.cloudstream3.CommonActivity.setActivityInstance import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale import com.lagradost.cloudstream3.databinding.ActivityMainBinding @@ -590,6 +591,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { override fun onResume() { super.onResume() afterPluginsLoadedEvent += ::onAllPluginsLoaded + setActivityInstance(this) try { if (isCastApiAvailable()) { //mCastSession = mSessionManager.currentCastSession diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index d181e175..4c3376bb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -110,4 +110,9 @@ class DownloadedPlayerActivity : AppCompatActivity() { return } } + + override fun onResume() { + super.onResume() + CommonActivity.setActivityInstance(this) + } } \ No newline at end of file From b120a7bce206a1c7f537dc686446be3f0255eac6 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 10 Oct 2023 18:16:35 +0300 Subject: [PATCH 130/441] Library on TV (#663) * implementation for Library on TV --- .../cloudstream3/ExampleInstrumentedTest.kt | 4 + .../lagradost/cloudstream3/MainActivity.kt | 6 +- .../ui/library/LibraryFragment.kt | 98 +++++++-- .../ui/library/ViewpagerAdapter.kt | 5 +- app/src/main/res/layout/fragment_library.xml | 17 +- .../main/res/layout/fragment_library_tv.xml | 200 ++++++++++++++++++ .../res/layout/library_viewpager_page.xml | 2 + 7 files changed, 305 insertions(+), 27 deletions(-) create mode 100644 app/src/main/res/layout/fragment_library_tv.xml diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index df41ef91..a84b2457 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -9,6 +9,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding +import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding +import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding import com.lagradost.cloudstream3.databinding.FragmentResultBinding @@ -120,6 +122,8 @@ class ExampleInstrumentedTest { testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) + testAllLayouts(activity, R.layout.fragment_library_tv, R.layout.fragment_library) + testAllLayouts(activity, R.layout.fragment_library_tv, R.layout.fragment_library) } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 5595c377..f9fff88c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -543,9 +543,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { navRailView.isVisible = isNavVisible && landscape // Hide library on TV since it is not supported yet :( - val isTrueTv = isTrueTvSettings() - navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv - navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + //val isTrueTv = isTrueTvSettings() + //navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + //navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv // Hide downloads on TV //navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index a3cc16c9..85f0aedd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -1,38 +1,52 @@ package com.lagradost.cloudstream3.ui.library +import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.res.Configuration import android.os.Bundle import android.os.Handler import android.os.Looper +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.FOCUS_AFTER_DESCENDANTS +import android.view.ViewGroup.FOCUS_BLOCK_DESCENDANTS import android.view.animation.AlphaAnimation +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast import androidx.annotation.StringRes import androidx.appcompat.widget.SearchView +import androidx.core.view.allViews import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.viewpager2.widget.ViewPager2 +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.debugAssert +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA +import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity @@ -80,9 +94,21 @@ class LibraryFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val localBinding = FragmentLibraryBinding.inflate(inflater, container, false) - binding = localBinding - return localBinding.root + val layout = + if (SettingsFragment.isTvSettings()) R.layout.fragment_library_tv else R.layout.fragment_library + val root = inflater.inflate(layout, container, false) + binding = try { + FragmentLibraryBinding.bind(root) + } catch (t: Throwable) { + CommonActivity.showToast( + txt(R.string.unable_to_inflate, t.message ?: ""), + Toast.LENGTH_LONG + ) + logError(t) + null + } + + return root //return inflater.inflate(R.layout.fragment_library, container, false) } @@ -99,24 +125,16 @@ class LibraryFragment : Fragment() { super.onSaveInstanceState(outState) } + @SuppressLint("ResourceType", "CutPasteId") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) fixPaddingStatusbar(binding?.searchStatusBarPadding) - binding?.sortFab?.setOnClickListener { - val methods = libraryViewModel.sortingMethods.map { - txt(it.stringRes).asString(view.context) - } + binding?.sortFab?.setOnClickListener(sortChangeClickListener) + binding?.librarySort?.setOnClickListener(sortChangeClickListener) - activity?.showBottomDialog(methods, - libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod), - txt(R.string.sort_by).asString(view.context), - false, - {}, - { - val method = libraryViewModel.sortingMethods[it] - libraryViewModel.sort(method) - }) + binding?.libraryRoot?.findViewById(R.id.search_src_text)?.apply { + tag = "tv_no_focus_tag" } binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { @@ -266,7 +284,10 @@ class LibraryFragment : Fragment() { // selects the whole list opener val savedListSelection = getKey("$currentAccount/$LIBRARY_FOLDER", syncName.name) - val savedSelection = getKey("$currentAccount/$LIBRARY_FOLDER", syncId).takeIf { + val savedSelection = getKey( + "$currentAccount/$LIBRARY_FOLDER", + syncId + ).takeIf { it?.openType != LibraryOpenerType.Default } ?: savedListSelection @@ -354,6 +375,12 @@ class LibraryFragment : Fragment() { } (viewpager.adapter as? ViewpagerAdapter)?.pages = pages + //fix focus on the viewpager itself + (viewpager.getChildAt(0) as RecyclerView).apply { + tag = "tv_no_focus_tag" + //isFocusable = false + } + // Using notifyItemRangeChanged keeps the animations when sorting viewpager.adapter?.notifyItemRangeChanged( 0, @@ -396,6 +423,9 @@ class LibraryFragment : Fragment() { viewpager, ) { tab, position -> tab.text = pages.getOrNull(position)?.title?.asStringNull(context) + tab.view.tag = "tv_no_focus_tag" + tab.view.nextFocusDownId = R.id.search_result_root + tab.view.setOnClickListener { val currentItem = binding?.viewpager?.currentItem ?: return@setOnClickListener @@ -418,17 +448,45 @@ class LibraryFragment : Fragment() { } } } - binding?.viewpager?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + binding?.viewpager?.registerOnPageChangeCallback(object : + ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { - libraryViewModel.currentPage = position + val all = binding?.viewpager?.allViews?.toList() + ?.filterIsInstance() + + all?.forEach { view -> + view.isVisible = view.tag == position + view.isFocusable = view.tag == position + + if (view.tag == position) + view.descendantFocusability = FOCUS_AFTER_DESCENDANTS + else + view.descendantFocusability = FOCUS_BLOCK_DESCENDANTS + } + super.onPageSelected(position) } }) } - override fun onConfigurationChanged(newConfig: Configuration) { (binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind() super.onConfigurationChanged(newConfig) } + + private val sortChangeClickListener = View.OnClickListener { view -> + val methods = libraryViewModel.sortingMethods.map { + txt(it.stringRes).asString(view.context) + } + + activity?.showBottomDialog(methods, + libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod), + txt(R.string.sort_by).asString(view.context), + false, + {}, + { + val method = libraryViewModel.sortingMethods[it] + libraryViewModel.sort(method) + }) + } } class MenuSearchView(context: Context) : SearchView(context) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt index 95fefcbe..76028487 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -25,7 +25,7 @@ class ViewpagerAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is PageViewHolder -> { - holder.bind(pages[position], unbound.remove(position)) + holder.bind(pages[position], position, unbound.remove(position)) } } } @@ -43,7 +43,8 @@ class ViewpagerAdapter( inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(page: SyncAPI.Page, rebind: Boolean) { + fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) { + binding.pageRecyclerview.tag = position binding.pageRecyclerview.apply { spanCount = this@PageViewHolder.itemView.context.getSpanCount() ?: 3 diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml index 985d055d..879ddbd9 100644 --- a/app/src/main/res/layout/fragment_library.xml +++ b/app/src/main/res/layout/fragment_library.xml @@ -41,6 +41,20 @@ android:src="@drawable/ic_baseline_extension_24" app:tint="?attr/textColor" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/library_viewpager_page.xml b/app/src/main/res/layout/library_viewpager_page.xml index 7d278cff..aa9745fb 100644 --- a/app/src/main/res/layout/library_viewpager_page.xml +++ b/app/src/main/res/layout/library_viewpager_page.xml @@ -5,5 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" + android:focusable="false" + android:tag="tv_no_focus_tag" tools:listitem="@layout/home_result_grid_expanded" /> From abbad1bc949588009bb1cf4c22405e33d790d09c Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 10 Oct 2023 18:17:18 +0300 Subject: [PATCH 131/441] Delete Focus frame from empty Downloads list & Search TV Layout (#675) * Delete Focus frame in search TV layout * Delete focus frame for empty Downloads list * Chip rounded stroke frame --- .../com/lagradost/cloudstream3/ui/search/SearchFragment.kt | 3 ++- app/src/main/res/layout/fragment_downloads.xml | 1 + app/src/main/res/layout/fragment_search_tv.xml | 2 ++ app/src/main/res/values/styles.xml | 5 +++-- 4 files changed, 8 insertions(+), 3 deletions(-) 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 0e994be8..ce92d723 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 @@ -11,6 +11,7 @@ import android.widget.AbsListView import android.widget.ArrayAdapter import android.widget.ImageView import android.widget.ListView +import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible @@ -221,7 +222,7 @@ class SearchFragment : Fragment() { SearchHelper.handleSearchClickCallback(callback) } - + searchRoot.findViewById(R.id.search_src_text)?.tag = "tv_no_focus_tag" searchAutofitResults.adapter = adapter searchLoadingBar.alpha = 0f } diff --git a/app/src/main/res/layout/fragment_downloads.xml b/app/src/main/res/layout/fragment_downloads.xml index 65f36209..5623bc7e 100644 --- a/app/src/main/res/layout/fragment_downloads.xml +++ b/app/src/main/res/layout/fragment_downloads.xml @@ -137,6 +137,7 @@ android:background="?attr/primaryBlackBackground" android:descendantFocusability="afterDescendants" android:nextFocusLeft="@id/nav_rail_view" + android:tag = "@string/tv_no_focus_tag" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:listitem="@layout/download_header_episode" /> diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index b3f88cd2..e34b0ac3 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -85,6 +85,7 @@ android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" android:nextFocusDown="@id/tvtypes_chips_scroll" + android:tag = "@string/tv_no_focus_tag" android:src="@drawable/ic_baseline_tune_24" app:tint="?attr/textColor" /> @@ -141,6 +142,7 @@ android:nextFocusLeft="@id/nav_rail_view" android:nextFocusUp="@id/tvtypes_chips" android:nextFocusDown="@id/search_clear_call_history" + android:tag = "@string/tv_no_focus_tag" android:paddingBottom="50dp" android:visibility="visible" tools:listitem="@layout/search_history_item" /> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5f7adea4..c00970d3 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -86,8 +86,8 @@ From 91b195241e1f6a07a037732376c7007e4cbc3f30 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Tue, 10 Oct 2023 15:19:27 +0000 Subject: [PATCH 132/441] Automatic backups (#592) Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> --- .../lagradost/cloudstream3/MainActivity.kt | 2 +- .../services/BackupWorkManager.kt | 96 +++++++++++++++++++ .../ui/settings/SettingsUpdates.kt | 37 ++++++- .../cloudstream3/utils/BackupUtils.kt | 52 +++++----- .../lagradost/cloudstream3/utils/UIHelper.kt | 2 +- app/src/main/res/values/array.xml | 22 ++++- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/settings_updates.xml | 15 ++- 8 files changed, 193 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index f9fff88c..51032a6e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1110,7 +1110,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (appVer != lastAppAutoBackup) { setKey("VERSION_NAME", BuildConfig.VERSION_NAME) normalSafeApiCall { - backup() + backup(this) } normalSafeApiCall { // Recompile oat on new version diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt new file mode 100644 index 00000000..6ed7a447 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt @@ -0,0 +1,96 @@ +package com.lagradost.cloudstream3.services + +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ForegroundInfo +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.BackupUtils +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import java.util.concurrent.TimeUnit + +const val BACKUP_CHANNEL_ID = "cloudstream3.backups" +const val BACKUP_WORK_NAME = "work_backup" +const val BACKUP_CHANNEL_NAME = "Backups" +const val BACKUP_CHANNEL_DESCRIPTION = "Notifications for background backups" +const val BACKUP_NOTIFICATION_ID = 938712898 // Random unique + +class BackupWorkManager(val context: Context, workerParams: WorkerParameters) : + CoroutineWorker(context, workerParams) { + companion object { + fun enqueuePeriodicWork(context: Context?, intervalHours: Long) { + if (context == null) return + + if (intervalHours == 0L) { + WorkManager.getInstance(context).cancelUniqueWork(BACKUP_WORK_NAME) + return + } + + val constraints = Constraints.Builder() + .setRequiresStorageNotLow(true) + .build() + + val periodicSyncDataWork = + PeriodicWorkRequest.Builder( + BackupWorkManager::class.java, + intervalHours, + TimeUnit.HOURS + ) + .addTag(BACKUP_WORK_NAME) + .setConstraints(constraints) + .build() + + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + BACKUP_WORK_NAME, + ExistingPeriodicWorkPolicy.UPDATE, + periodicSyncDataWork + ) + + // Uncomment below for testing + +// val oneTimeBackupWork = +// OneTimeWorkRequest.Builder(BackupWorkManager::class.java) +// .addTag(BACKUP_WORK_NAME) +// .setConstraints(constraints) +// .build() +// +// WorkManager.getInstance(context).enqueue(oneTimeBackupWork) + } + } + + private val backupNotificationBuilder = + NotificationCompat.Builder(context, BACKUP_CHANNEL_ID) + .setColorized(true) + .setOnlyAlertOnce(true) + .setSilent(true) + .setAutoCancel(true) + .setContentTitle(context.getString(R.string.pref_category_backup)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setColor(context.colorFromAttribute(R.attr.colorPrimary)) + .setSmallIcon(R.drawable.ic_cloudstream_monochrome_big) + + override suspend fun doWork(): Result { + context.createNotificationChannel( + BACKUP_CHANNEL_ID, + BACKUP_CHANNEL_NAME, + BACKUP_CHANNEL_DESCRIPTION + ) + + setForeground( + ForegroundInfo( + BACKUP_NOTIFICATION_ID, + backupNotificationBuilder.build() + ) + ) + + BackupUtils.backup(context) + + return Result.success() + } +} \ No newline at end of file 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 62e46c08..2f796801 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 @@ -19,14 +19,16 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.databinding.LogcatBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.network.initClient +import com.lagradost.cloudstream3.services.BackupWorkManager import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.BackupUtils.backup +import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.VideoDownloadManager @@ -48,7 +50,30 @@ class SettingsUpdates : PreferenceFragmentCompat() { //val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) getPref(R.string.backup_key)?.setOnPreferenceClickListener { - activity?.backup() + BackupUtils.backup(activity) + return@setOnPreferenceClickListener true + } + + getPref(R.string.automatic_backup_key)?.setOnPreferenceClickListener { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) + + val prefNames = resources.getStringArray(R.array.periodic_work_names) + val prefValues = resources.getIntArray(R.array.periodic_work_values) + val current = settingsManager.getInt(getString(R.string.automatic_backup_key), 0) + + activity?.showDialog( + prefNames.toList(), + prefValues.indexOf(current), + getString(R.string.backup_frequency), + true, + {}) { index -> + settingsManager.edit() + .putInt(getString(R.string.automatic_backup_key), prefValues[index]).apply() + BackupWorkManager.enqueuePeriodicWork( + context ?: AcraApplication.context, + prefValues[index].toLong() + ) + } return@setOnPreferenceClickListener true } @@ -65,7 +90,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { val builder = AlertDialog.Builder(pref.context, R.style.AlertDialogCustom) - val binding = LogcatBinding.inflate(layoutInflater,null,false ) + val binding = LogcatBinding.inflate(layoutInflater, null, false) builder.setView(binding.root) val dialog = builder.create() @@ -176,7 +201,8 @@ class SettingsUpdates : PreferenceFragmentCompat() { val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context) val prefNames = resources.getStringArray(R.array.auto_download_plugin) - val prefValues = enumValues().sortedBy { x -> x.value }.map { x -> x.value } + val prefValues = + enumValues().sortedBy { x -> x.value }.map { x -> x.value } val current = settingsManager.getInt(getString(R.string.auto_download_plugins_key), 0) @@ -186,7 +212,8 @@ class SettingsUpdates : PreferenceFragmentCompat() { getString(R.string.automatic_plugin_download_mode_title), true, {}) { - settingsManager.edit().putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() + settingsManager.edit() + .putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } return@setOnPreferenceClickListener true 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 96593769..e50131fe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -10,6 +10,7 @@ import androidx.annotation.WorkerThread import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError @@ -90,9 +91,11 @@ object BackupUtils { ) @Suppress("UNCHECKED_CAST") - fun Context.getBackup(): BackupFile { - val allData = getSharedPrefs().all.filter { it.key.isTransferable() } - val allSettings = getDefaultSharedPrefs().all.filter { it.key.isTransferable() } + private fun getBackup(context: Context?): BackupFile? { + if (context == null) return null + + val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() } + val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() } val allDataSorted = BackupVars( allData.filter { it.value is Boolean } as? Map, @@ -119,46 +122,50 @@ object BackupUtils { } @WorkerThread - fun Context.restore( + fun restore( + context: Context?, backupFile: BackupFile, restoreSettings: Boolean, restoreDataStore: Boolean ) { + if (context == null) return if (restoreSettings) { - restoreMap(backupFile.settings._Bool, true) - restoreMap(backupFile.settings._Int, true) - restoreMap(backupFile.settings._String, true) - restoreMap(backupFile.settings._Float, true) - restoreMap(backupFile.settings._Long, true) - restoreMap(backupFile.settings._StringSet, true) + context.restoreMap(backupFile.settings._Bool, true) + context.restoreMap(backupFile.settings._Int, true) + context.restoreMap(backupFile.settings._String, true) + context.restoreMap(backupFile.settings._Float, true) + context.restoreMap(backupFile.settings._Long, true) + context.restoreMap(backupFile.settings._StringSet, true) } if (restoreDataStore) { - restoreMap(backupFile.datastore._Bool) - restoreMap(backupFile.datastore._Int) - restoreMap(backupFile.datastore._String) - restoreMap(backupFile.datastore._Float) - restoreMap(backupFile.datastore._Long) - restoreMap(backupFile.datastore._StringSet) + context.restoreMap(backupFile.datastore._Bool) + context.restoreMap(backupFile.datastore._Int) + context.restoreMap(backupFile.datastore._String) + context.restoreMap(backupFile.datastore._Float) + context.restoreMap(backupFile.datastore._Long) + context.restoreMap(backupFile.datastore._StringSet) } } @SuppressLint("SimpleDateFormat") - fun FragmentActivity.backup() = ioSafe { + fun backup(context: Context?) = ioSafe { + if (context == null) return@ioSafe + var fileStream: OutputStream? = null var printStream: PrintWriter? = null try { - if (!checkWrite()) { + if (!context.checkWrite()) { showToast(R.string.backup_failed, Toast.LENGTH_LONG) - requestRW() + context.getActivity()?.requestRW() return@ioSafe } val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) val ext = "txt" val displayName = "CS3_Backup_${date}" - val backupFile = getBackup() - val stream = setupStream(this@backup, displayName, null, ext, false) + val backupFile = getBackup(context) + val stream = setupStream(context, displayName, null, ext, false) fileStream = stream.openNew() printStream = PrintWriter(fileStream) @@ -198,7 +205,8 @@ object BackupUtils { val restoredValue = mapper.readValue(input) - activity.restore( + restore( + activity, restoredValue, restoreSettings = true, restoreDataStore = true 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 038a2f11..9b40e70e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -71,7 +71,7 @@ object UIHelper { val Int.toDp: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt() val Float.toDp: Float get() = (this / Resources.getSystem().displayMetrics.density) - fun Activity.checkWrite(): Boolean { + fun Context.checkWrite(): Boolean { return (ContextCompat.checkSelfPermission( this, Manifest.permission.WRITE_EXTERNAL_STORAGE diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 1df7b9d6..b8f0cbf8 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -41,7 +41,7 @@ 0 1 - + @string/disable @@ -126,6 +126,26 @@ 30min + + @string/none + 3h + 6h + 12h + 24h + 3d + 7d + + + + 0 + 3 + 6 + 12 + 24 + 72 + 168 + + 0 60 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13251c7c..c722f33f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ primary_color_key restore_key backup_key + automatic_backup_key prefer_media_type_key_2 app_theme_key episode_sync_enabled_key @@ -229,6 +230,7 @@ Automatically sync your current episode progress Restore data from backup Back up data + Backup frequency Loaded backup file Failed to restore data from file %s Data stored diff --git a/app/src/main/res/xml/settings_updates.xml b/app/src/main/res/xml/settings_updates.xml index 9989e47b..e3b36648 100644 --- a/app/src/main/res/xml/settings_updates.xml +++ b/app/src/main/res/xml/settings_updates.xml @@ -9,7 +9,7 @@ android:summaryOn="@string/bug_report_settings_on" android:title="@string/pref_disable_acra" /> - - + + - - + - Date: Tue, 10 Oct 2023 18:05:31 +0200 Subject: [PATCH 133/441] reverted to instant outline --- .../lagradost/cloudstream3/MainActivity.kt | 55 +++++---- app/src/main/res/drawable/outline.xml | 4 +- .../main/res/drawable/outline_drawable.xml | 4 +- .../res/drawable/outline_drawable_less.xml | 2 +- app/src/main/res/layout/fragment_result.xml | 2 +- app/src/main/res/layout/result_episode.xml | 4 +- .../main/res/layout/result_episode_both.xml | 9 -- .../res/layout/result_episode_both_old.xml | 14 +++ .../res/layout/result_episode_both_tv.xml | 21 ---- .../res/layout/result_episode_both_tv_old.xml | 21 ++++ .../main/res/layout/result_episode_large.xml | 1 + .../res/layout/result_episode_large_tv.xml | 111 ----------------- .../layout/result_episode_large_tv_old.xml | 112 ++++++++++++++++++ app/src/main/res/layout/result_episode_tv.xml | 60 ---------- .../main/res/layout/result_episode_tv_old.xml | 59 +++++++++ app/src/main/res/values/styles.xml | 9 +- 16 files changed, 246 insertions(+), 242 deletions(-) delete mode 100644 app/src/main/res/layout/result_episode_both.xml create mode 100644 app/src/main/res/layout/result_episode_both_old.xml delete mode 100644 app/src/main/res/layout/result_episode_both_tv.xml create mode 100644 app/src/main/res/layout/result_episode_both_tv_old.xml delete mode 100644 app/src/main/res/layout/result_episode_large_tv.xml create mode 100644 app/src/main/res/layout/result_episode_large_tv_old.xml delete mode 100644 app/src/main/res/layout/result_episode_tv.xml create mode 100644 app/src/main/res/layout/result_episode_tv_old.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 51032a6e..4e0d93c9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -283,6 +283,7 @@ var app = Requests(responseParser = object : ResponseParser { class MainActivity : AppCompatActivity(), ColorPickerDialogListener { companion object { const val TAG = "MAINACT" + const val ANIMATED_OUTLINE : Boolean = false var lastError: String? = null /** @@ -1070,7 +1071,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } - + private fun centerView(view : View?) { + if(view == null) return + try { + Log.v(TAG, "centerView: $view") + val r = Rect(0, 0, 0, 0) + view.getDrawingRect(r) + val x = r.centerX() + val y = r.centerY() + val dx = r.width() / 2 //screenWidth / 2 + val dy = screenHeight / 2 + val r2 = Rect(x - dx, y - dy, x + dx, y + dy) + view.requestRectangleOnScreen(r2, false) + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_: Throwable) { + } + } override fun onCreate(savedInstanceState: Bundle?) { app.initClient(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) @@ -1125,43 +1141,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) setContentView(newLocalBinding.root) - if(isTrueTvSettings()) { + if(isTrueTvSettings() && ANIMATED_OUTLINE) { TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) + newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { + TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) + } newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> - // println("refocus $oldFocus -> $newFocus") - try { - val r = Rect(0, 0, 0, 0) - newFocus.getDrawingRect(r) - val x = r.centerX() - val y = r.centerY() - val dx = 0 //screenWidth / 2 - val dy = screenHeight / 2 - val r2 = Rect(x - dx, y - dy, x + dx, y + dy) - newFocus.requestRectangleOnScreen(r2, false) - // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) - } catch (_: Throwable) { - } TvFocus.updateFocusView(newFocus) - /*var focus = newFocus - - while(focus != null) { - if(focus is ScrollingView && focus.canScrollVertically()) { - focus.scrollBy() - } - when(focus.parent) { - is View -> focus = newFocus - else -> break - } - }*/ } } else { newLocalBinding.focusOutline.isVisible = false } - newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { - TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) + if(isTrueTvSettings()) { + newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> + centerView(newFocus) + } } + + ActivityMainBinding.bind(newLocalBinding.root) // this may crash } else { val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false) diff --git a/app/src/main/res/drawable/outline.xml b/app/src/main/res/drawable/outline.xml index 30077a98..7b436c7d 100644 --- a/app/src/main/res/drawable/outline.xml +++ b/app/src/main/res/drawable/outline.xml @@ -2,11 +2,9 @@ - - \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_drawable.xml b/app/src/main/res/drawable/outline_drawable.xml index 8eec2d0b..16eba83c 100644 --- a/app/src/main/res/drawable/outline_drawable.xml +++ b/app/src/main/res/drawable/outline_drawable.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_drawable_less.xml b/app/src/main/res/drawable/outline_drawable_less.xml index db74a092..aa3a8d0d 100644 --- a/app/src/main/res/drawable/outline_drawable_less.xml +++ b/app/src/main/res/drawable/outline_drawable_less.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 87de7186..9d748c5a 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -875,7 +875,7 @@ android:descendantFocusability="afterDescendants" android:paddingBottom="100dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - tools:listitem="@layout/result_episode_both_tv" /> + tools:listitem="@layout/result_episode" /> diff --git a/app/src/main/res/layout/result_episode.xml b/app/src/main/res/layout/result_episode.xml index 80ff4bec..b56cdb1d 100644 --- a/app/src/main/res/layout/result_episode.xml +++ b/app/src/main/res/layout/result_episode.xml @@ -11,7 +11,9 @@ android:nextFocusRight="@id/download_button" app:cardBackgroundColor="@color/transparent" app:cardCornerRadius="@dimen/rounded_image_radius" - app:cardElevation="0dp"> + app:cardElevation="0dp" + android:foreground="@drawable/outline_drawable" + > diff --git a/app/src/main/res/layout/result_episode_both.xml b/app/src/main/res/layout/result_episode_both.xml deleted file mode 100644 index 61102e84..00000000 --- a/app/src/main/res/layout/result_episode_both.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_both_old.xml b/app/src/main/res/layout/result_episode_both_old.xml new file mode 100644 index 00000000..6472ecc1 --- /dev/null +++ b/app/src/main/res/layout/result_episode_both_old.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_both_tv.xml b/app/src/main/res/layout/result_episode_both_tv.xml deleted file mode 100644 index 13888b7e..00000000 --- a/app/src/main/res/layout/result_episode_both_tv.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_both_tv_old.xml b/app/src/main/res/layout/result_episode_both_tv_old.xml new file mode 100644 index 00000000..f273a118 --- /dev/null +++ b/app/src/main/res/layout/result_episode_both_tv_old.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index 75292965..76e8c434 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -8,6 +8,7 @@ android:layout_height="wrap_content" android:layout_marginBottom="10dp" + android:foreground="@drawable/outline_drawable" android:nextFocusRight="@id/download_button" app:cardBackgroundColor="?attr/boxItemBackground" diff --git a/app/src/main/res/layout/result_episode_large_tv.xml b/app/src/main/res/layout/result_episode_large_tv.xml deleted file mode 100644 index 5a9dee30..00000000 --- a/app/src/main/res/layout/result_episode_large_tv.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_large_tv_old.xml b/app/src/main/res/layout/result_episode_large_tv_old.xml new file mode 100644 index 00000000..3a7cef3c --- /dev/null +++ b/app/src/main/res/layout/result_episode_large_tv_old.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_tv.xml b/app/src/main/res/layout/result_episode_tv.xml deleted file mode 100644 index 53590b6b..00000000 --- a/app/src/main/res/layout/result_episode_tv.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_tv_old.xml b/app/src/main/res/layout/result_episode_tv_old.xml new file mode 100644 index 00000000..62546cf9 --- /dev/null +++ b/app/src/main/res/layout/result_episode_tv_old.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index c00970d3..c047c749 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -86,8 +86,8 @@ + + + + + + + + + + + + @@ -506,8 +506,8 @@ ?attr/colorPrimary - @android:dimen/dialog_min_width_major - @android:dimen/dialog_min_width_minor + @dimen/abc_dialog_min_width_major + @dimen/abc_dialog_min_width_minor @drawable/dialog__window_background From 35e38a53ad7638ef9407c8f710966ea09d6dac8b Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 25 Mar 2024 05:29:55 +0530 Subject: [PATCH 313/441] refactor: format build date and time and make it copyable (#1002) --- app/build.gradle.kts | 6 +++--- .../ui/settings/SettingsFragment.kt | 17 ++++++++++++----- app/src/main/res/layout/main_settings.xml | 13 +++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c2ba2907..7ba682be 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -70,9 +70,9 @@ android { val localProperties = gradleLocalProperties(rootDir) buildConfigField( - "String", - "BUILDDATE", - "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));" + "long", + "BUILD_DATE", + "${System.currentTimeMillis()}" ) buildConfigField( "String", 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 72e22269..dfa84998 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 @@ -30,6 +30,11 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import java.io.File +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone class SettingsFragment : Fragment() { companion object { @@ -180,12 +185,14 @@ class SettingsFragment : Fragment() { val appVersion = getString(R.string.app_version) val commitInfo = getString(R.string.commit_hash) - val buildDate = BuildConfig.BUILDDATE + val buildTimestamp = SimpleDateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, + Locale.getDefault() + ).apply { timeZone = TimeZone.getTimeZone("UTC") + }.format(Date(BuildConfig.BUILD_DATE)).replace("UTC", "") - binding?.buildDate?.text = buildDate - - binding?.appVersionInfo?.setOnLongClickListener{ - clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo") + binding?.buildDate?.text = buildTimestamp + binding?.appVersionInfo?.setOnLongClickListener { + clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo $buildTimestamp") true } } diff --git a/app/src/main/res/layout/main_settings.xml b/app/src/main/res/layout/main_settings.xml index c3bdc17d..2c90d958 100644 --- a/app/src/main/res/layout/main_settings.xml +++ b/app/src/main/res/layout/main_settings.xml @@ -112,9 +112,9 @@ android:orientation="horizontal"> @@ -123,8 +123,6 @@ android:id="@+id/delimiter0" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center" - android:padding="10dp" android:text="•" android:textColor="?attr/textColor" /> @@ -132,7 +130,6 @@ android:id="@+id/commit_hash" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center" android:padding="10dp" android:text="@string/commit_hash" android:textColor="?attr/textColor" /> @@ -141,9 +138,6 @@ android:id="@+id/delimiter1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_weight="1" - android:gravity="center" - android:padding="10dp" android:text="•" android:textColor="?attr/textColor" /> @@ -151,10 +145,9 @@ android:id="@+id/build_date" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_weight="1" - android:gravity="center" android:padding="10dp" - android:textColor="?attr/textColor" /> + android:textColor="?attr/textColor" + tools:text="21/03/2024 09:02 pm"/> From 22937424fa7e96119a665bb10668df8cb89f7d35 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 25 Mar 2024 05:33:04 +0530 Subject: [PATCH 314/441] feat(ui): authenticate first when enabling security settings (#991) --- app/build.gradle.kts | 2 +- .../lagradost/cloudstream3/MainActivity.kt | 14 ++-- .../ui/account/AccountSelectActivity.kt | 14 ++-- .../ui/settings/SettingsAccount.kt | 67 ++++++++++++++----- .../utils/BiometricAuthenticator.kt | 33 ++++----- app/src/main/res/values/strings.xml | 4 +- build.gradle.kts | 8 +-- 7 files changed, 91 insertions(+), 51 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7ba682be..02946e85 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -154,7 +154,7 @@ repositories { dependencies { // Testing testImplementation("junit:junit:4.13.2") - testImplementation("org.json:json:20231013") + testImplementation("org.json:json:20240303") androidTestImplementation("androidx.test:core") implementation("androidx.test.ext:junit-ktx:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 67bf19fb..7baac71c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -135,7 +135,10 @@ import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -1231,18 +1234,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, changeStatusBarState(isLayout(EMULATOR)) /** Biometric stuff for users without accounts **/ - val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) val noAccounts = settingsManager.getBoolean( getString(R.string.skip_startup_account_select_key), false ) || accounts.count() <= 1 - if (isLayout(PHONE) && authEnabled && noAccounts) { + if (isLayout(PHONE) && isAuthEnabled(this) && noAccounts) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication(this, R.string.biometric_authentication_title, false) - BiometricAuthenticator.promptInfo?.let { promt -> - BiometricAuthenticator.biometricPrompt?.authenticate(promt) + promptInfo?.let { prompt -> + biometricPrompt?.authenticate(prompt) } // hide background while authenticating, Sorry moms & dads 🙏 @@ -1825,6 +1827,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, binding?.navHostFragment?.isInvisible = false } + override fun onAuthenticationError() { + finish() + } + private var backPressedCallback: OnBackPressedCallback? = null private fun attachBackPressedCallback() { 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 41aef176..0b0d83db 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 @@ -23,7 +23,10 @@ import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex @@ -48,7 +51,6 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet ) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false ) || accounts.count() <= 1 @@ -56,7 +58,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet fun askBiometricAuth() { - if (isLayout(PHONE) && authEnabled) { + if (isLayout(PHONE) && isAuthEnabled(this)) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication( this, @@ -64,8 +66,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet false ) - BiometricAuthenticator.promptInfo?.let { promt -> - BiometricAuthenticator.biometricPrompt?.authenticate(promt) + promptInfo?.let { prompt -> + biometricPrompt?.authenticate(prompt) } } } @@ -189,4 +191,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet override fun onAuthenticationSuccess() { Log.i(BiometricAuthenticator.TAG,"Authentication successful in AccountSelectActivity") } + + override fun onAuthenticationError() { + finish() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 298431ee..f0d402da 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -12,6 +12,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager +import androidx.preference.SwitchPreferenceCompat import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent @@ -30,6 +31,7 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API 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.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref @@ -38,13 +40,20 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setTool import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.BackupUtils +import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage -class SettingsAccount : PreferenceFragmentCompat() { +class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback { companion object { /** Used by nginx plugin too */ fun showLoginInfo( @@ -252,6 +261,31 @@ class SettingsAccount : PreferenceFragmentCompat() { } } + private fun updateAuthPreference(enabled: Boolean) { + val biometricKey = getString(R.string.biometric_key) + + PreferenceManager.getDefaultSharedPreferences(context ?: return).edit() + .putBoolean(biometricKey, enabled).apply() + findPreference(biometricKey)?.isChecked = enabled + } + + override fun onAuthenticationError() { + updateAuthPreference(!isAuthEnabled(context ?: return)) + } + + override fun onAuthenticationSuccess() { + if (isAuthEnabled(context?: return)) { + updateAuthPreference(true) + BackupUtils.backup(activity) + activity?.showBottomDialogText( + getString(R.string.biometric_setting), + getString(R.string.biometric_warning).html() + ) { onDialogDismissedEvent } + } else { + updateAuthPreference(false) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setUpToolbar(R.string.category_account) @@ -263,22 +297,25 @@ class SettingsAccount : PreferenceFragmentCompat() { hideKeyboard() setPreferencesFromResource(R.xml.settings_account, rootKey) - getPref(R.string.biometric_key)?.setOnPreferenceClickListener { - val authEnabled = PreferenceManager.getDefaultSharedPreferences( - context ?: return@setOnPreferenceClickListener false - ) - .getBoolean(getString(R.string.biometric_key), false) + // hide preference on tvs and emulators + getPref(R.string.biometric_key)?.isEnabled = isLayout(PHONE) - if (authEnabled) { - BackupUtils.backup(activity) - val title = activity?.getString(R.string.biometric_setting) - val warning = activity?.getString(R.string.biometric_warning) - activity?.showBottomDialogText( - title as String, - warning.html() - ) { onDialogDismissedEvent } + getPref(R.string.biometric_key)?.setOnPreferenceClickListener { + val ctx = context ?: return@setOnPreferenceClickListener false + + if (deviceHasPasswordPinLock(ctx)) { + startBiometricAuthentication( + activity?: return@setOnPreferenceClickListener false, + R.string.biometric_authentication_title, + false + ) + promptInfo?.let { + authCallback = this + biometricPrompt?.authenticate(it) + } } - true + + false } val syncApis = diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt index de9b9963..c57600ee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt @@ -12,20 +12,20 @@ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.getString import androidx.fragment.app.FragmentActivity +import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R object BiometricAuthenticator { + const val TAG = "cs3Auth" private const val MAX_FAILED_ATTEMPTS = 3 private var failedAttempts = 0 - const val TAG = "cs3Auth" - private var biometricManager: BiometricManager? = null var biometricPrompt: BiometricPrompt? = null var promptInfo: BiometricPrompt.PromptInfo? = null - var authCallback: BiometricAuthCallback? = null // listen to authentication success private fun initializeBiometrics(activity: Activity) { @@ -37,20 +37,12 @@ object BiometricAuthenticator { activity as FragmentActivity, executor, object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) showToast("$errString") Log.e(TAG, "$errorCode") - failedAttempts++ - - if (failedAttempts >= MAX_FAILED_ATTEMPTS) { - failedAttempts = 0 - activity.finish() - } else { - failedAttempts = 0 - activity.finish() - } + authCallback?.onAuthenticationError() + //activity.finish() } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { @@ -89,7 +81,6 @@ object BiometricAuthenticator { .setDescription(description) .setAllowedAuthenticators(authFlag) .build() - } else { // for apis < 30 promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -98,7 +89,6 @@ object BiometricAuthenticator { .setDeviceCredentialAllowed(true) .build() } - } else { // fallback for A12+ when both fingerprint & Face unlock is absent but PIN is set promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -114,7 +104,6 @@ object BiometricAuthenticator { var result = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - when (biometricManager?.canAuthenticate( DEVICE_CREDENTIAL or BIOMETRIC_STRONG or BIOMETRIC_WEAK )) { @@ -126,7 +115,6 @@ object BiometricAuthenticator { BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> result = true BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> result = false } - } else { @Suppress("DEPRECATION") when (biometricManager?.canAuthenticate()) { @@ -153,12 +141,11 @@ object BiometricAuthenticator { // function to start authentication in any fragment or activity fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) { initializeBiometrics(activity) - + authCallback = activity as? BiometricAuthCallback if (isBiometricHardWareAvailable()) { authCallback = activity as? BiometricAuthCallback authenticationDialog(activity, title, setDeviceCred) promptInfo?.let { biometricPrompt?.authenticate(it) } - } else { if (deviceHasPasswordPinLock(activity)) { authCallback = activity as? BiometricAuthCallback @@ -171,7 +158,15 @@ object BiometricAuthenticator { } } + fun isAuthEnabled(ctx: Context):Boolean { + return ctx.let { + PreferenceManager.getDefaultSharedPreferences(ctx) + .getBoolean(getString(ctx, R.string.biometric_key), false) + } + } + interface BiometricAuthCallback { fun onAuthenticationSuccess() + fun onAuthenticationError() } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b5dae57b..ab56a849 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -249,7 +249,7 @@ Search Library Accounts and Security - Updates and backup + Updates and Backup Info Advanced Search Gives you the search results separated by provider @@ -611,7 +611,7 @@ Tracks Audio tracks Video tracks - Apply on Restart + Restart the app to see changes. Restart Stop Safe mode on diff --git a/build.gradle.kts b/build.gradle.kts index 06af44d0..801a3c0f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,3 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() @@ -6,12 +5,9 @@ buildscript { } dependencies { - classpath("com.android.tools.build:gradle:8.2.1") + classpath("com.android.tools.build:gradle:8.2.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle.kts files } } @@ -23,7 +19,7 @@ allprojects { } plugins { - id("com.google.devtools.ksp") version "1.9.22-1.0.16" apply false + id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false } tasks.register("clean") { From 7db7742c734421e2e350448b6e08c0b4e8cfb1d0 Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 00:04:49 +0000 Subject: [PATCH 315/441] chore(locales): fix locale issues --- app/src/main/res/values-af/strings.xml | 2 +- app/src/main/res/values-ajp/strings.xml | 2 +- app/src/main/res/values-am/strings.xml | 2 +- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-ars/strings.xml | 2 +- app/src/main/res/values-bg/strings.xml | 2 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-el/strings.xml | 2 +- app/src/main/res/values-eo/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 2 +- app/src/main/res/values-hr/strings.xml | 2 +- app/src/main/res/values-hu/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-iw/strings.xml | 2 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-kn/strings.xml | 2 +- app/src/main/res/values-ko/strings.xml | 2 +- app/src/main/res/values-lt/strings.xml | 2 +- app/src/main/res/values-lv/strings.xml | 2 +- app/src/main/res/values-mk/strings.xml | 2 +- app/src/main/res/values-ml/strings.xml | 2 +- app/src/main/res/values-my/strings.xml | 2 +- app/src/main/res/values-ne/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-nn/strings.xml | 2 +- app/src/main/res/values-no/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-qt/strings.xml | 2 +- app/src/main/res/values-ro/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-sk/strings.xml | 2 +- app/src/main/res/values-so/strings.xml | 2 +- app/src/main/res/values-sv/strings.xml | 2 +- app/src/main/res/values-ta/strings.xml | 2 +- app/src/main/res/values-tl/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-ur/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- 50 files changed, 50 insertions(+), 50 deletions(-) diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 5c19185c..45e9a1d4 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -106,4 +106,4 @@ Voer lettertipes in deur dit in %s te plaas Rolverdeling: %s Nuwe episode notifikasie - \ No newline at end of file + diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index eb2bf74a..4d1fc074 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -614,4 +614,4 @@ في ارور بالنسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ. هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. \nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. - \ No newline at end of file + diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml index 63f28ba8..7fd3274b 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -108,4 +108,4 @@ ተጨማሪ መረጃ ዓይነቶችን በመጠቀም ይፈልጉ ቅርጸ-ቁምፊዎችን በ%s ውስጥ በማስቀመጥ ያጫኑ - \ No newline at end of file + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 2ce9fd22..3140afeb 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -641,4 +641,4 @@ خطأ في الوصول الي حافظة النسخ، برجاء المحاولة مرة اخرى. تم النسخ! خطأ في عملية النسخ، برجاء نسخ ال logcat و ارساله الى مسؤولين دعم التطبيق. - \ No newline at end of file + diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index 530b07c9..f3811d3d 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -352,4 +352,4 @@ وثائقي موقع عنوان مشغل الفيديو بحد أقصى لعدد الأحرف - \ No newline at end of file + diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 4a67f8c5..2be08369 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -601,4 +601,4 @@ Покажи предложения Добавя опция за промяна на скоростта в плеъра Този тест е направен за програмисти и не проверява работата на никакви добавки. - \ No newline at end of file + diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index aa3def8f..867dd4ed 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -229,4 +229,4 @@ আপনার বর্তমান পর্বের অগ্রগতি স্বয়ংক্রিয়ভাবে সিঙ্ক করুন প্লাগইন ডাউনলোড ফিল্টার করতে মোড নির্বাচন করুন লিঙ্ক পুনরায় লোড হয়েছে - \ No newline at end of file + diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 1a81021a..0cf1bb2c 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -631,4 +631,4 @@ Erro ao acessar a área de transferência. Tente novamente. Nome e URL do repositório Erro ao copiar. Copie o logcat e entre em contato com o suporte do aplicativo. - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8a11823e..519b05b6 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -633,4 +633,4 @@ Chyba při kopírování, zkopírujte prosím protokol a kontaktujte podporu aplikace. Zkopírováno! Chyba při přístupu ke schránce, zkuste to prosím znovu. - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7c56787c..5a871217 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -607,4 +607,4 @@ Beim kopieren ist ein Fehler aufgetreten, bitte kopieren sie logical und wenden sich an den Support. Fehler beim zugriff auf die Zwischenablage, bitte erneut versuchen. Repository Name und URL - \ No newline at end of file + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 69bc390b..a539f374 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -546,4 +546,4 @@ Επιλέξτε κατάσταση για φιλτράρισμα επεκτάσεων για λήψη Απενεργοποιημένο Τέλος - \ No newline at end of file + diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 35b04402..275a4bfb 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -127,4 +127,4 @@ Elŝutite Elŝutante Elŝuto Malsukcesite - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d3e90a41..055fc06b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -609,4 +609,4 @@ ¡Copiado! Error al copiar. Por favor, copie el logcat y comuníquese con el soporte de la aplicación. Error al acceder al portapapeles. Inténtelo de nuevo. - \ No newline at end of file + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 85b65919..486f7a00 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -128,4 +128,4 @@ به پایان رسیده باز کردن در مرورگر برنامه‌ریزی برای تماشا - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 35b93ac6..17f6a667 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -595,4 +595,4 @@ Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. Copié ! Nom du dépôt et adresse internet - \ No newline at end of file + diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 2efe1991..ae3105cf 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -164,4 +164,4 @@ Selecciona o modo para filtrar a descarga dos complementos Instala automáticamente todos os complementos aínda non instalados dos repositorios engadidos. Mostrar actualizacións da aplicación - \ No newline at end of file + diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 68bd645e..8ce224b3 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -192,4 +192,4 @@ लिंक पुन्ह खुली वर्तमान पिन दर्ज करें नेटवर्क स्ट्रीम - \ No newline at end of file + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 4c31c274..ea6a80eb 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -629,4 +629,4 @@ Lozinka/PIN autentifikacija Ovaj uređaj ne podržava biometrijsku autentifikaciju Ovaj je ekran zatvoren zbog višestrukih neuspjelih pokušaja. Pokrenite aplikaciju ponovo. - \ No newline at end of file + diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index b27f9df4..5533cdc0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -592,4 +592,4 @@ A PIN 4 karakter hosszú kell legyen Auto elforgatás Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása - \ No newline at end of file + diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 6079d47d..d9a10c61 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -630,4 +630,4 @@ Gagal mengakses Papan Klip, mohon coba lagi. disalin! Gagal menyalin, mohon salin logcat dan hubungi pengembang aplikasi. - \ No newline at end of file + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 08a05c35..7b958ad3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -629,4 +629,4 @@ copiato! Errore durante l\'accesso agli Appunti. Riprova. Errore durante la copia. Copia logcat e contatta il supporto dell\'app. - \ No newline at end of file + diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 79c9e276..da2952a0 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -550,4 +550,4 @@ \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! - \ No newline at end of file + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 5c80d77e..acb2cfc3 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -242,4 +242,4 @@ 現在のエピソードが終了したら次のエピソードを開始する 長押しするとデフォルトにリセットされます ダウンロードを再開 - \ No newline at end of file + diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 1c9d4e4c..f3fb665d 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -130,4 +130,4 @@ Brightness ಅಥವಾ volume ಬದಲಾಯಿಸಲು ಎಡ ಅಥವಾ ಬಲಭಾಗದಲ್ಲಿ ಮೇಲಕ್ಕೆ ಅಥವಾ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ ಈಗಿನ ಎಪಿಸೋಡ್ ಮುಗಿದಾಗ ಮುಂದಿನ ಎಪಿಸೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸ್ವೈಪ್ ಮಾಡಿ - \ No newline at end of file + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index cb60b51c..1a63050a 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -527,4 +527,4 @@ 구독중 구독 %s 구독 취소 %s - \ No newline at end of file + diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index cf951ab9..f61bcfc0 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -260,4 +260,4 @@ Ar tikrai norite išeiti\? Pašalinti iš žiūrimų Garso takelis - \ No newline at end of file + diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index deacfdca..49b333e3 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -527,4 +527,4 @@ Abonēto šovu atjaunināšana Abonēts Abonēts %s - \ No newline at end of file + diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 814a5ed3..fe82a90b 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -591,4 +591,4 @@ Зачестеност на зачувување на бекап Овозможете автоматско префрлување на ориентацијата на екранот врз основа на видео ориентација Автоматска ротација - \ No newline at end of file + diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index b4180f23..0ddd8577 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -245,4 +245,4 @@ ഉറവിട പിശക് നിലവിലെ പിൻ നൽകുക ഓഡിയോ ട്രാക്കുകൾ - \ No newline at end of file + diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index b29ca920..ef796f9f 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -550,4 +550,4 @@ သင်နဂိုတည်းကသတ်မှတ်ပြီး လိုက်ဘရီရွေးချယ်ရန် ဖြင့်ဖွင့်မည် - \ No newline at end of file + diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 97bda0a3..1e23f8af 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -85,4 +85,4 @@ स्रोतहरू स्वचालित बग रिपोर्टिङ असक्षम गर्नुहोस् लागू गर्नुहोस् - \ No newline at end of file + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 0844c7ec..fc537837 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -608,4 +608,4 @@ Link opnieuw geladen Autoroteer Roteer - \ No newline at end of file + diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 4835bcfb..95c527f9 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -195,4 +195,4 @@ Bilde i bilde Fortsett å sjå Prøv tilkopling på nytt… - \ No newline at end of file + diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index e599c2b0..724f4a63 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -538,4 +538,4 @@ Bruk Hjelp Profilbakgrunn - \ No newline at end of file + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index c2e1000a..3e22ba16 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -610,4 +610,4 @@ Błąd dostępu do schowka. Spróbuj ponownie. skopiowano! Błąd podczas kopiowania. Skopiuj logcat i skontaktuj się z pomocą techniczną aplikacji. - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index ff0f952f..b3180fee 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -607,4 +607,4 @@ Desbloqueie a aplicação com impressão digital, ID facial, PIN, padrão e palavra-passe. Esta janela fechar-se-á após algumas tentativas falhadas. Terá de reiniciar a aplicação. Foi feita uma cópia de segurança dos seus dados CloudStream, embora a probabilidade deste caso raro seja muito baixa, mas todos os dispositivos se comportam de forma diferente. No caso de ficar impedido de aceder à aplicação, na pior das hipóteses, limpe totalmente os dados da aplicação e restaure a cópia de segurança. Lamentamos profundamente qualquer inconveniente. - \ No newline at end of file + diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 583c6e0e..5de97c7d 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -247,4 +247,4 @@ oooooh uuaagh @string/home_play oouuhhh ahhooo-ahah - \ No newline at end of file + diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 224ba880..d7da44b4 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -593,4 +593,4 @@ Adaugă o opțiune de viteză la player Favoriți/te Frecvența de backup - \ No newline at end of file + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 77defab5..16f4449b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -593,4 +593,4 @@ Этот тест предназначен только для разработчиков и не подтверждает или не опровергает работоспособность провайдеров. Добавление настроек скорости в плеер Протестировать всех провайдеров - \ No newline at end of file + diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 1734b39f..ebaaa2ae 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -355,4 +355,4 @@ Maximálny počet znakov v názve prehrávača Spôsobuje problémy, ak je nastavená príliš vysoko v zariadeniach s malým ukladacím priestorom, ako je napríklad Android TV. Frekvencia zálohovania - \ No newline at end of file + diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index a1739399..7b0d2870 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -485,4 +485,4 @@ Bilowga Bilow isku qasan Qoraalka dhamaadka - \ No newline at end of file + diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 3c57956e..76508c43 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -604,4 +604,4 @@ Ta bort från favoriter %s \nkvarstår - \ No newline at end of file + diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 788afc34..e981d05a 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -119,4 +119,4 @@ போஸ்டர் பிரதான போஸ்டர் %1$s Ep %2$d - \ No newline at end of file + diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 0bdc57c1..b4308eb7 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -265,4 +265,4 @@ Mga Subtitle ng Chromecast Mga setting ng mga subtitle ng Chromecast Maglaro ng Trailer - \ No newline at end of file + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 06ff7498..7005fd95 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -656,4 +656,4 @@ kopyalandı! Panoya erişimde hata oluştu. Lütfen tekrar deneyin. Kopyalama hatası. Lütfen logcat\'i kopyalayın ve uygulama desteğiyle iletişime geçin. - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 19424fda..130e50af 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -609,4 +609,4 @@ Назва репозиторію та URL Помилка копіювання, будь ласка, скопіюйте logcat й зверніться до служби підтримки застосунку. Помилка доступу до буфера обміну, спробуйте ще раз. - \ No newline at end of file + diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index c462a7d7..0bcad1cf 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -588,4 +588,4 @@ آغاز پر اکاؤنٹ کا انتخاب چھوڑ دیں ویڈیو واقفیت کی بنیاد پر اسکرین کی سمت بندی کی خودکار سوئچنگ کو فعال کریں خود بخود گھومنا - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 3853f1c8..ad60f597 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -606,4 +606,4 @@ Hiển thị nút xoay màn hình Kích hoạt chế độ xoay màn hình tự động Tự động xoay - \ No newline at end of file + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3dc282c0..0e5e34ea 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -637,4 +637,4 @@ 旋轉 根據影片方向自動切換畫面方向 連結已重新載入 - \ No newline at end of file + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index a7c4ebc3..2360a7eb 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -651,4 +651,4 @@ \n剩余 测试所有扩展 已复制! - \ No newline at end of file + From 51d91bf9a79be692dab6964ef84c15fd83497b99 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 25 Mar 2024 05:48:26 +0530 Subject: [PATCH 316/441] feat(ui): add ignore battery optimisation dialog for uniterrupted downloads and notifications (#915) --- .../ui/result/ResultFragmentPhone.kt | 12 ++- .../ui/settings/SettingsGeneral.kt | 19 +++- .../cloudstream3/utils/PowerManagerAPI.kt | 86 +++++++++++++++++++ app/src/main/res/drawable/ic_battery.xml | 12 +++ app/src/main/res/values/strings.xml | 11 +++ app/src/main/res/xml/settings_general.xml | 22 +++-- 6 files changed, 152 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt create mode 100644 app/src/main/res/drawable/ic_battery.xml 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 8d0ca37b..fb5160a7 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,7 @@ import com.google.android.gms.cast.framework.CastState import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.updateHasTrailers -import com.lagradost.cloudstream3.CommonActivity +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent @@ -61,6 +61,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.openBrowser +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.openBatteryOptimizationSettings import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant @@ -442,8 +443,9 @@ open class ResultFragmentPhone : FullScreenPlayer() { val name = (viewModel.page.value as? Resource.Success)?.value?.title ?: txt(R.string.no_data).asStringNull(context) ?: "" - CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + showToast(txt(message, name), Toast.LENGTH_SHORT) } + context?.let { openBatteryOptimizationSettings(it) } } resultFavorite.setOnClickListener { viewModel.toggleFavoriteStatus(context) { newStatus: Boolean? -> @@ -457,7 +459,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { val name = (viewModel.page.value as? Resource.Success)?.value?.title ?: txt(R.string.no_data).asStringNull(context) ?: "" - CommonActivity.showToast(txt(message, name), Toast.LENGTH_SHORT) + showToast(txt(message, name), Toast.LENGTH_SHORT) } } mediaRouteButton.apply { @@ -465,7 +467,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { alpha = if (chromecastSupport) 1f else 0.3f if (!chromecastSupport) { setOnClickListener { - CommonActivity.showToast( + showToast( R.string.no_chromecast_support_toast, Toast.LENGTH_LONG ) @@ -640,6 +642,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { ), null ) { click -> + context?.let { openBatteryOptimizationSettings(it) } + when (click.action) { DOWNLOAD_ACTION_DOWNLOAD -> { viewModel.handleAction( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index 6cf00375..c3d84867 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -27,11 +27,15 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.EasterEggMonke +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.isAppRestricted +import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.showBatteryOptimizationDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog @@ -45,7 +49,6 @@ import com.lagradost.safefile.SafeFile // Change local language settings in the app. fun getCurrentLocale(context: Context): String { - // val dm = res.displayMetrics val res = context.resources val conf = res.configuration @@ -204,6 +207,20 @@ class SettingsGeneral : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + // disable preference on tvs and emulators + getPref(R.string.battery_optimisation_key)?.isEnabled = isLayout(PHONE) + getPref(R.string.battery_optimisation_key)?.setOnPreferenceClickListener { + val ctx = context ?: return@setOnPreferenceClickListener false + + if (isAppRestricted(ctx)) { + showBatteryOptimizationDialog(ctx) + } else { + showToast(R.string.app_unrestricted_toast) + } + + true + } + fun showAdd() { val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } } activity?.showDialog( diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt new file mode 100644 index 00000000..27609730 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt @@ -0,0 +1,86 @@ +package com.lagradost.cloudstream3.utils + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.PowerManager +import android.provider.Settings +import android.util.Log +import androidx.appcompat.app.AlertDialog +import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout + +const val packageName = BuildConfig.APPLICATION_ID +const val TAG = "PowerManagerAPI" + +object BatteryOptimizationChecker { + + fun isAppRestricted(context: Context?): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context != null) { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + return !powerManager.isIgnoringBatteryOptimizations(context.packageName) + } + + return false // below Marshmallow, it's always unrestricted when app is in background + } + + fun openBatteryOptimizationSettings(context: Context) { + if (shouldShowBatteryOptimizationDialog(context)) { + showBatteryOptimizationDialog(context) + } + } + + fun showBatteryOptimizationDialog(context: Context) { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) + + try { + context.let { + AlertDialog.Builder(it) + .setTitle(R.string.battery_dialog_title) + .setIcon(R.drawable.ic_battery) + .setMessage(R.string.battery_dialog_message) + .setPositiveButton(R.string.ok) { _, _ -> + intentOpenAppInfo(it) + } + .setNegativeButton(R.string.cancel) { _, _ -> + settingsManager.edit() + .putBoolean(context.getString(R.string.battery_optimisation_key), false) + .apply() + } + .show() + } + } catch (t: Throwable) { + Log.e(TAG, "Error showing battery optimization dialog", t) + } + } + + private fun shouldShowBatteryOptimizationDialog(context: Context): Boolean { + val isRestricted = isAppRestricted(context) + val isOptimizedNotShown = PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.battery_optimisation_key), true) + return isRestricted && isOptimizedNotShown && isLayout(PHONE) + } + + private fun intentOpenAppInfo(context: Context) { + val intent = Intent() + try { + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", packageName, null)) + context.startActivity(intent, Bundle()) + } catch (t: Throwable) { + Log.e(TAG, "Unable to invoke any intent", t) + if (t is ActivityNotFoundException) { + showToast("Exception: Activity Not Found") + } else { + showToast(R.string.app_info_intent_error) + } + } + } +} diff --git a/app/src/main/res/drawable/ic_battery.xml b/app/src/main/res/drawable/ic_battery.xml new file mode 100644 index 00000000..24d0a77f --- /dev/null +++ b/app/src/main/res/drawable/ic_battery.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d88489a4..378fa16a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -654,6 +654,17 @@ Are you sure you want to exit\? Yes No + OK + Disable Battery optimization + To ensure uninterrupted downloads and notifications for subscribed + TV shows, CloudStream needs permission to run in background. By pressing "OK", you\'ll be directed to App info. + There, scroll to 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 and set battery usage to 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Please note, this permission + does not mean CS3 will drain your battery. It will only operate in the background when necessary, such as + when receiving notifications or downloading videos from official extensions. If you choose to cancel, + you can adjust this setting later in 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. + battery_optimisation + App battery usage is already set to unrestricted + Unable to open CloudStream\'s App info. Downloading app update… Installing app update… Could not install the new version of the app diff --git a/app/src/main/res/xml/settings_general.xml b/app/src/main/res/xml/settings_general.xml index c4900bca..cdda6d85 100644 --- a/app/src/main/res/xml/settings_general.xml +++ b/app/src/main/res/xml/settings_general.xml @@ -6,10 +6,7 @@ android:title="@string/app_language" android:icon="@drawable/ic_baseline_language_24" /> - + + android:title="@string/title_downloads"> + + + + + + + + Date: Mon, 25 Mar 2024 01:38:39 +0100 Subject: [PATCH 317/441] New TvTypes + General fixes --- .../com/lagradost/cloudstream3/MainAPI.kt | 6 ++- .../lagradost/cloudstream3/ui/BaseAdapter.kt | 2 +- .../ui/home/HomeParentItemAdapter.kt | 4 ++ .../ui/result/ResultViewModel2.kt | 37 ++++++++++++------- app/src/main/res/values/strings.xml | 3 ++ 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 273e267b..ecbdcbbc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -865,7 +865,11 @@ enum class TvType(value: Int?) { AsianDrama(9), Live(10), NSFW(11), - Others(12) + Others(12), + Music(13), + AudioBook(14), + /** Wont load the built in player, make your own interaction */ + CustomMedia(15), } public enum class AutoDownloadMode(val value: Int) { 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 7439bfdf..d90177f5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt @@ -85,7 +85,7 @@ abstract class BaseAdapter< AsyncDifferConfig.Builder(diffCallback).build() ) - fun submitList(list: List?) { + open fun submitList(list: List?) { // deep copy at least the top list, because otherwise adapter can go crazy mDiffer.submitList(list?.let { CopyOnWriteArrayList(it) }) } 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 fb75e772..4b0360d7 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 @@ -63,6 +63,10 @@ open class ParentItemAdapter( } } + override fun submitList(list: List?) { + super.submitList(list?.sortedBy { it.list.list.isEmpty() }) + } + override fun onUpdateContent( holder: ViewHolderState, item: HomeViewModel.ExpandableHomepageList, 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 c90e01d0..8bf743be 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 @@ -246,6 +246,9 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { TvType.Live -> R.string.live_singular TvType.Others -> R.string.other_singular TvType.NSFW -> R.string.nsfw_singular + TvType.Music -> R.string.music_singlar + TvType.AudioBook -> R.string.audio_book_singular + TvType.CustomMedia -> R.string.custom_media_singluar } ), yearText = txt(year?.toString()), @@ -1759,20 +1762,28 @@ class ResultViewModel2 : ViewModel() { val data = currentResponse?.syncData?.toList() ?: emptyList() val list = HashMap().apply { putAll(data) } - - activity?.navigate( - R.id.global_to_navigation_player, - GeneratorPlayer.newInstance( - generator?.also { - it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work - ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } - ?.let { index -> - if (index >= 0) - it.goto(index) - } - } ?: return, list + generator?.also { + it.getAll() // I know kinda shit to iterate all, but it is 100% sure to work + ?.indexOfFirst { value -> value is ResultEpisode && value.id == click.data.id } + ?.let { index -> + if (index >= 0) + it.goto(index) + } + } + if (currentResponse?.type == TvType.CustomMedia) { + generator?.generateLinks( + clearCache = true, + LoadType.Unknown, + callback = {}, + subtitleCallback = {}) + } else { + activity?.navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + generator ?: return, list + ) ) - ) + } } ACTION_MARK_AS_WATCHED -> { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 378fa16a..c2136b4d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -764,4 +764,7 @@ Unlock the app with Fingerprint, Face ID, PIN, Pattern and Password. This screen was closed due to multiple failed attempts. Please restart the application. Your CloudStream data has been backed up now. Although the possibility of this is very low, all devices can behave differently. In the rare case, that you get locked out from accessing the app, clear the app data completely and restore from a backup. We are very sorry for any inconvenience arising from this. + Music + Audio Book + Media \ No newline at end of file From 0a24661e4cf301fac304ce6f28f32b718fdc81b1 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 25 Mar 2024 01:48:23 +0100 Subject: [PATCH 318/441] fix latest commit --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 3 +++ 1 file changed, 3 insertions(+) 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 8bf743be..84b8cf48 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 @@ -630,6 +630,9 @@ class ResultViewModel2 : ViewModel() { TvType.Live -> "LiveStreams" TvType.NSFW -> "NSFW" TvType.Others -> "Others" + TvType.Music -> "Music" + TvType.AudioBook -> "AudioBooks" + TvType.CustomMedia -> "Media" } } From a74563d003ba0d4e5f2cc48b527b53c801b90ee4 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 8 Apr 2024 04:02:02 +0200 Subject: [PATCH 319/441] Translated using Weblate (Russian) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Russian) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Russian) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Vietnamese) Currently translated at 75.0% (3 of 4 strings) Translated using Weblate (Vietnamese) Currently translated at 98.7% (688 of 697 strings) Translated using Weblate (French) Currently translated at 96.8% (675 of 697 strings) Translated using Weblate (Arabic (Levantine)) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Persian) Currently translated at 34.7% (242 of 697 strings) Translated using Weblate (Vietnamese) Currently translated at 98.8% (689 of 697 strings) Translated using Weblate (Russian) Currently translated at 97.1% (677 of 697 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Malayalam) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Malayalam) Currently translated at 48.4% (338 of 697 strings) Translated using Weblate (Indonesian) Currently translated at 99.8% (696 of 697 strings) Translated using Weblate (Maltese) Currently translated at 100.0% (4 of 4 strings) Translated using Weblate (Maltese) Currently translated at 32.1% (224 of 697 strings) Translated using Weblate (Portuguese) Currently translated at 100.0% (697 of 697 strings) Added translation using Weblate (Maltese) Translated using Weblate (Spanish) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 98.1% (684 of 697 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Polish) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Italian) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Czech) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (697 of 697 strings) Translated using Weblate (Arabic) Currently translated at 100.0% (697 of 697 strings) Merge remote-tracking branch 'origin/master' Translated using Weblate (Spanish) Currently translated at 99.4% (690 of 694 strings) Translated using Weblate (Odia) Currently translated at 37.5% (258 of 688 strings) Co-authored-by: Andre Costa Co-authored-by: Anonymous Co-authored-by: Argo Carpathians Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com> Co-authored-by: Colgrave Co-authored-by: Fjuro Co-authored-by: Fqwe1 Co-authored-by: Gnkalk Co-authored-by: Herderson Riker Co-authored-by: Hosted Weblate Co-authored-by: Joshua Joseph Co-authored-by: Long Kim Co-authored-by: Massimo Pissarello Co-authored-by: Matthaiks Co-authored-by: Michael John Scerri Co-authored-by: Mika Co-authored-by: Pizza Party Co-authored-by: Rex_sa Co-authored-by: Thanh Co-authored-by: aleksej0R Co-authored-by: gallegonovato Co-authored-by: kaajjo Co-authored-by: maxim Co-authored-by: samwiaba Co-authored-by: Ömer Faruk Sancak Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/apc/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fa/ 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/it/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ml/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/or/ 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/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/ 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/fastlane/ml/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/mt/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ru/ Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/vi/ Translation: Cloudstream/App Translation: Cloudstream/Fastlane --- app/src/main/res/values-ajp/strings.xml | 14 +- app/src/main/res/values-ar/strings.xml | 13 +- app/src/main/res/values-bp/strings.xml | 18 ++- app/src/main/res/values-cs/strings.xml | 12 +- app/src/main/res/values-es/strings.xml | 12 +- app/src/main/res/values-fa/strings.xml | 75 ++++++++++- app/src/main/res/values-fr/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 14 +- app/src/main/res/values-it/strings.xml | 12 +- app/src/main/res/values-ml/strings.xml | 63 +++++++-- app/src/main/res/values-mt/strings.xml | 126 ++++++++++++++++++ app/src/main/res/values-or/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 16 ++- app/src/main/res/values-pt/strings.xml | 18 ++- app/src/main/res/values-ru/strings.xml | 33 ++++- app/src/main/res/values-tr/strings.xml | 14 +- app/src/main/res/values-uk/strings.xml | 12 +- app/src/main/res/values-vi/strings.xml | 39 ++++-- app/src/main/res/values-zh/strings.xml | 10 +- .../metadata/android/ml-IN/changelogs/2.txt | 1 + .../android/ml-IN/full_description.txt | 10 ++ .../android/ml-IN/short_description.txt | 1 + fastlane/metadata/android/ml-IN/title.txt | 1 + fastlane/metadata/android/mt/changelogs/2.txt | 1 + .../metadata/android/mt/full_description.txt | 10 ++ .../metadata/android/mt/short_description.txt | 1 + fastlane/metadata/android/mt/title.txt | 1 + .../metadata/android/ru-RU/changelogs/2.txt | 1 + .../android/ru-RU/full_description.txt | 10 ++ .../android/ru-RU/short_description.txt | 1 + fastlane/metadata/android/ru-RU/title.txt | 1 + fastlane/metadata/android/vi/title.txt | 2 +- 32 files changed, 477 insertions(+), 71 deletions(-) create mode 100644 app/src/main/res/values-mt/strings.xml create mode 100644 fastlane/metadata/android/ml-IN/changelogs/2.txt create mode 100644 fastlane/metadata/android/ml-IN/full_description.txt create mode 100644 fastlane/metadata/android/ml-IN/short_description.txt create mode 100644 fastlane/metadata/android/ml-IN/title.txt create mode 100644 fastlane/metadata/android/mt/changelogs/2.txt create mode 100644 fastlane/metadata/android/mt/full_description.txt create mode 100644 fastlane/metadata/android/mt/short_description.txt create mode 100644 fastlane/metadata/android/mt/title.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/2.txt create mode 100644 fastlane/metadata/android/ru-RU/full_description.txt create mode 100644 fastlane/metadata/android/ru-RU/short_description.txt create mode 100644 fastlane/metadata/android/ru-RU/title.txt diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index 4d1fc074..e4e17942 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -143,7 +143,7 @@ لودينگ… شيل بَعِد مَعلومات - التجديد والنسخات الاحتياطية + التجديدات والنسخات الاحتياطية خبي هيدي الجودات من نتائج التنبيش موقف موقتًا كلود ستريم @@ -448,7 +448,7 @@ التلخيص إِيه الرئيسي - طبّق وقتما سكّر الآپ + سكر الآپ حتى تطبق التغيرات ساعدوني عوز هيدا إذا عم بتبين الترجمة %d ميلي ثانية بعدما لازم ما نلاقا الآپ @@ -614,4 +614,12 @@ في ارور بالنسخ. پليز نسوخ الـLogcat 🐈 وبعته ل المسؤولين عن دعم الآپ. هلّق نعمل نسخة احتياطية للداتا تبع \"كلود ستريم\". إذا مابق ينفتح ويمشي الآپ، فيك تعمل كلير للداتا تبعه وترَجع الداتا من النسخة الاحتياطية اللي هلّق عملنالك ياها. \nالاحتمال انو مابق ينفتح الآپ احتمالية زغيرة كتير، بس كل جهاز بيتصرف بشكل مختلف، ونحنا منعتذر إذا سببنا أي إزعاج. - + أوكي + وقف اپتميزايشن بطارية جهازك + بطارية الآپ اصلًا محطوطة ع «غير مقيد» \"Unrestricted\" + ما قدرنا نفتح معلومات الآپ تبع \"كلود ستريم\". + موسيقى + أوديو بوك + الميديا + لتضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يلي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي بـ الباكگروند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". ملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآن بـ الباكگروند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ بـ«الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 3140afeb..2ac4d973 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -440,7 +440,7 @@ المسارات مسار الصوت مسار الفيديو - تطبيق بعد إعادة التشغيل + أعد تشغيل التطبيق لرؤية التغييرات. الوضع الآمن قيد التشغيل تم إيقاف تشغيل جميع الملحقات بسبب عطل لمساعدتك في العثور على الإضافة التي تسبب مشكلة. عرض بيانات الاعطال @@ -641,4 +641,13 @@ خطأ في الوصول الي حافظة النسخ، برجاء المحاولة مرة اخرى. تم النسخ! خطأ في عملية النسخ، برجاء نسخ ال logcat و ارساله الى مسؤولين دعم التطبيق. - + تعطيل تحسين البطارية + تم ضبط استخدام بطارية التطبيق بالفعل على غير مقيد + غير قادر على فتح معلومات تطبيق CloudStream. + كتاب صوتي + حسناً + لضمان عدم انقطاع التنزيلات والإشعارات للبرامج التلفزيونية المشتركة، يحتاج CloudStream إلى إذن للتشغيل في الخلفية. بالضغط على موافق، سيتم توجيهك إلى معلومات التطبيق. هناك، انتقل إلى استخدام بطارية التطبيق +\nواضبط استخدام البطارية على غير مقيد. يرجى ملاحظة أن هذا الإذن لا يعني أن CS3 سوف يستنزف البطارية. ولن يعمل إلا في الخلفية عند الضرورة، كما هو الحال عند تلقي الإشعارات أو تنزيل مقاطع الفيديو من الملحقات الرسمية. إذا اخترت الإلغاء، فيمكنك ضبط هذا الإعداد لاحقًا في الإعدادات العامة. + موسيقى + الوسائط + \ No newline at end of file diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 0cf1bb2c..e4f47749 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -50,7 +50,7 @@ Fontes Legendas Tentando conectar novamente… - Voltar + Volte Reproduzir episódio Download @@ -145,7 +145,7 @@ Erro no backup de %s Procurar Contas e Segurança - Atualizações e backup + Atualizações e Backup Info Procura Avançada Mostrar resultados separados por fornecedor @@ -473,7 +473,7 @@ Conteúdo +18 Ajuda Processo de configuração de Redo - Não pudemos instalar a nova versão do App + Não foi possível instalar a nova versão do aplicativo instalador de pacotes Organizar por Votação (Alta para Baixa) @@ -541,7 +541,7 @@ MPV Abrindo mistura VLC - Aplicar quando reiniciar + Reinicie o aplicativo para ver as alterações. Visualização info de crash Faixas de áudio Adicionado em (novo para antigo) @@ -631,4 +631,12 @@ Erro ao acessar a área de transferência. Tente novamente. Nome e URL do repositório Erro ao copiar. Copie o logcat e entre em contato com o suporte do aplicativo. - + Para garantir downloads e notificações ininterruptos para programas de TV assinados, o CloudStream precisa de permissão para ser executado em segundo plano. Ao pressionar OK, você será direcionado para as informações do aplicativo. Lá, vá até Uso da bateria do aplicativo e defina o uso da bateria como Irrestrito. Observe que esta permissão não significa que o CS3 irá descarregar sua bateria. Ele só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se você optar por cancelar, poderá ajustar essa configuração posteriormente nas Configurações Gerais. + Ok + Desativar otimização de bateria + O uso da bateria do app já está definido como irrestrito + Não foi possível abrir as informações do aplicativo CloudStream. + Música + Áudio-livro + Mídia + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 519b05b6..cc357373 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -383,7 +383,7 @@ Staženo: %d Zvukové stopy Videostopy - Použít při restartu + Restartujte aplikaci pro použití změn. Bezpečný režim povolen Velikost Autoři @@ -633,4 +633,12 @@ Chyba při kopírování, zkopírujte prosím protokol a kontaktujte podporu aplikace. Zkopírováno! Chyba při přístupu ke schránce, zkuste to prosím znovu. - + OK + Využití baterie aplikací je již nastaveno na neomezené + Nepodařilo se otevřít informace o aplikaci CloudStream. + Hudba + Média + Zakažte optimalizace baterie + Aby bylo zajištěno nepřetržité stahování a upozornění na odebírané seriály, potřebuje aplikace CloudStream povolení ke spuštění na pozadí. Stisknutím tlačítka OK budete přesměrováni na informace o aplikaci. Tam přejděte na položku Využití baterie aplikací a nastavte možnost Využití baterie na hodnotu Neomezené. Upozorňujeme, že toto povolení neznamená, že CS3 bude vybíjet baterii. Na pozadí bude pracovat pouze v případě potřeby, například při přijímání oznámení nebo stahování videí z oficiálních rozšíření. Pokud se rozhodnete toto nastavení zrušit, můžete jej později upravit v Obecných nastaveních. + Audiokniha + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 055fc06b..bcff5139 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -281,7 +281,7 @@ Omitir configuración Qué quieres ver Poner en MAYÚSCULAS todos los subtítulos - Se aplicarán los cambios al reiniciar la App + Se aplicarán los cambios al reiniciar la App. Reproductor interno Idioma Legacy (método antiguo) @@ -609,4 +609,12 @@ ¡Copiado! Error al copiar. Por favor, copie el logcat y comuníquese con el soporte de la aplicación. Error al acceder al portapapeles. Inténtelo de nuevo. - + De acuerdo + Desactivar optimización de batería + Música + El uso de la batería de la aplicación está configurado sin restricciones + No se puede abrir la información de la aplicación CloudStream. + Media + Audiolibro + Para garantizar descargas y notificaciones ininterrumpidas para programas de televisión suscritos, CloudStream necesita permiso para ejecutarse en segundo plano. Al presionar OK, se le dirigirá a información de la aplicación. Allí, desplácese hasta Uso de la batería de la aplicación y establezca el uso de la batería en Sin restricciones. Tenga en cuenta que este permiso no significa que CS3 agotará su batería. Solo funcionará en segundo plano cuando sea necesario, como cuando reciba notificaciones o descargue videos de extensiones oficiales. Si decide cancelar, puede ajustar esta configuración más adelante en los ajustes generales. + \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 486f7a00..e9847af6 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -33,16 +33,16 @@ %1$dساعت %2$dدقیقه %dدقیقه پوستر اصلی - تورنت + تورنت‌ها آزاد - مستند ها + مستند‌ها انیمیشن ویدیویی اصلی حداکثر فیلم‌ها سریال های تلویزیونی - درام های آسیایی + درام‌های آسیایی انیمه - کارتونها + کارتون‌ها استفاده شده برنامه بازگشت @@ -52,7 +52,7 @@ اطلاعات بیشتر شرح زبان زیرنویس - زیرنویس + زیرنویس‌ها حذف بارگیری آغاز شد غیرفعال کردن گذارش باگ خودکار @@ -128,4 +128,67 @@ به پایان رسیده باز کردن در مرورگر برنامه‌ریزی برای تماشا - + برای بازنشانی به پیشفرض نگه‌دارید + کتابخانه + درادامه + این فرآیند بطور کامل %s را حذف می‌کند +\nآیا از این کار اطمینان دارید؟ + نام مخزن و نشانی + رونویسی شد! + درباره + قلم + اندازه قلم + برای تغییر تنظیمات بکشید + برای تغییر میزان روشنایی یا صدا در سمت چپ و راست به بالا یا پایین بکشید + بروزرسانی خودکار افزونه + آغاز + زبان برنامه + پخش قسمت + سال + فیلم + سریال + انیمه + رنگ حاشیه متن + دکمه تغییر‌اندازه پخش‌کننده + افزودن گزینه سرعت در پخش‌کننده + بروزرسانی‌ و پشتیبانی + نمایش پوستر از طریق Kitsu + جستجوی پیشرفته + فصل + قسمت + ف + ق + ویدئو + خطای منبع + گزارش + حذف حاشیه سیاه + تنظیمات زیرنویس پخش‌کننده + حساب‌ها و امنیت + نمایش گزارش‌پیوسته 🐈 + پیوند در بریده‌دان رونویسی شد + هیچ پیوندی یافت‌نشد + درام آسیایی + بارگیری خودکار افزونه‌ها + مستند + سرعت پخش + هیچ قسمتی یافت‌نشد + امتیاز: %.1f + قلم‌ها را با گذاشتن در %s وارد کنید + ادامه + بازگردانی به مقدار پیشفرض + −۳۰ + +۳۰ + حذف پرونده + نمایش پیش‌پرده‌ها + قسمت‌ها + %dد +\nباقی‌مانده + گیتهاب + نهان کردن کیفیت ویدئو انتخابی در نتایج جستجو + لغو + %s +\nباقی‌مانده + پیش‌فرض + کارتون + تورنت + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 17f6a667..1370ff2b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -279,7 +279,7 @@ Permissions de stockage manquantes. Veuillez réessayer. Erreur de sauvegarde %s Recherche - Comptes + Comptes et Sécurité Mises à jour et sauvegarde Info Recherche avancée @@ -595,4 +595,4 @@ Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. Copié ! Nom du dépôt et adresse internet - + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index d9a10c61..c3b55ba2 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -138,7 +138,7 @@ Error saat mencadang %s Cari Akun dan Keamanan - Update dan cadangan + Update dan Cadangan Info Pencarian Lanjutan Memberikan hasil pencarian yang dipisahkan berdasarkan provider @@ -432,7 +432,7 @@ Semua Umur %s (Tidak aktif) Trek - Terapkan saat dimuat ulang + Terapkan saat dimuat ulang untuk melihat perubahan. Keterangan Versi Status @@ -630,4 +630,12 @@ Gagal mengakses Papan Klip, mohon coba lagi. disalin! Gagal menyalin, mohon salin logcat dan hubungi pengembang aplikasi. - + Oke + Matikan pengoptimalan Baterai + Pemakaian baterai untuk aplikasi ini sudah diatur menjadi tidak dibatasi + Gagal membuka info aplikasi CloudStream. + Musik + Buku Audio + Media + Untuk memastikan unduhan dan pemberitahuan tanpa gangguan untuk acara TV berlangganan, CloudStream memerlukan izin untuk berjalan di latar belakang. Dengan menekan OK, Anda akan diarahkan ke Info aplikasi. Di sana, gulir ke Penggunaan baterai aplikasi dan atur penggunaan baterai ke Tidak Terbatas. Harap dicatat, izin ini tidak berarti CS3 akan menguras baterai Anda. Ini hanya akan beroperasi di latar belakang ketika diperlukan, seperti ketika menerima pemberitahuan atau mengunduh video dari ekstensi resmi. Jika Anda memilih untuk membatalkannya, Anda dapat menyesuaikan pengaturan ini nanti di Pengaturan Umum. + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7b958ad3..01031297 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -432,7 +432,7 @@ Tracce Traccia audio Traccia video - Applica al riavvio + Riavvia app per visualizzare le modifiche. Safe mode attiva Tutte le estensioni sono state disabilitate a causa di un arresto anomalo per aiutarti a trovare l\'estensione che causa il problema. Vedi informazioni del crash @@ -629,4 +629,12 @@ copiato! Errore durante l\'accesso agli Appunti. Riprova. Errore durante la copia. Copia logcat e contatta il supporto dell\'app. - + OK + Disabilita ottimizzazione della batteria + Impossibile aprire le informazioni sull\'app CloudStream. + Media + Per garantire download e notifiche ininterrotti per i programmi TV sottoscritti, CloudStream necessita dell\'autorizzazione per l\'esecuzione in background. Premendo OK, verrai indirizzato alle informazioni sull\'app. Successivamente, scorri fino a \"Utilizzo della batteria\" e imposta l\'utilizzo della batteria su \"Senza restrizioni\". Tieni presente che questa autorizzazione non significa che CS3 scaricherà la batteria. Funzionerà in background solo quando necessario, ad esempio quando si ricevono notifiche o si scaricano video da estensioni ufficiali. Se scegli di annullare, puoi modificare questa impostazione più tardi in \"Impostazioni generali\". + L\'utilizzo della batteria dell\'app è già impostato su \"Senza restrizioni\" + Musica + Audiolibro + \ No newline at end of file diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 0ddd8577..a26f902b 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -3,14 +3,14 @@ വേഗം (%.2fx) റേറ്റിംഗ്: %.1f - പുതിയ അപ്ഡേറ്റ് + പുതിയ അപ്ഡേറ്റ്! \n%1$s -> %2$s - CloudStream + ക്ലൗഡ് സ്ട്രീം ഹോം തിരയുക ഡൗൺലോഡ്സ് സെറ്റിങ്‌സ് - തിരയുക + തിരയുക… ടാറ്റ ലഭ്യമല്ല കൂടുതൽ ഓപ്ഷൻസ് അടുത്ത എപ്പിസോഡ് @@ -167,11 +167,11 @@ ഔചിത്യ വീഡിയോ ക്വാളിറ്റി ചരിത്രം കണ്ടതാണെന്ന് അടയാളപ്പെടുത്തുക - %1$d%2$d - yg5t4r%dujyhtg + %d ദിവസങ്ങൾ %d മണിക്കൂർ %d മിനിറ്റ് + അധ്യായം%dൽ റിലീസ് ചെയ്യും %1$d മണിക്കൂർ %2$d മിനിറ്റ് - %1$sghj%2$d - rtf:% + %1$sഅധ്യാ%2$d + കാസ്റ്റ്:%s അക്കൗണ്ട് ഉണ്ടാക്കുക പുറത്ത്പോകുന്നതോടുകൂടി ആപ് അപ്ഡേറ്റ് ആവുന്നതാണ് ലൈബ്രറി തിരഞ്ഞെടുക്കുക @@ -179,8 +179,8 @@ ട്രെയിലർ പ്ലേ ചെയ്യുക ലൈവ് സ്ട്രീം പ്ലേ ചെയ്യുക ഫില്ലർ - %d min - ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് കളിക്കുക + %d മിനിറ്റ് + ക്ലൗഡ് സ്ട്രീം ഉപയോഗിച്ച് പ്രവർത്തിപ്പിക്കുക അടുത്ത ക്രമരഹിതമായ എപ്പിസോഡ് പോസ്റ്റർ അപ്ഡേറ്റ് ആരംഭിച്ചു @@ -188,7 +188,7 @@ പോസ്റ്റർ ലോഡിംഗ് ഒഴിവാക്കുക തിരയുക %s… - %dm + %dമിനിറ്റ് മടങ്ങിപ്പോവുക പശ്ചാത്തല പ്രിവ്യൂ പോസ്റ്റർ @@ -202,8 +202,6 @@ പൊതു പട്ടിക CloudStream-ന് സ്ഥിരസ്ഥിതിയായി സൈറ്റുകളൊന്നും ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. നിങ്ങൾ റിപ്പോസിറ്ററികളിൽ നിന്ന് സൈറ്റുകൾ ഇൻസ്റ്റാൾ ചെയ്യേണ്ടതുണ്ട്. \n -\nസ്‌കൈ യുകെ ലിമിറ്റഡിലെ ഡോഗ്‌ഷിറ്റ് ആളുകളിൽ നിന്ന് DMCA നീക്കം ചെയ്‌തതിനാൽ 🤮 ഞങ്ങൾക്ക് ആപ്പിൽ റിപ്പോസിറ്ററി സൈറ്റ് ലിങ്ക് ചെയ്യാൻ കഴിയില്ല. -\n \nഞങ്ങളുടെ ഡിസ്കോർഡിൽ ചേരുക അല്ലെങ്കിൽ ഓൺലൈനിൽ തിരയുക. പകർത്തുക എല്ലാ സബ്‌ടൈറ്റിലുകളും വലിയക്ഷരമാക്കുക @@ -214,7 +212,7 @@ അക്കൗണ്ടുകൾ കൈകാര്യം ചെയ്യുക ഉടൻ വരുന്നു… പുനരാരംഭിക്കുമ്പോൾ പ്രയോഗിക്കുക - അക്കൗണ്ട് എഡിറ്റ് ചെയ്യുക + അക്കൗണ്ട് തിരുത്തുക തെറ്റായ പിൻ. ദയവായി വീണ്ടും ശ്രമിക്കുക. നിർത്തുക ട്രാക്കുകൾ @@ -245,4 +243,41 @@ ഉറവിട പിശക് നിലവിലെ പിൻ നൽകുക ഓഡിയോ ട്രാക്കുകൾ - + ചിത്രം-ഇൻ-ചിത്രം + പുതുക്കിയത് (പഴയത് മുതൽ പുതിയത് വരെ) + റേറ്റിംഗ് (ഉയർന്നത് മുതൽ താഴ്ന്നത്) + പാരമ്പര്യം + വിൻഡോ നിറം + ക്ലിയർ + ലോഗ് + ശുപാർശകൾ കാണിക്കുക + %s ആയി ലോഗിൻ ചെയ്തു + ഇങ്ങനെ അടുക്കുക + അടുക്കുക + തിരുത്തുക + പുതുക്കിയത് (പുതിയത് മുതൽ പഴയത് വരെ) + NSFW + ആപ്പ് അപ്ഡേറ്റ് ഇൻസ്റ്റാൾ ചെയ്യുന്നു… + അപ്ഡേറ്റുകളും ഒപ്പം ബാക്കപ്പും + %s(അപ്രാപ്തമാക്കി) + റേറ്റിംഗ് (താഴ്ന്നത് മുതൽ ഉയർന്നത് വരെ) + വാചക നിറം + ആപ്പിൻ്റെ പുതിയ പതിപ്പ് ഇൻസ്റ്റാൾ ചെയ്യാനായില്ല + പാക്കേജ് ഇൻസ്റ്റാളർ + അക്ഷരമാലാക്രമം (A മുതൽ Z വരെ) + അക്ഷരമാലാക്രമം (Z മുതൽ A വരെ) + ഈ ലിസ്റ്റ് ശൂന്യമാണ്. മറ്റൊന്നിലേക്ക് മാറാൻ ശ്രമിക്കുക. + ചരിത്രം മായ്ക്കുക + ലോഗ്കാറ്റ് കാണിക്കുക 🐈 + നിങ്ങളുടെ ലൈബ്രറി ശൂന്യമാണ് :( +\nഒരു ലൈബ്രറി അക്കൗണ്ടിൽ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ നിങ്ങളുടെ പ്രാദേശിക ലൈബ്രറിയിലേക്ക് ഷോകൾ ചേർക്കുക. + വീഡിയോ + റിപ്പോസിറ്ററി നാമവും URL ഉം + പകർത്തി! + പുതിയ എപ്പിസോഡ് അറിയിപ്പ് + മറ്റ് വിപുലീകരണങ്ങളിൽ തിരയുക + ഉപശീർഷകം ക്രമീകരണങ്ങൾ + എഡ്ജ് തരം + ഔട്ട്ലൈൻ നിറം + പശ്ചാത്തല നിറം + \ No newline at end of file diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml new file mode 100644 index 00000000..356a2caa --- /dev/null +++ b/app/src/main/res/values-mt/strings.xml @@ -0,0 +1,126 @@ + + + Preferenzi tas-sottotitli + Kulur tal-kitba + Kulur tat-Tieqa + Fittex bl-użu ta \'tipi + Importa fonts billi tpoġġihom ġo %s + Dan il-fornitur huwa torrent, VPN huwa rakkomandat + Atturi: %s + L-episodju %d ha johrog fil + %1$dh %2$dm + %dm + Kartellun + Kartellun + Kartellun tal-episodju + Kartellun Principali + Li jmiss bl\'addoċċ + Ibdel Il-fornitur + veloċità (%.2fx) + Klassifikazzjoni: %.1f + Aġġornament ġdid misjub! +\n%1$s -> %2$s + %d min + CloudStream + Ara bil-CloudStream + Dar + Fittex + Imnizzel + Preferenzi + Fittex… + Fittex%s… + Bla dejta + Iktar Preferenzi + L-episodju li\'jmiss + Ġeneri + Aqsam + Iftah fil-brawser + Brawser + Aqbez it-tagħbija + Tagħbija… + Jaraw + Stenna ftit + Lest + Imwaqqa + Pjana biex tara + Terġa\' tara + Ibda t-trejler + Ibda l-livestream + Stream Torrent + Sorsi + Erġa\' pprova l-konnessjoni… + Mur lura + Ibda l-episodju + Tniżżila ppawzata + Qed jinżlu + Imniżżel + Tniżżil ikkanċellat + Lest it-tniżżil + Beda l-aġġornament + Network stream + Tagħbija tal-Links falliet + Links regaw gew mogħbija + Ħażna Interna + Dub + Ibda + Info + Issettja l-istatus ta-rajtux + Applika + Ikkopja + Għalaq + Neħħi + Issevja + Isem tar-repożitorju u URL + Ikkupjat! + Notifika ta\' episodju ġdid + Fittex f\'estensjonijiet oħra + Uri r-rakkomandazzjonijiet + Veloċità tal-Plejer + Kulur tal-Kontorn + Kulur tal-Isfond + Tip tat-tarf + Elevazzjoni tas-Sottotitolu + Font + Daqs tal-font + Fittex bl-użu ta\' fornituri + %d Benenes mogħtija lil devs + Ebda Benenes mogħtija + Agħżel il-Lingwa Awtomatikament + Niżżel Lingwi + Lingwa tas-sottotitolu + Żomm biex tirrisettja għal default + Kompli Ara + Neħħi + Iktar informazzjoni + \@string/home_play + Jista\' jkun hemm bżonn ta\' VPN biex dan il-fornitur jaħdem b\'mod korrett + Il-metadata mhix ipprovduta mis-sit, it-tagħbija tal-vidjo se tfalli jekk ma teżistix fuq is-sit. + Deskrizzjoni + Lebda Plot misjub + Lebda Deskrizzjoni misjuba + Uri Logcat 🐈 + ġurnal + Stampa f-istampa + Ikompli d-daqq fi player minjatura fuq apps oħra + %1$s Ep %2$d + %1$dd %2$dh %3$dm + Mur Lura + Ara l\'isfond + Mili + Ibda l-film + Sottotitli + Sut + Ibda l-fajl + Niżżel + Hassar il-fajl + Kompli Nizzel + Ieqaf Nizzel + Iddiżattiva r-rappurtar awtomatiku tal-bugs + Iktar Informazzjoni + Aħbi + Iffiltra l-Bookmarks + Beda t-tniżżil + Bookmarks + Neħħi + Falla t-tniżżil + \ No newline at end of file diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index bdc55780..4bf0f565 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -159,4 +159,4 @@ କୌଣସି ତଥ୍ୟ ନାହିଁ %1$s ଅ %2$d ଆଦ୍ୟ ବାଦ୍ ଦିଅ - + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 3e22ba16..b80c1b79 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -137,7 +137,7 @@ Błąd tworzenia kopii zapasowej %s Szukaj Konta i zabezpieczenia - Aktualizacje i kopia zapasowa + Aktualizacje i kopie zapasowe Informacje Zaawansowane wyszukiwanie Szukaj z podziałem na źródła @@ -278,7 +278,7 @@ Pokaż przycisk do losowania na stronie głównej i w bibliotece Języki rozszerzeń Układ aplikacji - Preferowane media + Preferowane multimedia Włącz NSFW w obsługiwanych rozszerzeniach Kodowanie napisów Źródła @@ -405,7 +405,7 @@ Ścieżki Ścieżki audio Ścieżki wideo - Zastosuj po ponownym uruchomieniu + Uruchom ponownie aplikację, aby zobaczyć zmiany. Tryb bezpieczny włączony Z powodu wystąpienia błędu wszystkie rozszerzenia zostały wyłączone, aby ułatwić wykrycie tego wadliwego. Wyświetl informacje o błędzie @@ -610,4 +610,12 @@ Błąd dostępu do schowka. Spróbuj ponownie. skopiowano! Błąd podczas kopiowania. Skopiuj logcat i skontaktuj się z pomocą techniczną aplikacji. - + Wyłącz optymalizację akumulatora + Nie można otworzyć informacji o aplikacji CloudStream. + Muzyka + Audiobook + OK + Multimedia + Użycie akumulatora przez aplikację jest już ustawione na nieograniczone + Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach telewizyjnych, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do informacji o aplikacji. Tam przewiń do użycia akumulatora przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać akumulator. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach głównych. + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b3180fee..82054b6f 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -143,7 +143,7 @@ Erro no backup de %s Procurar Contas e segurança - Atualizações e backup + Atualizações e cópias de segurança Info Procura Avançada Mostra resultados separados por fornecedor @@ -458,7 +458,7 @@ Inscrição cancelada em %s Final misto Avaliações (Decrescente) - Aplicar ao reiniciar + Reinicie a aplicação para ver as alterações. Referenciador (opcional) Player oculto - Quantidade de Busca Proxy do GitHub @@ -605,6 +605,14 @@ Autenticação por palavra-passe/PIN A autenticação biométrica não é suportada neste dispositivo Desbloqueie a aplicação com impressão digital, ID facial, PIN, padrão e palavra-passe. - Esta janela fechar-se-á após algumas tentativas falhadas. Terá de reiniciar a aplicação. - Foi feita uma cópia de segurança dos seus dados CloudStream, embora a probabilidade deste caso raro seja muito baixa, mas todos os dispositivos se comportam de forma diferente. No caso de ficar impedido de aceder à aplicação, na pior das hipóteses, limpe totalmente os dados da aplicação e restaure a cópia de segurança. Lamentamos profundamente qualquer inconveniente. - + Este ecrã foi encerrado devido a várias tentativas falhadas. Reinicie a aplicação. + Os dados do seu CloudStream já foram copiados. Embora a possibilidade de isto acontecer ser muito baixa, todos os dispositivos podem comportar-se de forma diferente. No caso raro de ficar impedido de aceder à aplicação, limpe completamente os dados da aplicação e restaure a partir de uma cópia de segurança. Lamentamos qualquer incómodo causado por esta situação. + OK + A utilização da bateria da aplicação já está definida como sem restrições + Não é possível abrir a informação da aplicação CloudStream. + Música + Livro Aúdio + Multimédia + Desativar a otimização da bateria + Para garantir descarregamentos ininterruptos e notificações de programas de TV subscritos, o CloudStream precisa de permissão para ser executado em segundo plano. Ao premir OK, será direcionado para informações da aplicação. Aí, desloque-se para utilização da bateria da aplicação e defina a utilização da bateria para sem restrições. Tenha em atenção que esta permissão não significa que o CS3 irá esgotar a sua bateria. Este só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se optar por cancelar, pode ajustar esta definição mais tarde em definições gerais. + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 16f4449b..a71cc71b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -39,7 +39,7 @@ Заполнитель CloudStream Убирать - %1$s Серия %2$d + %1$s Ep %2$d Смотреть с CloudStream Главная Поиск @@ -148,8 +148,8 @@ Загружена резервная копия Не удалось восстановить данные из %s Отсутствует разрешение на хранение. Пожалуйста попробуйте снова. - Аккаунты - Обновления и резервное копирование + Аккаунты и Безопасность + Обновления и Резервное копирование Информация Расширенный поиск Показывать трейлеры @@ -457,7 +457,7 @@ Загрузить из интернета Загрузка обновления приложения… Недопустимый URL - Будет применено при перезапуске + Перезапустите приложение, чтобы увидеть изменения. Отчеты ошибках Что вы хотите увидеть Смотрите видео на этих языках @@ -593,4 +593,27 @@ Этот тест предназначен только для разработчиков и не подтверждает или не опровергает работоспособность провайдеров. Добавление настроек скорости в плеер Протестировать всех провайдеров - + скопировано! + ОК + Имя репозитория и URL адрес + Ошибка доступа к буферу обмена, пожалуйста, попробуйте ещё раз + Ошибка при копировании, пожалуйста, скопируйте лог и свяжитесь с технической поддержкой. + Нелюбимое + Разблокировать CloudStream + Любимое + Использование батареи приложением уже настроено на неограниченное + Не удается открыть информацию о приложении CloudStream. + Заблокировать биометрией + Музыка + Аудиокнига + Медиа + Разблокируйте приложение с помощью отпечатка пальца, Face ID, PIN-кода, шаблона и пароля. + %s +\nосталось + Отключить оптимизацию батареи + Аутентификация по паролю/PIN-коду + Биометрическая аутентификация на этом устройстве не поддерживается + Этот экран был закрыт из-за нескольких неудачных попыток. Пожалуйста, перезапустите приложение. + Ваши данные в CloudStream были скопированы. Хотя вероятность этого очень мала, все устройства могут вести себя по-разному. В редких случаях, когда доступ к приложению заблокирован, полностью удалите данные приложения и восстановите их из резервной копии. Мы приносим свои извинения за любые неудобства, связанные с этим. + Чтобы обеспечить бесперебойную загрузку и получение уведомлений о телепередачах, на которые вы подписаны, CloudStream необходимо разрешение на запуск в фоновом режиме. Нажав OK, вы перейдете к информации о приложении. Там перейдите к разделу 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и установите значение \"Использование батареи\" 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Пожалуйста, обратите внимание, что это разрешение не означает, что CS3 разрядит вашу батарею. Он будет работать в фоновом режиме только при необходимости, например, при получении уведомлений или загрузке видео с официальных расширений. Если вы решите отменить, вы можете изменить эту настройку позже в 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 7005fd95..3a5170a3 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -162,7 +162,7 @@ %s yedeklenirken hata Ara Hesaplar ve Güvenlik - Güncellemeler ve yedekleme + Güncellemeler ve Yedekleme Bilgi Gelişmiş arama Arama sonuçlarını sağlayıcıya göre ayırır @@ -466,7 +466,7 @@ Parçalar Ses parçaları Video parçaları - Yeniden başlatmada uygula + Değişiklikleri görmek için uygulamayı yeniden başlatın. Güvenli mod açık Çöküşe neden olan eklentiyi bulmaya yardımcı olabilmek için tüm eklentiler kapatıldı. Çökme bilgisini göster @@ -656,4 +656,12 @@ kopyalandı! Panoya erişimde hata oluştu. Lütfen tekrar deneyin. Kopyalama hatası. Lütfen logcat\'i kopyalayın ve uygulama desteğiyle iletişime geçin. - + Tamam + Pil optimizasyonunu devre dışı bırak + CloudStream\'in Uygulama bilgileri açılamıyor. + Müzik + Sesli Kitap + Medya + Abone olunan TV şovları için kesintisiz indirmeleri ve bildirimleri sağlamak için, CloudStream\'in arka planda çalışmasına izin vermeniz gerekmektedir. Tamam\'a basarak Uygulama bilgilerine yönlendirileceksiniz. Orada, 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 (Uygulama pil kullanımı) kısmına gidip pil kullanımını 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙 (Sınırsız) olarak ayarlayın. Bu iznin CS3\'ün pilinizi hızlıca tüketeceği anlamına gelmediğini lütfen unutmayın. Sadece gerektiğinde, resmi eklentilerden bildirim almak veya videoları indirmek gibi durumlarda arka planda çalışacaktır. İptal etmeyi seçerseniz, bu ayarı daha sonra 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨 (Genel Ayarlar) bölümünden ayarlayabilirsiniz. + Uygulama pil kullanımı zaten sınırsız olarak ayarlanmış + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 130e50af..981ac19b 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -433,7 +433,7 @@ Усі субтитри у верхньому регістрі %s (Вимкнено) Відео доріжки - Застосується при перезавантаженні + Перезапустіть застосунок, щоб побачити зміни. Переглянути інформацію про збій Рейтинг: %s Опис @@ -609,4 +609,12 @@ Назва репозиторію та URL Помилка копіювання, будь ласка, скопіюйте logcat й зверніться до служби підтримки застосунку. Помилка доступу до буфера обміну, спробуйте ще раз. - + Добре + Вимкнути оптимізацію батареї + Щоб забезпечити безперебійне завантаження та повідомлення про підписані телепередачі, CloudStream потребує дозволу на роботу у фоновому режимі. Натиснувши \"Добре\", вас буде перенаправлено до інформації про застосунок. Там прокрутіть до \"Споживання батареї застосунком\" й встановіть використання батареї на \"Без обмежень\". Зверніть увагу, що цей дозвіл не означає, що CS3 розряджатиме вашу батарею. Він працюватиме у фоновому режимі лише за необхідності, наприклад, під час отримання повідомлень або завантаження відео з офіційних розширень. Якщо ви вирішите скасувати дозвіл, ви зможете змінити це налаштування пізніше в загальних налаштуваннях. + Споживання батареї застосунком вже налаштовано на без обмежень + Не вдається відкрити інформацію про застосунок CloudStream. + Аудіо книга + Музика + Медіа + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index ad60f597..202af75c 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -123,7 +123,7 @@ Cài đặt phụ đề Phụ đề Chromecast Cài đặt phụ đề Chromecast - Chỉnh tốc độ phim + 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 @@ -147,8 +147,8 @@ Thiếu quyền truy cập bộ nhớ, hãy thử lại. Lỗi khi sao lưu %s Tìm kiếm - Tài khoản - Cập nhật và sao lưu + 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 @@ -286,7 +286,7 @@ Disclaimer Tổng quan Nút ngẫu nhiên - Hiện nút ngẫu nhiên trên trang chủ + Hiện nút ngẫu nhiên trên Trang chủ và Thư viện Ngôn ngữ nguồn phim Giao diện App Thể loại ưu tiên @@ -307,7 +307,7 @@ Tài khoản Email 127.0.0.1 - Địa chỉ trang web + Địa chỉ trang web­ https://example.com Mã ngôn ngữ (vi) %1$s %2$s @@ -431,7 +431,7 @@ Thêm Âm thanh Chất lượng Video - Áp dụng khi khởi động lại + Áp dụng khi khởi động lạ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ố. Xem thông tin sự cố @@ -535,7 +535,7 @@ \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 Đang cập nhật các phim đã đăng kí - Bỏ qua chặn GitHub bằng cách dùng jsDelivr. Có thể gây ra việc cập nhật bị chậm vài ngày. + Bỏ qua chặn đường link GitHub bằng cách dùng jsDelivr. Có thể gây ra việc cập nhật bị chậm vài ngày. Lượng tua thêm được sử dụng khi trình phát ẩn Lượng tua thêm Lượng tua thêm được sử dụng khi trình phát hiện lên @@ -606,4 +606,27 @@ Hiển thị nút xoay màn hình Kích hoạt chế độ xoay màn hình tự động 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. + 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 + Mở khóa Cloudstream + Nhạc + Sách nói + Khóa với 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 + Dữ liệu CloudStream của bạn đã được sao lưu. Dù khả năng rất thấp, nhưng mỗi thiết bị có thể hoạt động khác nhau. Trong trường hợp thiểu số, bạn sẽ bị khóa khỏi ứng dụng, hãy xóa dữ liệu ứng dụng và khởi tạo 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. + 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. + Phần kiểm thử này chỉ dành cho nhà phát triển và không xác nhận hay từ chối việc hoạt động của nguồn phim. + Chế độ tiêu thụ pin của ứng dụng đã được đặt ở mức không giới hạn + …  +\n——— + \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2360a7eb..4423f20f 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -651,4 +651,12 @@ \n剩余 测试所有扩展 已复制! - + 访问剪贴板出错,请重试。 + 应用程序电池使用量已设置为不受限制 + 有声书 + 媒体 + 禁用电池最佳化 + 音乐 + 无法打开 CloudStream 的应用程序信息。 + 使用指纹、面部 ID、PIN 码、图案和密码解锁应用程序。 + \ No newline at end of file diff --git a/fastlane/metadata/android/ml-IN/changelogs/2.txt b/fastlane/metadata/android/ml-IN/changelogs/2.txt new file mode 100644 index 00000000..f7523831 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/changelogs/2.txt @@ -0,0 +1 @@ +-ചേഞ്ച്ലോഗ് ചേർത്തു! diff --git a/fastlane/metadata/android/ml-IN/full_description.txt b/fastlane/metadata/android/ml-IN/full_description.txt new file mode 100644 index 00000000..218f9f98 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/full_description.txt @@ -0,0 +1,10 @@ +ക്ലൗഡ് സ്ട്രീം-3 സിനിമകൾ, ടിവി സീരീസ്, ആനിമേഷൻ എന്നിവ സ്ട്രീം ചെയ്യാനും ഡൗൺലോഡ് ചെയ്യാനും നിങ്ങളെ അനുവദിക്കുന്നു. + +പരസ്യങ്ങളും അനലിറ്റിക്‌സും കൂടാതെ ആപ്പ് വരുന്നു ഒപ്പം +ഒന്നിലധികം ട്രെയിലർ, മൂവി സൈറ്റുകൾ എന്നിവയും മറ്റും പിന്തുണയ്ക്കുന്നു, ഉദാഹരണം + +ബുക്ക്മാർക്കുകൾ + +ഉപശീർഷകം ഡൗൺലോഡുകൾ + +ക്രോംകാസ്റ്റ് പിന്തുണ diff --git a/fastlane/metadata/android/ml-IN/short_description.txt b/fastlane/metadata/android/ml-IN/short_description.txt new file mode 100644 index 00000000..f12fe5b5 --- /dev/null +++ b/fastlane/metadata/android/ml-IN/short_description.txt @@ -0,0 +1 @@ +സ്ട്രീം ഒപ്പം ഡൗൺലോഡ് സിനിമകളും, ടിവി സീരീസുകളും, ആനിമേഷനും . diff --git a/fastlane/metadata/android/ml-IN/title.txt b/fastlane/metadata/android/ml-IN/title.txt new file mode 100644 index 00000000..8e89348a --- /dev/null +++ b/fastlane/metadata/android/ml-IN/title.txt @@ -0,0 +1 @@ +ക്ലൗഡ് സ്ട്രീം diff --git a/fastlane/metadata/android/mt/changelogs/2.txt b/fastlane/metadata/android/mt/changelogs/2.txt new file mode 100644 index 00000000..66bbca8f --- /dev/null +++ b/fastlane/metadata/android/mt/changelogs/2.txt @@ -0,0 +1 @@ +- Changelog miżjud! diff --git a/fastlane/metadata/android/mt/full_description.txt b/fastlane/metadata/android/mt/full_description.txt new file mode 100644 index 00000000..da507aae --- /dev/null +++ b/fastlane/metadata/android/mt/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 iħallik tistrimja u tniżżel Films, Serje TV u Anime. + +L-app tiġi mingħajr reklami u analytics u +jappoġġja siti multipli ta' trejlers u films, u aktar, eż. + +Bookmarks + +Downloads tas-sottotitli + +Appoġġ tal-Chromecast diff --git a/fastlane/metadata/android/mt/short_description.txt b/fastlane/metadata/android/mt/short_description.txt new file mode 100644 index 00000000..542b8614 --- /dev/null +++ b/fastlane/metadata/android/mt/short_description.txt @@ -0,0 +1 @@ +Tistrimja u tniżżel films, serje tat-TV u Anime. diff --git a/fastlane/metadata/android/mt/title.txt b/fastlane/metadata/android/mt/title.txt new file mode 100644 index 00000000..dde89d58 --- /dev/null +++ b/fastlane/metadata/android/mt/title.txt @@ -0,0 +1 @@ +CloudStream diff --git a/fastlane/metadata/android/ru-RU/changelogs/2.txt b/fastlane/metadata/android/ru-RU/changelogs/2.txt new file mode 100644 index 00000000..4b9464b6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/2.txt @@ -0,0 +1 @@ +- Добавлен список изменений! diff --git a/fastlane/metadata/android/ru-RU/full_description.txt b/fastlane/metadata/android/ru-RU/full_description.txt new file mode 100644 index 00000000..1790888e --- /dev/null +++ b/fastlane/metadata/android/ru-RU/full_description.txt @@ -0,0 +1,10 @@ +CloudStream-3 позволяет транслировать и скачивать фильмы, сериалы и аниме. + +Приложение поставляется без рекламы и аналитики и +поддерживает множество сайтов с трейлерами и фильмами, а также многое другое, например + +Книжные закладки + +Загрузка субтитров + +Поддержка Chromecast diff --git a/fastlane/metadata/android/ru-RU/short_description.txt b/fastlane/metadata/android/ru-RU/short_description.txt new file mode 100644 index 00000000..a43bc8a1 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/short_description.txt @@ -0,0 +1 @@ +Транслируйте и скачивайте фильмы, сериалы и аниме. diff --git a/fastlane/metadata/android/ru-RU/title.txt b/fastlane/metadata/android/ru-RU/title.txt new file mode 100644 index 00000000..3c0406a6 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/title.txt @@ -0,0 +1 @@ +Облачный поток diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt index dde89d58..0afff90c 100644 --- a/fastlane/metadata/android/vi/title.txt +++ b/fastlane/metadata/android/vi/title.txt @@ -1 +1 @@ -CloudStream +double_tap_seek_time_key2 From d8f89df16363b17945b24c77f5777bdeb5d068bc Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 10 Apr 2024 17:14:47 +0200 Subject: [PATCH 320/441] Show player controls on pressing Pad Down (#1031) --- .../com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt | 1 + 1 file changed, 1 insertion(+) 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 d79c44b7..56983190 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 @@ -1159,6 +1159,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } + KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_UP -> { if (!isShowing) { onClickChange() From ff0dea3fbb4d17e05d0077db55407981b8b83abd Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 10 Apr 2024 17:16:04 +0200 Subject: [PATCH 321/441] Fix focus for Tracks selection on TV (#1030) --- .../main/res/layout/player_select_tracks.xml | 47 +++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/app/src/main/res/layout/player_select_tracks.xml b/app/src/main/res/layout/player_select_tracks.xml index d32e1b4e..94e09d60 100644 --- a/app/src/main/res/layout/player_select_tracks.xml +++ b/app/src/main/res/layout/player_select_tracks.xml @@ -38,21 +38,20 @@ android:requiresFadingEdge="vertical" android:id="@+id/video_tracks_list" android:layout_width="match_parent" - android:layout_height="match_parent" android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" - android:nextFocusLeft="@id/sort_subtitles" - android:nextFocusRight="@id/apply_btt" + android:nextFocusRight="@id/audio_tracks_holder" + tools:listitem="@layout/sort_bottom_single_choice" /> + android:id="@+id/audio_tracks_holder" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="50" + android:orientation="vertical"> @@ -107,17 +106,16 @@ + android:requiresFadingEdge="vertical" + android:id="@+id/auto_tracks_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_rowWeight="1" + android:background="?attr/primaryBlackBackground" + android:nextFocusRight="@id/apply_btt" + android:nextFocusLeft="@id/video_tracks_list" + tools:listfooter="@layout/sort_bottom_footer_add_choice" + tools:listitem="@layout/sort_bottom_single_choice" /> @@ -132,11 +130,12 @@ + style="@style/WhiteButton" + android:layout_gravity="center_vertical|end" + android:text="@string/sort_apply" + android:id="@+id/apply_btt" + android:nextFocusLeft="@id/auto_tracks_list" + android:layout_width="wrap_content" /> Date: Wed, 10 Apr 2024 20:54:15 +0530 Subject: [PATCH 322/441] Created vtbe and EPlay Extractor (#1014) --- .../cloudstream3/extractors/EPlay.kt | 28 +++++++++++++ .../lagradost/cloudstream3/extractors/Vtbe.kt | 40 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 6 ++- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt new file mode 100644 index 00000000..565a2680 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt @@ -0,0 +1,28 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson + +open class EPlayExtractor : ExtractorApi() { + override var name = "EPlay" + override var mainUrl = "https://eplayvid.net" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List? { + val response = app.get(url).document + val trueUrl = response.select("source").attr("src") + return listOf( + ExtractorLink( + this.name, + this.name, + trueUrl, + mainUrl, + getQualityFromName(""), // this needs to be auto + false + ) + ) + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt new file mode 100644 index 00000000..65af01ec --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt @@ -0,0 +1,40 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.fasterxml.jackson.annotation.JsonProperty +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.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.getQualityFromName +import java.net.URI + + +open class Vtbe : ExtractorApi() { + override var name = "Vtbe" + override var mainUrl = "https://vtbe.to" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?): List? { + val response = app.get(url,referer=mainUrl).document + 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( + ExtractorLink( + this.name, + this.name, + link, + referer ?: "", + Qualities.Unknown.value, + URI(link).path.endsWith(".m3u8") + ) + ) + } + } + return null + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 637f65b9..e5d82d39 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -217,6 +217,8 @@ import com.lagradost.cloudstream3.extractors.Zorofile import com.lagradost.cloudstream3.extractors.Zplayer import com.lagradost.cloudstream3.extractors.ZplayerV2 import com.lagradost.cloudstream3.extractors.Ztreamhub +import com.lagradost.cloudstream3.extractors.EPlayExtractor +import com.lagradost.cloudstream3.extractors.Vtbe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay @@ -864,7 +866,9 @@ val extractorApis: MutableList = arrayListOf( Megacloud(), VidhideExtractor(), StreamWishExtractor(), - EmturbovidExtractor() + EmturbovidExtractor(), + Vtbe(), + EPlayExtractor() ) From ffa7b0248a86b8e2dcc8aa13c742741d7ff99b6d Mon Sep 17 00:00:00 2001 From: "recloudstream[bot]" <111277985+recloudstream[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 15:26:36 +0000 Subject: [PATCH 323/441] chore(locales): fix locale issues --- .../com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt | 1 + app/src/main/res/values-ajp/strings.xml | 2 +- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-bp/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-in/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-ml/strings.xml | 2 +- app/src/main/res/values-mt/strings.xml | 4 ++-- app/src/main/res/values-or/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 2 +- app/src/main/res/values-uk/strings.xml | 2 +- app/src/main/res/values-vi/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 2 +- 20 files changed, 21 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index c3d84867..ff891c43 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -98,6 +98,7 @@ val appLanguages = arrayListOf( Triple("", "македонски", "mk"), Triple("", "മലയാളം", "ml"), Triple("", "bahasa Melayu", "ms"), + Triple("", "Malti", "mt"), Triple("", "ဗမာစာ", "my"), Triple("", "नेपाली", "ne"), Triple("", "Nederlands", "nl"), diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index e4e17942..734d5644 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -622,4 +622,4 @@ أوديو بوك الميديا لتضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يلي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي بـ الباكگروند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". ملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآن بـ الباكگروند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ بـ«الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. - \ No newline at end of file + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 2ac4d973..8681398d 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -650,4 +650,4 @@ \nواضبط استخدام البطارية على غير مقيد. يرجى ملاحظة أن هذا الإذن لا يعني أن CS3 سوف يستنزف البطارية. ولن يعمل إلا في الخلفية عند الضرورة، كما هو الحال عند تلقي الإشعارات أو تنزيل مقاطع الفيديو من الملحقات الرسمية. إذا اخترت الإلغاء، فيمكنك ضبط هذا الإعداد لاحقًا في الإعدادات العامة. موسيقى الوسائط - \ No newline at end of file + diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index e4f47749..40847edf 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -639,4 +639,4 @@ Música Áudio-livro Mídia - \ No newline at end of file + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index cc357373..0a8cf997 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -641,4 +641,4 @@ Zakažte optimalizace baterie Aby bylo zajištěno nepřetržité stahování a upozornění na odebírané seriály, potřebuje aplikace CloudStream povolení ke spuštění na pozadí. Stisknutím tlačítka OK budete přesměrováni na informace o aplikaci. Tam přejděte na položku Využití baterie aplikací a nastavte možnost Využití baterie na hodnotu Neomezené. Upozorňujeme, že toto povolení neznamená, že CS3 bude vybíjet baterii. Na pozadí bude pracovat pouze v případě potřeby, například při přijímání oznámení nebo stahování videí z oficiálních rozšíření. Pokud se rozhodnete toto nastavení zrušit, můžete jej později upravit v Obecných nastaveních. Audiokniha - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index bcff5139..20484cd9 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -617,4 +617,4 @@ Media Audiolibro Para garantizar descargas y notificaciones ininterrumpidas para programas de televisión suscritos, CloudStream necesita permiso para ejecutarse en segundo plano. Al presionar OK, se le dirigirá a información de la aplicación. Allí, desplácese hasta Uso de la batería de la aplicación y establezca el uso de la batería en Sin restricciones. Tenga en cuenta que este permiso no significa que CS3 agotará su batería. Solo funcionará en segundo plano cuando sea necesario, como cuando reciba notificaciones o descargue videos de extensiones oficiales. Si decide cancelar, puede ajustar esta configuración más adelante en los ajustes generales. - \ No newline at end of file + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index e9847af6..db432a61 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -191,4 +191,4 @@ پیش‌فرض کارتون تورنت - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1370ff2b..77c3db15 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -595,4 +595,4 @@ Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. Copié ! Nom du dépôt et adresse internet - \ No newline at end of file + diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index c3b55ba2..d537a1d5 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -638,4 +638,4 @@ Buku Audio Media Untuk memastikan unduhan dan pemberitahuan tanpa gangguan untuk acara TV berlangganan, CloudStream memerlukan izin untuk berjalan di latar belakang. Dengan menekan OK, Anda akan diarahkan ke Info aplikasi. Di sana, gulir ke Penggunaan baterai aplikasi dan atur penggunaan baterai ke Tidak Terbatas. Harap dicatat, izin ini tidak berarti CS3 akan menguras baterai Anda. Ini hanya akan beroperasi di latar belakang ketika diperlukan, seperti ketika menerima pemberitahuan atau mengunduh video dari ekstensi resmi. Jika Anda memilih untuk membatalkannya, Anda dapat menyesuaikan pengaturan ini nanti di Pengaturan Umum. - \ No newline at end of file + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 01031297..040b0f31 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -637,4 +637,4 @@ L\'utilizzo della batteria dell\'app è già impostato su \"Senza restrizioni\" Musica Audiolibro - \ No newline at end of file + diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index a26f902b..279f5511 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -280,4 +280,4 @@ എഡ്ജ് തരം ഔട്ട്ലൈൻ നിറം പശ്ചാത്തല നിറം - \ No newline at end of file + diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml index 356a2caa..b2c0356a 100644 --- a/app/src/main/res/values-mt/strings.xml +++ b/app/src/main/res/values-mt/strings.xml @@ -92,7 +92,7 @@ Kompli Ara Neħħi Iktar informazzjoni - \@string/home_play + @string/home_play Jista\' jkun hemm bżonn ta\' VPN biex dan il-fornitur jaħdem b\'mod korrett Il-metadata mhix ipprovduta mis-sit, it-tagħbija tal-vidjo se tfalli jekk ma teżistix fuq is-sit. Deskrizzjoni @@ -123,4 +123,4 @@ Bookmarks Neħħi Falla t-tniżżil - \ No newline at end of file + diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index 4bf0f565..bdc55780 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -159,4 +159,4 @@ କୌଣସି ତଥ୍ୟ ନାହିଁ %1$s ଅ %2$d ଆଦ୍ୟ ବାଦ୍ ଦିଅ - \ No newline at end of file + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index b80c1b79..c61f0104 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -618,4 +618,4 @@ Multimedia Użycie akumulatora przez aplikację jest już ustawione na nieograniczone Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach telewizyjnych, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do informacji o aplikacji. Tam przewiń do użycia akumulatora przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać akumulator. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach głównych. - \ No newline at end of file + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 82054b6f..06e2352c 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -615,4 +615,4 @@ Multimédia Desativar a otimização da bateria Para garantir descarregamentos ininterruptos e notificações de programas de TV subscritos, o CloudStream precisa de permissão para ser executado em segundo plano. Ao premir OK, será direcionado para informações da aplicação. Aí, desloque-se para utilização da bateria da aplicação e defina a utilização da bateria para sem restrições. Tenha em atenção que esta permissão não significa que o CS3 irá esgotar a sua bateria. Este só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se optar por cancelar, pode ajustar esta definição mais tarde em definições gerais. - \ No newline at end of file + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a71cc71b..cf456f56 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -616,4 +616,4 @@ Этот экран был закрыт из-за нескольких неудачных попыток. Пожалуйста, перезапустите приложение. Ваши данные в CloudStream были скопированы. Хотя вероятность этого очень мала, все устройства могут вести себя по-разному. В редких случаях, когда доступ к приложению заблокирован, полностью удалите данные приложения и восстановите их из резервной копии. Мы приносим свои извинения за любые неудобства, связанные с этим. Чтобы обеспечить бесперебойную загрузку и получение уведомлений о телепередачах, на которые вы подписаны, CloudStream необходимо разрешение на запуск в фоновом режиме. Нажав OK, вы перейдете к информации о приложении. Там перейдите к разделу 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и установите значение \"Использование батареи\" 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Пожалуйста, обратите внимание, что это разрешение не означает, что CS3 разрядит вашу батарею. Он будет работать в фоновом режиме только при необходимости, например, при получении уведомлений или загрузке видео с официальных расширений. Если вы решите отменить, вы можете изменить эту настройку позже в 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. - \ No newline at end of file + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3a5170a3..c3e5959a 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -664,4 +664,4 @@ Medya Abone olunan TV şovları için kesintisiz indirmeleri ve bildirimleri sağlamak için, CloudStream\'in arka planda çalışmasına izin vermeniz gerekmektedir. Tamam\'a basarak Uygulama bilgilerine yönlendirileceksiniz. Orada, 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 (Uygulama pil kullanımı) kısmına gidip pil kullanımını 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙 (Sınırsız) olarak ayarlayın. Bu iznin CS3\'ün pilinizi hızlıca tüketeceği anlamına gelmediğini lütfen unutmayın. Sadece gerektiğinde, resmi eklentilerden bildirim almak veya videoları indirmek gibi durumlarda arka planda çalışacaktır. İptal etmeyi seçerseniz, bu ayarı daha sonra 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨 (Genel Ayarlar) bölümünden ayarlayabilirsiniz. Uygulama pil kullanımı zaten sınırsız olarak ayarlanmış - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 981ac19b..403640b9 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -617,4 +617,4 @@ Аудіо книга Музика Медіа - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 202af75c..a12570ad 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -629,4 +629,4 @@ Chế độ tiêu thụ pin của ứng dụng đã được đặt ở mức không giới hạn …  \n——— - \ No newline at end of file + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 4423f20f..828703d1 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -659,4 +659,4 @@ 音乐 无法打开 CloudStream 的应用程序信息。 使用指纹、面部 ID、PIN 码、图案和密码解锁应用程序。 - \ No newline at end of file + From e6c111532dd0db555393c78b94bde5c047a168d0 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 13 Apr 2024 19:51:39 +0200 Subject: [PATCH 324/441] Defaults Play button to first unwatched Episode (#1035) --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 69f8e8aa..6a83f396 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 @@ -33,6 +33,7 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer +import com.lagradost.cloudstream3.ui.player.NEXT_WATCH_EPISODE_PERCENTAGE import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent @@ -782,7 +783,7 @@ class ResultFragmentTv : Fragment() { // resultEpisodeLoading.isVisible = episodes is Resource.Loading if (episodes is Resource.Success) { - val lastWatchedIndex = episodes.value.indexOfLast { ep -> ep.videoWatchState == VideoWatchState.Watched } + val lastWatchedIndex = episodes.value.indexOfLast { ep -> ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f } val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() } if (firstUnwatched != null) { From afdc4988ac5fa54060c6fae4dc56abdf7679b08f Mon Sep 17 00:00:00 2001 From: Rushikesh Chavan <66415100+rushi-chavan@users.noreply.github.com> Date: Sat, 13 Apr 2024 10:52:08 -0700 Subject: [PATCH 325/441] Extractor: Update Vidplay Extractor (#1036) --- .../main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt index b9a07a6d..d5d0fb32 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -66,7 +66,7 @@ open class Vidplay : ExtractorApi() { } private suspend fun callFutoken(id: String, url: String): String? { - val script = app.get("$mainUrl/futoken").text + val script = app.get("$mainUrl/futoken", referer = url).text val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null val a = mutableListOf(k) for (i in id.indices) { From aa8972870ccbaf1362be32ba134115463259fe5a Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Sat, 13 Apr 2024 22:45:58 +0000 Subject: [PATCH 326/441] Show download size on videos (#1038) --- .../ui/result/ResultViewModel2.kt | 9 +++++-- .../cloudstream3/utils/ExtractorApi.kt | 26 ++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) 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 84b8cf48..37a905a7 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 @@ -5,6 +5,7 @@ import android.content.* import android.net.Uri import android.os.Build import android.os.Bundle +import android.text.format.Formatter.formatFileSize import android.util.Log import android.widget.Toast import androidx.annotation.MainThread @@ -20,7 +21,6 @@ import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS -import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.getCastSession @@ -1280,9 +1280,14 @@ class ResultViewModel2 : ViewModel() { callback: (Pair) -> Unit, ) { loadLinks(result, isVisible = true, type) { links -> + // Could not find a better way to do this + val context = AcraApplication.context postPopup( text, - links.links.map { txt("${it.name} ${Qualities.getStringByInt(it.quality)}") }) { + links.links.apmap { + val size = it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: "" + txt("${it.name} ${Qualities.getStringByInt(it.quality)}$size") + }) { callback.invoke(links to (it ?: return@postPopup)) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index e5d82d39..5a845326 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -404,9 +404,29 @@ open class ExtractorLink constructor( open val extractorData: String? = null, open val type: ExtractorLinkType, ) : VideoDownloadManager.IDownloadableMinimum { - val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 - val isDash : Boolean get() = type == ExtractorLinkType.DASH - + val isM3u8: Boolean get() = type == ExtractorLinkType.M3U8 + val isDash: Boolean get() = type == ExtractorLinkType.DASH + + // Cached video size + private var videoSize: Long? = null + + /** + * Get video size in bytes with one head request. Only available for ExtractorLinkType.Video + * @param timeoutSeconds timeout of the head request. + */ + suspend fun getVideoSize(timeoutSeconds: Long = 3L): Long? { + // Content-Length is not applicable to other types of formats + if (this.type != ExtractorLinkType.VIDEO) return null + + videoSize = videoSize ?: runCatching { + val response = + app.head(this.url, headers = headers, referer = referer, timeout = timeoutSeconds) + response.headers["Content-Length"]?.toLong() + }.getOrNull() + + return videoSize + } + @JsonIgnore fun getAllHeaders() : Map { if (referer.isBlank()) { From 5db541d7ccdc6e305002e2169fa84c56aa0018ab Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Sun, 14 Apr 2024 02:13:12 +0200 Subject: [PATCH 327/441] feat(ui): added reset button to subtitle delay (#1040) --- .../cloudstream3/ui/player/FullScreenPlayer.kt | 14 ++++++-------- app/src/main/res/layout/subtitle_offset.xml | 7 +++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 14 insertions(+), 8 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 56983190..c357ce9c 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 @@ -14,13 +14,7 @@ import android.os.Bundle import android.provider.Settings import android.text.Editable import android.text.format.DateUtils -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.Surface -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager +import android.view.* import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES import android.view.animation.AlphaAnimation import android.view.animation.Animation @@ -50,7 +44,6 @@ import com.lagradost.cloudstream3.ui.settings.Globals 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.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog @@ -498,6 +491,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { dialog.dismissSafe(activity) player.seekTime(1L) } + resetBtt.setOnClickListener { + subtitleDelay = 0 + dialog.dismissSafe(activity) + player.seekTime(1L) + } cancelBtt.setOnClickListener { subtitleDelay = beforeOffset dialog.dismissSafe(activity) diff --git a/app/src/main/res/layout/subtitle_offset.xml b/app/src/main/res/layout/subtitle_offset.xml index c17c5eff..d5e303b6 100644 --- a/app/src/main/res/layout/subtitle_offset.xml +++ b/app/src/main/res/layout/subtitle_offset.xml @@ -113,6 +113,13 @@ + + Music Audio Book Media + Reset \ No newline at end of file From 6df3ef14f66dd3cfc038ee922c563684eb84ce4e Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Tue, 16 Apr 2024 21:07:28 +0000 Subject: [PATCH 328/441] First steps for multiplatform API (#1003) * First steps for multiplatform api * Buildconfig testing * Fix publishing and classes.jar * Update build.gradle.kts --- .idea/gradle.xml | 7 +- app/build.gradle.kts | 34 ++++++++-- .../com/lagradost/cloudstream3/MainAPI.kt | 2 - .../lagradost/cloudstream3/mvvm/Lifecycle.kt | 16 +++++ build.gradle.kts | 8 ++- library/build.gradle.kts | 68 +++++++++++++++++++ library/src/androidMain/AndroidManifest.xml | 2 + .../kotlin/com/lagradost/api/Log.kt | 21 ++++++ .../kotlin/com/lagradost/api/Log.kt | 8 +++ .../com/lagradost/cloudstream3/MainApi.kt | 3 + .../cloudstream3/mvvm/ArchComponentExt.kt | 35 +++------- .../jvmMain/kotlin/com/lagradost/api/Log.kt | 19 ++++++ settings.gradle.kts | 3 +- 13 files changed, 185 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt create mode 100644 library/build.gradle.kts create mode 100644 library/src/androidMain/AndroidManifest.xml create mode 100644 library/src/androidMain/kotlin/com/lagradost/api/Log.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/api/Log.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt (86%) create mode 100644 library/src/jvmMain/kotlin/com/lagradost/api/Log.kt diff --git a/.idea/gradle.xml b/.idea/gradle.xml index c5c0ff3b..d7c08c9c 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,17 +4,16 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 02946e85..e07162d7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import org.jetbrains.dokka.gradle.DokkaTask +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import java.io.ByteArrayOutputStream import java.net.URL @@ -13,6 +14,7 @@ plugins { val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/" val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first() +var isLibraryDebug = false fun String.execute() = ByteArrayOutputStream().use { baot -> if (project.exec { @@ -103,6 +105,7 @@ android { ) } debug { + isLibraryDebug = true isDebuggable = true applicationIdSuffix = ".debug" proguardFiles( @@ -232,18 +235,37 @@ dependencies { implementation("androidx.work:work-runtime:2.9.0") implementation("androidx.work:work-runtime-ktx:2.9.0") implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib + + implementation(project(":library") { + this.extra.set("isDebug", isLibraryDebug) + }) } -tasks.register("androidSourcesJar", Jar::class) { +tasks.register("androidSourcesJar") { archiveClassifier.set("sources") from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources } -// For GradLew Plugin -tasks.register("makeJar", Copy::class) { - from("build/intermediates/compile_app_classes_jar/prereleaseDebug") - into("build") - include("classes.jar") +tasks.register("copyJar") { + from( + "build/intermediates/compile_app_classes_jar/prereleaseDebug", + "../library/build/libs" + ) + into("build/app-classes") + include("classes.jar", "library-jvm*.jar") + // Remove the version + rename("library-jvm.*.jar", "library-jvm.jar") +} + +// Merge the app classes and the library classes into classes.jar +tasks.register("makeJar") { + dependsOn(tasks.getByName("copyJar")) + from( + zipTree("build/app-classes/classes.jar"), + zipTree("build/app-classes/library-jvm.jar") + ) + destinationDirectory.set(layout.buildDirectory) + archivesName = "classes" } tasks.withType { diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index ecbdcbbc..7b1b5775 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -743,8 +743,6 @@ fun base64Encode(array: ByteArray): String { } } -class ErrorLoadingException(message: String? = null) : Exception(message) - fun MainAPI.fixUrlNull(url: String?): String? { if (url.isNullOrEmpty()) { return null diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt new file mode 100644 index 00000000..3df5197c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt @@ -0,0 +1,16 @@ +package com.lagradost.cloudstream3.mvvm + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData + +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { it?.let { t -> action(t) } } +} + +/** NOTE: Only one observer at a time per value */ +fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { + liveData.removeObservers(this) + liveData.observe(this) { action(it) } +} diff --git a/build.gradle.kts b/build.gradle.kts index 801a3c0f..ab1918fe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,8 @@ buildscript { classpath("com.android.tools.build:gradle:8.2.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") + // Universal build config + classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:0.15.1") } } @@ -22,6 +24,6 @@ plugins { id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false } -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} +//tasks.register("clean") { +// delete(rootProject.layout.buildDirectory) +//} diff --git a/library/build.gradle.kts b/library/build.gradle.kts new file mode 100644 index 00000000..42a8c943 --- /dev/null +++ b/library/build.gradle.kts @@ -0,0 +1,68 @@ +import com.codingfeline.buildkonfig.compiler.FieldSpec + +plugins { + kotlin("multiplatform") + id("maven-publish") + id("com.android.library") + id("com.codingfeline.buildkonfig") +} + +kotlin { + version = "1.0.0" + androidTarget() + jvm() + + sourceSets { + commonMain.dependencies { + implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser + ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API + Level 25 or Less. */ + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + } + } +} + +repositories { + mavenLocal() + maven("https://jitpack.io") +} + +tasks.withType { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +buildkonfig { + packageName = "com.lagradost.api" + exposeObjectWithName = "BuildConfig" + + defaultConfigs { + val isDebug = kotlin.runCatching { extra.get("isDebug") }.getOrNull() == true + buildConfigField(FieldSpec.Type.BOOLEAN, "DEBUG", isDebug.toString()) + } +} + +android { + compileSdk = 34 + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + + defaultConfig { + minSdk = 21 + targetSdk = 33 + } + + // If this is the same com.lagradost.cloudstream3.R stops working + namespace = "com.lagradost.api" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} +publishing { + publications { + withType { + groupId = "com.lagradost.api" + } + } +} \ No newline at end of file diff --git a/library/src/androidMain/AndroidManifest.xml b/library/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..568741e5 --- /dev/null +++ b/library/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/library/src/androidMain/kotlin/com/lagradost/api/Log.kt b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..12524411 --- /dev/null +++ b/library/src/androidMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,21 @@ +package com.lagradost.api + +import android.util.Log + +actual object Log { + actual fun d(tag: String, message: String) { + Log.d(tag, message) + } + + actual fun i(tag: String, message: String) { + Log.i(tag, message) + } + + actual fun w(tag: String, message: String) { + Log.w(tag, message) + } + + actual fun e(tag: String, message: String) { + Log.e(tag, message) + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/api/Log.kt b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..4b8e6329 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,8 @@ +package com.lagradost.api + +expect object Log { + fun d(tag: String, message: String) + fun i(tag: String, message: String) + fun w(tag: String, message: String) + fun e(tag: String, message: String) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt new file mode 100644 index 00000000..87ee4815 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt @@ -0,0 +1,3 @@ +package com.lagradost.cloudstream3 + +class ErrorLoadingException(message: String? = null) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt similarity index 86% rename from app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt index 817d7db3..d3b4999a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt @@ -1,10 +1,7 @@ package com.lagradost.cloudstream3.mvvm -import android.util.Log -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import com.bumptech.glide.load.HttpException -import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.api.BuildConfig +import com.lagradost.api.Log import com.lagradost.cloudstream3.ErrorLoadingException import kotlinx.coroutines.* import java.io.InterruptedIOException @@ -49,18 +46,6 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) { } } -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { it?.let { t -> action(t) } } -} - -/** NOTE: Only one observer at a time per value */ -fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) { - liveData.removeObservers(this) - liveData.observe(this) { action(it) } -} - sealed class Resource { data class Success(val value: T) : Resource() data class Failure( @@ -158,14 +143,14 @@ fun throwAbleToResource( "Connection Timeout\nPlease try again later." ) } - is HttpException -> { - Resource.Failure( - false, - throwable.statusCode, - null, - throwable.message ?: "HttpException" - ) - } +// is HttpException -> { +// Resource.Failure( +// false, +// throwable.statusCode, +// null, +// throwable.message ?: "HttpException" +// ) +// } is UnknownHostException -> { Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}") } diff --git a/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt new file mode 100644 index 00000000..e9a0e6b4 --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/api/Log.kt @@ -0,0 +1,19 @@ +package com.lagradost.api + +actual object Log { + actual fun d(tag: String, message: String) { + println("DEBUG $tag: $message") + } + + actual fun i(tag: String, message: String) { + println("INFO $tag: $message") + } + + actual fun w(tag: String, message: String) { + println("WARNING $tag: $message") + } + + actual fun e(tag: String, message: String) { + println("ERROR $tag: $message") + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 17070047..eabd9f0e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ rootProject.name = "CloudStream" -include(":app") \ No newline at end of file +include(":app") +include(":library") \ No newline at end of file From 9a18ef641136cf9335c830145cf5b1bc4a62f8e3 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Wed, 17 Apr 2024 23:48:33 +0200 Subject: [PATCH 329/441] bugfix: fixing regex special chars break it (#1047) --- .../main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt index 153dbd3e..d9f0b382 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt @@ -50,7 +50,7 @@ class JsUnpacker(packedJS: String?) { throw Exception("Unknown p.a.c.k.e.r. encoding") } val unbase = Unbase(radix) - p = Pattern.compile("\\b\\w+\\b") + p = Pattern.compile("""\b[a-zA-Z0-9_]+\b""") m = p.matcher(payload) val decoded = StringBuilder(payload) var replaceOffset = 0 From 6cef9f7ea257f4af8ed3f739f79c1d01b1b3b36e Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 20 Apr 2024 22:18:49 +0200 Subject: [PATCH 330/441] Filtering first unwatched episode respects watched state (#1049) --- .../com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 6a83f396..13621cda 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 @@ -783,7 +783,10 @@ class ResultFragmentTv : Fragment() { // resultEpisodeLoading.isVisible = episodes is Resource.Loading if (episodes is Resource.Success) { - val lastWatchedIndex = episodes.value.indexOfLast { ep -> ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f } + val lastWatchedIndex = episodes.value.indexOfLast { ep -> + ep.getWatchProgress() >= NEXT_WATCH_EPISODE_PERCENTAGE.toFloat() / 100.0f || ep.videoWatchState == VideoWatchState.Watched + } + val firstUnwatched = episodes.value.getOrElse(lastWatchedIndex + 1) { episodes.value.firstOrNull() } if (firstUnwatched != null) { From e01ff4d843810467660add2a8464973a673daa08 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Mon, 22 Apr 2024 01:13:55 +0200 Subject: [PATCH 331/441] Fix NewPipeExtractor lib path (#1050) --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e07162d7..f854865d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -202,7 +202,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:6dc25f7") /* For Trailers + implementation("com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:6dc25f7b97") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding From 4399a612dfa0672acefc7de17c37884ee64331c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Mon, 22 Apr 2024 02:14:36 +0300 Subject: [PATCH 332/441] Update Vidmoly.kt (#1051) --- .../java/com/lagradost/cloudstream3/extractors/Vidmoly.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt index 615cfd74..979fd8c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt @@ -25,9 +25,13 @@ open class Vidmoly : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - + val headers = mapOf( + "User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36", + "Sec-Fetch-Dest" to "iframe" + ) val script = app.get( url, + headers = headers, referer = referer, ).document.select("script") .find { it.data().contains("sources:") }?.data() @@ -66,4 +70,4 @@ open class Vidmoly : ExtractorApi() { @JsonProperty("kind") val kind: String? = null, ) -} \ No newline at end of file +} From 0744189020fb3132ebf0debed899e522ab4df246 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:18:54 +0530 Subject: [PATCH 333/441] feat(ui): show account name and image on main settings page (#1001) --- .../ui/settings/SettingsFragment.kt | 52 ++++++++++++++----- app/src/main/res/drawable/rounded_outline.xml | 13 +++++ app/src/main/res/layout/main_settings.xml | 9 ++-- 3 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/rounded_outline.xml 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 dfa84998..443eeda7 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 @@ -1,13 +1,13 @@ package com.lagradost.cloudstream3.ui.settings import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.annotation.StringRes import androidx.core.view.children -import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.preference.Preference @@ -18,12 +18,14 @@ import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.result.txt 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.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate @@ -133,7 +135,6 @@ class SettingsFragment : Fragment() { val localBinding = MainSettingsBinding.inflate(inflater, container, false) binding = localBinding return localBinding.root - //return inflater.inflate(R.layout.main_settings, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -141,21 +142,44 @@ class SettingsFragment : Fragment() { activity?.navigate(id, Bundle()) } - // used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}") + /** used to debug leaks + showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : + ${VideoDownloadManager.downloadProgressEvent.size}") **/ - for (syncApi in accountManagers) { - val login = syncApi.loginInfo() - val pic = login?.profilePicture ?: continue - if (binding?.settingsProfilePic?.setImage( - pic, - errorImageDrawable = HomeFragment.errorProfilePic - ) == true - ) { - binding?.settingsProfileText?.text = login.name - binding?.settingsProfile?.isVisible = true - break + fun hasProfilePictureFromAccountManagers(accountManagers: List): Boolean { + for (syncApi in accountManagers) { + val login = syncApi.loginInfo() + val pic = login?.profilePicture ?: continue + + if (binding?.settingsProfilePic?.setImage( + pic, + errorImageDrawable = HomeFragment.errorProfilePic + ) == true + ) { + binding?.settingsProfileText?.text = login.name + return true // sync profile exists + } } + return false // not syncing } + + // display local account information if not syncing + if (!hasProfilePictureFromAccountManagers(accountManagers)) { + val activity = activity ?: return + val currentAccount = try { + DataStoreHelper.accounts.firstOrNull { + it.keyIndex == DataStoreHelper.selectedKeyIndex + } ?: activity.let { DataStoreHelper.getDefaultAccount(activity) } + + } catch (t: IllegalStateException) { + Log.e("AccountManager", "Activity not found", t) + null + } + + binding?.settingsProfilePic?.setImage(currentAccount?.image) + binding?.settingsProfileText?.text = currentAccount?.name + } + binding?.apply { listOf( settingsGeneral to R.id.action_navigation_global_to_navigation_settings_general, diff --git a/app/src/main/res/drawable/rounded_outline.xml b/app/src/main/res/drawable/rounded_outline.xml new file mode 100644 index 00000000..b85ace8e --- /dev/null +++ b/app/src/main/res/drawable/rounded_outline.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/main_settings.xml b/app/src/main/res/layout/main_settings.xml index 2c90d958..0b931843 100644 --- a/app/src/main/res/layout/main_settings.xml +++ b/app/src/main/res/layout/main_settings.xml @@ -24,7 +24,6 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:padding="20dp" - android:visibility="gone" tools:visibility="visible"> + android:scaleType="centerCrop" + android:foreground="@drawable/rounded_outline" + tools:src="@drawable/profile_bg_orange" + android:contentDescription="@string/account"/> + + tools:text="Quick Brown Fox" /> Date: Mon, 22 Apr 2024 16:59:14 +0200 Subject: [PATCH 334/441] Trakt meta provider for extensions (#1026) --- .../metaproviders/TraktProvider.kt | 430 ++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt new file mode 100644 index 00000000..98e12bcd --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -0,0 +1,430 @@ +package com.lagradost.cloudstream3.metaproviders + +import android.net.Uri +import com.lagradost.cloudstream3.* +import com.fasterxml.jackson.annotation.JsonAlias +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId +import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import java.util.Locale +import java.text.SimpleDateFormat +import kotlin.math.roundToInt + +open class TraktProvider : MainAPI() { + override var name = "Trakt" + override val hasMainPage = true + override val providerType = ProviderType.MetaProvider + override val supportedTypes = setOf( + TvType.Movie, + TvType.TvSeries, + TvType.Anime, + ) + + private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==") + private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2") + + override val mainPage = mainPageOf( + "$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now + "$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time + "$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now + "$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time + ) + + override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse { + + val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page") + + val results = parseJson>(apiResponse).map { element -> + element.toSearchResponse() + } + return newHomePageResponse(request.name, results) + } + + private fun MediaDetails.toSearchResponse(): SearchResponse { + + val media = this.media ?: this + val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries + val poster = media.images?.poster?.firstOrNull() + + if (mediaType == TvType.Movie) { + return newMovieSearchResponse( + name = media.title!!, + url = Data( + type = mediaType, + mediaDetails = media, + ).toJson(), + type = TvType.Movie, + ) { + posterUrl = fixPath(poster) + } + } else { + return newTvSeriesSearchResponse( + name = media.title!!, + url = Data( + type = mediaType, + mediaDetails = media, + ).toJson(), + type = TvType.TvSeries, + ) { + this.posterUrl = fixPath(poster) + } + } + } + + override suspend fun search(query: String): List? { + val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") + + val results = parseJson>(apiResponse).map { element -> + element.toSearchResponse() + } + + return results + } + override suspend fun load(url: String): LoadResponse { + + val data = parseJson(url) + val mediaDetails = data.mediaDetails + val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows" + + val posterUrl = mediaDetails?.images?.poster?.firstOrNull() + val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull() + + val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full") + + val actors = parseJson(resActor).cast?.map { + ActorData( + Actor( + name = it.person?.name!!, + image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500") + ), + roleString = it.character + ) + } + + val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20") + + val relatedMedia = parseJson>(resRelated).map { it.toSearchResponse() } + + val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true + val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja") + val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko") + val isBollywood = mediaDetails?.country == "in" + + if (data.type == TvType.Movie) { + + val linkData = LinkData( + id = mediaDetails?.ids?.tmdb, + imdbId = mediaDetails?.ids?.imdb.toString(), + tvdbId = mediaDetails?.ids?.tvdb, + type = data.type.toString(), + title = mediaDetails?.title, + year = mediaDetails?.year, + orgTitle = mediaDetails?.title, + isAnime = isAnime, + //jpTitle = later if needed as it requires another network request, + airedDate = mediaDetails?.released + ?: mediaDetails?.firstAired, + isAsian = isAsian, + isBollywood = isBollywood, + ).toJson() + + return newMovieLoadResponse( + name = mediaDetails?.title!!, + url = data.toJson(), + dataUrl = linkData.toJson(), + type = if (isAnime) TvType.AnimeMovie else TvType.Movie, + ) { + this.name = mediaDetails.title + this.apiName = "Trakt" + this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie + this.posterUrl = getOriginalWidthImageUrl(posterUrl) + this.year = mediaDetails.year + this.plot = mediaDetails.overview + this.rating = mediaDetails.rating?.times(1000)?.roundToInt() + this.tags = mediaDetails.genres + this.duration = mediaDetails.runtime + this.recommendations = relatedMedia + this.actors = actors + this.comingSoon = isUpcoming(mediaDetails.released) + //posterHeaders + this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) + this.contentRating = mediaDetails.certification + addTrailer(mediaDetails.trailer) + addImdbId(mediaDetails.ids?.imdb) + addTMDbId(mediaDetails.ids?.tmdb.toString()) + } + } else { + + val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") + val episodes = mutableListOf() + val seasons = parseJson>(resSeasons) + val seasonsNames = mutableListOf() + + seasons.forEach { season -> + + seasonsNames.add( + SeasonData( + season.number!!, + season.title + ) + ) + + season.episodes?.map { episode -> + + val linkData = LinkData( + id = mediaDetails?.ids?.tmdb, + imdbId = mediaDetails?.ids?.imdb.toString(), + tvdbId = mediaDetails?.ids?.tvdb, + type = data.type.toString(), + season = episode.season, + episode = episode.number, + title = mediaDetails?.title, + year = mediaDetails?.year, + orgTitle = mediaDetails?.title, + isAnime = isAnime, + airedYear = mediaDetails?.year, + lastSeason = seasons.size, + epsTitle = episode.title, + //jpTitle = later if needed as it requires another network request, + date = episode.firstAired, + airedDate = episode.firstAired, + isAsian = isAsian, + isBollywood = isBollywood, + isCartoon = isCartoon + ).toJson() + + episodes.add( + Episode( + data = linkData.toJson(), + name = episode.title, + season = episode.season, + episode = episode.number, + posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()), + rating = episode.rating?.times(10)?.roundToInt(), + description = episode.overview, + ).apply { + this.addDate(episode.firstAired) + } + ) + } + } + + return newTvSeriesLoadResponse( + name = mediaDetails?.title!!, + url = data.toJson(), + type = if (isAnime) TvType.Anime else TvType.TvSeries, + episodes = episodes + ) { + this.name = mediaDetails.title + this.apiName = "Trakt" + this.type = if (isAnime) TvType.Anime else TvType.TvSeries + this.episodes = episodes + this.posterUrl = getOriginalWidthImageUrl(posterUrl) + this.year = mediaDetails.year + this.plot = mediaDetails.overview + this.showStatus = getStatus(mediaDetails.status) + this.rating = mediaDetails.rating?.times(1000)?.roundToInt() + this.tags = mediaDetails.genres + this.duration = mediaDetails.runtime + this.recommendations = relatedMedia + this.actors = actors + this.comingSoon = isUpcoming(mediaDetails.released) + //posterHeaders + this.seasonNames = seasonsNames + this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) + this.contentRating = mediaDetails.certification + addTrailer(mediaDetails.trailer) + addImdbId(mediaDetails.ids?.imdb) + addTMDbId(mediaDetails.ids?.tmdb.toString()) + } + } + } + + private suspend fun getApi(url: String) : String { + return app.get( + url = url, + headers = mapOf( + "Content-Type" to "application/json", + "trakt-api-version" to "2", + "trakt-api-key" to traktClientId, + ) + ).toString() + } + + private fun isUpcoming(dateString: String?): Boolean { + return try { + val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val dateTime = dateString?.let { format.parse(it)?.time } ?: return false + APIHolder.unixTimeMS < dateTime + } catch (t: Throwable) { + logError(t) + false + } + } + + private fun getStatus(t: String?): ShowStatus { + return when (t) { + "returning series" -> ShowStatus.Ongoing + "continuing" -> ShowStatus.Ongoing + else -> ShowStatus.Completed + } + } + + private fun fixPath(url: String?): String? { + url ?: return null + return "https://$url" + } + + private fun getWidthImageUrl(path: String?, width: String) : String? { + if (path == null) return null + if (!path.contains("image.tmdb.org")) return fixPath(path) + val fileName = Uri.parse(path).lastPathSegment ?: return null + return "https://image.tmdb.org/t/p/${width}/${fileName}" + } + + private fun getOriginalWidthImageUrl(path: String?) : String? { + if (path == null) return null + if (!path.contains("image.tmdb.org")) return fixPath(path) + return getWidthImageUrl(path, "original") + } + + data class Data( + val type: TvType? = null, + val mediaDetails: MediaDetails? = null, + ) + + data class MediaDetails( + @JsonProperty("title") val title: String? = null, + @JsonProperty("year") val year: Int? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("tagline") val tagline: String? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("released") val released: String? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("country") val country: String? = null, + @JsonProperty("updatedAt") val updatedAt: String? = null, + @JsonProperty("trailer") val trailer: String? = null, + @JsonProperty("homepage") val homepage: String? = null, + @JsonProperty("status") val status: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("votes") val votes: Long? = null, + @JsonProperty("comment_count") val commentCount: Long? = null, + @JsonProperty("language") val language: String? = null, + @JsonProperty("languages") val languages: List? = null, + @JsonProperty("available_translations") val availableTranslations: List? = null, + @JsonProperty("genres") val genres: List? = null, + @JsonProperty("certification") val certification: String? = null, + @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("airs") val airs: Airs? = null, + @JsonProperty("network") val network: String? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null + ) + + data class Airs( + @JsonProperty("day") val day: String? = null, + @JsonProperty("time") val time: String? = null, + @JsonProperty("timezone") val timezone: String? = null, + ) + + data class Ids( + @JsonProperty("trakt") val trakt: Int? = null, + @JsonProperty("slug") val slug: String? = null, + @JsonProperty("tvdb") val tvdb: Int? = null, + @JsonProperty("imdb") val imdb: String? = null, + @JsonProperty("tmdb") val tmdb: Int? = null, + @JsonProperty("tvrage") val tvrage: String? = null, + ) + + data class Images( + @JsonProperty("fanart") val fanart: List? = null, + @JsonProperty("poster") val poster: List? = null, + @JsonProperty("logo") val logo: List? = null, + @JsonProperty("clearart") val clearart: List? = null, + @JsonProperty("banner") val banner: List? = null, + @JsonProperty("thumb") val thumb: List? = null, + @JsonProperty("screenshot") val screenshot: List? = null, + @JsonProperty("headshot") val headshot: List? = null, + ) + + data class People( + @JsonProperty("cast") val cast: List? = null, + ) + + data class Cast( + @JsonProperty("character") val character: String? = null, + @JsonProperty("characters") val characters: List? = null, + @JsonProperty("episode_count") val episodeCount: Long? = null, + @JsonProperty("person") val person: Person? = null, + @JsonProperty("images") val images: Images? = null, + ) + + data class Person( + @JsonProperty("name") val name: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + ) + + data class Seasons( + @JsonProperty("aired_episodes") val airedEpisodes: Int? = null, + @JsonProperty("episode_count") val episodeCount: Int? = null, + @JsonProperty("episodes") val episodes: List? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("network") val network: String? = null, + @JsonProperty("number") val number: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") val votes: Int? = null, + ) + + data class TraktEpisode( + @JsonProperty("available_translations") val availableTranslations: List? = null, + @JsonProperty("comment_count") val commentCount: Int? = null, + @JsonProperty("episode_type") val episodeType: String? = null, + @JsonProperty("first_aired") val firstAired: String? = null, + @JsonProperty("ids") val ids: Ids? = null, + @JsonProperty("images") val images: Images? = null, + @JsonProperty("number") val number: Int? = null, + @JsonProperty("number_abs") val numberAbs: Int? = null, + @JsonProperty("overview") val overview: String? = null, + @JsonProperty("rating") val rating: Double? = null, + @JsonProperty("runtime") val runtime: Int? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("title") val title: String? = null, + @JsonProperty("updated_at") val updatedAt: String? = null, + @JsonProperty("votes") val votes: Int? = null, + ) + + data class LinkData( + val id: Int? = null, + val imdbId: String? = null, + val tvdbId: Int? = null, + val type: String? = null, + val season: Int? = null, + val episode: Int? = null, + val aniId: String? = null, + val animeId: String? = null, + val title: String? = null, + val year: Int? = null, + val orgTitle: String? = null, + val isAnime: Boolean = false, + val airedYear: Int? = null, + val lastSeason: Int? = null, + val epsTitle: String? = null, + val jpTitle: String? = null, + val date: String? = null, + val airedDate: String? = null, + val isAsian: Boolean = false, + val isBollywood: Boolean = false, + val isCartoon: Boolean = false, + ) +} \ No newline at end of file From e6b9d621f96beba6e427aa092d09bb448caf8d93 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:00:27 +0200 Subject: [PATCH 335/441] feat(ui): added option to reset sub delay (#1041) --- .../ui/player/AbstractPlayerFragment.kt | 14 ++++++++------ .../lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 3 +++ 2 files changed, 11 insertions(+), 6 deletions(-) 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 cfa6682d..0865b220 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 @@ -1,7 +1,10 @@ package com.lagradost.cloudstream3.ui.player import android.annotation.SuppressLint -import android.content.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.graphics.drawable.AnimatedImageDrawable import android.graphics.drawable.AnimatedVectorDrawable import android.media.metrics.PlaybackErrorEvent @@ -24,11 +27,7 @@ import androidx.fragment.app.Fragment import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession -import androidx.media3.ui.AspectRatioFrameLayout -import androidx.media3.ui.DefaultTimeBar -import androidx.media3.ui.PlayerView -import androidx.media3.ui.SubtitleView -import androidx.media3.ui.TimeBar +import androidx.media3.ui.* import androidx.preference.PreferenceManager import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.github.rubensousa.previewseekbar.PreviewBar @@ -442,6 +441,9 @@ abstract class AbstractPlayerFragment( is VideoEndedEvent -> { context?.let { ctx -> + // Resets subtitle delay on ended video + player.setSubtitleOffset(0) + // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(ctx) ?.getBoolean( 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 210bfdca..31adbc87 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 @@ -1118,6 +1118,9 @@ class CS3IPlayer : IPlayer { } Player.STATE_ENDED -> { + // Resets subtitle delay on ended video + setSubtitleOffset(0) + // Only play next episode if autoplay is on (default) if (PreferenceManager.getDefaultSharedPreferences(context) ?.getBoolean( From e2946cad6b0eb2ef602174f8da38ab1a289ac8e2 Mon Sep 17 00:00:00 2001 From: b4byhuey <60543438+b4byhuey@users.noreply.github.com> Date: Sun, 28 Apr 2024 00:00:40 +0800 Subject: [PATCH 336/441] Added Vidguard Extractor (#1053) --- .../cloudstream3/extractors/Vidguard.kt | 101 ++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 4 +- 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt new file mode 100644 index 00000000..230a9e1a --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt @@ -0,0 +1,101 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE +import com.lagradost.cloudstream3.utils.Qualities +import org.mozilla.javascript.Context +import org.mozilla.javascript.NativeJSON +import org.mozilla.javascript.NativeObject +import org.mozilla.javascript.Scriptable +import java.util.Base64 + +open class Vidguardto : ExtractorApi() { + override val name = "Vidguard" + override val mainUrl = "https://vidguard.to" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val res = app.get(url) + val resc = res.document.select("script:containsData(eval)").firstOrNull()?.data() + resc?.let { + val jsonStr2 = AppUtils.parseJson(runJS2(it)) + val watchlink = sigDecode(jsonStr2.stream) + + callback.invoke( + ExtractorLink( + this.name, + name, + watchlink, + this.mainUrl, + Qualities.Unknown.value, + INFER_TYPE + ) + ) + } + } + + private fun sigDecode(url: String): String { + val sig = url.split("sig=")[1].split("&")[0] + var t = "" + for (v in sig.chunked(2)) { + val byteValue = Integer.parseInt(v, 16) xor 2 + t += byteValue.toChar() + } + val padding = when (t.length % 4) { + 2 -> "==" + 3 -> "=" + else -> "" + } + val decoded = Base64.getDecoder().decode((t + padding).toByteArray(Charsets.UTF_8)) + t = String(decoded).dropLast(5).reversed() + val charArray = t.toCharArray() + for (i in 0 until charArray.size - 1 step 2) { + val temp = charArray[i] + charArray[i] = charArray[i + 1] + charArray[i + 1] = temp + } + val modifiedSig = String(charArray).dropLast(5) + return url.replace(sig, modifiedSig) + } + + private fun runJS2(hideMyHtmlContent: String): String { + Log.d("runJS", "start") + val rhino = Context.enter() + rhino.initSafeStandardObjects() + rhino.optimizationLevel = -1 + val scope: Scriptable = rhino.initSafeStandardObjects() + scope.put("window", scope, scope) + var result = "" + try { + Log.d("runJS", "Executing JavaScript: $hideMyHtmlContent") + rhino.evaluateString(scope, hideMyHtmlContent, "JavaScript", 1, null) + val svgObject = scope.get("svg", scope) + result = if (svgObject is NativeObject) { + NativeJSON.stringify(Context.getCurrentContext(), scope, svgObject, null, null).toString() + } else { + Context.toString(svgObject) + } + Log.d("runJS", "Result: $result") + } catch (e: Exception) { + Log.e("runJS", "Error executing JavaScript", e) + } finally { + Context.exit() + } + return result + } + + data class SvgObject( + val stream: String, + val hash: String + ) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 5a845326..592dc6f9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -186,6 +186,7 @@ import com.lagradost.cloudstream3.extractors.VideoVard import com.lagradost.cloudstream3.extractors.VideovardSX import com.lagradost.cloudstream3.extractors.Vidgomunime import com.lagradost.cloudstream3.extractors.Vidgomunimesb +import com.lagradost.cloudstream3.extractors.Vidguardto import com.lagradost.cloudstream3.extractors.VidhideExtractor import com.lagradost.cloudstream3.extractors.Vidmoly import com.lagradost.cloudstream3.extractors.Vidmolyme @@ -888,7 +889,8 @@ val extractorApis: MutableList = arrayListOf( StreamWishExtractor(), EmturbovidExtractor(), Vtbe(), - EPlayExtractor() + EPlayExtractor(), + Vidguardto() ) From 004c481a5eb8ac8bb0c5a486f2e1f5b35e414f52 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 27 Apr 2024 19:11:22 +0300 Subject: [PATCH 337/441] feat(ui): Episode Air date & Upcoming countdown (#1058) --- .../cloudstream3/ui/result/EpisodeAdapter.kt | 34 ++++++++++++++++++- .../cloudstream3/ui/result/ResultFragment.kt | 5 ++- .../ui/result/ResultViewModel2.kt | 6 ++-- app/src/main/res/drawable/hourglass_24.xml | 9 +++++ .../main/res/layout/result_episode_large.xml | 23 +++++++++++-- app/src/main/res/values/strings.xml | 1 + 6 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/drawable/hourglass_24.xml 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 fad349c8..2019aa50 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 @@ -9,9 +9,11 @@ import androidx.core.view.isVisible import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable 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 @@ -23,6 +25,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import java.text.DateFormat +import java.text.SimpleDateFormat import java.util.* const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 @@ -104,7 +108,7 @@ class EpisodeAdapter( override fun getItemViewType(position: Int): Int { val item = getItem(position) - return if (item.poster.isNullOrBlank()) 0 else 1 + return if (item.poster.isNullOrBlank() && item.description.isNullOrBlank()) 0 else 1 } @@ -260,6 +264,33 @@ class EpisodeAdapter( } } + if (card.airDate != null) { + val isUpcoming = unixTimeMS < card.airDate + + if (isUpcoming) { + episodePlayIcon.isVisible = false + episodeUpcomingIcon.isVisible = !episodePoster.isVisible + episodeDate.setText( + txt( + R.string.episode_upcoming_format, + secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "") + ) + ) + } else { + episodeUpcomingIcon.isVisible = false + + val formattedAirDate = SimpleDateFormat.getDateInstance( + DateFormat.LONG, + Locale.getDefault() + ).apply { + }.format(Date(card.airDate)) + + episodeDate.setText(txt(formattedAirDate)) + } + } else { + episodeDate.isVisible = false + } + if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) @@ -271,6 +302,7 @@ class EpisodeAdapter( } } } + itemView.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index a1574eec..1d3f5a08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -50,6 +50,7 @@ data class ResultEpisode( val videoWatchState: VideoWatchState, /** Sum of all previous season episode counts + episode */ val totalEpisodeIndex: Int? = null, + val airDate: Long? = null, ) fun ResultEpisode.getRealPosition(): Long { @@ -85,6 +86,7 @@ fun buildResultEpisode( tvType: TvType, parentId: Int, totalEpisodeIndex: Int? = null, + airDate: Long? = null, ): ResultEpisode { val posDur = getViewPos(id) val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None @@ -107,7 +109,8 @@ fun buildResultEpisode( tvType, parentId, videoWatchState, - totalEpisodeIndex + totalEpisodeIndex, + airDate, ) } 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 37a905a7..499fced2 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 @@ -2277,7 +2277,8 @@ class ResultViewModel2 : ViewModel() { fillers.getOrDefault(episode, false), loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = i.date ) val season = eps.seasonIndex ?: 0 @@ -2326,7 +2327,8 @@ class ResultViewModel2 : ViewModel() { null, loadResponse.type, mainId, - totalIndex + totalIndex, + airDate = episode.date ) val season = ep.seasonIndex ?: 0 diff --git a/app/src/main/res/drawable/hourglass_24.xml b/app/src/main/res/drawable/hourglass_24.xml new file mode 100644 index 00000000..7bd1ebbd --- /dev/null +++ b/app/src/main/res/drawable/hourglass_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index 76e8c434..e5a6881a 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -43,14 +43,26 @@ android:foreground="?android:attr/selectableItemBackgroundBorderless" android:nextFocusRight="@id/download_button" android:scaleType="centerCrop" - tools:src="@drawable/example_poster" /> + tools:src="@drawable/example_poster" + tools:visibility="invisible"/> + android:src="@drawable/play_button" + tools:visibility="invisible"/> + + + + Episodes %1$d-%2$d %1$d %2$s + Upcoming in %s S E No Episodes found From 138e1a1f0ea4515c33274ac4fa3805e9595dd85e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 28 Apr 2024 04:40:15 +0800 Subject: [PATCH 338/441] Don't check year when checking duplicates if year is empty (#1060) Some sources don't use year which makes this not match when it really should match --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 499fced2..de339aee 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 @@ -1099,13 +1099,14 @@ class ResultViewModel2 : ViewModel() { val duplicateEntries = data.filter { it: DataStoreHelper.LibrarySearchResponse -> val librarySyncData = it.syncData + val yearCheck = year == it.year || year == null || it.year == null val checks = listOf( { imdbId != null && getImdbIdFromSyncData(librarySyncData) == imdbId }, { tmdbId != null && getTMDbIdFromSyncData(librarySyncData) == tmdbId }, { malId != null && librarySyncData?.get(AccountManager.malApi.idPrefix) == malId }, { aniListId != null && librarySyncData?.get(AccountManager.aniListApi.idPrefix) == aniListId }, - { normalizedName == normalizeString(it.name) && year == it.year } + { normalizedName == normalizeString(it.name) && yearCheck } ) checks.any { it() } From ff1ffbeb836a1bc94d002044ba1863e93fd654dc Mon Sep 17 00:00:00 2001 From: b4byhuey <60543438+b4byhuey@users.noreply.github.com> Date: Mon, 29 Apr 2024 03:42:38 +0800 Subject: [PATCH 339/441] Update Voe.kt (#1062) --- .../lagradost/cloudstream3/extractors/Voe.kt | 66 ++++++++++++++++--- .../cloudstream3/utils/ExtractorApi.kt | 10 ++- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt index 2c6998de..67fd7eea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt @@ -1,19 +1,46 @@ package com.lagradost.cloudstream3.extractors +import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper class Tubeless : Voe() { - override var mainUrl = "https://tubelessceliolymph.com" + override val name = "Tubeless" + override val mainUrl = "https://tubelessceliolymph.com" +} + +class Simpulumlamerop : Voe() { + override val name = "Simplum" + override var mainUrl = "https://simpulumlamerop.com" +} + +class Urochsunloath : Voe() { + override val name = "Uroch" + override var mainUrl = "https://urochsunloath.com" +} + +class Yipsu : Voe() { + override val name = "Yipsu" + override var mainUrl = "https://yip.su" +} + +class MetaGnathTuggers : Voe() { + override val name = "Metagnath" + override val mainUrl = "https://metagnathtuggers.com" } open class Voe : ExtractorApi() { override val name = "Voe" override val mainUrl = "https://voe.sx" override val requiresReferer = true + + private val linkRegex = "(http|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])".toRegex() + private val base64Regex = Regex("'.*'") override suspend fun getUrl( url: String, @@ -25,12 +52,33 @@ open class Voe : ExtractorApi() { val script = res.select("script").find { it.data().contains("sources =") }?.data() val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1) - M3u8Helper.generateM3u8( - name, - link ?: return, - "$mainUrl/", - headers = mapOf("Origin" to "$mainUrl/") - ).forEach(callback) - + val videoLinks = mutableListOf() + + if (!link.isNullOrBlank()) { + videoLinks.add( + when { + linkRegex.matches(link) -> link + else -> String(Base64.decode(link, Base64.DEFAULT)) + } + ) + } else { + val link2 = base64Regex.find(script)?.value ?: return + val decoded = Base64.decode(link2, Base64.DEFAULT).toString() + val videoLinkDTO = AppUtils.parseJson(decoded) + videoLinkDTO.let { videoLinks.add(it.toString()) } + } + + videoLinks.forEach { videoLink -> + M3u8Helper.generateM3u8( + name, + videoLink, + "$mainUrl/", + headers = mapOf("Origin" to "$mainUrl/") + ).forEach(callback) + } } -} \ No newline at end of file + + data class WcoSources( + @JsonProperty("VideoLinkDTO") val VideoLinkDTO: String, + ) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 592dc6f9..75dceb54 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -83,6 +83,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream import com.lagradost.cloudstream3.extractors.Mcloud import com.lagradost.cloudstream3.extractors.Megacloud import com.lagradost.cloudstream3.extractors.Meownime +import com.lagradost.cloudstream3.extractors.MetaGnathTuggers import com.lagradost.cloudstream3.extractors.Minoplres import com.lagradost.cloudstream3.extractors.MixDrop import com.lagradost.cloudstream3.extractors.MixDropBz @@ -139,6 +140,7 @@ import com.lagradost.cloudstream3.extractors.Sbspeed import com.lagradost.cloudstream3.extractors.Sbthe import com.lagradost.cloudstream3.extractors.Sendvid import com.lagradost.cloudstream3.extractors.ShaveTape +import com.lagradost.cloudstream3.extractors.Simpulumlamerop import com.lagradost.cloudstream3.extractors.Solidfiles import com.lagradost.cloudstream3.extractors.Ssbstream import com.lagradost.cloudstream3.extractors.StreamM4u @@ -175,6 +177,7 @@ import com.lagradost.cloudstream3.extractors.UpstreamExtractor import com.lagradost.cloudstream3.extractors.Uqload import com.lagradost.cloudstream3.extractors.Uqload1 import com.lagradost.cloudstream3.extractors.Uqload2 +import com.lagradost.cloudstream3.extractors.Urochsunloath import com.lagradost.cloudstream3.extractors.Userload import com.lagradost.cloudstream3.extractors.Userscloud import com.lagradost.cloudstream3.extractors.Uservideo @@ -208,6 +211,7 @@ import com.lagradost.cloudstream3.extractors.Watchx import com.lagradost.cloudstream3.extractors.WcoStream import com.lagradost.cloudstream3.extractors.Wibufile import com.lagradost.cloudstream3.extractors.XStreamCdn +import com.lagradost.cloudstream3.extractors.Yipsu import com.lagradost.cloudstream3.extractors.YourUpload import com.lagradost.cloudstream3.extractors.YoutubeExtractor import com.lagradost.cloudstream3.extractors.YoutubeMobileExtractor @@ -890,7 +894,11 @@ val extractorApis: MutableList = arrayListOf( EmturbovidExtractor(), Vtbe(), EPlayExtractor(), - Vidguardto() + Vidguardto(), + Simpulumlamerop(), + Urochsunloath(), + Yipsu(), + MetaGnathTuggers() ) From 949b5830b644d3ac23216dd533d40943ab5f6347 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 1 May 2024 20:29:49 +0300 Subject: [PATCH 340/441] feat(ui): Fix downloads focus on TV (#1066) --- .../cloudstream3/ui/download/DownloadChildFragment.kt | 3 ++- .../cloudstream3/ui/download/DownloadFragment.kt | 3 +++ .../java/com/lagradost/cloudstream3/utils/UIHelper.kt | 10 ++++++++++ app/src/main/res/layout/download_child_episode.xml | 5 ++++- app/src/main/res/layout/download_header_episode.xml | 6 +++++- 5 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index c3ec2bbd..d138a1e6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import kotlinx.coroutines.Dispatchers @@ -89,9 +90,9 @@ class DownloadChildFragment : Fragment() { setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() } + setAppBarNoScrollFlagsOnTV() } - val adapter: RecyclerView.Adapter = DownloadChildAdapter( ArrayList(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index e08eb772..31790b0f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -41,6 +41,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI @@ -97,6 +98,8 @@ class DownloadFragment : Fragment() { super.onViewCreated(view, savedInstanceState) hideKeyboard() + binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() + observe(downloadsViewModel.noDownloadsText) { binding?.textNoDownloads?.text = it } 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 eedb626a..cb527020 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -45,6 +45,7 @@ import androidx.core.view.marginBottom import androidx.core.view.marginLeft import androidx.core.view.marginRight import androidx.core.view.marginTop +import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.navigation.fragment.NavHostFragment @@ -58,6 +59,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions.bitmapTransform import com.bumptech.glide.request.target.Target +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup @@ -208,6 +210,14 @@ object UIHelper { } } + fun View?.setAppBarNoScrollFlagsOnTV() { + if (isLayout(Globals.TV or EMULATOR)) { + this?.updateLayoutParams { + scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL + } + } + } + fun Activity.hideKeyboard() { window?.decorView?.clearFocus() this.findViewById(android.R.id.content)?.rootView?.let { diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml index fd845ee8..4974a027 100644 --- a/app/src/main/res/layout/download_child_episode.xml +++ b/app/src/main/res/layout/download_child_episode.xml @@ -9,6 +9,7 @@ android:layout_height="50dp" android:layout_marginBottom="5dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" android:nextFocusLeft="@id/nav_rail_view" android:nextFocusRight="@id/download_button" app:cardBackgroundColor="@color/transparent" @@ -84,7 +85,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/download_child_episode_holder" android:padding="10dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index 226c1632..21f79ca6 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -9,6 +9,8 @@ android:layout_marginTop="10dp" android:layout_marginEnd="10dp" android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusRight="@id/download_button" app:cardBackgroundColor="?attr/boxItemBackground" app:cardCornerRadius="@dimen/rounded_image_radius"> @@ -71,7 +73,9 @@ android:layout_height="@dimen/download_size" android:layout_gravity="center_vertical|end" android:layout_marginStart="-50dp" - android:background="?selectableItemBackgroundBorderless" + android:foreground="@drawable/outline_drawable" + android:focusable="true" + android:nextFocusLeft="@id/episode_holder" android:padding="10dp" /> \ No newline at end of file From c07e6d3222123ce9b711cafa8827f682f9ad9516 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Thu, 2 May 2024 23:58:32 +0200 Subject: [PATCH 341/441] hotfix: Remove resume information (#1063) --- .../cloudstream3/ui/download/button/PieFetchButton.kt | 4 ++++ .../com/lagradost/cloudstream3/utils/VideoDownloadManager.kt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index a729f33a..f1031c24 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -13,6 +13,8 @@ import androidx.annotation.MainThread import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE @@ -25,6 +27,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager +import com.lagradost.cloudstream3.utils.VideoDownloadManager.KEY_RESUME_PACKAGES open class PieFetchButton(context: Context, attributeSet: AttributeSet) : @@ -167,6 +170,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : this.setPersistentId(card.id) view.setOnClickListener { if (isZeroBytes) { + removeKey(KEY_RESUME_PACKAGES, card.id.toString()) callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card)) //callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) } else { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 50a8df02..7d4d5d98 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -187,7 +187,7 @@ object VideoDownloadManager { private val DOWNLOAD_BAD_CONFIG = DownloadStatus(retrySame = false, tryNext = false, success = false) - private const val KEY_RESUME_PACKAGES = "download_resume" + const val KEY_RESUME_PACKAGES = "download_resume" const val KEY_DOWNLOAD_INFO = "download_info" private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume" From d3828eeafed0fd4fbeb32c4d37dee2126296b564 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Thu, 2 May 2024 23:59:05 +0200 Subject: [PATCH 342/441] refact: rename logcat file (#1061) Rename logcat file to prevent override --- .../cloudstream3/ui/settings/SettingsUpdates.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 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 fb24c185..4aaa5e12 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 @@ -35,6 +35,9 @@ import okhttp3.internal.closeQuietly import java.io.BufferedReader import java.io.InputStreamReader import java.io.OutputStream +import java.lang.System.currentTimeMillis +import java.text.SimpleDateFormat +import java.util.* class SettingsUpdates : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -125,12 +128,12 @@ class SettingsUpdates : PreferenceFragmentCompat() { } binding.saveBtt.setOnClickListener { + val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) var fileStream: OutputStream? = null try { - fileStream = - VideoDownloadManager.setupStream( + fileStream = VideoDownloadManager.setupStream( it.context, - "logcat", + "logcat_${date}", null, "txt", false From c28a3cb9873d64634b1e7bb131ef648ab40fd22e Mon Sep 17 00:00:00 2001 From: RowdyRushya <66415100+rushi-chavan@users.noreply.github.com> Date: Sat, 4 May 2024 04:15:34 -0700 Subject: [PATCH 343/441] Extractor: new VidSrcTo extractor (#1044) --- .../cloudstream3/extractors/VidSrcTo.kt | 65 +++++++++++++++++++ .../cloudstream3/extractors/Vidplay.kt | 4 ++ .../metaproviders/TmdbProvider.kt | 2 + .../cloudstream3/utils/ExtractorApi.kt | 2 + 4 files changed, 73 insertions(+) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt new file mode 100644 index 00000000..b9065688 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -0,0 +1,65 @@ +package com.lagradost.cloudstream3.extractors + +import android.util.Base64 +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import java.net.URLDecoder +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec + +class VidSrcTo : ExtractorApi() { + override val name = "VidSrcTo" + override val mainUrl = "https://vidsrc.to" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return + val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return + if (res.status != 200) return + res.result?.amap { source -> + val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap + val finalUrl = DecryptUrl(embedRes.result.encUrl) + if(finalUrl.equals(embedRes.result.encUrl)) return@amap + when (source.title) { + "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + } + } + } + + private fun DecryptUrl(encUrl: String): String { + var data = encUrl.toByteArray() + data = Base64.decode(data, Base64.URL_SAFE) + val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters) + data = cipher.doFinal(data) + return URLDecoder.decode(data.toString(Charsets.UTF_8), "utf-8") + } + + data class VidsrctoEpisodeSources( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: List? + ) + + data class VidsrctoResult( + @JsonProperty("id") val id: String, + @JsonProperty("title") val title: String + ) + + data class VidsrctoEmbedSource( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: VidsrctoUrl + ) + + data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt index d5d0fb32..c5e01552 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -13,6 +13,10 @@ import javax.crypto.spec.SecretKeySpec // Code found in https://github.com/KillerDogeEmpire/vidplay-keys // special credits to @KillerDogeEmpire for providing key +class AnyVidplay(hostUrl: String) : Vidplay() { + override val mainUrl = hostUrl +} + class MyCloud : Vidplay() { override val name = "MyCloud" override val mainUrl = "https://mcloud.bz" diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt index 50301e22..c5b4d453 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt @@ -105,6 +105,7 @@ open class TmdbProvider : MainAPI() { this.id, episode.episode_number, episode.season_number, + this.name ?: this.original_name, ).toJson(), episode.name, episode.season_number, @@ -122,6 +123,7 @@ open class TmdbProvider : MainAPI() { this.id, episodeNum, season.season_number, + this.name ?: this.original_name, ).toJson(), season = season.season_number ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 75dceb54..6106845e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -185,6 +185,7 @@ import com.lagradost.cloudstream3.extractors.Vanfem import com.lagradost.cloudstream3.extractors.Vicloud import com.lagradost.cloudstream3.extractors.VidSrcExtractor import com.lagradost.cloudstream3.extractors.VidSrcExtractor2 +import com.lagradost.cloudstream3.extractors.VidSrcTo import com.lagradost.cloudstream3.extractors.VideoVard import com.lagradost.cloudstream3.extractors.VideovardSX import com.lagradost.cloudstream3.extractors.Vidgomunime @@ -876,6 +877,7 @@ val extractorApis: MutableList = arrayListOf( Streamlare(), VidSrcExtractor(), VidSrcExtractor2(), + VidSrcTo(), PlayLtXyz(), AStreamHub(), Vidplay(), From 83c473d9f801cc43c0716453bea79afc539a1fea Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 4 May 2024 14:16:09 +0300 Subject: [PATCH 344/441] More external Ids in Trakt meta provider (#1075) --- .../cloudstream3/metaproviders/TraktProvider.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 98e12bcd..37c6be1b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -118,8 +118,12 @@ open class TraktProvider : MainAPI() { val linkData = LinkData( id = mediaDetails?.ids?.tmdb, + traktId = mediaDetails?.ids?.trakt, + traktSlug = mediaDetails?.ids?.slug, + tmdbId = mediaDetails?.ids?.tmdb, imdbId = mediaDetails?.ids?.imdb.toString(), tvdbId = mediaDetails?.ids?.tvdb, + tvrageId = mediaDetails?.ids?.tvrage, type = data.type.toString(), title = mediaDetails?.title, year = mediaDetails?.year, @@ -139,7 +143,6 @@ open class TraktProvider : MainAPI() { type = if (isAnime) TvType.AnimeMovie else TvType.Movie, ) { this.name = mediaDetails.title - this.apiName = "Trakt" this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie this.posterUrl = getOriginalWidthImageUrl(posterUrl) this.year = mediaDetails.year @@ -177,8 +180,12 @@ open class TraktProvider : MainAPI() { val linkData = LinkData( id = mediaDetails?.ids?.tmdb, + traktId = mediaDetails?.ids?.trakt, + traktSlug = mediaDetails?.ids?.slug, + tmdbId = mediaDetails?.ids?.tmdb, imdbId = mediaDetails?.ids?.imdb.toString(), tvdbId = mediaDetails?.ids?.tvdb, + tvrageId = mediaDetails?.ids?.tvrage, type = data.type.toString(), season = episode.season, episode = episode.number, @@ -220,7 +227,6 @@ open class TraktProvider : MainAPI() { episodes = episodes ) { this.name = mediaDetails.title - this.apiName = "Trakt" this.type = if (isAnime) TvType.Anime else TvType.TvSeries this.episodes = episodes this.posterUrl = getOriginalWidthImageUrl(posterUrl) @@ -406,8 +412,12 @@ open class TraktProvider : MainAPI() { data class LinkData( val id: Int? = null, + val traktId: Int? = null, + val traktSlug: String? = null, + val tmdbId: Int? = null, val imdbId: String? = null, val tvdbId: Int? = null, + val tvrageId: String? = null, val type: String? = null, val season: Int? = null, val episode: Int? = null, From 71bd48f4930d255beabe6f86b7e4057b732dc70e Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 4 May 2024 14:17:52 +0300 Subject: [PATCH 345/441] feat(ui): Hide Downloads & Settings Back button on TV (#1074) --- .../ui/download/DownloadChildFragment.kt | 11 ++++++++--- .../ui/quicksearch/QuickSearchFragment.kt | 12 ++++++++++-- .../ui/settings/SettingsFragment.kt | 19 ++++++++++++------- app/src/main/res/layout/quick_search.xml | 7 +++---- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index d138a1e6..f54c8698 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -11,6 +11,9 @@ import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys @@ -86,9 +89,11 @@ class DownloadChildFragment : Fragment() { binding?.downloadChildToolbar?.apply { title = name - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } setAppBarNoScrollFlagsOnTV() } 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 e9e00736..85e20d1c 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 @@ -34,6 +34,9 @@ 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.settings.Globals +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.isLayout import com.lagradost.cloudstream3.utils.AppUtils.ownShow @@ -274,8 +277,13 @@ class QuickSearchFragment : Fragment() { // UIHelper.showInputMethod(view.findFocus()) // } //} - binding?.quickSearchBack?.setOnClickListener { - activity?.popCurrentPage() + if (isLayout(PHONE or EMULATOR)) { + binding?.quickSearchBack?.apply { + isVisible = true + setOnClickListener { + activity?.popCurrentPage() + } + } } if (isLayout(TV)) { 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 443eeda7..8ac17928 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 @@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.account import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.result.txt 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.isLayout import com.lagradost.cloudstream3.utils.DataStoreHelper @@ -84,9 +85,11 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } } UIHelper.fixPaddingStatusbar(settingsToolbar) @@ -98,10 +101,12 @@ class SettingsFragment : Fragment() { settingsToolbar.apply { setTitle(title) - setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) - setNavigationOnClickListener { - activity?.onBackPressedDispatcher?.onBackPressed() + if (isLayout(PHONE or EMULATOR)) { + setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) + children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag) + setNavigationOnClickListener { + activity?.onBackPressedDispatcher?.onBackPressed() + } } } UIHelper.fixPaddingStatusbar(settingsToolbar) diff --git a/app/src/main/res/layout/quick_search.xml b/app/src/main/res/layout/quick_search.xml index 12d94aaa..84f2c548 100644 --- a/app/src/main/res/layout/quick_search.xml +++ b/app/src/main/res/layout/quick_search.xml @@ -23,11 +23,10 @@ android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_baseline_arrow_back_24" app:tint="@android:color/white" - android:focusable="true" + android:visibility="gone" android:layout_width="25dp" - android:layout_height="wrap_content"> - - + android:layout_height="wrap_content" + tools:visibility="visible"> Date: Sun, 5 May 2024 04:30:42 +0530 Subject: [PATCH 346/441] Updates and Chillx Extractor Updated (#1065) --- .../cloudstream3/extractors/Chillx.kt | 48 ++++++++++--------- .../cloudstream3/extractors/EPlay.kt | 1 - .../lagradost/cloudstream3/extractors/Vtbe.kt | 1 - 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index f03a5525..26567c7a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -2,9 +2,7 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.extractors.helper.* import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler -import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper @@ -28,30 +26,39 @@ open class Chillx : ExtractorApi() { override val name = "Chillx" override val mainUrl = "https://chillx.top" override val requiresReferer = true - private var key: String? = null + companion object { + private var key: String? = null + + suspend fun fetchKey(): String { + return if (key != null) { + key!! + } else { + val fetch = app.get("https://raw.githubusercontent.com/rushi-chavan/multi-keys/keys/keys.json").parsedSafe()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key") + key = fetch + key!! + } + } + } + + @Suppress("NAME_SHADOWING") override suspend fun getUrl( url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val master = Regex("\\s*=\\s*'([^']+)").find( + val master = Regex("""JScript[\w+]?\s*=\s*'([^']+)""").find( app.get( url, - referer = referer ?: "", - headers = mapOf( - "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", - "Accept-Language" to "en-US,en;q=0.5", - ) + referer = url, ).text )?.groupValues?.get(1) - val decrypt = cryptoAESHandler(master ?: return, getKey().toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") - + val key = fetchKey() + val decrypt = cryptoAESHandler(master ?: "", key.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val subtitles = Regex("""subtitle"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1) - val subtitlePattern = """\[(.*?)\](https?://[^\s,]+)""".toRegex() + val subtitlePattern = """\[(.*?)](https?://[^\s,]+)""".toRegex() val matches = subtitlePattern.findAll(subtitles ?: "") val languageUrlPairs = matches.map { matchResult -> val (language, url) = matchResult.destructured @@ -83,23 +90,18 @@ open class Chillx : ExtractorApi() { headers = headers ).forEach(callback) } - + private fun decodeUnicodeEscape(input: String): String { val regex = Regex("u([0-9a-fA-F]{4})") return regex.replace(input) { it.groupValues[1].toInt(16).toChar().toString() } } - - suspend fun getKey() = key ?: fetchKey().also { key = it } - private suspend fun fetchKey(): String { - return app.get("https://raw.githubusercontent.com/Sofie99/Resources/main/chillix_key.json").parsed() - } - data class Tracks( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, - @JsonProperty("kind") val kind: String? = null, + + data class Keys( + @JsonProperty("chillx") val key: List ) + } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt index 565a2680..2cb12e16 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt index 65af01ec..919a9cbd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* From 3874cb9f9d3a2b0894c59346b92fb6eec4fa2b2e Mon Sep 17 00:00:00 2001 From: b4byhuey <60543438+b4byhuey@users.noreply.github.com> Date: Thu, 9 May 2024 23:06:33 +0800 Subject: [PATCH 347/441] Update Dailymotion Extractor (#1081) --- .../cloudstream3/extractors/Dailymotion.kt | 23 +++++++++++++------ .../cloudstream3/utils/ExtractorApi.kt | 5 +++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt index 0df93dc5..2343a92e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt @@ -9,10 +9,16 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import java.net.URL +class Geodailymotion : Dailymotion() { + override val name = "GeoDailymotion" + override val mainUrl = "https://geo.dailymotion.com" +} + open class Dailymotion : ExtractorApi() { override val mainUrl = "https://www.dailymotion.com" override val name = "Dailymotion" override val requiresReferer = false + private val baseUrl = "https://www.dailymotion.com" @Suppress("RegExpSimplifiable") private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex() @@ -34,7 +40,7 @@ open class Dailymotion : ExtractorApi() { val dmV1st = config.dmInternalData.v1st val dmTs = config.dmInternalData.ts val embedder = config.context.embedder - val metaDataUrl = "$mainUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" + val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies) .parsedSafe() ?: return metaData.qualities.forEach { (_, video) -> @@ -45,16 +51,19 @@ open class Dailymotion : ExtractorApi() { } private fun getEmbedUrl(url: String): String? { - if (url.contains("/embed/")) { - return url - } - val vid = getVideoId(url) ?: return null - return "$mainUrl/embed/video/$vid" + if (url.contains("/embed/") || url.contains("/video/")) { + return url } + if (url.contains("geo.dailymotion.com")) { + val videoId = url.substringAfter("video=") + return "$baseUrl/embed/video/$videoId" + } + return null + } private fun getVideoId(url: String): String? { val path = URL(url).path - val id = path.substringAfter("video/") + val id = path.substringAfter("/video/") if (id.matches(videoIdRegex)) { return id } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 6106845e..0e4dc870 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -53,6 +53,7 @@ import com.lagradost.cloudstream3.extractors.FileMoonIn import com.lagradost.cloudstream3.extractors.FileMoonSx import com.lagradost.cloudstream3.extractors.Filesim import com.lagradost.cloudstream3.extractors.Fplayer +import com.lagradost.cloudstream3.extractors.Geodailymotion import com.lagradost.cloudstream3.extractors.GMPlayer import com.lagradost.cloudstream3.extractors.Gdriveplayer import com.lagradost.cloudstream3.extractors.Gdriveplayerapi @@ -900,7 +901,9 @@ val extractorApis: MutableList = arrayListOf( Simpulumlamerop(), Urochsunloath(), Yipsu(), - MetaGnathTuggers() + MetaGnathTuggers(), + Geodailymotion(), + ) From f1cc4db89cc6c1a2cd6316e81340206b56a72ad6 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 9 May 2024 18:08:18 +0300 Subject: [PATCH 348/441] Show Season number for next airing episode (#1071) --- .../com/lagradost/cloudstream3/MainAPI.kt | 17 +++++++-- .../ui/result/ResultViewModel2.kt | 6 +++- .../main/res/layout/fragment_result_tv.xml | 36 +++++++++---------- app/src/main/res/values/strings.xml | 1 + 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 7b1b5775..699159b5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -1448,11 +1448,24 @@ fun TvType?.isEpisodeBased(): Boolean { return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama) } - data class NextAiring( val episode: Int, val unixTime: Long, -) + val season: Int? = null, +) { + /** + * Secondary constructor for backwards compatibility without season. + * TODO Remove this constructor after there is a new stable release and extensions are updated to support season. + */ + constructor( + episode: Int, + unixTime: Long, + ) : this ( + episode, + unixTime, + null + ) +} /** * @param season To be mapped with episode season, not shown in UI if displaySeason is defined 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 de339aee..61b65bc2 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 @@ -197,7 +197,11 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { else -> null }?.also { - nextAiringEpisode = txt(R.string.next_episode_format, airing.episode) + nextAiringEpisode = when (airing.season) { + + null -> txt(R.string.next_episode_format, airing.episode) + else -> txt(R.string.next_season_episode_format, airing.season, airing.episode) + } } } } diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 2ec2ae0a..893c19ff 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -178,42 +178,40 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:textStyle="bold" tools:text="The Perfect Run The Perfect Run" /> + + - - + android:orientation="horizontal"> + android:layout_marginEnd="5dp" + tools:text="Season 2 Episode 1022 will be released in" /> %1$s Ep %2$d Cast: %s Episode %d will be released in + Season %1$d Episode %2$d will be released in %1$dd %2$dh %3$dm %1$dh %2$dm %dm From ee4d1dedc5adb1be656a05d1ee0f41b11f9d0a84 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 9 May 2024 19:46:54 +0000 Subject: [PATCH 349/441] Add basic fcast support (#1084) --- .../lagradost/cloudstream3/MainActivity.kt | 3 + .../cloudstream3/ui/player/IGenerator.kt | 10 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 2 + .../ui/result/ResultViewModel2.kt | 44 ++++++ .../cloudstream3/utils/ExtractorApi.kt | 13 +- .../cloudstream3/utils/fcast/FcastManager.kt | 135 ++++++++++++++++++ .../cloudstream3/utils/fcast/FcastSession.kt | 60 ++++++++ .../cloudstream3/utils/fcast/Packets.kt | 62 ++++++++ app/src/main/res/values/strings.xml | 3 + 9 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 7baac71c..56322b73 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -161,6 +161,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.USER_PROVIDER_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.utils.fcast.FcastManager import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.ResponseParser import com.lagradost.safefile.SafeFile @@ -1756,6 +1757,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, runAutoUpdate() } + FcastManager().init(this, false) + APIRepository.dubStatusActive = getApiDubstatusSettings() try { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index af74cb57..c5de1a1c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -10,7 +10,8 @@ enum class LoadType { InAppDownload, ExternalApp, Browser, - Chromecast + Chromecast, + Fcast } fun LoadType.toSet() : Set { @@ -29,12 +30,17 @@ fun LoadType.toSet() : Set { ExtractorLinkType.VIDEO, ExtractorLinkType.M3U8 ) - LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.values().toSet() + LoadType.ExternalApp, LoadType.Unknown -> ExtractorLinkType.entries.toSet() LoadType.Chromecast -> setOf( ExtractorLinkType.VIDEO, ExtractorLinkType.DASH, ExtractorLinkType.M3U8 ) + LoadType.Fcast -> setOf( + ExtractorLinkType.VIDEO, + ExtractorLinkType.DASH, + ExtractorLinkType.M3U8 + ) } } 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 2019aa50..e4fd0559 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 @@ -55,6 +55,8 @@ const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16 const val ACTION_PLAY_EPISODE_IN_MPV = 17 const val ACTION_MARK_AS_WATCHED = 18 +const val ACTION_FCAST = 19 + const val TV_EP_SIZE_LARGE = 400 const val TV_EP_SIZE_SMALL = 300 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) 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 61b65bc2..a32942f6 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 @@ -83,6 +83,10 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.cloudstream3.utils.fcast.FcastManager +import com.lagradost.cloudstream3.utils.fcast.FcastSession +import com.lagradost.cloudstream3.utils.fcast.Opcode +import com.lagradost.cloudstream3.utils.fcast.PlayMessage import kotlinx.coroutines.* import java.io.File import java.util.concurrent.TimeUnit @@ -1519,6 +1523,13 @@ class ResultViewModel2 : ViewModel() { ) ) } + + if (FcastManager.currentDevices.isNotEmpty()) { + options.add( + txt(R.string.player_settings_play_in_fcast) to ACTION_FCAST + ) + } + options.add(txt(R.string.episode_action_play_in_app) to ACTION_PLAY_EPISODE_IN_PLAYER) for (app in apps) { @@ -1694,6 +1705,39 @@ class ResultViewModel2 : ViewModel() { } } + ACTION_FCAST -> { + val devices = FcastManager.currentDevices.toList() + postPopup( + txt(R.string.player_settings_select_cast_device), + devices.map { txt(it.name) }) { index -> + if (index == null) return@postPopup + val device = devices.getOrNull(index) + + acquireSingleLink( + click.data, + LoadType.Fcast, + txt(R.string.episode_action_cast_mirror) + ) { (result, index) -> + val host = device?.host ?: return@acquireSingleLink + val link = result.links.firstOrNull() ?: return@acquireSingleLink + + FcastSession(host).use { session -> + session.sendMessage( + Opcode.Play, + PlayMessage( + link.type.getMimeType(), + link.url, + headers = mapOf( + "referer" to link.referer, + "user-agent" to USER_AGENT + ) + link.headers + ) + ) + } + } + } + } + ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink( click.data, LoadType.Browser, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 0e4dc870..61cdd26a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -308,7 +308,18 @@ enum class ExtractorLinkType { /** No support at the moment */ TORRENT, /** No support at the moment */ - MAGNET, + MAGNET; + + // See https://www.iana.org/assignments/media-types/media-types.xhtml + fun getMimeType(): String { + return when (this) { + VIDEO -> "video/mp4" + M3U8 -> "application/x-mpegURL" + DASH -> "application/dash+xml" + TORRENT -> "application/x-bittorrent" + MAGNET -> "application/x-bittorrent" + } + } } private fun inferTypeFromUrl(url: String): ExtractorLinkType { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt new file mode 100644 index 00000000..9ff5cc08 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastManager.kt @@ -0,0 +1,135 @@ +package com.lagradost.cloudstream3.utils.fcast + +import android.content.Context +import android.net.nsd.NsdManager +import android.net.nsd.NsdManager.ResolveListener +import android.net.nsd.NsdServiceInfo +import android.os.Build +import android.util.Log +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe + +class FcastManager { + private var nsdManager: NsdManager? = null + + // Used for receiver + private val registrationListenerTcp = DefaultRegistrationListener() + private fun getDeviceName(): String { + return "${Build.MANUFACTURER}-${Build.MODEL}" + } + + /** + * Start the fcast service + * @param registerReceiver If true will register the app as a compatible fcast receiver for discovery in other app + */ + fun init(context: Context, registerReceiver: Boolean) = ioSafe { + nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager + val serviceType = "_fcast._tcp" + + if (registerReceiver) { + val serviceName = "$APP_PREFIX-${getDeviceName()}" + + val serviceInfo = NsdServiceInfo().apply { + this.serviceName = serviceName + this.serviceType = serviceType + this.port = TCP_PORT + } + + nsdManager?.registerService( + serviceInfo, + NsdManager.PROTOCOL_DNS_SD, + registrationListenerTcp + ) + } + + nsdManager?.discoverServices( + serviceType, + NsdManager.PROTOCOL_DNS_SD, + DefaultDiscoveryListener() + ) + } + + fun stop() { + nsdManager?.unregisterService(registrationListenerTcp) + } + + inner class DefaultDiscoveryListener : NsdManager.DiscoveryListener { + val tag = "DiscoveryListener" + override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) { + Log.d(tag, "Discovery failed: $serviceType, error code: $errorCode") + } + + override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) { + Log.d(tag, "Stop discovery failed: $serviceType, error code: $errorCode") + } + + override fun onDiscoveryStarted(serviceType: String?) { + Log.d(tag, "Discovery started: $serviceType") + } + + override fun onDiscoveryStopped(serviceType: String?) { + Log.d(tag, "Discovery stopped: $serviceType") + } + + override fun onServiceFound(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + nsdManager?.resolveService(serviceInfo, object : ResolveListener { + override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) { + } + + override fun onServiceResolved(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + + currentDevices.add(PublicDeviceInfo(serviceInfo)) + + Log.d( + tag, + "Service found: ${serviceInfo.serviceName}, Net: ${serviceInfo.host.hostAddress}" + ) + } + }) + } + + override fun onServiceLost(serviceInfo: NsdServiceInfo?) { + if (serviceInfo == null) return + + // May remove duplicates, but net and port is null here, preventing device specific identification + currentDevices.removeAll { + it.rawName == serviceInfo.serviceName + } + + Log.d(tag, "Service lost: ${serviceInfo.serviceName}") + } + } + + companion object { + const val APP_PREFIX = "CloudStream" + val currentDevices: MutableList = mutableListOf() + + class DefaultRegistrationListener : NsdManager.RegistrationListener { + val tag = "DiscoveryService" + override fun onServiceRegistered(serviceInfo: NsdServiceInfo) { + Log.d(tag, "Service registered: ${serviceInfo.serviceName}") + } + + override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.e(tag, "Service registration failed: errorCode=$errorCode") + } + + override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) { + Log.d(tag, "Service unregistered: ${serviceInfo.serviceName}") + } + + override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + Log.e(tag, "Service unregistration failed: errorCode=$errorCode") + } + } + + const val TCP_PORT = 46899 + } +} + +class PublicDeviceInfo(serviceInfo: NsdServiceInfo) { + val rawName: String = serviceInfo.serviceName + val host: String? = serviceInfo.host.hostAddress + val name = rawName.replace("-", " ") + host?.let { " $it" } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt new file mode 100644 index 00000000..1f33bca4 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/FcastSession.kt @@ -0,0 +1,60 @@ +package com.lagradost.cloudstream3.utils.fcast + +import android.util.Log +import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.safefile.closeQuietly +import java.io.DataOutputStream +import java.net.Socket +import kotlin.jvm.Throws + +class FcastSession(private val hostAddress: String): AutoCloseable { + val tag = "FcastSession" + + private var socket: Socket? = null + @Throws + @WorkerThread + fun open(): Socket { + val socket = Socket(hostAddress, FcastManager.TCP_PORT) + this.socket = socket + return socket + } + + override fun close() { + socket?.closeQuietly() + socket = null + } + + @Throws + private fun acquireSocket(): Socket { + return socket ?: open() + } + + fun ping() { + sendMessage(Opcode.Ping, null) + } + + fun sendMessage(opcode: Opcode, message: T) { + ioSafe { + val socket = acquireSocket() + val outputStream = DataOutputStream(socket.getOutputStream()) + + val json = message?.toJson() + val content = json?.toByteArray() ?: ByteArray(0) + + // Little endian starting from 1 + // https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 + val size = content.size + 1 + + val sizeArray = ByteArray(4) { num -> + (size shr 8 * num and 0xff).toByte() + } + + Log.d(tag, "Sending message with size: $size, opcode: $opcode") + outputStream.write(sizeArray) + outputStream.write(ByteArray(1) { opcode.value }) + outputStream.write(content) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt new file mode 100644 index 00000000..61c00d6e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/fcast/Packets.kt @@ -0,0 +1,62 @@ +package com.lagradost.cloudstream3.utils.fcast + +// See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 +enum class Opcode(val value: Byte) { + None(0), + Play(1), + Pause(2), + Resume(3), + Stop(4), + Seek(5), + PlaybackUpdate(6), + VolumeUpdate(7), + SetVolume(8), + PlaybackError(9), + SetSpeed(10), + Version(11), + Ping(12), + Pong(13); +} + + +data class PlayMessage( + val container: String, + val url: String? = null, + val content: String? = null, + val time: Double? = null, + val speed: Double? = null, + val headers: Map? = null +) + +data class SeekMessage( + val time: Double +) + +data class PlaybackUpdateMessage( + val generationTime: Long, + val time: Double, + val duration: Double, + val state: Int, + val speed: Double +) + +data class VolumeUpdateMessage( + val generationTime: Long, + val volume: Double +) + +data class PlaybackErrorMessage( + val message: String +) + +data class SetSpeedMessage( + val speed: Double +) + +data class SetVolumeMessage( + val volume: Double +) + +data class VersionMessage( + val version: Long +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9bd2426c..a8108623 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -357,6 +357,7 @@ Download error, check storage permissions Chromecast episode Chromecast mirror + Cast mirror Play in app Play in %s Play in browser @@ -634,7 +635,9 @@ VLC MPV Web Video Cast + Fcast Web browser + Select cast device App not found All Languages Skip %s From af828de8d5264e7d2c3a6d6954b0a2a228ca2264 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 18 May 2024 14:41:37 +0300 Subject: [PATCH 350/441] feat(TV UI: Fix online subtitles dialog focus (#1085) --- app/src/main/res/layout/dialog_online_subtitles.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/dialog_online_subtitles.xml b/app/src/main/res/layout/dialog_online_subtitles.xml index 7803e261..d480bd34 100644 --- a/app/src/main/res/layout/dialog_online_subtitles.xml +++ b/app/src/main/res/layout/dialog_online_subtitles.xml @@ -40,7 +40,7 @@ android:layout_width="match_parent" android:layout_height="30dp" android:layout_gravity="center_vertical" - android:layout_marginEnd="30dp"> + android:layout_marginEnd="40dp"> @@ -106,7 +107,7 @@ android:layout_margin="10dp" android:background="?selectableItemBackgroundBorderless" android:contentDescription="@string/change_providers_img_des" - android:nextFocusLeft="@id/main_search" + android:nextFocusLeft="@id/year_btt" android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" android:nextFocusDown="@id/search_autofit_results" From 4d5cd288abd07c74fc88900c58e119c27f1b7867 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Sat, 18 May 2024 11:47:12 +0000 Subject: [PATCH 351/441] Ported more files for multiplatform (#1056) --- .../lagradost/cloudstream3/CommonActivity.kt | 1 + .../com/lagradost/cloudstream3/MainAPI.kt | 114 ++++++++-- .../lagradost/cloudstream3/MainActivity.kt | 197 ++++++++---------- .../ui/result/ResultViewModel2.kt | 10 +- .../cloudstream3/utils/Coroutines.android.kt | 11 + .../lagradost/cloudstream3/MainActivity.kt | 35 ++++ .../com/lagradost/cloudstream3/MainApi.kt | 3 + .../lagradost/cloudstream3/ParCollections.kt | 0 .../cloudstream3/utils/Coroutines.kt | 8 +- .../lagradost/cloudstream3/utils/JsHunter.kt | 0 .../cloudstream3/utils/JsUnpacker.kt | 0 .../cloudstream3/utils/Coroutines.jvm.kt | 5 + 12 files changed, 243 insertions(+), 141 deletions(-) create mode 100644 library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/ParCollections.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/Coroutines.kt (90%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/JsHunter.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/JsUnpacker.kt (100%) create mode 100644 library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 4dc78dc7..82e985db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -29,6 +29,7 @@ import com.google.android.material.chip.ChipGroup import com.google.android.material.navigationrail.NavigationRailView import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps import com.lagradost.cloudstream3.databinding.ToastBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.PlayerEventType diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 699159b5..07a82583 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -31,19 +31,16 @@ import java.text.SimpleDateFormat import java.util.* import kotlin.math.absoluteValue -const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" - -//val baseHeader = mapOf("User-Agent" to USER_AGENT) -val mapper = JsonMapper.builder().addModule(kotlinModule()) - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! - /** * Defines the constant for the all languages preference, if this is set then it is * the equivalent of all languages being set **/ const val AllLanguagesName = "universal" +//val baseHeader = mapOf("User-Agent" to USER_AGENT) +val mapper = JsonMapper.builder().addModule(kotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! + object APIHolder { val unixTime: Long get() = System.currentTimeMillis() / 1000L @@ -121,7 +118,8 @@ object APIHolder { fun LoadResponse.getId(): Int { // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked - return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) ?: getLoadResponseIdFromUrl(url, apiName) + return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) + ?: getLoadResponseIdFromUrl(url, apiName) } /** @@ -222,10 +220,15 @@ object APIHolder { } ?: false val matchingTypes = types?.any { it.name.equals(media.format, true) } == true - if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears + if (lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears } ?: return null - Tracker(res.idMal, res.id.toString(), res.coverImage?.extraLarge ?: res.coverImage?.large, res.bannerImage) + Tracker( + res.idMal, + res.id.toString(), + res.coverImage?.extraLarge ?: res.coverImage?.large, + res.bannerImage + ) } catch (t: Throwable) { logError(t) null @@ -866,6 +869,7 @@ enum class TvType(value: Int?) { Others(12), Music(13), AudioBook(14), + /** Wont load the built in player, make your own interaction */ CustomMedia(15), } @@ -1253,13 +1257,15 @@ interface LoadResponse { fun LoadResponse.getImdbId(): String? { return normalSafeApiCall { - SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Imdb) + SimklApi.readIdFromString(this.syncData[simklIdPrefix]) + ?.get(SimklApi.Companion.SyncServices.Imdb) } } fun LoadResponse.getTMDbId(): String? { return normalSafeApiCall { - SimklApi.readIdFromString(this.syncData[simklIdPrefix])?.get(SimklApi.Companion.SyncServices.Tmdb) + SimklApi.readIdFromString(this.syncData[simklIdPrefix]) + ?.get(SimklApi.Companion.SyncServices.Tmdb) } } @@ -1556,8 +1562,26 @@ data class TorrentLoadResponse( posterHeaders: Map? = null, backgroundPosterUrl: String? = null, ) : this( - name, url, apiName, magnet, torrent, plot, type, posterUrl, year, rating, tags, duration, trailers, - recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null + name, + url, + apiName, + magnet, + torrent, + plot, + type, + posterUrl, + year, + rating, + tags, + duration, + trailers, + recommendations, + actors, + comingSoon, + syncData, + posterHeaders, + backgroundPosterUrl, + null ) } @@ -1609,7 +1633,8 @@ data class AnimeLoadResponse( return this.episodes.maxOf { (_, episodes) -> episodes.count { episodeData -> // Prioritize display season as actual season may be something random to fit multiple seasons into one. - val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE + val episodeSeason = + displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE // Count all episodes from season 1 to below the current season. episodeSeason in 1..? = null, backgroundPosterUrl: String? = null, ) : this( - engName, japName, name, url, apiName, type, posterUrl, year, episodes, showStatus, plot, tags, - synonyms, rating, duration, trailers, recommendations, actors, comingSoon, syncData, posterHeaders, - nextAiring, seasonNames, backgroundPosterUrl, null + engName, + japName, + name, + url, + apiName, + type, + posterUrl, + year, + episodes, + showStatus, + plot, + tags, + synonyms, + rating, + duration, + trailers, + recommendations, + actors, + comingSoon, + syncData, + posterHeaders, + nextAiring, + seasonNames, + backgroundPosterUrl, + null ) } @@ -1780,7 +1827,7 @@ data class MovieLoadResponse( backgroundPosterUrl: String? = null, ) : this( name, url, apiName, type, dataUrl, posterUrl, year, plot, rating, tags, duration, trailers, - recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl,null + recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null ) } @@ -1923,7 +1970,8 @@ data class TvSeriesLoadResponse( return episodes.count { episodeData -> // Prioritize display season as actual season may be something random to fit multiple seasons into one. - val episodeSeason = displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE + val episodeSeason = + displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE // Count all episodes from season 1 to below the current season. episodeSeason in 1..? = null, backgroundPosterUrl: String? = null, ) : this( - name, url, apiName, type, episodes, posterUrl, year, plot, showStatus, rating, tags, duration, - trailers, recommendations, actors, comingSoon, syncData, posterHeaders, nextAiring, seasonNames, - backgroundPosterUrl, null + name, + url, + apiName, + type, + episodes, + posterUrl, + year, + plot, + showStatus, + rating, + tags, + duration, + trailers, + recommendations, + actors, + comingSoon, + syncData, + posterHeaders, + nextAiring, + seasonNames, + backgroundPosterUrl, + null ) } @@ -2022,6 +2089,7 @@ data class AniSearch( @JsonProperty("extraLarge") var extraLarge: String? = null, @JsonProperty("large") var large: String? = null, ) + data class Title( @JsonProperty("romaji") var romaji: String? = null, @JsonProperty("english") var english: String? = null, diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 56322b73..1ff0575b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -174,7 +174,6 @@ import java.net.URLDecoder import java.nio.charset.Charset import kotlin.math.abs import kotlin.math.absoluteValue -import kotlin.reflect.KClass import kotlin.system.exitProcess //https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898 @@ -187,117 +186,93 @@ import kotlin.system.exitProcess //https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225 -const val VLC_PACKAGE = "org.videolan.vlc" -const val MPV_PACKAGE = "is.xyz.mpv" -const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo" - -val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity") -val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") - -//TODO REFACTOR AF -open class ResultResume( - val packageString: String, - val action: String = Intent.ACTION_VIEW, - val position: String? = null, - val duration: String? = null, - var launcher: ActivityResultLauncher? = null, -) { - val defaultTime = -1L - - val lastId get() = "${packageString}_last_open_id" - suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { - val intent = Intent(action) - - if (id != null) - setKey(lastId, id) - else - removeKey(lastId) - - intent.setPackage(packageString) - callback.invoke(intent) - launcher?.launch(intent) - } - - open fun getPosition(intent: Intent?): Long { - return defaultTime - } - - open fun getDuration(intent: Intent?): Long { - return defaultTime - } -} - -val VLC = object : ResultResume( - VLC_PACKAGE, - // Android 13 intent restrictions fucks up specifically launching the VLC player - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - "org.videolan.vlc.player.result" - } else { - Intent.ACTION_VIEW - }, - "extra_position", - "extra_duration", -) { - override fun getPosition(intent: Intent?): Long { - return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime - } - - override fun getDuration(intent: Intent?): Long { - return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime - } -} - -val MPV = object : ResultResume( - MPV_PACKAGE, - //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: - position = "position", - duration = "duration", -) { - override fun getPosition(intent: Intent?): Long { - return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime - } - - override fun getDuration(intent: Intent?): Long { - return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime - } -} - -val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) - -val resumeApps = arrayOf( - VLC, MPV, WEB_VIDEO -) - -// Short name for requests client to make it nicer to use - -var app = Requests(responseParser = object : ResponseParser { - val mapper: ObjectMapper = jacksonObjectMapper().configure( - DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, - false - ) - - override fun parse(text: String, kClass: KClass): T { - return mapper.readValue(text, kClass.java) - } - - override fun parseSafe(text: String, kClass: KClass): T? { - return try { - mapper.readValue(text, kClass.java) - } catch (e: Exception) { - null - } - } - - override fun writeValueAsString(obj: Any): String { - return mapper.writeValueAsString(obj) - } -}).apply { - defaultHeaders = mapOf("user-agent" to USER_AGENT) -} - class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAuthenticator.BiometricAuthCallback { companion object { + const val VLC_PACKAGE = "org.videolan.vlc" + const val MPV_PACKAGE = "is.xyz.mpv" + const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo" + + val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity") + val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") + + //TODO REFACTOR AF + open class ResultResume( + val packageString: String, + val action: String = Intent.ACTION_VIEW, + val position: String? = null, + val duration: String? = null, + var launcher: ActivityResultLauncher? = null, + ) { + val defaultTime = -1L + + val lastId get() = "${packageString}_last_open_id" + suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { + val intent = Intent(action) + + if (id != null) + setKey(lastId, id) + else + removeKey(lastId) + + intent.setPackage(packageString) + callback.invoke(intent) + launcher?.launch(intent) + } + + open fun getPosition(intent: Intent?): Long { + return defaultTime + } + + open fun getDuration(intent: Intent?): Long { + return defaultTime + } + } + + val VLC = object : ResultResume( + VLC_PACKAGE, + // Android 13 intent restrictions fucks up specifically launching the VLC player + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + "org.videolan.vlc.player.result" + } else { + Intent.ACTION_VIEW + }, + "extra_position", + "extra_duration", + ) { + override fun getPosition(intent: Intent?): Long { + return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime + } + + override fun getDuration(intent: Intent?): Long { + return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime + } + } + + val MPV = object : ResultResume( + MPV_PACKAGE, + //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: + position = "position", + duration = "duration", + ) { + override fun getPosition(intent: Intent?): Long { + return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() + ?: defaultTime + } + + override fun getDuration(intent: Intent?): Long { + return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() + ?: defaultTime + } + } + + val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) + + val resumeApps = arrayOf( + VLC, MPV, WEB_VIDEO + ) + + const val TAG = "MAINACT" const val ANIMATED_OUTLINE: Boolean = false var lastError: String? = null @@ -1403,7 +1378,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, } } - observe(viewModel.watchStatus,::setWatchStatus) + observe(viewModel.watchStatus, ::setWatchStatus) observe(syncViewModel.userData, ::setUserData) observeNullable(viewModel.subscribeStatus, ::setSubscribeStatus) @@ -1831,7 +1806,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, } override fun onAuthenticationError() { - finish() + finish() } private var backPressedCallback: OnBackPressedCallback? = null 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 a32942f6..0af01ca8 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 @@ -29,6 +29,14 @@ import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie +import com.lagradost.cloudstream3.MainActivity.Companion.MPV +import com.lagradost.cloudstream3.MainActivity.Companion.MPV_COMPONENT +import com.lagradost.cloudstream3.MainActivity.Companion.MPV_PACKAGE +import com.lagradost.cloudstream3.MainActivity.Companion.VLC +import com.lagradost.cloudstream3.MainActivity.Companion.VLC_COMPONENT +import com.lagradost.cloudstream3.MainActivity.Companion.VLC_PACKAGE +import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO +import com.lagradost.cloudstream3.MainActivity.Companion.WEB_VIDEO_CAST_PACKAGE import com.lagradost.cloudstream3.metaproviders.SyncRedirector import com.lagradost.cloudstream3.mvvm.* import com.lagradost.cloudstream3.syncproviders.AccountManager @@ -1354,7 +1362,7 @@ class ResultViewModel2 : ViewModel() { private fun launchActivity( activity: Activity?, - resumeApp: ResultResume, + resumeApp: MainActivity.Companion.ResultResume, id: Int? = null, work: suspend (Intent.(Activity) -> Unit) ): Job? { diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt new file mode 100644 index 00000000..48a709eb --- /dev/null +++ b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.android.kt @@ -0,0 +1,11 @@ +package com.lagradost.cloudstream3.utils + +import android.os.Handler +import android.os.Looper + +actual fun runOnMainThreadNative(work: () -> Unit) { + val mainHandler = Handler(Looper.getMainLooper()) + mainHandler.post { + work() + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt new file mode 100644 index 00000000..6502cc83 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainActivity.kt @@ -0,0 +1,35 @@ +package com.lagradost.cloudstream3 + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.lagradost.nicehttp.Requests +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 { + val mapper: ObjectMapper = jacksonObjectMapper().configure( + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, + false + ) + + override fun parse(text: String, kClass: KClass): T { + return mapper.readValue(text, kClass.java) + } + + override fun parseSafe(text: String, kClass: KClass): T? { + return try { + mapper.readValue(text, kClass.java) + } catch (e: Exception) { + null + } + } + + override fun writeValueAsString(obj: Any): String { + return mapper.writeValueAsString(obj) + } +}).apply { + defaultHeaders = mapOf("user-agent" to USER_AGENT) +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt index 87ee4815..160ff098 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt @@ -1,3 +1,6 @@ package com.lagradost.cloudstream3 +const val USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" + class ErrorLoadingException(message: String? = null) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt similarity index 90% rename from app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt index c3b244c2..f87ddc6a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -1,12 +1,11 @@ package com.lagradost.cloudstream3.utils -import android.os.Handler -import android.os.Looper import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* import java.util.Collections.synchronizedList +expect fun runOnMainThreadNative(work: (() -> Unit)) object Coroutines { fun T.main(work: suspend ((T) -> Unit)): Job { val value = this @@ -50,10 +49,7 @@ object Coroutines { } fun runOnMainThread(work: (() -> Unit)) { - val mainHandler = Handler(Looper.getMainLooper()) - mainHandler.post { - work() - } + runOnMainThreadNative(work) } /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsHunter.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/utils/JsHunter.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsHunter.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/utils/JsUnpacker.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsUnpacker.kt diff --git a/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt new file mode 100644 index 00000000..0a9667cb --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/utils/Coroutines.jvm.kt @@ -0,0 +1,5 @@ +package com.lagradost.cloudstream3.utils + +actual fun runOnMainThreadNative(work: () -> Unit) { + work.invoke() +} \ No newline at end of file From 469a71236b6e78018f3f72c83f0b051bdab189c3 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 18 May 2024 19:15:23 +0300 Subject: [PATCH 352/441] SubDL subtitles provider (#1082) --- .../subtitles/AbstractSubtitleEntities.kt | 6 +- .../syncproviders/AccountManager.kt | 5 +- .../providers/IndexSubtitleApi.kt | 2 +- .../providers/OpenSubtitlesApi.kt | 2 +- .../syncproviders/providers/Subdl.kt | 102 ++++++++++++++++++ .../ui/player/FullScreenPlayer.kt | 3 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 18 +++- .../ui/player/PlayerGeneratorViewModel.kt | 4 + .../ui/result/ResultTrailerPlayer.kt | 3 +- .../ui/result/ResultViewModel2.kt | 3 +- 10 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt index f6424c4c..ed4ccb74 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.subtitles +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.TvType class AbstractSubtitleEntities { @@ -19,8 +20,11 @@ class AbstractSubtitleEntities { data class SubtitleSearch( var query: String = "", - var imdb: Long? = null, var lang: String? = null, + var imdbId: String? = null, + var tmdbId: Int? = null, + var malId: Int? = null, + var aniListId: Int? = null, var epNumber: Int? = null, var seasonNumber: Int? = null, var year: Int? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index bae8a5df..55418890 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey -import com.lagradost.cloudstream3.syncproviders.providers.SubScene import com.lagradost.cloudstream3.syncproviders.providers.* import java.util.concurrent.TimeUnit @@ -16,6 +15,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() val subScene = SubScene() + val subDl = SubDL() val localListApi = LocalList() // used to login via app intent @@ -44,7 +44,8 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { openSubtitlesApi, indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, - subScene + subScene, + subDl ) const val appString = "cloudstreamapp" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt index 1adecce9..5ca3f3d5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt @@ -98,7 +98,7 @@ class IndexSubtitleApi : AbstractSubApi { } override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List { - val imdbId = query.imdb ?: 0 + val imdbId = query.imdbId?.replace("tt", "")?.toLong() ?: 0 val lang = query.lang val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString()) val queryText = query.query diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index 4030649d..7d0514d1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -185,7 +185,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi throwIfCantDoRequest() val fixedLang = fixLanguage(query.lang) - val imdbId = query.imdb ?: 0 + val imdbId = query.imdbId?.replace("tt", "")?.toInt() ?: 0 val queryText = query.query val epNum = query.epNumber ?: 0 val seasonNum = query.seasonNumber ?: 0 diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt new file mode 100644 index 00000000..d25d3f22 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt @@ -0,0 +1,102 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.subtitles.AbstractSubProvider +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities +import com.lagradost.cloudstream3.subtitles.SubtitleResource + +class SubDL : AbstractSubProvider { + //API Documentation: https://subdl.com/api-doc + val mainUrl = "https://subdl.com/" + val name = "SubDL" + override val idPrefix = "subdl" + companion object { + const val APIKEY = "zRJl5QA-8jNA2i0pE8cxANbEukANp7IM" + const val APIENDPOINT = "https://api.subdl.com/api/v1/subtitles" + const val DOWNLOADENDPOINT = "https://dl.subdl.com" + } + + override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { + + val queryText = query.query + val epNum = query.epNumber ?: 0 + val seasonNum = query.seasonNumber ?: 0 + val yearNum = query.year ?: 0 + + val idQuery = when { + query.imdbId != null -> "&imdb_id=${query.imdbId}" + query.tmdbId != null -> "&tmdb_id=${query.tmdbId}" + else -> null + } + + val epQuery = if (epNum > 0) "&episode_number=$epNum" else "" + val seasonQuery = if (seasonNum > 0) "&season_number=$seasonNum" else "" + val yearQuery = if (yearNum > 0) "&year=$yearNum" else "" + + val searchQueryUrl = when (idQuery) { + //Use imdb/tmdb id to search if its valid + null -> "$APIENDPOINT?api_key=$APIKEY&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + else -> "$APIENDPOINT?api_key=$APIKEY$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + } + + val req = app.get( + url = searchQueryUrl, + headers = mapOf( + "Accept" to "application/json" + ) + ) + + return req.parsedSafe()?.subtitles?.map { subtitle -> + val name = subtitle.releaseName + val lang = subtitle.lang.replaceFirstChar { it.uppercase() } + val resEpNum = subtitle.episode ?: query.epNumber + val resSeasonNum = subtitle.season ?: query.seasonNumber + val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie + + AbstractSubtitleEntities.SubtitleEntity( + idPrefix = this.idPrefix, + name = name, + lang = lang, + data = "${DOWNLOADENDPOINT}${subtitle.url}", + type = type, + source = this.name, + epNumber = resEpNum, + seasonNumber = resSeasonNum, + ) + } + } + + override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { + this.addZipUrl(data.data) { name, _ -> + name + } + } + + data class ApiResponse( + @JsonProperty("status") val status: Boolean? = null, + @JsonProperty("results") val results: List? = null, + @JsonProperty("subtitles") val subtitles: List? = null, + ) + + data class Result( + @JsonProperty("sd_id") val sdId: Int? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("name") val name: String? = null, + @JsonProperty("imdb_id") val imdbId: String? = null, + @JsonProperty("tmdb_id") val tmdbId: Long? = null, + @JsonProperty("first_air_date") val firstAirDate: String? = null, + @JsonProperty("year") val year: Int? = null, + ) + + data class Subtitle( + @JsonProperty("release_name") val releaseName: String, + @JsonProperty("name") val name: String, + @JsonProperty("lang") val lang: String, + @JsonProperty("author") val author: String? = null, + @JsonProperty("url") val url: String? = null, + @JsonProperty("season") val season: Int? = null, + @JsonProperty("episode") val episode: Int? = null, + ) +} \ No newline at end of file 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 c357ce9c..aa25157b 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 @@ -32,6 +32,7 @@ import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.screenWidth +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding @@ -177,7 +178,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { open fun openOnlineSubPicker( context: Context, - imdbId: Long?, + loadResponse: LoadResponse?, dismissCallback: (() -> Unit) ) { throw NotImplementedError() 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 7ff56886..c77f9404 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 @@ -25,6 +25,10 @@ import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId +import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId +import com.lagradost.cloudstream3.LoadResponse.Companion.getTMDbId import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding @@ -39,7 +43,6 @@ import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSub import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog import com.lagradost.cloudstream3.ui.result.* -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout @@ -258,6 +261,7 @@ class GeneratorPlayer : FullScreenPlayer() { var episode: Int? = null, var season: Int? = null, var name: String? = null, + var imdbId: String? = null, ) private fun getMetaData(): TempMetaData { @@ -284,7 +288,7 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun openOnlineSubPicker( - context: Context, imdbId: Long?, dismissCallback: (() -> Unit) + context: Context, loadResponse: LoadResponse?, dismissCallback: (() -> Unit) ) { val providers = subsProviders val isSingleProvider = subsProviders.size == 1 @@ -377,6 +381,7 @@ class GeneratorPlayer : FullScreenPlayer() { } val currentTempMeta = getMetaData() + // bruh idk why it is not correct val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent)) binding.searchLoadingBar.progressTintList = color @@ -424,7 +429,10 @@ class GeneratorPlayer : FullScreenPlayer() { val search = AbstractSubtitleEntities.SubtitleSearch( query = query ?: return@ioSafe, - imdb = imdbId, + imdbId = loadResponse?.getImdbId(), + tmdbId = loadResponse?.getTMDbId()?.toInt(), + malId = loadResponse?.getMalId()?.toInt(), + aniListId = loadResponse?.getAniListId()?.toInt(), epNumber = currentTempMeta.episode, seasonNumber = currentTempMeta.season, lang = currentLanguageTwoLetters.ifBlank { null }, @@ -633,6 +641,8 @@ class GeneratorPlayer : FullScreenPlayer() { } if (subsProvidersIsActive) { + val currentLoadResponse = viewModel.getLoadResponse() + val loadFromOpenSubsFooter: TextView = layoutInflater.inflate( R.layout.sort_bottom_footer_add_choice, null ) as TextView @@ -643,7 +653,7 @@ class GeneratorPlayer : FullScreenPlayer() { loadFromOpenSubsFooter.setOnClickListener { shouldDismiss = false sourceDialog.dismissSafe(activity) - openOnlineSubPicker(it.context, null) { + openOnlineSubPicker(it.context, currentLoadResponse) { dismiss() } } 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 0d98f205..ee44567f 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 @@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError @@ -111,6 +112,9 @@ class PlayerGeneratorViewModel : ViewModel() { } } } + fun getLoadResponse(): LoadResponse? { + return normalSafeApiCall { (generator as? RepoLinkGenerator?)?.page } + } fun getMeta(): Any? { return normalSafeApiCall { generator?.getCurrent() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index ef3db0b4..135dc530 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -12,6 +12,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.screenWidth +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.PlayerEventSource @@ -110,7 +111,7 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { override fun openOnlineSubPicker( context: Context, - imdbId: Long?, + loadResponse: LoadResponse?, dismissCallback: () -> Unit ) { } 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 0af01ca8..e1a52074 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 @@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId +import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie import com.lagradost.cloudstream3.MainActivity.Companion.MPV @@ -2417,7 +2418,7 @@ class ResultViewModel2 : ViewModel() { null, loadResponse.type, mainId, - null + null, ) ) } From db2bf5e7be3f952e440e02989a0c0878c4bc4b15 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 19 May 2024 18:43:46 +0800 Subject: [PATCH 353/441] Remove subscene (#1096) subscene.com just shows a "Subscene is closed" message now. --- .../syncproviders/AccountManager.kt | 2 - .../syncproviders/providers/SubScene.kt | 118 ------------------ 2 files changed, 120 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 55418890..e96499f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -14,7 +14,6 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val simklApi = SimklApi(0) val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() - val subScene = SubScene() val subDl = SubDL() val localListApi = LocalList() @@ -44,7 +43,6 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { openSubtitlesApi, indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, - subScene, subDl ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt deleted file mode 100644 index fbe05026..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubScene.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.lagradost.cloudstream3.syncproviders.providers - -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.debugPrint -import com.lagradost.cloudstream3.subtitles.AbstractSubProvider -import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities -import com.lagradost.cloudstream3.subtitles.SubtitleResource -import com.lagradost.cloudstream3.syncproviders.providers.IndexSubtitleApi.Companion.getOrdinal -import com.lagradost.cloudstream3.utils.SubtitleHelper - -class SubScene : AbstractSubProvider { - val mainUrl = "https://subscene.com" - val name = "Subscene" - override val idPrefix = "subscene" - - override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { - val seasonName = - query.seasonNumber?.let { number -> - // Need to translate "7" to "Seventh Season" - getOrdinal(number)?.let { words -> " - $words Season" } - } ?: "" - - val fullQuery = query.query + seasonName - - val doc = app.post( - "$mainUrl/subtitles/searchbytitle", - data = mapOf("query" to fullQuery, "l" to "") - ).document - - return doc.select("div.title a").map { element -> - val href = "$mainUrl${element.attr("href")}" - val title = element.text() - - AbstractSubtitleEntities.SubtitleEntity( - idPrefix = idPrefix, - name = title, - source = name, - data = href, - lang = query.lang ?: "en", - epNumber = query.epNumber - ) - }.distinctBy { it.data } - } - - override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { - val resultDoc = app.get(data.data).document - val queryLanguage = SubtitleHelper.fromTwoLettersToLanguage(data.lang) ?: "English" - - val results = resultDoc.select("table tbody tr").mapNotNull { element -> - val anchor = element.select("a") - val href = anchor.attr("href") ?: return@mapNotNull null - val fixedHref = "$mainUrl${href}" - val spans = anchor.select("span") - val language = spans.firstOrNull()?.text() - val title = spans.getOrNull(1)?.text() - val isPositive = anchor.select("span.positive-icon").isNotEmpty() - - TableElement(title, language, fixedHref, isPositive) - }.sortedBy { - it.getScore(queryLanguage, data.epNumber) - } - - debugPrint { "$name found subtitles: ${results.takeLast(3)}" } - // Last = highest score - val selectedResult = results.lastOrNull() ?: return - - val subtitleDocument = app.get(selectedResult.href).document - val subtitleDownloadUrl = - "$mainUrl${subtitleDocument.select("div.download a").attr("href")}" - - this.addZipUrl(subtitleDownloadUrl) { name, _ -> - name - } - } - - /** - * Class to manage the various different subtitle results and rank them. - */ - data class TableElement( - val title: String?, - val language: String?, - val href: String, - val isPositive: Boolean - ) { - private fun matchesLanguage(other: String): Boolean { - return language != null && (language.contains(other, ignoreCase = true) || - other.contains(language, ignoreCase = true)) - } - - /** - * Scores in this order: - * Preferred Language > Episode number > Positive rating > English Language - */ - fun getScore(queryLanguage: String, episodeNum: Int?): Int { - var score = 0 - if (this.matchesLanguage(queryLanguage)) { - score += 8 - } - // Matches Episode 7 using "E07" with any number of leading zeroes - if (episodeNum != null && title != null && title.contains( - Regex( - """E0*${episodeNum}""", - RegexOption.IGNORE_CASE - ) - ) - ) { - score += 4 - } - if (isPositive) { - score += 2 - } - if (this.matchesLanguage("English")) { - score += 1 - } - return score - } - } -} \ No newline at end of file From e697bf75544d0777e21f67c5af3bef8392bbd35b Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 21 May 2024 23:06:28 +0300 Subject: [PATCH 354/441] Next Airing episode support in Trakt meta provider (#1072) --- .../cloudstream3/metaproviders/TraktProvider.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 37c6be1b..07c9f316 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -4,6 +4,7 @@ import android.net.Uri import com.lagradost.cloudstream3.* import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer @@ -166,6 +167,7 @@ open class TraktProvider : MainAPI() { val episodes = mutableListOf() val seasons = parseJson>(resSeasons) val seasonsNames = mutableListOf() + var nextAir: NextAiring? = null seasons.forEach { season -> @@ -215,6 +217,13 @@ open class TraktProvider : MainAPI() { description = episode.overview, ).apply { this.addDate(episode.firstAired) + if (nextAir == null && this.date != null && this.date!! > unixTimeMS) { + nextAir = NextAiring( + episode = this.episode!!, + unixTime = this.date!!.div(1000L), + season = if (this.season == 1) null else this.season, + ) + } } ) } @@ -240,6 +249,7 @@ open class TraktProvider : MainAPI() { this.actors = actors this.comingSoon = isUpcoming(mediaDetails.released) //posterHeaders + this.nextAiring = nextAir this.seasonNames = seasonsNames this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) this.contentRating = mediaDetails.certification From d0852449a50f548f88cd72785c338f2a5ad45184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Mon, 27 May 2024 16:54:25 +0300 Subject: [PATCH 355/441] Extractor: Added FourPichive (#1103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🕊 --- .../cloudstream3/extractors/HotlingerExtractor.kt | 7 ++++++- .../java/com/lagradost/cloudstream3/utils/ExtractorApi.kt | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt index b557a53e..db721108 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt @@ -20,4 +20,9 @@ class PlayRu : ContentX() { class FourPlayRu : ContentX() { override var name = "FourPlayRu" override var mainUrl = "https://four.playru.net" -} \ No newline at end of file +} + +class FourPichive : ContentX() { + override var name = "FourPichive" + override var mainUrl = "https://four.pichive.online" +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 61cdd26a..c6cad804 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -110,6 +110,7 @@ import com.lagradost.cloudstream3.extractors.Hotlinger import com.lagradost.cloudstream3.extractors.FourCX import com.lagradost.cloudstream3.extractors.PlayRu import com.lagradost.cloudstream3.extractors.FourPlayRu +import com.lagradost.cloudstream3.extractors.FourPichive import com.lagradost.cloudstream3.extractors.HDMomPlayer import com.lagradost.cloudstream3.extractors.HDPlayerSystem import com.lagradost.cloudstream3.extractors.VideoSeyred @@ -748,6 +749,7 @@ val extractorApis: MutableList = arrayListOf( FourCX(), PlayRu(), FourPlayRu(), + FourPichive(), HDMomPlayer(), HDPlayerSystem(), VideoSeyred(), From 960f8449b7eda687f70b9cebbbfd76502cffa398 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Mon, 27 May 2024 13:54:51 +0000 Subject: [PATCH 356/441] Update ResultViewModel2.kt (#1102) --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e1a52074..4285feb1 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 @@ -1728,7 +1728,7 @@ class ResultViewModel2 : ViewModel() { txt(R.string.episode_action_cast_mirror) ) { (result, index) -> val host = device?.host ?: return@acquireSingleLink - val link = result.links.firstOrNull() ?: return@acquireSingleLink + val link = result.links.getOrNull(index) ?: return@acquireSingleLink FcastSession(host).use { session -> session.sendMessage( From 5502e478c4f9227e6da2f785b436e9c599d06fdf Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 27 May 2024 19:35:56 +0530 Subject: [PATCH 357/441] chore: update material,kotlin compiler,newpipe extractor,rhino-js,guava,corektx (#1091) --- app/build.gradle.kts | 12 +++++------- .../java/com/lagradost/cloudstream3/MainActivity.kt | 2 +- .../ui/player/DownloadedPlayerActivity.kt | 2 +- build.gradle.kts | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f854865d..61a0634f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -164,7 +164,7 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") // Android Core & Lifecycle - implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.6.1") implementation("androidx.navigation:navigation-ui-ktx:2.7.7") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") @@ -174,7 +174,7 @@ dependencies { // Design & UI implementation("jp.wasabeef:glide-transformations:4.3.0") implementation("androidx.preference:preference-ktx:1.2.1") - implementation("com.google.android.material:material:1.11.0") + implementation("com.google.android.material:material:1.12.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") @@ -185,7 +185,7 @@ dependencies { // For KSP -> Official Annotation Processors are Not Yet Supported for KSP ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") - implementation("com.google.guava:guava:32.1.3-android") + implementation("com.google.guava:guava:33.2.0-android") implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") // Media 3 (ExoPlayer) @@ -202,7 +202,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:6dc25f7b97") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding @@ -219,9 +219,7 @@ dependencies { implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview // Extensions & Other Libs - implementation("org.mozilla:rhino:1.7.13") /* run JavaScript - ^ Don't Bump RhinoJS to 1.7.14,`NoClassDefFoundError` Occurs and Trailers won't play (even with Desugaring) - NewPipeExtractor Issue */ + implementation("org.mozilla:rhino:1.7.15") // run JavaScript implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9 diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 1ff0575b..cc2c99de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -652,7 +652,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, } } - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + override fun dispatchKeyEvent(event: KeyEvent): Boolean { val response = CommonActivity.dispatchKeyEvent(this, event) if (response != null) return response diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 1e2ea540..4d8860f8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -17,7 +17,7 @@ import com.lagradost.safefile.SafeFile const val DTAG = "PlayerActivity" class DownloadedPlayerActivity : AppCompatActivity() { - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + override fun dispatchKeyEvent(event: KeyEvent): Boolean { CommonActivity.dispatchKeyEvent(this, event)?.let { return it } diff --git a/build.gradle.kts b/build.gradle.kts index ab1918fe..34f141b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ buildscript { dependencies { classpath("com.android.tools.build:gradle:8.2.2") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") // Universal build config classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:0.15.1") From dff56026de873d0f35cdd134decd1fa1008c0f5f Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 29 May 2024 23:39:55 +0300 Subject: [PATCH 358/441] SubDL Account login support (#1101) --- .../syncproviders/AccountManager.kt | 11 +- .../syncproviders/providers/Subdl.kt | 167 ++++++++++++++++-- .../ui/settings/SettingsAccount.kt | 2 + .../cloudstream3/utils/BackupUtils.kt | 2 + app/src/main/res/drawable/subdl_logo_big.xml | 10 ++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/settings_account.xml | 4 + 7 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 app/src/main/res/drawable/subdl_logo_big.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index e96499f0..1fd7900f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -14,7 +14,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val simklApi = SimklApi(0) val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() - val subDl = SubDL() + val subDlApi = SubDlApi(0) val localListApi = LocalList() // used to login via app intent @@ -26,7 +26,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { // this needs init with context and can be accessed in settings val accountManagers get() = listOf( - malApi, aniListApi, openSubtitlesApi, simklApi //nginxApi + malApi, aniListApi, openSubtitlesApi, subDlApi, simklApi //nginxApi ) // used for active syncing @@ -36,14 +36,17 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { ) val inAppAuths - get() = listOf(openSubtitlesApi)//, nginxApi) + get() = listOf( + openSubtitlesApi, + subDlApi + )//, nginxApi) val subtitleProviders get() = listOf( openSubtitlesApi, indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, - subDl + subDlApi ) const val appString = "cloudstreamapp" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt index d25d3f22..29544e65 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Subdl.kt @@ -1,21 +1,80 @@ package com.lagradost.cloudstream3.syncproviders.providers import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.ErrorLoadingException +import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.subtitles.AbstractSubProvider +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.subtitles.AbstractSubApi import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.SubtitleResource +import com.lagradost.cloudstream3.syncproviders.AuthAPI.LoginInfo +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI +import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager -class SubDL : AbstractSubProvider { - //API Documentation: https://subdl.com/api-doc - val mainUrl = "https://subdl.com/" - val name = "SubDL" +class SubDlApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi { override val idPrefix = "subdl" + override val name = "SubDL" + override val icon = R.drawable.subdl_logo_big + override val requiresPassword = true + override val requiresEmail = true + override val createAccountUrl = "https://subdl.com/login" + companion object { - const val APIKEY = "zRJl5QA-8jNA2i0pE8cxANbEukANp7IM" - const val APIENDPOINT = "https://api.subdl.com/api/v1/subtitles" + const val APIURL = "https://api.subdl.com" + const val APIENDPOINT = "$APIURL/api/v1/subtitles" const val DOWNLOADENDPOINT = "https://dl.subdl.com" + const val SUBDL_SUBTITLES_USER_KEY: String = "subdl_user" + var currentSession: SubtitleOAuthEntity? = null + } + + override suspend fun initialize() { + currentSession = getAuthKey() + } + + override fun logOut() { + setAuthKey(null) + removeAccountKeys() + currentSession = getAuthKey() + } + override suspend fun login(data: InAppAuthAPI.LoginData): Boolean { + val email = data.email ?: throw ErrorLoadingException("Requires Email") + val password = data.password ?: throw ErrorLoadingException("Requires Password") + switchToNewAccount() + try { + if (initLogin(email, password)) { + registerAccount() + return true + } + } catch (e: Exception) { + logError(e) + switchToOldAccount() + } + switchToOldAccount() + return false + } + + override fun getLatestLoginData(): InAppAuthAPI.LoginData? { + val current = getAuthKey() ?: return null + return InAppAuthAPI.LoginData( + email = current.userEmail, + password = current.pass + ) + } + + override fun loginInfo(): LoginInfo? { + getAuthKey()?.let { user -> + return LoginInfo( + profilePicture = null, + name = user.name ?: user.userEmail, + accountIndex = accountIndex + ) + } + return null } override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { @@ -37,8 +96,8 @@ class SubDL : AbstractSubProvider { val searchQueryUrl = when (idQuery) { //Use imdb/tmdb id to search if its valid - null -> "$APIENDPOINT?api_key=$APIKEY&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery" - else -> "$APIENDPOINT?api_key=$APIKEY$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + null -> "$APIENDPOINT?api_key=${currentSession?.apiKey}&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery" + else -> "$APIENDPOINT?api_key=${currentSession?.apiKey}$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery" } val req = app.get( @@ -49,7 +108,7 @@ class SubDL : AbstractSubProvider { ) return req.parsedSafe()?.subtitles?.map { subtitle -> - val name = subtitle.releaseName + val lang = subtitle.lang.replaceFirstChar { it.uppercase() } val resEpNum = subtitle.episode ?: query.epNumber val resSeasonNum = subtitle.season ?: query.seasonNumber @@ -57,13 +116,14 @@ class SubDL : AbstractSubProvider { AbstractSubtitleEntities.SubtitleEntity( idPrefix = this.idPrefix, - name = name, + name = subtitle.releaseName, lang = lang, data = "${DOWNLOADENDPOINT}${subtitle.url}", type = type, source = this.name, epNumber = resEpNum, seasonNumber = resSeasonNum, + isHearingImpaired = subtitle.hearingImpaired ?: false, ) } } @@ -74,6 +134,88 @@ class SubDL : AbstractSubProvider { } } + private suspend fun initLogin(useremail: String, password: String): Boolean { + + val tokenResponse = app.post( + url = "$APIURL/login", + data = mapOf( + "email" to useremail, + "password" to password + ) + ).parsedSafe() + + if (tokenResponse?.token == null) return false + + val apiResponse = app.get( + url = "$APIURL/user/userApi", + headers = mapOf( + "Authorization" to "Bearer ${tokenResponse.token}" + ) + ).parsedSafe() + + if (apiResponse?.ok == false) return false + + setAuthKey( + SubtitleOAuthEntity( + userEmail = useremail, + pass = password, + name = tokenResponse.userData?.username ?: tokenResponse.userData?.name, + accessToken = tokenResponse.token, + apiKey = apiResponse?.apiKey + ) + ) + return true + } + + private fun getAuthKey(): SubtitleOAuthEntity? { + return getKey(accountId, SUBDL_SUBTITLES_USER_KEY) + } + + private fun setAuthKey(data: SubtitleOAuthEntity?) { + if (data == null) removeKey( + accountId, + SUBDL_SUBTITLES_USER_KEY + ) + currentSession = data + setKey(accountId, SUBDL_SUBTITLES_USER_KEY, data) + } + + data class SubtitleOAuthEntity( + @JsonProperty("userEmail") var userEmail: String, + @JsonProperty("pass") var pass: String, + @JsonProperty("name") var name: String? = null, + @JsonProperty("accessToken") var accessToken: String? = null, + @JsonProperty("apiKey") var apiKey: String? = null, + ) + + data class OAuthTokenResponse( + @JsonProperty("token") val token: String? = null, + @JsonProperty("userData") val userData: UserData? = null, + @JsonProperty("status") val status: Boolean? = null, + @JsonProperty("message") val message: String? = null, + ) + + data class UserData( + @JsonProperty("email") val email: String, + @JsonProperty("name") val name: String, + @JsonProperty("country") val country: String, + @JsonProperty("scStepCode") val scStepCode: String, + @JsonProperty("scVerified") val scVerified: Boolean, + @JsonProperty("username") val username: String? = null, + @JsonProperty("scUsername") val scUsername: String, + ) + + data class ApiKeyResponse( + @JsonProperty("ok") val ok: Boolean? = false, + @JsonProperty("api_key") val apiKey: String? = null, + @JsonProperty("usage") val usage: Usage? = null, + ) + + data class Usage( + @JsonProperty("total") val total: Long? = 0, + @JsonProperty("today") val today: Long? = 0, + ) + data class ApiResponse( @JsonProperty("status") val status: Boolean? = null, @JsonProperty("results") val results: List? = null, @@ -96,7 +238,10 @@ class SubDL : AbstractSubProvider { @JsonProperty("lang") val lang: String, @JsonProperty("author") val author: String? = null, @JsonProperty("url") val url: String? = null, + @JsonProperty("subtitlePage") val subtitlePage: String? = null, @JsonProperty("season") val season: Int? = null, @JsonProperty("episode") val episode: Int? = null, + @JsonProperty("language") val language: String? = null, + @JsonProperty("hi") val hearingImpaired: Boolean? = null, ) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index f0d402da..27233525 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniList import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.openSubtitlesApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlApi import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API @@ -324,6 +325,7 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome R.string.anilist_key to aniListApi, R.string.simkl_key to simklApi, R.string.opensubtitles_key to openSubtitlesApi, + R.string.subdl_key to subDlApi, ) for ((key, api) in syncApis) { 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 279a0cb5..1d23e503 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -26,6 +26,7 @@ import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_T import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_UNIXTIME_KEY import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_USER_KEY import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi.Companion.OPEN_SUBTITLES_USER_KEY +import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi.Companion.SUBDL_SUBTITLES_USER_KEY import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -64,6 +65,7 @@ object BackupUtils { PLUGINS_KEY_LOCAL, OPEN_SUBTITLES_USER_KEY, + SUBDL_SUBTITLES_USER_KEY, DOWNLOAD_EPISODE_CACHE, diff --git a/app/src/main/res/drawable/subdl_logo_big.xml b/app/src/main/res/drawable/subdl_logo_big.xml new file mode 100644 index 00000000..a6cbb311 --- /dev/null +++ b/app/src/main/res/drawable/subdl_logo_big.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8108623..44171dc5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -471,6 +471,7 @@ simkl_key mal_key opensubtitles_key + subdl_key nginx_key password123 Username diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml index 5cde06c4..d1d18a0f 100644 --- a/app/src/main/res/xml/settings_account.xml +++ b/app/src/main/res/xml/settings_account.xml @@ -17,6 +17,10 @@ android:icon="@drawable/open_subtitles_icon" android:key="@string/opensubtitles_key" /> + + Date: Sat, 1 Jun 2024 19:16:42 +0300 Subject: [PATCH 359/441] feat(TV UI): Account switch focus fix (#1112) --- app/src/main/res/layout/account_managment.xml | 14 +++++---- app/src/main/res/layout/account_single.xml | 29 ++++++++++--------- app/src/main/res/layout/account_switch.xml | 22 +++++++------- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/app/src/main/res/layout/account_managment.xml b/app/src/main/res/layout/account_managment.xml index 389a3406..e7afb382 100644 --- a/app/src/main/res/layout/account_managment.xml +++ b/app/src/main/res/layout/account_managment.xml @@ -62,14 +62,16 @@ + android:id="@+id/account_switch_account" + android:text="@string/switch_account" + style="@style/SettingsItem" + android:focusable="true"/> + android:id="@+id/account_logout" + android:text="@string/logout" + style="@style/SettingsItem" + android:focusable="true"> diff --git a/app/src/main/res/layout/account_single.xml b/app/src/main/res/layout/account_single.xml index cbfb9f18..c4f7fa39 100644 --- a/app/src/main/res/layout/account_single.xml +++ b/app/src/main/res/layout/account_single.xml @@ -1,10 +1,11 @@ + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:foreground="?android:attr/selectableItemBackgroundBorderless" + android:orientation="horizontal" + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:focusable="true"> + android:id="@+id/account_profile_picture" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="ContentDescription" /> + android:foreground="@null" + android:id="@+id/account_name" + tools:text="Account 1" + style="@style/SettingsItem" /> diff --git a/app/src/main/res/layout/account_switch.xml b/app/src/main/res/layout/account_switch.xml index 659ad840..5153f0e3 100644 --- a/app/src/main/res/layout/account_switch.xml +++ b/app/src/main/res/layout/account_switch.xml @@ -7,18 +7,20 @@ android:layout_height="match_parent"> + android:id="@+id/account_list" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + android:background="?attr/primaryBlackBackground" + tools:listitem="@layout/account_single" + android:layout_width="match_parent" + android:layout_rowWeight="1" + android:layout_height="wrap_content" + android:focusable="true"/> + android:id="@+id/account_add" + android:text="@string/add_account" + style="@style/SettingsItem" + android:focusable="true"> From b3e3dadc72e61cd9925f721749578b074026f745 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 1 Jun 2024 19:17:41 +0300 Subject: [PATCH 360/441] Remove IndexSubtitles provider (#1111) --- .../syncproviders/AccountManager.kt | 2 - .../providers/IndexSubtitleApi.kt | 265 ------------------ 2 files changed, 267 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 1fd7900f..a14f8438 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -12,7 +12,6 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val aniListApi = AniListApi(0) val openSubtitlesApi = OpenSubtitlesApi(0) val simklApi = SimklApi(0) - val indexSubtitlesApi = IndexSubtitleApi() val addic7ed = Addic7ed() val subDlApi = SubDlApi(0) val localListApi = LocalList() @@ -44,7 +43,6 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val subtitleProviders get() = listOf( openSubtitlesApi, - indexSubtitlesApi, // they got anti scraping measures in place :( addic7ed, subDlApi ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt deleted file mode 100644 index 5ca3f3d5..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt +++ /dev/null @@ -1,265 +0,0 @@ -package com.lagradost.cloudstream3.syncproviders.providers - -import android.util.Log -import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.imdbUrlToIdNullable -import com.lagradost.cloudstream3.subtitles.AbstractSubApi -import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities -import com.lagradost.cloudstream3.utils.SubtitleHelper - -class IndexSubtitleApi : AbstractSubApi { - override val name = "IndexSubtitle" - override val idPrefix = "indexsubtitle" - override val requiresLogin = false - override val icon: Nothing? = null - override val createAccountUrl: Nothing? = null - - override fun loginInfo(): Nothing? = null - - override fun logOut() {} - - - companion object { - const val host = "https://indexsubtitle.com" - const val TAG = "INDEXSUBS" - - fun getOrdinal(num: Int?): String? { - return when (num) { - 1 -> "First" - 2 -> "Second" - 3 -> "Third" - 4 -> "Fourth" - 5 -> "Fifth" - 6 -> "Sixth" - 7 -> "Seventh" - 8 -> "Eighth" - 9 -> "Ninth" - 10 -> "Tenth" - 11 -> "Eleventh" - 12 -> "Twelfth" - 13 -> "Thirteenth" - 14 -> "Fourteenth" - 15 -> "Fifteenth" - 16 -> "Sixteenth" - 17 -> "Seventeenth" - 18 -> "Eighteenth" - 19 -> "Nineteenth" - 20 -> "Twentieth" - 21 -> "Twenty-First" - 22 -> "Twenty-Second" - 23 -> "Twenty-Third" - 24 -> "Twenty-Fourth" - 25 -> "Twenty-Fifth" - 26 -> "Twenty-Sixth" - 27 -> "Twenty-Seventh" - 28 -> "Twenty-Eighth" - 29 -> "Twenty-Ninth" - 30 -> "Thirtieth" - 31 -> "Thirty-First" - 32 -> "Thirty-Second" - 33 -> "Thirty-Third" - 34 -> "Thirty-Fourth" - 35 -> "Thirty-Fifth" - else -> null - } - } - } - - private fun fixUrl(url: String): String { - if (url.startsWith("http")) { - return url - } - if (url.isEmpty()) { - return "" - } - - val startsWithNoHttp = url.startsWith("//") - if (startsWithNoHttp) { - return "https:$url" - } else { - if (url.startsWith('/')) { - return host + url - } - return "$host/$url" - } - } - - private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean { - val FILTER_EPS_REGEX = - Regex("(?i)((Chapter\\s?0?${epNum})|((Season)?\\s?0?${seasonNum}?\\s?(Episode)\\s?0?${epNum}[^0-9]))|(?i)((S?0?${seasonNum}?E0?${epNum}[^0-9])|(0?${seasonNum}[a-z]0?${epNum}[^0-9]))") - return text.contains(FILTER_EPS_REGEX) - } - - private fun haveEps(text: String): Boolean { - val HAVE_EPS_REGEX = - Regex("(?i)((Chapter\\s?0?\\d)|((Season)?\\s?0?\\d?\\s?(Episode)\\s?0?\\d))|(?i)((S?0?\\d?E0?\\d)|(0?\\d[a-z]0?\\d))") - return text.contains(HAVE_EPS_REGEX) - } - - override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List { - val imdbId = query.imdbId?.replace("tt", "")?.toLong() ?: 0 - val lang = query.lang - val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString()) - val queryText = query.query - val epNum = query.epNumber ?: 0 - val seasonNum = query.seasonNumber ?: 0 - val yearNum = query.year ?: 0 - - val urlItems = ArrayList() - - fun cleanResources( - results: MutableList, - name: String, - link: String - ) { - results.add( - AbstractSubtitleEntities.SubtitleEntity( - idPrefix = idPrefix, - name = name, - lang = queryLang.toString(), - data = link, - source = this.name, - type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie, - epNumber = epNum, - seasonNumber = seasonNum, - year = yearNum, - ) - ) - } - - val document = app.get("$host/?search=$queryText").document - - document.select("div.my-3.p-3 div.media").map { block -> - if (seasonNum > 0) { - val name = block.select("strong.text-primary, strong.text-info").text().trim() - val season = getOrdinal(seasonNum) - if ((block.selectFirst("a")?.attr("href") - ?.contains( - "$season", - ignoreCase = true - )!! || name.contains( - "$season", - ignoreCase = true - )) && name.contains(queryText, ignoreCase = true) - ) { - block.select("div.media").mapNotNull { - urlItems.add( - fixUrl( - it.selectFirst("a")!!.attr("href") - ) - ) - } - } - } else { - if (block.selectFirst("strong")!!.text().trim() - .matches(Regex("(?i)^$queryText\$")) - ) { - if (block.select("span[title=Release]").isNullOrEmpty()) { - block.select("div.media").mapNotNull { - val urlItem = fixUrl( - it.selectFirst("a")!!.attr("href") - ) - val itemDoc = app.get(urlItem).document - val id = imdbUrlToIdNullable( - itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent() - ?.attr("href") - )?.toLongOrNull() - val year = itemDoc.selectFirst("div.d-flex span.badge.badge-success") - ?.ownText() - ?.trim().toString() - Log.i(TAG, "id => $id \nyear => $year||$yearNum") - if (imdbId > 0) { - if (id == imdbId) { - urlItems.add(urlItem) - } - } else { - if (year.contains("$yearNum")) { - urlItems.add(urlItem) - } - } - } - } else { - if (block.select("span[title=Release]").text().trim() - .contains("$yearNum") - ) { - block.select("div.media").mapNotNull { - urlItems.add( - fixUrl( - it.selectFirst("a")!!.attr("href") - ) - ) - } - } - } - } - } - } - Log.i(TAG, "urlItems => $urlItems") - val results = mutableListOf() - - urlItems.forEach { url -> - val request = app.get(url) - if (request.isSuccessful) { - request.document.select("div.my-3.p-3 div.media").map { block -> - if (block.select("span.d-block span[data-original-title=Language]").text() - .trim() - .contains("$queryLang") - ) { - var name = block.select("strong.text-primary, strong.text-info").text().trim() - val link = fixUrl(block.selectFirst("a")!!.attr("href")) - if (seasonNum > 0) { - when { - isRightEps(name, seasonNum, epNum) -> { - cleanResources(results, name, link) - } - !(haveEps(name)) -> { - name = "$name (S${seasonNum}:E${epNum})" - cleanResources(results, name, link) - } - } - } else { - cleanResources(results, name, link) - } - } - } - } - } - return results - } - - override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? { - val seasonNum = data.seasonNumber - val epNum = data.epNumber - - val req = app.get(data.data) - - if (req.isSuccessful) { - val document = req.document - val link = if (document.select("div.my-3.p-3 div.media").size == 1) { - fixUrl( - document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href") - ) - } else { - document.select("div.my-3.p-3 div.media").firstNotNullOf { block -> - val name = - block.selectFirst("strong.d-block")?.text()?.trim().toString() - if (seasonNum!! > 0) { - if (isRightEps(name, seasonNum, epNum)) { - fixUrl(block.selectFirst("a")!!.attr("href")) - } else { - null - } - } else { - fixUrl(block.selectFirst("a")!!.attr("href")) - } - } - } - return link - } - - return null - - } - -} \ No newline at end of file From 9bebfe459005021970e25bd5ca38816fa5a66ba4 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:07:54 +0200 Subject: [PATCH 361/441] feature(ui): hide NSFW plugins (#1117) Hide NSFW plugins if Settings / Providers NSFW is disabled --- .../cloudstream3/ui/settings/extensions/PluginsViewModel.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 151c8d57..2b026e0d 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 @@ -9,6 +9,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.amap @@ -181,8 +182,11 @@ class PluginsViewModel : ViewModel() { } private suspend fun updatePluginListPrivate(context: Context, repositoryUrl: String) { + val isAdult = settingsForProvider.enableAdult val plugins = getPlugins(repositoryUrl) - val list = plugins.map { plugin -> + val list = plugins.filter { + return@filter !(it.second.tvTypes?.contains("NSFW") == true && !isAdult) + }.map { plugin -> PluginViewData(plugin, isDownloaded(context, plugin.second.internalName, plugin.first)) } From 0391a3b89cb3d4266afc1e1a710366b9266f1241 Mon Sep 17 00:00:00 2001 From: int3debug <164035730+int3debug@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:09:05 +0200 Subject: [PATCH 362/441] feature(ui): added wikipedia to links (#1119) --- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/settings_general.xml | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 44171dc5..deee5ad2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -774,4 +774,5 @@ Audio Book Media Reset + CloudStream Wiki \ No newline at end of file diff --git a/app/src/main/res/xml/settings_general.xml b/app/src/main/res/xml/settings_general.xml index cdda6d85..853bbda1 100644 --- a/app/src/main/res/xml/settings_general.xml +++ b/app/src/main/res/xml/settings_general.xml @@ -86,6 +86,14 @@ android:action="android.intent.action.VIEW" android:data="https://discord.gg/5Hus6fM" /> + + + \ No newline at end of file From 358a20eb7786daa661ef60d20caca67d8eec105f Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Thu, 6 Jun 2024 02:48:33 +0530 Subject: [PATCH 363/441] chore: refactor gradlelocalproperties and update gradle plugin (#957) --- app/build.gradle.kts | 2 +- build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61a0634f..21f22dd1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,7 +69,7 @@ android { resValue("bool", "is_prerelease", "false") // Reads local.properties - val localProperties = gradleLocalProperties(rootDir) + val localProperties = gradleLocalProperties(rootDir, providers) buildConfigField( "long", diff --git a/build.gradle.kts b/build.gradle.kts index 34f141b4..ba87b6f4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath("com.android.tools.build:gradle:8.2.2") + classpath("com.android.tools.build:gradle:8.4.0") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") // Universal build config diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fc2d0f86..2968a1b2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 30 17:11:15 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 7eec0eff02b1e7f8bd18a948515adae5fdc13f9e Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:41:06 +0200 Subject: [PATCH 364/441] Revert "chore: refactor gradlelocalproperties and update gradle plugin (#957)" (#1120) This reverts commit 358a20eb7786daa661ef60d20caca67d8eec105f. --- app/build.gradle.kts | 2 +- build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 21f22dd1..61a0634f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -69,7 +69,7 @@ android { resValue("bool", "is_prerelease", "false") // Reads local.properties - val localProperties = gradleLocalProperties(rootDir, providers) + val localProperties = gradleLocalProperties(rootDir) buildConfigField( "long", diff --git a/build.gradle.kts b/build.gradle.kts index ba87b6f4..34f141b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath("com.android.tools.build:gradle:8.4.0") + classpath("com.android.tools.build:gradle:8.2.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23") classpath("org.jetbrains.dokka:dokka-gradle-plugin:1.9.10") // Universal build config diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2968a1b2..fc2d0f86 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 30 17:11:15 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From f775c1725d30d6f105dc114c8e6ecbfc6d2d56d9 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 8 Jun 2024 22:07:33 +0300 Subject: [PATCH 365/441] feat(TV UI): Subtitles Filter button focus fix (#1125) --- app/src/main/res/layout/dialog_online_subtitles.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/dialog_online_subtitles.xml b/app/src/main/res/layout/dialog_online_subtitles.xml index d480bd34..e0eac5e0 100644 --- a/app/src/main/res/layout/dialog_online_subtitles.xml +++ b/app/src/main/res/layout/dialog_online_subtitles.xml @@ -107,6 +107,7 @@ android:layout_margin="10dp" android:background="?selectableItemBackgroundBorderless" android:contentDescription="@string/change_providers_img_des" + android:focusable="true" android:nextFocusLeft="@id/year_btt" android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" From 607a4510b6d941293fd29115818d79deb9b03681 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sat, 8 Jun 2024 22:08:35 +0300 Subject: [PATCH 366/441] feat(Extensions): Trakt season names remove (#1124) --- .../cloudstream3/metaproviders/TraktProvider.kt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 07c9f316..8d149888 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -166,18 +166,10 @@ open class TraktProvider : MainAPI() { val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") val episodes = mutableListOf() val seasons = parseJson>(resSeasons) - val seasonsNames = mutableListOf() var nextAir: NextAiring? = null seasons.forEach { season -> - seasonsNames.add( - SeasonData( - season.number!!, - season.title - ) - ) - season.episodes?.map { episode -> val linkData = LinkData( @@ -250,7 +242,6 @@ open class TraktProvider : MainAPI() { this.comingSoon = isUpcoming(mediaDetails.released) //posterHeaders this.nextAiring = nextAir - this.seasonNames = seasonsNames this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl) this.contentRating = mediaDetails.certification addTrailer(mediaDetails.trailer) From 3345326cb2987b98b87db0d93e291aa0d0b27b7e Mon Sep 17 00:00:00 2001 From: RowdyRushya <66415100+rushi-chavan@users.noreply.github.com> Date: Sat, 8 Jun 2024 12:19:29 -0700 Subject: [PATCH 367/441] Extractor: VidSrcTo: better handling of runtime errors (#1121) --- .../cloudstream3/extractors/VidSrcTo.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt index b9065688..2655670d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import java.net.URLDecoder @@ -26,12 +27,16 @@ class VidSrcTo : ExtractorApi() { val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return if (res.status != 200) return res.result?.amap { source -> - val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap - val finalUrl = DecryptUrl(embedRes.result.encUrl) - if(finalUrl.equals(embedRes.result.encUrl)) return@amap - when (source.title) { - "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) - "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + try { + val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap + val finalUrl = DecryptUrl(embedRes.result.encUrl) + if(finalUrl.equals(embedRes.result.encUrl)) return@amap + when (source.title) { + "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + } + } catch (e: Exception) { + logError(e) } } } From 4c95610238d671cd8e11c3e85786a53e0db003c7 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Sun, 9 Jun 2024 17:38:08 +0300 Subject: [PATCH 368/441] feat(UI): Hide Platform's not related settings (#1128) --- .../ui/settings/SettingsAccount.kt | 6 ++--- .../ui/settings/SettingsFragment.kt | 25 +++++++++++++++++++ .../ui/settings/SettingsGeneral.kt | 7 +++--- .../ui/settings/SettingsPlayer.kt | 19 +++++++++++++- app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/settings_player.xml | 6 +++-- 6 files changed, 55 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 27233525..3ec47648 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -36,6 +36,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -298,10 +299,7 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome hideKeyboard() setPreferencesFromResource(R.xml.settings_account, rootKey) - // hide preference on tvs and emulators - getPref(R.string.biometric_key)?.isEnabled = isLayout(PHONE) - - getPref(R.string.biometric_key)?.setOnPreferenceClickListener { + getPref(R.string.biometric_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener { val ctx = context ?: return@setOnPreferenceClickListener false if (deviceHasPasswordPinLock(ctx)) { 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 8ac17928..6ba93c0f 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 @@ -26,6 +26,7 @@ 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.isLayout +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper @@ -53,6 +54,30 @@ class SettingsFragment : Fragment() { } } + /** + * Hide many Preferences on selected layouts. + **/ + fun PreferenceFragmentCompat?.hidePrefs(ids: List, layoutFlags: Int) { + if (this == null) return + + try { + ids.forEach { + getPref(it)?.isVisible = !isLayout(layoutFlags) + } + } catch (e: Exception) { + logError(e) + } + } + + /** + * Hide the Preference on selected layouts. + **/ + fun Preference?.hideOn(layoutFlags: Int): Preference? { + if (this == null) return null + this.isVisible = !isLayout(layoutFlags) + return this + } + /** * On TV you cannot properly scroll to the bottom of settings, this fixes that. * */ diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index ff891c43..22a7e098 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -27,10 +27,13 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.EasterEggMonke +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.beneneCount import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -208,9 +211,7 @@ class SettingsGeneral : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - // disable preference on tvs and emulators - getPref(R.string.battery_optimisation_key)?.isEnabled = isLayout(PHONE) - getPref(R.string.battery_optimisation_key)?.setOnPreferenceClickListener { + getPref(R.string.battery_optimisation_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener { val ctx = context ?: return@setOnPreferenceClickListener false if (isAppRestricted(ctx)) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 3d0bcb1f..20279cd1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -7,8 +7,14 @@ import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError +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.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hidePrefs import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -31,6 +37,18 @@ class SettingsPlayer : PreferenceFragmentCompat() { setPreferencesFromResource(R.xml.settings_player, rootKey) val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) + //Hide specific prefs on TV/EMULATOR + hidePrefs( + listOf( + R.string.pref_category_gestures_key, + R.string.rotate_video_key, + R.string.auto_rotate_video_key + ), + TV or EMULATOR + ) + + getPref(R.string.pref_category_android_tv_key)?.hideOn(PHONE) + getPref(R.string.video_buffer_length_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.video_buffer_length_names) val prefValues = resources.getIntArray(R.array.video_buffer_length_values) @@ -227,6 +245,5 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } } - } } \ 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 deee5ad2..fad44ad4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -438,7 +438,9 @@ Actions Cache Android TV + pref_category_android_tv_key Gestures + pref_category_gestures_key Player features Subtitles Layout diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index 82505511..5d5b11d0 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -101,7 +101,8 @@ + android:title="@string/pref_category_gestures" + app:key="@string/pref_category_gestures_key"> + android:title="@string/pref_category_android_tv" + android:key="@string/pref_category_android_tv_key" > Date: Sat, 15 Jun 2024 21:47:30 +0000 Subject: [PATCH 369/441] goodstream (#1133) --- .../extractors/GoodstreamExtractor.kt | 37 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 2 + 2 files changed, 39 insertions(+) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt new file mode 100644 index 00000000..9f6ba611 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt @@ -0,0 +1,37 @@ +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 + +class GoodstreamExtractor : ExtractorApi() { + override var name = "Goodstream" + override val mainUrl = "https://goodstream.uno" + override val requiresReferer = false + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + app.get(url).document.select("script").map { script -> + if (script.data().contains(Regex("file|player"))) { + val urlRegex = Regex("file: \"(https:\\/\\/[a-z0-9.\\/-_?=&]+)\",") + urlRegex.find(script.data())?.groupValues?.get(1).let { link -> + callback.invoke( + ExtractorLink( + name, + name, + link!!, + mainUrl, + Qualities.Unknown.value, + ) + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index c6cad804..5d696d33 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -67,6 +67,7 @@ import com.lagradost.cloudstream3.extractors.Gdriveplayerorg import com.lagradost.cloudstream3.extractors.Gdriveplayerus import com.lagradost.cloudstream3.extractors.Gofile import com.lagradost.cloudstream3.extractors.GuardareStream +import com.lagradost.cloudstream3.extractors.GoodstreamExtractor import com.lagradost.cloudstream3.extractors.Guccihide import com.lagradost.cloudstream3.extractors.Hxfile import com.lagradost.cloudstream3.extractors.JWPlayer @@ -879,6 +880,7 @@ val extractorApis: MutableList = arrayListOf( Gdriveplayerorg(), Gdriveplayerus(), Gdriveplayerco(), + GoodstreamExtractor(), Gdriveplayer(), DatabaseGdrive(), DatabaseGdrive2(), From 30d223cfe3c65b8f104245d58056087e7913adbd Mon Sep 17 00:00:00 2001 From: KingLucius Date: Mon, 17 Jun 2024 04:01:14 +0300 Subject: [PATCH 370/441] feat(UI): Reorganize Settings (#1137) - Accounts Section & Remove "account" from title. - Security Section for Biometric that is hidden on TV. - Move "send logs" to "Action" section. --- .../ui/settings/SettingsAccount.kt | 6 +- app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/settings_account.xml | 66 +++++++++++-------- app/src/main/res/xml/settings_updates.xml | 14 ++-- 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 3ec47648..a8358d0d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -299,6 +299,9 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome hideKeyboard() setPreferencesFromResource(R.xml.settings_account, rootKey) + //Hides the security category on TV as it's only Biometric for now + getPref(R.string.pref_category_security_key)?.hideOn(TV or EMULATOR) + getPref(R.string.biometric_key)?.hideOn(TV or EMULATOR)?.setOnPreferenceClickListener { val ctx = context ?: return@setOnPreferenceClickListener false @@ -328,8 +331,7 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome for ((key, api) in syncApis) { getPref(key)?.apply { - title = - getString(R.string.login_format).format(api.name, getString(R.string.account)) + title = api.name setOnPreferenceClickListener { val info = api.loginInfo() if (info != null) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fad44ad4..d9317ccd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -441,6 +441,9 @@ pref_category_android_tv_key Gestures pref_category_gestures_key + Security + pref_category_security_key + Accounts Player features Subtitles Layout diff --git a/app/src/main/res/xml/settings_account.xml b/app/src/main/res/xml/settings_account.xml index d1d18a0f..d165cd87 100644 --- a/app/src/main/res/xml/settings_account.xml +++ b/app/src/main/res/xml/settings_account.xml @@ -1,37 +1,49 @@ - + - + - + - + - + - + - + - + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/settings_updates.xml b/app/src/main/res/xml/settings_updates.xml index e3b36648..102f8ee4 100644 --- a/app/src/main/res/xml/settings_updates.xml +++ b/app/src/main/res/xml/settings_updates.xml @@ -1,13 +1,6 @@ - @@ -80,5 +73,12 @@ android:icon="@drawable/ic_baseline_construction_24" android:title="@string/redo_setup_process" app:key="@string/redo_setup_key" /> + From 7a0cd07dc19f3d1523292ef0569338b326cb1784 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 18 Jun 2024 06:02:32 +0300 Subject: [PATCH 371/441] feat(TV UI): Press Right to focus save on Logcat (#1136) --- app/src/main/res/layout/logcat.xml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/layout/logcat.xml b/app/src/main/res/layout/logcat.xml index caa8c5cb..5cbb3f53 100644 --- a/app/src/main/res/layout/logcat.xml +++ b/app/src/main/res/layout/logcat.xml @@ -6,20 +6,20 @@ android:layout_height="match_parent"> + android:layout_marginBottom="60dp" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:nextFocusRight="@id/save_btt"> + android:id="@+id/text1" + android:padding="15dp" + android:textSize="15sp" + android:textColor="?attr/textColor" + android:layout_width="match_parent" + android:layout_rowWeight="1" + tools:text="Test" + android:layout_height="wrap_content"/> Date: Wed, 19 Jun 2024 00:24:35 +0300 Subject: [PATCH 372/441] feat(Extensions): Consider time zone in Trakt durations (#1140) --- .../metaproviders/TraktProvider.kt | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 8d149888..736e05f2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -1,18 +1,39 @@ package com.lagradost.cloudstream3.metaproviders import android.net.Uri -import com.lagradost.cloudstream3.* import com.fasterxml.jackson.annotation.JsonAlias import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.Actor +import com.lagradost.cloudstream3.ActorData +import com.lagradost.cloudstream3.Episode +import com.lagradost.cloudstream3.HomePageResponse +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainPageRequest +import com.lagradost.cloudstream3.NextAiring +import com.lagradost.cloudstream3.ProviderType +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.ShowStatus +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.addDate +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Decode +import com.lagradost.cloudstream3.mainPageOf import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.newHomePageResponse +import com.lagradost.cloudstream3.newMovieLoadResponse +import com.lagradost.cloudstream3.newMovieSearchResponse +import com.lagradost.cloudstream3.newTvSeriesLoadResponse +import com.lagradost.cloudstream3.newTvSeriesSearchResponse import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson -import java.util.Locale import java.text.SimpleDateFormat +import java.util.Locale import kotlin.math.roundToInt open class TraktProvider : MainAPI() { @@ -25,7 +46,8 @@ open class TraktProvider : MainAPI() { TvType.Anime, ) - private val traktClientId = base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==") + private val traktClientId = + base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==") private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2") override val mainPage = mainPageOf( @@ -77,7 +99,8 @@ open class TraktProvider : MainAPI() { } override suspend fun search(query: String): List? { - val apiResponse = getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") + val apiResponse = + getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query") val results = parseJson>(apiResponse).map { element -> element.toSearchResponse() @@ -85,6 +108,7 @@ open class TraktProvider : MainAPI() { return results } + override suspend fun load(url: String): LoadResponse { val data = parseJson(url) @@ -94,7 +118,8 @@ open class TraktProvider : MainAPI() { val posterUrl = mediaDetails?.images?.poster?.firstOrNull() val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull() - val resActor = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full") + val resActor = + getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full") val actors = parseJson(resActor).cast?.map { ActorData( @@ -106,12 +131,15 @@ open class TraktProvider : MainAPI() { ) } - val resRelated = getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20") + val resRelated = + getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20") val relatedMedia = parseJson>(resRelated).map { it.toSearchResponse() } - val isCartoon = mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true - val isAnime = isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja") + val isCartoon = + mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true + val isAnime = + isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja") val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko") val isBollywood = mediaDetails?.country == "in" @@ -163,10 +191,11 @@ open class TraktProvider : MainAPI() { } } else { - val resSeasons = getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") + val resSeasons = + getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes") val episodes = mutableListOf() val seasons = parseJson>(resSeasons) - var nextAir: NextAiring? = null + var nextAir: NextAiring? = null seasons.forEach { season -> @@ -208,7 +237,7 @@ open class TraktProvider : MainAPI() { rating = episode.rating?.times(10)?.roundToInt(), description = episode.overview, ).apply { - this.addDate(episode.firstAired) + this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") if (nextAir == null && this.date != null && this.date!! > unixTimeMS) { nextAir = NextAiring( episode = this.episode!!, @@ -251,7 +280,7 @@ open class TraktProvider : MainAPI() { } } - private suspend fun getApi(url: String) : String { + private suspend fun getApi(url: String): String { return app.get( url = url, headers = mapOf( @@ -286,14 +315,14 @@ open class TraktProvider : MainAPI() { return "https://$url" } - private fun getWidthImageUrl(path: String?, width: String) : String? { + private fun getWidthImageUrl(path: String?, width: String): String? { if (path == null) return null if (!path.contains("image.tmdb.org")) return fixPath(path) val fileName = Uri.parse(path).lastPathSegment ?: return null return "https://image.tmdb.org/t/p/${width}/${fileName}" } - private fun getOriginalWidthImageUrl(path: String?) : String? { + private fun getOriginalWidthImageUrl(path: String?): String? { if (path == null) return null if (!path.contains("image.tmdb.org")) return fixPath(path) return getWidthImageUrl(path, "original") From b702b7b1ecfc254dd9b3f8a408a8092452c0cf7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Misael=20Jim=C3=A9nez?= Date: Wed, 19 Jun 2024 07:40:23 -0600 Subject: [PATCH 373/441] Fix DoodExtractor. (#1134) Fix StreamWishExtractor --- .../cloudstream3/extractors/DoodExtractor.kt | 19 +++++- .../extractors/StreamWishExtractor.kt | 60 +++++++++++++------ .../cloudstream3/utils/ExtractorApi.kt | 15 +++++ 3 files changed, 72 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt index 8dcfb859..370dcaca 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt @@ -7,6 +7,18 @@ import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.getQualityFromName import kotlinx.coroutines.delay +class D0000d : DoodLaExtractor() { + override var mainUrl = "https://d0000d.com" +} + +class D000dCom : DoodLaExtractor() { + override var mainUrl = "https://d000d.com" +} + +class DoodstreamCom : DoodLaExtractor() { + override var mainUrl = "https://doodstream.com" +} + class Dooood : DoodLaExtractor() { override var mainUrl = "https://dooood.com" } @@ -56,9 +68,10 @@ open class DoodLaExtractor : ExtractorApi() { } override suspend fun getUrl(url: String, referer: String?): List? { - val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/... - val md5 =mainUrl+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/... - val trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random) + val newUrl= url.replace(mainUrl, "https://d0000d.com") + val response0 = app.get(newUrl).text // html of DoodStream page to look for /pass_md5/... + val md5 ="https://d0000d.com"+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/... + val trueUrl = app.get(md5, referer = newUrl).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random) val quality = Regex("\\d{3,4}p").find(response0.substringAfter("").substringBefore(""))?.groupValues?.get(0) return listOf( ExtractorLink( diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt index 77d98e49..551d1ef6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt @@ -1,34 +1,56 @@ package com.lagradost.cloudstream3.extractors +import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.getQualityFromName + +class WishembedPro : StreamWishExtractor() { + override val mainUrl = "https://wishembed.pro" +} +class CdnwishCom : StreamWishExtractor() { + override val mainUrl = "https://cdnwish.com" +} +class FlaswishCom : StreamWishExtractor() { + override val mainUrl = "https://flaswish.com" +} +class SfastwishCom : StreamWishExtractor() { + override val mainUrl = "https://sfastwish.com" +} open class StreamWishExtractor : ExtractorApi() { override var name = "StreamWish" - override var mainUrl = "https://streamwish.to" + override val mainUrl = "https://streamwish.to" override val requiresReferer = false - override suspend fun getUrl(url: String, referer: String?): List? { - val response = app.get( - url, referer = referer ?: "$mainUrl/", interceptor = WebViewResolver( - Regex("""master\.m3u8""") - ) - ) - val sources = mutableListOf() - if (response.url.contains("m3u8")) - sources.add( + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val doc = app.get( + url, + referer = referer, + allowRedirects = false + ).document + var script = doc.select("script").find { + it.html().contains("jwplayer(\"vplayer\").setup(") + } + var scriptContent = script?.html() + val extractedurl = Regex("""sources: \[\{file:"(.*?)"""").find(scriptContent ?: "")?.groupValues?.get(1) + if (!extractedurl.isNullOrBlank()) { + callback( ExtractorLink( - source = name, - name = name, - url = response.url, - referer = referer ?: "$mainUrl/", - quality = Qualities.Unknown.value, - isM3u8 = true + this.name, + this.name, + extractedurl, + referer ?: "$mainUrl/", + getQualityFromName(""), + extractedurl.contains("m3u8") ) ) - return sources + } } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 5d696d33..1302453a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -17,6 +17,7 @@ import com.lagradost.cloudstream3.extractors.BullStream import com.lagradost.cloudstream3.extractors.ByteShare import com.lagradost.cloudstream3.extractors.Cda import com.lagradost.cloudstream3.extractors.Cdnplayer +import com.lagradost.cloudstream3.extractors.CdnwishCom import com.lagradost.cloudstream3.extractors.Chillx import com.lagradost.cloudstream3.extractors.CineGrabber import com.lagradost.cloudstream3.extractors.Cinestart @@ -106,6 +107,9 @@ import com.lagradost.cloudstream3.extractors.Odnoklassniki import com.lagradost.cloudstream3.extractors.TauVideo import com.lagradost.cloudstream3.extractors.SibNet import com.lagradost.cloudstream3.extractors.ContentX +import com.lagradost.cloudstream3.extractors.D0000d +import com.lagradost.cloudstream3.extractors.D000dCom +import com.lagradost.cloudstream3.extractors.DoodstreamCom import com.lagradost.cloudstream3.extractors.EmturbovidExtractor import com.lagradost.cloudstream3.extractors.Hotlinger import com.lagradost.cloudstream3.extractors.FourCX @@ -227,7 +231,10 @@ import com.lagradost.cloudstream3.extractors.Zplayer import com.lagradost.cloudstream3.extractors.ZplayerV2 import com.lagradost.cloudstream3.extractors.Ztreamhub import com.lagradost.cloudstream3.extractors.EPlayExtractor +import com.lagradost.cloudstream3.extractors.FlaswishCom +import com.lagradost.cloudstream3.extractors.SfastwishCom import com.lagradost.cloudstream3.extractors.Vtbe +import com.lagradost.cloudstream3.extractors.WishembedPro import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import kotlinx.coroutines.delay @@ -777,6 +784,9 @@ val extractorApis: MutableList = arrayListOf( DoodSoExtractor(), DoodLaExtractor(), Dooood(), + D0000d(), + D000dCom(), + DoodstreamCom(), DoodWsExtractor(), DoodShExtractor(), DoodWatchExtractor(), @@ -854,6 +864,7 @@ val extractorApis: MutableList = arrayListOf( Guccihide(), FileMoon(), FileMoonSx(), + Vido(), Linkbox(), Acefile(), @@ -909,6 +920,10 @@ val extractorApis: MutableList = arrayListOf( Megacloud(), VidhideExtractor(), StreamWishExtractor(), + WishembedPro(), + CdnwishCom(), + FlaswishCom(), + SfastwishCom(), EmturbovidExtractor(), Vtbe(), EPlayExtractor(), From afa178a63a7173316cc04fbbd3fb989f77a06515 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Wed, 19 Jun 2024 17:06:08 +0300 Subject: [PATCH 374/441] feat(TV UI): Accounts PIN login support (#1123) --- app/build.gradle.kts | 1 + .../cloudstream3/syncproviders/OAuth2API.kt | 16 ++ .../syncproviders/providers/AniListApi.kt | 1 + .../syncproviders/providers/DropboxApi.kt | 1 + .../syncproviders/providers/LocalList.kt | 1 + .../syncproviders/providers/MALApi.kt | 1 + .../syncproviders/providers/SimklApi.kt | 55 +++++++ .../cloudstream3/ui/result/UiText.kt | 13 ++ .../ui/settings/SettingsAccount.kt | 141 +++++++++++++++--- app/src/main/res/drawable/cloud_2_solid.xml | 8 + app/src/main/res/drawable/example_qr.png | Bin 0 -> 46354 bytes app/src/main/res/layout/device_auth.xml | 59 ++++++++ app/src/main/res/values/strings.xml | 6 + 13 files changed, 286 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/cloud_2_solid.xml create mode 100644 app/src/main/res/drawable/example_qr.png create mode 100644 app/src/main/res/layout/device_auth.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 61a0634f..fc2e9131 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -217,6 +217,7 @@ dependencies { implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview + implementation("io.github.g0dkar:qrcode-kotlin:4.1.1") // QR code for PIN Auth on TV // Extensions & Other Libs implementation("org.mozilla:rhino:1.7.15") // run JavaScript diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt index ef74edfc..3d0bb940 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt @@ -5,7 +5,23 @@ import androidx.fragment.app.FragmentActivity interface OAuth2API : AuthAPI { val key: String val redirectUrl: String + val supportDeviceAuth: Boolean suspend fun handleRedirect(url: String) : Boolean fun authenticate(activity: FragmentActivity?) + suspend fun getDevicePin() : PinAuthData? { + return null + } + + suspend fun handleDeviceAuth(pinAuthData: PinAuthData) : Boolean { + return false + } + + data class PinAuthData( + val deviceCode: String, + val userCode: String, + val verificationUrl: String, + val expiresIn: Int, + val interval: Int, + ) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 5c02e7f7..0551fe6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -32,6 +32,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override val redirectUrl = "anilistlogin" override val idPrefix = "anilist" override var requireLibraryRefresh = true + override val supportDeviceAuth = false override var mainUrl = "https://anilist.co" override val icon = R.drawable.ic_anilist_icon override val requiresLogin = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt index 7ec168da..94537ea3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt @@ -11,6 +11,7 @@ class Dropbox : OAuth2API { override val key = "zlqsamadlwydvb2" override val redirectUrl = "dropboxlogin" override val requiresLogin = true + override val supportDeviceAuth = false override val createAccountUrl: String? = null override val icon: Int diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt index 7552fe9d..00f8d00c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -21,6 +21,7 @@ class LocalList : SyncAPI { override val name = "Local" override val icon: Int = R.drawable.ic_baseline_storage_24 override val requiresLogin = false + override val supportDeviceAuth = false override val createAccountUrl: Nothing? = null override val idPrefix = "local" override var requireLibraryRefresh = true diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index fdbe763a..4249f949 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -40,6 +40,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private val apiUrl = "https://api.myanimelist.net" override val icon = R.drawable.mal_logo override val requiresLogin = false + override val supportDeviceAuth = false override val syncIdName = SyncIdName.MyAnimeList override var requireLibraryRefresh = true override val createAccountUrl = "$mainUrl/register.php" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 08c8588b..4385fa5e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -22,6 +22,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AuthAPI +import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType @@ -45,6 +46,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override var name = "Simkl" override val key = "simkl-key" override val redirectUrl = "simkl" + override val supportDeviceAuth = true override val idPrefix = "simkl" override var requireLibraryRefresh = true override var mainUrl = "https://api.simkl.com" @@ -267,6 +269,21 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ) } + data class PinAuthResponse( + @JsonProperty("result") val result: String, + @JsonProperty("device_code") val deviceCode: String, + @JsonProperty("user_code") val userCode: String, + @JsonProperty("verification_url") val verificationUrl: String, + @JsonProperty("expires_in") val expiresIn: Int, + @JsonProperty("interval") val interval: Int, + ) + + data class PinExchangeResponse( + @JsonProperty("result") val result: String, + @JsonProperty("message") val message: String? = null, + @JsonProperty("access_token") val accessToken: String? = null, + ) + // ------------------- data class ActivitiesResponse( val all: String?, @@ -1045,6 +1062,44 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { return simklUrlRegex.find(url)?.groupValues?.get(1) ?: "" } + override suspend fun getDevicePin(): OAuth2API.PinAuthData? { + val pinAuthResp = app.get( + "$mainUrl/oauth/pin?client_id=$clientId&redirect_uri=$appString://${redirectUrl}" + ).parsedSafe() ?: return null + + return OAuth2API.PinAuthData( + deviceCode = pinAuthResp.deviceCode, + userCode = pinAuthResp.userCode, + verificationUrl = pinAuthResp.verificationUrl, + expiresIn = pinAuthResp.expiresIn, + interval = pinAuthResp.interval + ) + } + + override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean { + val pinAuthResp = app.get( + "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$clientId" + ).parsedSafe() ?: return false + + if (pinAuthResp.accessToken != null) { + switchToNewAccount() + setKey(accountId, SIMKL_TOKEN_KEY, pinAuthResp.accessToken) + + val user = getUser() + if (user == null) { + removeKey(accountId, SIMKL_TOKEN_KEY) + switchToOldAccount() + return false + } + + setKey(accountId, SIMKL_USER_KEY, user) + registerAccount() + requireLibraryRefresh = true + return true + } + return false + } + override suspend fun handleRedirect(url: String): Boolean { val uri = url.toUri() val state = uri.getQueryParameter("state") diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index 0e8160db..e0762cc5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.result import android.content.Context +import android.graphics.Bitmap import android.util.Log import android.widget.ImageView import android.widget.TextView @@ -84,12 +85,14 @@ sealed class UiImage { ) : UiImage() data class Drawable(@DrawableRes val resId: Int) : UiImage() + data class Bitmap(val bitmap: android.graphics.Bitmap) : UiImage() } fun ImageView?.setImage(value: UiImage?, fadeIn: Boolean = true) { when (value) { is UiImage.Image -> setImageImage(value, fadeIn) is UiImage.Drawable -> setImageDrawable(value) + is UiImage.Bitmap -> setImageBitmap(value) null -> { this?.isVisible = false } @@ -107,6 +110,12 @@ fun ImageView?.setImageDrawable(value: UiImage.Drawable) { this.setImage(UiImage.Drawable(value.resId)) } +fun ImageView?.setImageBitmap(value: UiImage.Bitmap) { + if (this == null) return + this.isVisible = true + this.setImageBitmap(value.bitmap) +} + @JvmName("imgNull") fun img( url: String?, @@ -129,6 +138,10 @@ fun img(@DrawableRes drawable: Int): UiImage { return UiImage.Drawable(drawable) } +fun img(bitmap: Bitmap): UiImage { + return UiImage.Bitmap(bitmap) +} + fun txt(value: String): UiText { return UiText.DynamicString(value) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index a8358d0d..d227f9f6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -1,12 +1,16 @@ package com.lagradost.cloudstream3.ui.settings +import android.graphics.Bitmap import android.os.Bundle +import android.os.CountDownTimer import android.view.View -import android.view.View.* +import android.view.View.FOCUS_DOWN import android.view.inputmethod.EditorInfo import android.widget.TextView import androidx.annotation.UiThread import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmapOrNull import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity @@ -21,6 +25,7 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.AccountManagmentBinding import com.lagradost.cloudstream3.databinding.AccountSwitchBinding import com.lagradost.cloudstream3.databinding.AddAccountInputBinding +import com.lagradost.cloudstream3.databinding.DeviceAuthBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi @@ -31,6 +36,10 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlAp import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API +import com.lagradost.cloudstream3.ui.result.img +import com.lagradost.cloudstream3.ui.result.setImage +import com.lagradost.cloudstream3.ui.result.setText +import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -51,9 +60,13 @@ import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.UIHelper.toPx +import qrcode.QRCode +import java.io.ByteArrayOutputStream class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback { companion object { @@ -134,7 +147,109 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome try { when (api) { is OAuth2API -> { - api.authenticate(activity) + if (isLayout(PHONE) || !api.supportDeviceAuth) { + api.authenticate(activity) + } else if (api.supportDeviceAuth && activity != null) { + + val binding: DeviceAuthBinding = + DeviceAuthBinding.inflate(activity.layoutInflater, null, false) + + val builder = + AlertDialog.Builder(activity) + .setView(binding.root) + + builder.apply { + setNegativeButton(R.string.cancel) { _, _ -> } + setPositiveButton(R.string.auth_locally) { _, _ -> + api.authenticate(activity) + } + } + + val dialog = builder.create() + + ioSafe { + try { + val pinCodeData = api.getDevicePin() + if (pinCodeData == null) { + showToast(R.string.device_pin_error_message) + api.authenticate(activity) + return@ioSafe + } + + /*val logoBytes = ContextCompat.getDrawable( + activity, + R.drawable.cloud_2_solid + )?.toBitmapOrNull()?.let { bitmap -> + val csLogo = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, csLogo) + csLogo.toByteArray() + }*/ + + val qrCodeImage = QRCode.ofRoundedSquares() + .withColor(activity.colorFromAttribute(R.attr.textColor)) + .withBackgroundColor(activity.colorFromAttribute(R.attr.primaryBlackBackground)) + //.withLogo(logoBytes, 200.toPx, 200.toPx) //For later if logo needed anytime + .build(pinCodeData.verificationUrl) + .render().nativeImage() as Bitmap + + activity.runOnUiThread { + dialog.show() + binding.apply { + devicePinCode.setText(txt(pinCodeData.userCode)) + deviceAuthMessage.setText( + txt( + R.string.device_pin_url_message, + pinCodeData.verificationUrl + ) + ) + deviceAuthQrcode.setImage( + img(qrCodeImage) + ) + } + + val expirationMillis = + pinCodeData.expiresIn.times(1000).toLong() + + object : CountDownTimer(expirationMillis, 1000) { + + override fun onTick(millisUntilFinished: Long) { + val secondsUntilFinished = + millisUntilFinished.div(1000).toInt() + + binding.deviceAuthValidationCounter.setText( + txt( + R.string.device_pin_counter_text, + secondsUntilFinished.div(60), + secondsUntilFinished.rem(60) + ) + ) + + ioSafe { + if (secondsUntilFinished.rem(pinCodeData.interval) == 0 && api.handleDeviceAuth(pinCodeData)) { + showToast( + txt( + R.string.authenticated_user, + api.name + ) + ) + dialog.dismissSafe(activity) + cancel() + } + } + } + + override fun onFinish() { + showToast(R.string.device_pin_expired_message) + dialog.dismissSafe(activity) + } + + }.start() + } + } catch (e: Exception) { + logError(e) + } + } + } } is InAppAuthAPI -> { @@ -227,23 +342,15 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome server = if (api.requiresServer) binding.loginServerInput.text?.toString() else null, ) ioSafe { - val isSuccessful = try { - api.login(loginData) + try { + showToast( + txt( + if (api.login(loginData)) R.string.authenticated_user else R.string.authenticated_user_fail, + api.name + ) + ) } catch (e: Exception) { logError(e) - false - } - activity.runOnUiThread { - try { - showToast( - activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail) - .format( - api.name - ) - ) - } catch (e: Exception) { - logError(e) // format might fail - } } } dialog.dismissSafe(activity) diff --git a/app/src/main/res/drawable/cloud_2_solid.xml b/app/src/main/res/drawable/cloud_2_solid.xml new file mode 100644 index 00000000..3810b4bf --- /dev/null +++ b/app/src/main/res/drawable/cloud_2_solid.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/example_qr.png b/app/src/main/res/drawable/example_qr.png new file mode 100644 index 0000000000000000000000000000000000000000..18decbac49533dcd2890ac8112305b19d05cfa11 GIT binary patch literal 46354 zcmeFa3pmtk_cxv~gJ$H^FvwvhVn_xlr-X5?90nDoCgqfBqm !=O#FGnJfjN;>Qk zwbM?{6~dm*X&0jqB_x$8nRnfz_S617`}zHz|MmR;*L%J1e_z*r_S3by=lfmvTI*hG z-D`c;`W|q1b)17&#?P8HYmT#%oyV+Mq5||k2{HIDM=`N$;QvH+c{tK%o$pi`nl+0& z%h}GxYwyP4XRmiEudie?UVl-4qD`-GBW|E?e9W5qQTa>mqQgKmZV{6RXm#bkj4%CnK)EzRLly=+DLiP1^x^Y7~ z?X&uR6@BxZDRJeVS5`-|jQML=AN;zd66g*(f>7;+>Jk{M6l+T9{*ilAU)(%)PODuz zF-;49doOkR%WqBW;`bk4MRmq)4Z72}i_?FK&I^*E`g6twi+w-o$n0H-#Zy+#TGOgi z!8HrvV@T^AiQ`2txLwYBt6p^YEZ7?dEJ^JA2VbAsyWf2}XbJBbCDQ>B5RXqG z>JvLzq>z(Wcxm+I_b(YPakG*3JaK)X10xj}Z#_rInkB2V#W580@c5Fjr!VMy+TNly z2KZWPpRLP0&v(QLgZEwAy7hRQ#3b$uS;F*WN0DXd=oO}Dj~oHK0$1|)EB08e_53TQEEkP_JjB>=M(FkQJ{2=?EW}+Bt^cgW?;L-*p2s;wo&U9 zlo#7Syo*{Z!%LIrbiT`BsdXm);)aKSjW;SIk@uhu|G7B6smE}zE_y9?lFqNY^Gjy* z+Oz5(cg;TiV&}*7RzmdNn=i)~Q{eeEb203g$_g6Zv?l9(?9n*Kcp4|JU(mC!PkWiH z2<5$9nDk@`A=;F@-60tQxG@hb{^S+ma?ak+qcf|mLhtR8rj~Yg2n72-vD6GO!qBlt z!<~Aay^m3!u!d4$FMKRht5jCTV{7!r<-Ru3Gy%6;24cgW!2@sf2xi`E}foJ7y0=DP;9blpBjoztqxijO(=Y@7fCrW@bEW_rI^^FsfB`D zAI5%nqm_0SJWpA^$wpdw&hQNse7dH7TRgYzH^*r)ky`iZBXP1nIaoWL9E#$H;Vf&W zY-4QEbH!~7-J=PJa}A|io@#50q<{*KnNG8yS77eFiq{Oo_!cU~nVp0Wdf-IMTTY`BQ`m3I? zm@9%`3sWMS?!Qbx0=XwaToLNWB!*3H{ozZ^mKn0+woBK2nV`RVqqBcdRAFL_Tfb2f zsYlYc#im75v3;R3JVHN{PmJ51=o%a8Fw=B@ zrV-WBcauJEOs-Kdre$=dzVUtb=gC3IUE|Jpetm8%ql&_e7)d$QBeFD!lPTvBgPmm2 z;GYoX$DJA_=EKD5!9qg|XWqH#)m%}Pz}?<4kqqBts!tJW3THob-C*~(Q%t;d!8`SlA0ZE=gl@4lm&Wn^EnPDy@|mO)F_ zESw_63pOXyyVB_I#xM5yYiskL_m9_oPL7owXV-mDpWh^Sz|=aKL}dCmD=H@;CbSfX zd6SF}q+u}mO8A^U7q@APjKN`-+UieqzINL>YvRDg8h&wM(-XeXBR^Nr@=WfQ#AI|Sf&#HA=9djWh2MX; zdGj8%=&(6tD77{?upv;toii95ER!{m;3l2n;Q zV(})TxBE_Wy@>Gy6Fv)hdM5uuzjd_Qf?Kj#3WMA*c;1an><`^*8mJY8eG%S+eWIb` zIOKxr#+kPhCYG(p(9l}ME$W>FDk-x;$9Q$wMeZMpxiztRW?ik$wR^8ljn-0_NwK{b zzNEB_#l1h~tV6j$QZh^y>jggJE2%0EaW2n`R`oEQKUQE;}FhMEbvS5_O~^{8g53g+lAH=yX@ z!qlUkWXnT`U!*z{B7#kU%FRxgwm6YVS|F8kooY-|U+KDM=V97LeZjTR77~RyxlKSZ z0yzhGtZ;g3?~q<(gZRwS!>8=F$*QK3xyOqNox3Oc>RzOFcK4;71ql|P!`b<%-Oi=q z&ChkzBPn0%&D8x~c*C-Aj06ty8Bf-&YVWj_&dBz`E$zui3QTSO^C@lfcF3oHe%nsei{g`G2ZzsLNU@BtfR_b& zgOpx^8>sQzIr+|V1CmpVGci)Q)kFmck(5SBU~Wc6VTgk<-$)t zl#R!|w`WQATV;7naA&_}QSpS5cw0Y2q$v?B6HCs#ZnlSYqLxK8HH^{H%A(;H!*0&6 znLZ+*YxM=nc;sj5rZ$N%HYI3TcD)Aj4x}HJ9&gAJtkd7|GJI{M(FzDuZ~GW7&CPt3 zbU#~SY3Oi;euZ^b#^(&FZCNc;K3SauYjp%M4su@_>-eL3lDDtTT|ax5*tC+9ND51L zZ&f{A`z#@tXsn}-Us2ue&b1YplQTS6RF7j4eY?7z;Vd=5CCm-bRyNPlY(G5}gk^Xu zDj6h;>+Id7$XeCaWN$M&zjE!@J@`eEd`}1U?-xZD*3~#d>*6KQL1daZSI@`eQaY75o{hUOGmK ztrUDWoXPduA*S@;YCrx7=tSlN_}ge|yR~$F`gmVY);}@&edAKX`D}VS`iSCuEaE z=iA=60Ga^27ivPlg?KTSJrnM{;>;{yKIol$kJq%VKelZng^XV;aT?5XbeJ{KyRdZ; z0q3Hyv$CYU%NZY?ZEwB2CFMGXU(9REFd;-!hh6GE{EvF)7EK7|`a4SF^zmjsWA=Vx z(SuJPAUD#I^i-05X08V=T&Uuu+A*rC54C<`jN2^V3r^K|7lLrCM0I%X?zm6| z??M0FA_rMo{kArEe-b#|Y3akYZ6p=^aWs@BjmqO~ZRFW( zMtzZisEhds0Zcx*#Qch{%{(v$mA}H@O0$&)i?SF6Q!)R<=>OT4T$qYHOfhzD%qy4* zB7T1gf4f2pmLuU7_zG>_j1qwyfY?Y{DiuR8z8X+e*h+WZ!!fgP zwvn!qXc6aEtR*8m@iq{)c75p4^o3fd%%Z6vj~~^{{Wi$h`}q;WsO|3Xwv?H-y>t%! zYG3Rr+Jsu+F}$`8j2Pi~X0DCXZu%Lrl(=02E@#63LP^sTO`{ddTg2U_w_UxIe`~|D zRw_h9w4fW0w<8VRGXHVS#in2DF~DmJ^K&lqOZpOH-C=F%)Tom41VJ!RS7o3#>biYF zyV7J385sp$2ZoS;PQQ@BEsg!>a~cp7Nt(!n1jqO1)XvY(zg|(Tl-J=WVH6+@Y>sH# z_;nBLXB)y!799CJM*_SSGC$3CUqPsY7w+I`r90Q3qHWQZmtR_#;)4a^9xu!Uu?+96 zDoknYo(L6kOLp(w-wzz<-oV9m9Y4|?&{GZ9-aNb`b{+r_~_q|3npYS_{(}w4h$q*W{hi5?dhqM775OEx-}8} z7z92O?%73flwujYVgqC!Rq#2@;5_!Md2`#Qdw8^D+!sC?=Rsp9~dHk_{JbL<0=CfxDgmg^x3ef2(DRR z!|{Bu$KMX_uC?fHLYcc0jIpazf3u;ndvB`FF zBINuxXWn*TOECG?Q<74pbU&!4HdN@Cigw+W=OWq&D}?-=ae%HyNm{aC$MLP3BZ#-2Qm3w((15rf z|3l(_0e9b;3xd}wf(Qz!)%Tps-139OwyIXSM?#i5ORG)gSCrMdQn&jmudKoV6?$mWlHkJ*5 zj=!o3?YSZp5Emi=aJsFzaFm77UDWt!Yn-+cJqwAI?QdxQHZ|vmPcQ(m%d>E3{9}H6G*<+8fOV#16T)&IvD0Kh+7OqEh9!k zkp1(g{{U8b_USy+WHDV8LmX4G;Y7^u5Ed2AL=wY#Zqpyl`&{4(LW}K{p5CpH4EYNB znUaJjnxJOHpOtA}Yj;;h*9$di5JFyKL?;BK5P)izEaQLd69zd*|LB#9sy7Tx@Npv@ zNcrafXT+UD!OM8yEfO0{UKZo~kOPUhFMA$l`6ndb!k-@C6wHxju`bC(af&H2A#iph zFT+KIWJEDZgrL{URmE0RJ|mk&OXmG{eFvOb_pf&4sJ7rACPG|bPOx>FtwVWm$9l(M zP_rM`96WY93Zkb^!@qxAB-EscAFg7Q5p59B{^6#3oRhhl$lFkS(lefC&tqGC96-wV z9}@Qv-5jE-UZgc@PH<=3+V^7J**-@^eIJr37p@@$$X`Hw#AtzV@|%6l|C86$)y^nN zz&oyLAlZ(+?EVD|3KiYr3$Yfrvjo?rqO#y>MPN=Q#^tjqrC=&w1s5)L2Gz1Zd-L}F zWjM=K`#!w+tS}RYUhzetK~z;?QYVbC~cpeum}UEM%*`|n!3sZ`~ukKJy*t=+#-}ZhzKsQ#6SS>*auXY zrd|wbcPYc_!7xTPczbm7z^8bLi09~4gn@yFI||-3!prY41``|prSdVDsifTvih+Y< za%mVNCg;AN|DMAvlOS2}|EzG!f#u81OY>q619y~G*Z1~$OuQ9{3)4}Wlv01YIU-S4~oRUU&sQ$K}7k~s1FZu(DU&# z9173Z(uggaR!VG#@{g+J_w1SBBXUM%w(o{zsly|Uoe)XH@n!6-opz@-*r$L3!*M`C z9^BDQx&8_4+mdiOLE%6NkOxJu*T-&5^O)M_2saIE#YHIWv?upUr8KIth&5(u)-RvQ zr@uCQDLoxv=L6AF3EtG-&My;->KQ!}45tOLTT|zN5H$W-2!3zUp3oiM2D%&{=tXss zW1^@|lA`v#wD8G7Y1IWKASG~v+i7fX&lK~zJ2`O?RJ~ve6Y*(MV2R*(@!Yc;M%GLd z%)KB|%8}}*axAR4MzB~3v1z|85g17k`;L*ekj3FyXWpUZRAjKDo(?W za%AizWhv-UhDz9-Z!sW#@#AN0aW*NJX(0rq8@WpYa#DneK`B&@QH}n7AypAAY!ejV zy3gs{TL!v0#zuD=dV=IHN%ab_g*&49=DtKN9mtG;hrPC*PaAZH{cSTh{!J~RaYTPu z4>3}jff%XyJtNt+XW~U&6ueP_z#qkK=4T9?MMP?=_xlWkgK7MAljPzE85xi2a!Z$2 z-=ElOwOt^52M(x&(V2K#2cnoWS(u8JVS6k4gQe;07%3%&qGkdrwVBX~ z9*>8FHo>&srt5c_G*lNH<)MOVi8+ur zu}$C)1@x!3WLb{{W)M5EWKEanhrT|MjCG%%XO%V{8_-`UVNL$kAXb|F ze5DMNHF-C-|Hm<9c-|G1fk4kQESS1bZ9TL)dY5B5O?;o(;O&MDv7}nf4P81LMH8wh znbiQ=s%q_g$*woY9LU`H)M7;SW9y5J%O{Omj@j%N-Tb9^Yy~{{#ZV0*stW`Bw%oYhvh#lk7 z<3c49mH~yxhQm>SbprOm({P(47`>H~NQaG_Jt8oizHW#e2i~CM;@5SgrDfpsXf4ev z$p*>R&mlv3Yx3hwnHG&q37we-q31ClXM;1WBhRHJ)GJ~RK!~FLqGz5h`H=PeQeidY zD`8u7|0ZmPvww5!|K~dPiq2QxQa=x1C{bAaEnl@#(wuDXbqTb@jo6P@LKD+4guwWE zwFH*QTZG%;sbSRHtik~g6M*Fiq%7DaN7eMB>!FmYl~At$s|?X9YyK&;HdBHcqxuBz zxX6+!P<714VhX12A?%1A*dHqeW*NJFNXGqxIcK4u`tkhwTvf~L(nM=WSOvpUnQ}_u z)FVYhX;_~a&$=UP*IRxP5^)w82D9Pk!smNSPzAT1-@YVcs)}jbn6k599nztZ#+wdn zVc2=w=Wo%Kur7>;;?SlBNr0ei0D?125jA_qd%}~P$-GvpIk1sibnE|04J33*M6v)i z+vh)5$X&^`w7*Hw;+nOL| ztb+o)kl+sz#GP_9gg6?3ZQT>Yk|Cbxdd>AT4H>e5(o>_RjQjLfgalH+vl1D)Qyux}hM_Jv`W;sR}RZvT4$k0dTBHf8EVgz!@dX!xGao$p7i4M&=hzMK&{l z7^v)IXsp7Bxcw_?*pShilU;fMhHW0-EKLsMj?xrF8l=|RL=QcOb z)o^cVo!c~80?HK!N}(+aF6bcI)~T@jcN1r%P~ljZnY9;TmBOThS_$#vZlWN-i@>8pH;!$kgqdSG*QNT~tn`187z=ci)*2$ii=>pb;3**>f+Y^^ zqTsB_g_|O<|LLZ}NWg223`7j*mWrR8#NK~N%`*OIaJ{Arl9Sy{FOZW6{I;H@1b8sF zY3ag*M#k&gWcbr2d5r?a1iEX1;(m2ijLf7crp@XAXyR71ElYHyKvO{q32?v5YbGE_ zR19qElE`0Z7AH;_v?_xgKOU!7yNZx-k5V|io4R&7{GxrliAA)5vpBqH4;Ky%+1=Kf zf5C7wpyYUBBDfNb)cwYCCeyj9Dnj5Q03 zhu`89+5yqFcRX&^9C!#*EDHW~R`G2!9G3CsvCo4bb9EUfPTVbD$5ZT+_R?S=?CzY| zYVB~x-zAG31u$?PZ$A|vCgoDfaGi^#lmsrR*caJf*JQu>Fw2ZUIYMrdQNm6_or~rh zA+^n?gQSYp&*J*0{6t?9$<59$FE>LK zRw=m!E;>rOv9d3K_vuB>c%{|xO<&wM6}`g+h#!Q)U2P_#O8If^wegcZwvRHp$m4Obif%upqJ#ulC<@Q8@8l8L|r{l zTR%*9kmrS20g7bWLJtn2KK>mNGkq~pO5(PKrbJ2Xe83=3c*VQCZ->+llDRr&OIAU4 z)c^hcpbOVSDQAb^(*76nW79Un;Yu|$c7_{zm{1zEg~JH$)Ic%{StI&;B+%=tsil)$ zCF3#MNb)DZ3>tX>Eagu-2TKdd5D>c30|p59-`X{Z@TR?=8ft~xX6ErWrSDJRAjZ&Di4DoN~xT`;{3!}O#kvR`*g6L zhRIqw{$3(dpdai`5=YfIP+Mn`?$8mk>GKG%Ijva}w@ZPpYm-bdk{Gj3#zR>9r=5e4 z74(JLZhi*X{BP|VTxm76y#x%kM2EX6S#LCxoIZYF7BeC#KCRPl(OG_RqV=i1`vy?7 z$b0}z7$0ic=gJ_X@K>U)O?}%}r*V_33F(9SUUdHWtjd3!jO{%e_@iMzWtVR1q&qEW zF|Y$rZHqg?;C{dJKb?$!ch?+&GKn39JaLf{UOcH^RW6CKT$Y!XArp4T>|DSSW`xWj zCr?=mlFOCRuc{t(U&(wh0ja&e5OpI7u2hlDiaDj344mmxX~+#S-S^Cx1f?Zs6W&|{ zdjIJxq3rqhI|r}nUA@XG(j&Zkg6hcG}1}Nfo~U2GpEZEbtfXVRDeklggu#$@QNcIX+rtqJ z|BWLpO&O<32paIdGs)&6Dm*GUC6_Bsvbe5b2cZGYSG{? zm-ene3<}dWCl1_zI=$7&_%xOVD)s;_YB&wj(<8ubT)&{~wT=L?myd0o0NX{9+# z7Lns)N+p;bNb59Q3$F9*21OaYh!6G+r2Rl#D@F+yBLNu7K%^Wo`c(hj zP~z7?yoYj%4Zr8i#X=Au{xb%^ciIAGsmK-s2S4ZRBA;ji9|_}AGf;qll0kBSS?%wc zbI)1R_vwqqtT?yn&`S)eB3rarWn)n8K`jeryZoiKEeKNp7@#@#+cq}j8l8E!z^Oq4 z(so5vG&w%6XNqt-mx-G_MLEkdG-m|JrBzj3!iVVfK~4{WOvIo;4w$^*&H3~HQi3$+ z@H?$gbYUy)t=i?LP+JH%B_6>S;2^*<+SC)l>MWcQ{sVNoDYUyUybi@RDDAI;zZar7 zwR&A@%fDzjHK?Lgtd>5V&ufiKk^Cxjz0OXPhX6E#($)97Xj}AB#9>js`RDmRNw?KO z-L50k=t#wRD95xmKZiWn36FaF23b^}+U531uJi@LmpA|8g43W)uUVE!0I^(rf5nB8 zUrL;PdBsPiRUHDgdpkNUhmPjHr!OkOck3B6k{+dmK!e~pB~iq>K$(a6J%7?Z@<+Rc zW9!X8|4vOkx_cTivv7(4l`}~l&)KhYOb9uv^g68@>g}~rX)=%pkN25mRf243VGP84 zeC-$jL2~pm85Jpjb=;J^ZfH@TE#L$Ko)^zReHYIT9k+jDVuK%niUP|dT(%8eE&UC| z^zq8&9h<`U)Lzv8?mQH(`GD`{Fn%zTHV7HI-$g==E$ktXb_wt;!@ay<`>MKU3#O!S50k{I{#$#|)uWGML=xO^2C;=?9YyQ5UTEzRA zP;n1*I=;gDQ4LfuHiwJh#q`Y)YGCG|G?F-wL5>xkf_;oJfGWp5@J~qpLsp?sh3nR% z&V}-r5LfLC7~v2A1%^37wW8)gnmeGbdMJs8VGOSI4zaYVcL#TWPtoPdwPR)JJ8wtD zGghAJtVL-<8Wg`5m#$iH4mvY~jV_vgc_KJP3}b}Z1tE-ARxNgD5<{wP+2?#_DiRX+ z(tt5-ki2~Nb@>~pf>xLJ+ot@4dRKS3{n{UR-TihsK-AKqwW%oQ&lbys3~)NAKD#`J z2wl0w@(zA#O$Jb+Be)##YBb7j(`CU~ErLJs z4_Sd#7z?yHUi}s8ys8mE_@7-e!Gu7S&c!zg;UJ}ya9aQ!hz&r+IfdlVv>DLVa7T_- z$Gz4N--vMl{IM;ddI6Yc2nt&^0RXZrq4BQnCJ*mC0Fl`^L{&bzQ{)UOf=Xpt{xxXc zfkFtc9^k$Z=@}_D`E9so?YC9lKu6CN@G)da^kzu86i*#n;{MoY&z1bu@0=PDn3%cP zfpTsea<~$w{{cH;*{qD z*zHWFe8U1r8ntzA*S+Ue({;xhg1kWyDRfD31~VGjOaxoAP8hc*-G{h+D}sLQ{qZ?m zGg7o5HawSrYIJYT%zpW zlngCGmgYp^l*MLzV7dIy!XSYU(#&syN zE6xjVLZxNPuZ=UUYt0hVD@}6SjO9c37`ww=0lbo97TZ}4k};}=V4i+lGbalw@02Cb zJ1j&16%>Ptg9w)KY1;}>QO4rzz)uMM*IyTyPWOITitvkb}^3tW#8oV`;q4Ez;_Twt3M2RJce=VbDhp1n*@?#`(k6=s(z;xr z&^@(naf@?pdA~mx;YlxkT_qX3YeVK`CFuOp8y%RNPG4hMx@_6lSRt=@KypOT7fal{nae-GkSGuv?u>c&7#pVGlHj2H!MS# z&Yn{j^Eg@TeH);QYZ6Y`uKvsKpi~CQ9pfNB-P^y9|578Y$1%Gc_s{jmoqShwQ@9J` zFCzeJh?0dH$ksburx9(T!m8Zbr9X|=vF=7~d;xo{R?pdwDt_|<)^)p-xVi*Ht%n!G z#cZKqp+(D=ivO|>%+AfheqUhaX@dZDmS zXn=qMW&lM;u#lSna*2?yMMIIoJ>_<95HM^aaP#*J z^Kohu;8(niIw)j*@TUzzTFXr4{)72#f;PIx^jdfG-?jnakUzXPDx}rq`tj^G;QXSm zELAxCPb~eLRe!UJxBq{KR;lX>6kf8^8yl%%A}N8vIU!I4qy@@`xj&Rz5sf<1Y#dt4 z*Ew9>YQfLsJ&*MJah5i_!ht;=>W6Ktmw>r3J{xAW)D8{<3QxFLNASbd>*~T_GSt z{fEtbKNKJ;s*pVCb9Aj>Qs?S2V5|BhV8Xc@J++$P0nE5!C9 zSUy{|w^abU&v-(2>c@sa)dPUdINqm$nl7_*Lq*VjDW=pw zJ$=V|306fWU$nSzPBdJ)rM5xikaJO(LKxNGYoxnzP+8~h2SSaP?;+!MT+?=oP#wNMHJfRmSn+toPvoa}r% zp$f^@rlJl}PMrO=JuCOW9bw1IaPl+TX5Y6iksyxy5#wLyLh>(O)UxH(3{3|BU4DX= z!@Lh3EY8pIDIf#~@GA*YW?Pl1@zxC_5yk!T!XF56TxK1IJ-H50UVsq~5E;+bpUREv zX_s$zU!OO7*Z$aaPtSTk6|88fOqew_=JGykjzzGi#!sUCNJj3Ijyr=fLw@X8U?8E?JUuDmlLCiQk6Xtt>~p zxbRkNw8G(m(V0wXXdZ^@anUJp+^%(blwF_dJ}qpyBWZ|qDP7koQEjlOunE$do1T}w zw@#ydaMcrP9l_!`90sAuh7S;7Dm<(rn_>!zN>1xMbSIdg3!4@e)Bz3H=ML66&L z?$wM1VSax1?*|tsW2CmF>|UfzP}5Q2{Id~ADNrM}Zw*gwA8L)W$p}oP@Ztyky=;^x ztfc`02wgUFO@0<%7ke>sv#?*6`B>IuF)`yRY6r_(I>-1BFmiw~woM=iw}w)6!=V+f zRei)uj25cN4uEiaLk(gA4|0k3t4R(0u@LQz)sE@b8xq!G&5FsnkvphP#9% z*B0QKm22ypNt}EdmEha*%5@EjZu}h4h1oSlEMz`xR7D2I=89Spg=kzV%;H)K6~dWb z6HtEU7k=RRP03Hd65UguI_rFInFt7~ZQ~9mcKh426;%l2oXIdcbCki3g$fK!2-vS! zXbI-e>XfbM5-`9KQmHKZV1n9L`@?SZsV{=VFT~Mj^1wWHW(inS!eU}NqrO2>HT153 z3YZ$Q`xraXgTJ=M;&?@VDYCA3C3afp98dHME_6#e$p?Z}iBE*AwTmdE#*$l)ztr(x z;(|{3Vm5SKSeIoos>9s6`%2+@X+xx+F-U|gAoqB~^JaT-k*xa#!}8-rh4`3zi`H`e z7OCU2iO}Q)d766A&VngY<=enymf_CAyuIYKD%<@DX#gxh&yPk;8jI7uy}%vZlVJWF zVHp&f)K;U0orYZFE}uNrfSgh^aE!=7tvkSB?=iO58-fm>d({sQM;#s$hjmOSDpJ zqZ&w%@a1}beTnV*OqtL|0( zsb;gBX(|`P+V7N(3+S!t13wF=tR+++#6NW7vc@lj?TJW5K%K;>bZfHQr2bp4n;cs~_+Xv=|9slbV2SC!bpEIJ6*548zI z_8wOTqRq}75$Jq5)+3xHM>N%iPA1Shs6gX`98;=M5~@u7h(L7AXvyZ@>|Ndq1)iU1 zb)iCzgn9iHM|1_^ynvAAM#GmCYorUU~dMN~@rq@&1iHEgB=>{wsRfu|%#6a`z{=pU; zsk)jk*B5CctLo}% zV+3(O=dIy9EE;~5g zx;;qP>Xj(8l|} ziBu?#MowgAmsS|aSt~xDD?&(=0T|&|a47yt-shqcC0`yuDuT%-pap%_W^b((ZXRg; zcxQAzRDGk-gtey-P~Dc_SqahlNhr~T9j7#)9#mO7KRFwy!7jF)UpjSJkU?H`*r!{l zHHA}g7eq{Ul!yJ=Z}c&d`T2^)4>B?w>_vNktBOlNbI~M52+;&b1lV{;88ed*)+O%7 z(w9+lb`_+&KH?VQcdoE+NsujEr5K8C%4DZY6FmR=5fiTs4rRQcGjwX=TOQltW^3pbz`MN;Sv>-2wGQdtlj0B z6@VjZWPnEU2sHo!8XL>I|~D(Kl(?=nv}?}w}H zUwDtE-(<|gVi(wKmvx^W62U;zOKm-ZGm1bI!3?+Y3mPI7A!HZxVn@`*ro5NE(fwKL z6cW>;b1&VETk6zreI3+`VoK%$CMz=0FtvIYRPg? z!y+_@K(C%eqvrAcgLpKDuubq$IqY6C{?vN?T~?Eqe$S+m6iC?jC*HYk<>AZpEG%UFyZdoL+p@uIN?@5-+ zG2?h1 zQ0LV73#Xg|+`4|hw%%d)j}NZ}j+UHALs-f%2DAvOsJ*|lcq2@G6n#)Pb=;1BtlqNM z3MPM?e2kZ=@QQ<)_F~NShJP@N>TrfbG545VS1;YktW?EN z)#?Xr9pFa#K1zwdS5+|FJ`7;W4@#DxO0WA^nKsb0j4yqWd`|x7>ne-VGwccCc4)){ zKnaO5g((Q11rX9Kc=NSu@wN@8+Ty??ST(aCIEAYy#Y9vd=3^|+$aV#gm5506AW-~h zq#TB34v#v?viD%eOyQ27c3LCgDwsM8snvi(ocze3=qGsv zZI$~45DFXZPn-W|SVfUdshRWXzkL=uEyK|{gLNor#tP25qk>k8{nf25DG*k*u!DRZYOD&u(TlOX%(Cic5J-uts?XT zU@ALvr<2)cdh!G$a{%~b$!DgPHjZpmd`G0|5IZqk=p?KLpL6ih@6(aivN#(eb+$Vr zo?`g5RgWrB{FS*;ehFIpY=#JJ1D+4TL%?+z@*Fezj7LMd>Kk{96%S`pO3hcEEd&e( zj7@^9SLAO8&~?<~35l~_|7n(LXqz*PA2Azpc1@M_dj=pIJ(w#*Fh)I7U19t#Ee#?$-dVyddD@V?b&Ejr(C1Dqr1 zUbv797>o5BtbKM7ui9%M=o=OQCUP6=l*B6a-7cgnK;lQh_JWC`dmJ9gpH(eX)2#f@G17fUSq7g~+Qa?74FN^wfpm_a`q<+Kx4Td6PR^ zC*^X}uP<)BftE*w%!@S0b$Lt^d`e+s3(@I|@#Hh_yjsb$<3va9WG;X~?a8 z%%hg{*=nEMXLGf`(X#7pSzgUfNP9&-KTNgom3R#RA1_cWh2R;L9IYy^xMs1{%;LcH zE;OzOWXm&ny#kOAf(}W$t|B|3M#|Utz*H?^aSFi8Tc^Hgeh3T-lCKV18=3bSA~{Gm z=lzQVMN5oT@@kpSbb4QxsXP4Dikt&TX|K5`V^52W2g-WgI`q3UyqUa(37PMckk@cR zE+NAE@v{#DyF)*j2o@gR*Rd^ie2M9Wnlz#C*5-|WdPJ<@e{#DX=lB-_1d;q)JF&!$ z?@?X1Z!x96Vvl#^R5hUJJ1|Bc*y)EEFkvGNMNjrA>}NYdbp;F8?HhtOoD75hzA#c_ zv|szFx+IemQg6rSar>weA)5&hQQd$svVq?=&?ilR z={v82$~^$&9ON?Qty3(RiaU(VK4qTj$0Z8>yAR5K6m_}Vr=d zf7ZVK(WAxiWp?f}_m0SyX;QT6dT9JX?~l9}4ZtUztO!iw_(-`dscqvA;h*NDwuf3r z91htdXJ(dGm;Q=(h#)PU6C(L+Cm?<1zs&2nQkqY*!Q&(DCo3f1&DFUc%Wygc;}qhd z`F#NJp9s=p@R7|KXF?+dN-vQ(D=yrIGeSJF7cJsk|>qDiy}31$7_U zS=m~f{%VW`6PHVFLM91rVE{Ki66c80({Xh$@^jD0yzL>-<5I($?`68rXUf@uo&~VN z(p7hLuFu|uZ*z4QgONgx>6Jk%KY5(HAma0^SAz5*(D5Longls|?a&s1s8n7w%55B5 z(4wsbrboW=&M})miAlP@PK)r)^qlI$j3@h~OgF0-#`;--Kpw@P8wpDSUHTJs3mo}t zch>gcGS*%5a9z3tdfGOI$?E1M^r+}j7Z(A0$hgHQ82-ACg@UCopkFufZn(f%QUBrY z&%ul*^uQsba&wWZw@Gw`)VdmT;J9`^io@D1vtP%$A5%bm)V;0m{Pie zB|gwD^2f3rO}nw2yxr#tzCbKDulN0lLxldg4T~;B1ptc#+gd*8zlKmjPvOpO6{;2&}>pq@aoIo<3DnSKpGiqr*qn4kYlR@3s+asPn z0<~D;@KhRd+cDVXGvWd3ViEGujF%eTJjVXAS6YP6y(6X{cG14x<#k!d8|bG0?%ka3 zr_U$m$sF&AAJlv>thsZiLY&fr26d8}n(+h0U;8L^*;Pwp?^YmuU z5S@SFA7|(!_^6{t!73QJx6W3Tfqu-+nuY!TS^Jb8^x{Y#?Eg^X5?GkA4)j5=pJW>H zW`$YT{aK-V_BPm;hkdG?%t}wv0syMh@lwy|xdTK_exYQTO;(0tz;8z3yBW9y2b!M`hxz#-guD=rj>ciZuphS%Fsx#h*^XkuKv8(U?|OJW63A0kCjB)r3)f245gMR$0e+YR8C z&jvIUVuGs^^JfM_B=i3zhoz_G|K{-D9R6E}|F#nUjUE1fY$X=I?Ne8RPNA7@+r_5j z;KBp(>Catm?R!^E(}cNoa<~^SN~~{9O5*fy+DyK`9csdKrULRr`X`1#kRO0-(08M7 ziDEetEmQy%LH=gQt0lz_hJ0BqfHG-?GZf}v6ks9*oTQqt#fkAP3?p-Q3e{0$Y`_c) zAJjtkNVtRF(bMiP&D%bJE|wPOc1b7(r$NWMG=O9-XRLGF@eU>lp%P|H6~dLEZxne^;PYWzmllDu$O)y9`qdX3q$5RZk5* z1ft7mTU_rRkLg`{5n`357QWfdvYyauFmnY(gJ|m9wx9v^5J=IUy0!_qh7v+qC{8|H z(l@cv6(S47g~!h(xOap~VPqo|AB@I~;G$_%hy!pVgrKLjj&>tf=b^ykT;@@BE}Dl! z*yt{Nw=On7xFNU$%y{oLOz*G5Rb%1X3NIjjnjJbVwF&^lvtW*y7EIKo2(!yP7dniY zLjxS+MpyVe(=o&8_mu)zw%N-@Z zWUd69z%XQS0C6NNqvu0b1r2bmK5k>(M^#d7d9K3l$&E{hPk#jsH3^_1_iH6};Y$U) zv>Y1#^ZZ^I==@tAZm~qFG9f<-58atfFpf&)@`o~*@hZ5kr`u=ihjM!GSsEc)pvnZ{ zKw!{h!PHCdK}rAcJ=F%SxET!zJ`6w~1bRUq4!9r7cubld8a@6mpAL7D8Bp@h%HvrD zfH7tm;I%KG3V}KLS!019pO*5U#r9@_)}8nfKDdJh{Wij6Zei#q)B#YX9|C1$%vLyv zSDHHZejEUlV2QcoDw{$;`CQh{W5YKrVj1u3{g|3H(3g0;YLl1A zmptsgRQ17?^B}(ms}n}~de((dksr3Y%?K5eyv8tntsZ7M?Y_VC&^kUBQbfaOJ~-AK z+D;G*=zJCwS%3(S>3<@EmWYQh93aSdW3AKPdl$Ye4r-9Z6yKx=70{t6S*Th+U*8%8 z^AP|h$cHaJL3Z^sN;ATN90strCCub(?2yFFcWfQBR(MIB>VBFfxTehOAQ+adt@pwlbfZ?*OV^wrOi zIt`!QfW|C1Wd5AWM|v^1(6(2ozZ8a@1YF3JM#na?Fc$^*jQ*;-QI%_XER&&zP?mAr+)|!Pzy%n! z{h6g^9u4CliuK4ca{ad)#WNe6^+q$a!~@143mm=&CYQrAeYc9CGRZ#da9ZaVclWgM zxPYRtc6ls>>Ut4$6x(YN&Y;*HO}&K#fB7944l@h;N#ZdHHD4)!0jRrE6i&sE9_tx0GgK&j&tHIq!8AX4pt8wLT`-I))hqhQ1yOh*%5#4 z9M=zHj3L&UbifX+3)2z3>Ui_7qO${11c_4e!iBxrX<}C{B2F~RRM5!MTWK zkTj53VG<0q3Zl=a5u0DN3gUWLv9ZKS#S=xF(&^UebKkyx1anNr6=75aXqR&GgfrE-nG8U?*$UOh6pw)%GjxZeq`~)mh5I~2CVjTX z)dz9FDRS}*r|zOm6)3q~zd)bBKDj#6NeJ^rA7@a(hZqahYdPiv43B(g(5gOlt)2l0 z+qeJjNkeuEyadk<{Q(3)U{`ID7y8gvmIj&(W6g`f4tI1y7wKx?Fbq_kD@=nicNS}u z?P{h9RvvyoAD#6Qp$?eoCJD?hz(AEhSe(4YZ|j1_Cj!+W?Kxm{jPO#pwSSyAp?*CV zG8tv}aE{<5&_Qmc_v(40&-Vbp`!A>nji8e8zYPDc_P#tG>imB^)5tJlNG2^A_mQh; zO(Zkpu3Tj+#iE3c{hBQ+p~j36Qc`p{&_xU}3-+#W3Uw>_D-ZSs#>v>gQ_cEjUHWF$oz(tv5B5`Sv~zRR*FMiXa(k5unv> z=)zJ0d#X$(3;b-5)Ni_J0xvS&?PW_f$MxI%{G51$o(fZJC3H8qs#5*IS!OuILQCye zJENnWkNH&{Ssd;8L34pzCH9ge#`Wovbl4Xa^y}0x+67U=>40%iNZf++n*eqjUnA2{ zKNh5H=#EO^o8etB2_Eg7G^g+mISC{&s60HAgvLCDc4-5jh&L7AXxWVBo4QzoZPX>^ zvnWlRCGdNwBhRR30&U_7H%_J@&lT!8FhTR@=dw5NqI0(RaFO?OE|g&)t(KPA>#JWe zQ548ISB@i^GKFTLH2L`K2A&>thBT#8hu?}Wy;Txj7pdM-%Y5^u7S%z9Kw1!+Xz+F4 zhA+stx?&~lCu)2%@=61`nOK%L=EICAu@ZNGeM|_~>~V|Fo*jEFq`@KsDXe&EDq3v# zV-}lO>zqL@Y8x3D!@sM%w^g1WzyES$WafK^n;(6mr%N8dZG)z_yj^WuNN8)2shN2J zE8E|q>c(af%ws+@zF4aD)azV&ozl&Z^{N@OHVXO?<2I*$yChPU=tYjH9xz-YbCmVH zsWUnp;i_Qtrz?~15Nn^AHTh1W#0h%fWTU7V2i2nH4a-CbTvtFg2I#c+Gf zP^<||bps)u18F@TCTZv+#LM&>+MVs?3CrB|5TOik#RZ> z2Qf|jEztB$Q>w!7+hq#?JM#0-QvFzSoGE_X)Ri4j=$+&HPm+bpD+8Z>u#>U4cTO{WP^A!P4Ea z^kmc$6inHz3l2;F1x)%EF#rF+X5tuf+Wi+-Fi&={g){2xl!KL|@u>jr=UxC4IfFNP zvaYbf9P2GSWg9auR5UNac0x2fKe=p{N{LdRHBh z$NS8Dh_~vOH-hU5a9JIu>CMgjgNPO#BJH%e7{&&`YtpMxV zVfr4D8&WON8gLnwG{H(8uolqpc8Li=5xV{Xy|`gJR9mVM0ud9^%v*Ge4)!OAv!@ zOoG8PkYn03ALs$=l;ZPg-`akIo*TJ!(8)Sk%SF-QiU`S#Vy1;tsh6Hdcz)10^g1r* z{oQnj^XJ=(8Nb@Bz$k3hBdKnqZN0u|x=Sz4H+tkn=6gT4)C!^Pxg8_s-*ttB8*{x` z%dm3EhCzB76CT2>#Z>o`6l~-;LwR8slm4}i4+*<;_r?yI`9$CA$1y4Lw9R_gX2*3YWAx>4f{llxu4&&vk}w~>pnHaJyP z*e^rt&mOZ~&bqLSiJRy{jF$Fe1pC&NQg9Y+c|~zS{Y_PuBsA37SCz@~M&O?(cZl(rV>?e3M~FgJlM@Nh6603Ds5- z0={BxWM;?IT7$tiQLl%6ko_5hO~@UksI1-N`New$jfxW_I^Ao9wKNLaRa|5wR+G;` zibHsKcCmo~qrrDhtyRv*wnXw5u%ih#w{`m)1|nYUWroN!@%17t0`mtb2KadAKNPfL&He#}1WKr=C5LH3XwGNj*%U2Tne8D* zyJk2`31igyJHB+?Q!W9-*VEzeKAvk2grB(B zF};CfndyrUKFgD0#-bVl1ufWt-wwy`n#ppQ?@QvkN)vHR#|)+0eAx=2D`L*P&Kr%FRYp zd5jdQsR#gC%Q0So6A1&{owVRCh0IYjAz)5e80^bdi5w5zL#db2xFRW+Pn&di04}Wh zA|U^%fY5~QY18+gEz&BY#o#VgF}64knJaWSQbzCJHhmGRS>((sdpSQXMZVVLO;HGU zq559pS0vN5tPY1>#`oI~8(}Q9DQvG^4=a6PMp?@?yyj5FHqJ}7ULN~EG7M_js1x_E-lw&Q;uI)_XX6+(W7S34r z?K_PN!6u~qQq2$ie)V8Mo)^jP>@Ia8Kd0z}7VibX-`3 zbywZlObyNVXdDSI?)1iO2CU#|#GwWnyo@uZIGCcS9U`tW9QJ#L;8MW2gN0!8L~ zkLt0r_jMoc%#*SC_qSGOj3_nb^$k4{fV`u`UaE(vkvSorz_k;2+VG}kc zJ8-lya!k3^tV5}iZ^vUJOX8keKtttQ__9t;(!Q7v4s78tc9&mA+I zNTd(6ZoaHSze95w=%B)xsaP*t=XBwQ9=qPxN=};`C?o)vVW|60$6Ff|LLADmgw|>R z&w?o@8)>u9(jJJ--!gJT;e#`rtu_*rTeyN0^~4!QFrIG`IsPA#&hLBC{}A3l$U&mx z8yFcIQiE150wP-8Z}n0j;0PK|I1H*x6CDtd69>d0DY-Ed#mq@6RET`>(swvmg>7nx zB(DbiE(KCsnOS(hJhH5Y_>&)P9vP(v?=!b8N1By+zFwkL-WKjyRg79!vq{E14bq3& z>{e}s2m(Q-pa}no%vxULs8j76Bv*%>vft>r_P=x67y~t+wG~!!ZYxWY!hdZmiy9xn z<+y0_6#A?AU$?En0Lx?IgAypF`@29T+Vxhc=Y6C$GzR~LGe2%x@lVJ5b%kA72(dzV z(1Ne6z>^O1=fB@ws)2$1NtKZ&PgFahXM3vk7tv_tK>JtIB8hlP{7=P6y5Hk)mGlr` zr)4P157O$T0*K!e9^)1_i;j)|58(}rvL{$zFHw77hDB;R5D{A2@{h$(K)H0y;5UXG zoezjGMv0S6m)Y={)%Y*^hWFb(B1L-&4>opcEAUZLGqWGQnHi)ZwoaA6FQS<(~vjX`Vw%9&Zmr2px7Csm$1lz{`3%hXMr=gONO`>lK3 zLIpzfiisyt`><;jQC?g2SC!H=v-f7m1soY`Ri5cW3yz?L&hEm$7c*~e9t8g)w<}_a4-w0V4ksZEN~@SjJ0vp?4BWam~g`s%MJJh3;3{`1cn=63hR(PgMcEGlcn<1wl(*{Vn1_KGwQ2z zqeF;pRzEQG^&UMuXuDao9C2G``4IhmMUxd6DQX4^=m52YWggp&r>4SR77x+T|AT`c zMU}+dF~nejW7jo<*qZe7_je60-ijI|-#uzJCHP<>+=c_Exi@c|NcjB}9>VK0TSVvf z^B%oa%zqJGxc<9y&vQHCq6rpKa+1L?RO$EOe)=~8rsQ}&}39(TQM@+a# zo#rcJY5`q&)d7!cmm`?IcJwWG`&V1^g_BZ(UR+m=M;-PG%cp*^jt9 z7iQhrxGf^d#)5R}X#TwQsO$hv!0&9R;?)He!hs+yvYO;WI=f)VZaWSKG)=|C8{@Cf z;Lp1Q4qYYA{tMPVqk&RNF$4vd# zHp#LqCAXsdOd#XH&jWzfzD98YSDhLp1ehBZA?nX*l+s4&3?=eD?}%Q%>gWs=Y^Y2T zsyk3tMuEax?K=v%fSgR){94H!yZLKN;$rCOi!2Q29XCfCs$zr|=D02;e`)YE{>o|a zErx*<8-K5jcQ^!iX{BDs#_pnBf-?+>@!dg$a)W9`a1Fi?=nH|qNYMYg1cB{jt-yN1 zR%{84ehoSKRWhnrKO9Il&OGno1Ld|rl^-T{MiSaCsU%|?_UK)CasL1jko z+5O1ygO!($@5hme>zIjKyawbfs#pQavKs0c zH5wk9t-W7?_Y4gpdSBgfVaE`TZC2%~Q?aRY=LKYv^B%6eYzg%_j$je<=H0RDcXBO} z3gI&gmqso2E&8PscEtB4PwllU7HULnB5C#HRcqLgCGEf)k){Ycv)wPd^9wGI_OKlnjckVeEH5xu^z7feUSJ=PN zBLQOc_z4ok)5+MepkBWN_KZp7WrBY*oOIbhjX?#=6!JX7VwD@@oZ1|>(gufZi!F}R zy);v`mr`mQ_Fr&%OQd%kJW+~L8fxTY{kNcIIN+$oKo3v|mnRjBcijl;`jJV06C+f# z>u_?DuFuuDoW)Y_Oi4dC(j$t%YQ>s?>0#Ps8098gZIEXz7aP)ZzNngi^ih@LTDlc~ z?mRi{#_$dsWjiFg2p&G30)}e>vOlQ9;Wl^J7T+1E6oOpnygGHJUCmIM4OL!P+X_D| z1_`xP@fO~WzsspnB@Zz#lDc`(ow-u!x9ay-pV-xVMm6pc(?b>YkIeNYLuCTlT;K$$0)Jd&$bxuES6F$c3HsqDt}(gwJ!%#2 zM0`|n9?hpX*1It6BIzjxUH=!zE6cPk?*Lt2wr6dqlZup=TxF4PHvB=+&8x9_#QMPmRk9R^@80aJrRyIuy?^0wm($TYauhN)V_l6{3O5d`Ltyi0d9G zHfyt2Gu30~Quza#-)vT2eBM&sV(k7tlie9g6DgMCL6!9VE*$&%c&MIVxJ)`Lk!I7R zy=ffON{H)jO4K#_rN!ran#vD74yYvf41{dt$EJ9)4F5QlhudVI`ZmYjtnY&ZhCZ+q z?vIkxx{}d=MrGB$M2Ot+H-&T-sr&Zh5-pcwFlG=i0Qj6Tyi4H-ty?8ZHE#zH|u8Q%SxB&t^;M zW0?(OL0xNpG4sP+3$rljr`GFJ7taN3&*OMleZ8phf5f?cs3=ZSH3(%AHBSg5LVEB4 z_-0u&`R%K(ww|0y=))e7RTP&G@{+u~4}1A{MZ*z_@8s=Kb$wkZm|ME{ zjO_wxaRH^x;qukBhGwB_Lwb@recAvI8linn(Sn}SUqg~>wiDU3_#SX!Bw^(`kGQ$;FkC}lq5+cXc8>~Y%!iGxs`;7aELoOU z_o6iqa$C7ajUhwyKgD;0$R(K_DwNLl4X|D*l78%I+JcgIGUv82T>!&QYJK>cGs(J= zHaHcXLfTwBRYpjg*9&EZ>|I_Wm4nJplTDX+%F4G1hR}#O#k1}#WK4=;>SS5E(NLw9N~ZuUQIIo{_PFQm~g=DJYjX^zsD|D;e>cXAjZDrXo!XU5%ni+5D2i-i=p zSbaS%(t#W|+@6l*JuDwc0DM!l{BYapK_rNlM`#0Lg{e2XGi9z$x~mWp;l@3cAJ`RU5BLUM!8RtFqjjEx*R_&)eMUr;b}6GrXUW-0BI|M{=G`Ik>Uvy<3Ixa>rS1F6%V(C2j6ykByYx zboo|pNRWq%&vS$1;6zS;x_}s0eMFHWiP>*AB{_qPqxmm zC#9fdA+C#u{cXE`Ghz(My@gdz+J0nj<3Z4C6~Zi6?TG>Q>ZszW+~jp~drtqnR42vM z@z<&~J{@s(urFRPctNLojg23o~n(Knapa$|lc322(=-%8i z_Uv1KGAsmpNOaqbFGG3ai1t~Dne?fBYgW6v%b#HkJs>?$bJZji&fvGxP159Gj~lx< z>TJw-m_{xdN%r9NuEaqtU^!Bp$X=Ui2D0IS25TNyES?f11Urm->IkB955QQXkt&p$ z-CFm|!>E1$Y zHW_p1MecKEr4X)Vl;kezB{gm>6U~hN?ZJl4P+-U6QlHd{gGr>*o|ffyz8FQn(`@V3 zajYRSZvEl4G}{-&fI{=IH>SCLJgztia_J*Yi0KiS6X~Ix_WXLE@XZYc=4XThgPEj3 z#!>|>&PNOOWm|8U`sLs%sJcJ2C!0b3P&nQ23 z7D#+LS};S>8AL;nPs}h~jHv&BBkrZj7k;yJZvv%q2?lwzGfoVh!IfZWKU5PvaQPhv zk#)NrpA34@JYPS;ns&l_KKqM9XI}b3&0&p{?Cl`kcfHm#O#X-{K&$L8Sl?N52 zGwpHf1l`B&dev|B4?QqcSC#(z6M#S`vfVVX7wPn$v&T+uo0!~rMm`z)nS59OL_Rd< zoM}(fQ($c*lizv>os1)=Z!}0bVLoe%r@w29xoobRW_Bu$YM{!6kTF-eVs>k9td>Lo zlv;t^&G!PkPecm_H34ssE@tSR;=8bUN^;GR3GhtnI&yWa=2T1(9u@Z5bV#fmimarE zpAu^aNE`f)5ksWVA=GxWu-lpPNyr;B{v_lOeZF?l4pY>CS#{=eXBdxjep0^@P1hz3 z)@HA;hU`m|BK^VzQ?74Mk~m>IqOE9^NJy)#QHgJ`-P8KweXmXGr*=3`Nr5A*rt7!n;IHZw%>!0_`xN6T zmHpKb!i$q5eokChMJCPz8*}BQx}Y)V>`L-+UVP1UgmF^YqSNH=Bk6A=?znFjsgqY0 z=r;8YKoO^K=G=T}rIC>k$Z2uCmtP$oRe=@_=v5xdDdTp>H6W(j>$w>goC3Qm^K-}i zL;x-a4m#P24y_)b&_88OZT+6K$m z$^0#&4)H%7p!e}{_SM=Rn>kVG8SD6Uj}MM4(@7bLUvdBshN(g+?dFD&)!Q#VGpK-4 zL}Y2fqUom!qta=fqU3Nl{r-S=Q?nCBf|vVNR^+i`XQ7f**8nj3Kbl>ehozVaC+APnm`lUuHJOd;P$)g zP5!9Wy-5GctlALQgJ0J?vR$AQfFkx#}2DG8j9KV zgUo70$T{xIn~p7#d?%M`8aa-(zN{Ll_)eL>6+HG$pd{wFGbwr1-NY8rWZcs4&eh1|LI{b5u=mV_c*U}joLQo zKR8MHyF=a*<{m2RH_?kg%U#dHI@VM{QD|drHl*}E5sG*9>#3EifJ&D}uFZyYnQ*N) zltm2ZfYUoiBfI#`RWBL)nWI|HawjO*4VYRr*o;-8(NBfi*iA6XlBN@AW-&vKQ(%dx zui^`aRJkgv#Q2u9tKW0Ldb&Vyq6pbOHAe9_sj99gwoF4*p-xg{tExc7_7A#wcE z?^oG+@&n80!)D@)8}alVm9>ifAQt>1*oW7|ED3RJ@Yy?=vzw$NJCGltUN`P;juWP! zI+U?mCu7K+>J1JZ5j!VD8<+o$5S^P2ME=5DhI2H65?CEB_^Hel>u5*l?}7_JpRkTR zQvQzx6sm>o0pl|?-kgA1%n2w<<8R;q<%~Y-C9*{kvYKqZ2dn81h3cazPx`_q-F5l{ zke`k)a(EBJ1up}hLz;ee4&{XQCSJwB;^)<${(=Nm|baAxtWV(B0GeKZEe<*A#r+ebq(Is z_SkQI2vh68p0<4#KxfDfEp)v0nMmZ~#-YJ15NlEBs~17cL$>$ow?}kcL9Lu${uI{( zSb&&7Cfl-JwkIBbkNW_%1%{Kgp3+3o`1EG7XH4N!xK!d*1 zSCBPY3JS^L;5Gn#>X)t=xInpR*SJy|T5ed4D!WC1OOKMLiN4climCSV!{CbFPFcNb z_t)<6o28CUYB~SiA-@xW9HAPzaAxmz=n~+eO@XCQ2*DSLF$D0Ru%?b ze7ojr%|RMJIFdfWXNiuHhk1g0?&In_l!+VVxRa6t+1Eb2fQ*@)sLMnZY z)ZwRhM{e>65_WT6onh1{G!Z1yD7~`MBOV?1?2CB|2E*mo? z=cs7LC_?jCfU$F4-S!Sz##R6GlDX0P>SOr>uGI8i=x%`evJP`qsn758MHp0F|IwcnxfZu%+YCFfrvZL!c`IeHq z&dN7;`hLcamngGHS-VYOyidb*vsoj{b8VXZq4tS7Wf&i=MLVt;*w! z^B&3^^)_pWAJYiU=z>L^YstZ|C1;K>9>RmaquRU14DTrPSli>;&S9;5B=v6a+6+E9 z2Uu8OA?;@eE?|H%B1D5$+RTK1=%7H$xj~`@j9o{>uUW kMAI)6KpC)<%tRy}L@sSz@FNfIDb1Vb;^1a~jpon#Z + + + + + + + + + + + \ 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 d9317ccd..f16ca7d0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -497,6 +497,7 @@ account Log out Log in + Auth Locally Switch account Add account Create account @@ -562,6 +563,7 @@ SDR Web Poster Image + QR Code Image Player Resolution and title Title @@ -780,4 +782,8 @@ Media Reset CloudStream Wiki + Visit %s on your smartphone or computer and enter the above code + Can\'t get the device PIN code, try local authentication + PIN code is now expired ! + Code expires in %1$dm %2$ds \ No newline at end of file From c71d5d8add602ba0be84ddf197da1918c6f65970 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Wed, 19 Jun 2024 20:06:40 +0530 Subject: [PATCH 375/441] feat(ui): new dialog on adding repository and auto redirection (#1025) --- .../lagradost/cloudstream3/MainActivity.kt | 5 +- .../ui/account/AccountSelectActivity.kt | 3 +- .../ui/settings/SettingsAccount.kt | 4 +- .../settings/extensions/ExtensionsFragment.kt | 6 +-- .../ui/settings/extensions/PluginsFragment.kt | 9 +++- .../lagradost/cloudstream3/utils/AppUtils.kt | 50 ++++++++++++------- .../utils/BiometricAuthenticator.kt | 10 ++-- app/src/main/res/menu/repository.xml | 3 +- .../main/res/navigation/mobile_navigation.xml | 21 ++++++++ app/src/main/res/values/strings.xml | 12 +++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 11 files changed, 84 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index cc2c99de..8d312ceb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -134,7 +134,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup -import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled @@ -186,8 +186,7 @@ import kotlin.system.exitProcess //https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225 -class MainActivity : AppCompatActivity(), ColorPickerDialogListener, - BiometricAuthenticator.BiometricAuthCallback { +class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback { companion object { const val VLC_PACKAGE = "org.videolan.vlc" const val MPV_PACKAGE = "is.xyz.mpv" 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 0b0d83db..0da69f9c 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 @@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled @@ -33,7 +34,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute -class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.BiometricAuthCallback { +class AccountSelectActivity : AppCompatActivity(), BiometricCallback { lateinit var viewModel: AccountViewModel diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index d227f9f6..67a2a15b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -51,7 +51,7 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setTool import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.BackupUtils -import com.lagradost.cloudstream3.utils.BiometricAuthenticator +import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock @@ -68,7 +68,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx import qrcode.QRCode import java.io.ByteArrayOutputStream -class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback { +class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback { companion object { /** Used by nginx plugin too */ fun showLoginInfo( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index ebd3260f..1364c376 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -33,7 +33,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.AppUtils.downloadAllPluginsDialog +import com.lagradost.cloudstream3.utils.AppUtils.addRepositoryDialog import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -273,9 +273,9 @@ class ExtensionsFragment : Fragment() { if (plugins.isNullOrEmpty()) { showToast(R.string.no_plugins_found_error, Toast.LENGTH_LONG) } else { - this@ExtensionsFragment.activity?.downloadAllPluginsDialog( + this@ExtensionsFragment.activity?.addRepositoryDialog( + fixedName, url, - fixedName ) } } 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 acfbc584..3bdcb251 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 @@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName +import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding @@ -70,6 +71,8 @@ class PluginsFragment : Fragment() { val name = arguments?.getString(PLUGINS_BUNDLE_NAME) val url = arguments?.getString(PLUGINS_BUNDLE_URL) val isLocal = arguments?.getBoolean(PLUGINS_BUNDLE_LOCAL) == true + // download all extensions button + val downloadAllButton = binding?.settingsToolbar?.menu?.findItem(R.id.download_all) if (url == null || name == null) { activity?.onBackPressedDispatcher?.onBackPressed() @@ -171,7 +174,7 @@ class PluginsFragment : Fragment() { if (isLocal) { // No download button and no categories on local - binding?.settingsToolbar?.menu?.findItem(R.id.download_all)?.isVisible = false + downloadAllButton?.isVisible = false binding?.settingsToolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false pluginViewModel.updatePluginListLocal() @@ -179,6 +182,10 @@ class PluginsFragment : Fragment() { } else { pluginViewModel.updatePluginList(context, url) binding?.tvtypesChipsScroll?.root?.isVisible = true + // not needed for users but may be useful for devs + downloadAllButton?.isVisible = BuildConfig.DEBUG + + bindChips( binding?.tvtypesChipsScroll?.tvtypesChips, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index ff27b192..626eca12 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -62,7 +62,8 @@ import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WebviewFragment import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.settings.Globals -import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel.Companion.downloadAll +import com.lagradost.cloudstream3.ui.settings.extensions.ExtensionsFragment +import com.lagradost.cloudstream3.ui.settings.extensions.PluginsFragment import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -386,7 +387,7 @@ object AppUtils { ) } afterRepositoryLoadedEvent.invoke(true) - downloadAllPluginsDialog(url, repo.name) + addRepositoryDialog(repo.name, url) } } @@ -429,25 +430,36 @@ object AppUtils { } } + fun Activity.addRepositoryDialog( + repositoryName: String, + repositoryURL: String, + ) { + val repos = RepositoryManager.getRepositories() - fun Activity.downloadAllPluginsDialog(repositoryUrl: String, repositoryName: String) { - runOnUiThread { - val context = this - val builder: AlertDialog.Builder = AlertDialog.Builder(this) - builder.setTitle( - repositoryName - ) - builder.setMessage( - R.string.download_all_plugins_from_repo - ) - builder.apply { - setPositiveButton(R.string.download) { _, _ -> - downloadAll(context, repositoryUrl, null) - } - - setNegativeButton(R.string.no) { _, _ -> } + // navigate to newly added repository on pressing Open Repository + fun openAddedRepo() { + if (repos.isNotEmpty()) { + navigate( + R.id.global_to_navigation_settings_plugins, + PluginsFragment.newInstance( + repositoryName, + repositoryURL, + false, + ) + ) + } + } + + runOnUiThread { + AlertDialog.Builder(this).apply { + setTitle(repositoryName) + setMessage(R.string.download_all_plugins_from_repo) + setPositiveButton(R.string.open_downloaded_repo) { _, _ -> + openAddedRepo() + } + setNegativeButton(R.string.dismiss, null) + show().setDefaultFocus() } - builder.show().setDefaultFocus() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt index c57600ee..45acbab4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BiometricAuthenticator.kt @@ -26,7 +26,7 @@ object BiometricAuthenticator { private var biometricManager: BiometricManager? = null var biometricPrompt: BiometricPrompt? = null var promptInfo: BiometricPrompt.PromptInfo? = null - var authCallback: BiometricAuthCallback? = null // listen to authentication success + var authCallback: BiometricCallback? = null // listen to authentication success private fun initializeBiometrics(activity: Activity) { val executor = ContextCompat.getMainExecutor(activity) @@ -141,14 +141,14 @@ object BiometricAuthenticator { // function to start authentication in any fragment or activity fun startBiometricAuthentication(activity: Activity, title: Int, setDeviceCred: Boolean) { initializeBiometrics(activity) - authCallback = activity as? BiometricAuthCallback + authCallback = activity as? BiometricCallback if (isBiometricHardWareAvailable()) { - authCallback = activity as? BiometricAuthCallback + authCallback = activity as? BiometricCallback authenticationDialog(activity, title, setDeviceCred) promptInfo?.let { biometricPrompt?.authenticate(it) } } else { if (deviceHasPasswordPinLock(activity)) { - authCallback = activity as? BiometricAuthCallback + authCallback = activity as? BiometricCallback authenticationDialog(activity, R.string.password_pin_authentication_title, true) promptInfo?.let { biometricPrompt?.authenticate(it) } @@ -165,7 +165,7 @@ object BiometricAuthenticator { } } - interface BiometricAuthCallback { + interface BiometricCallback { fun onAuthenticationSuccess() fun onAuthenticationError() } diff --git a/app/src/main/res/menu/repository.xml b/app/src/main/res/menu/repository.xml index be99b1a8..7aa1f200 100644 --- a/app/src/main/res/menu/repository.xml +++ b/app/src/main/res/menu/repository.xml @@ -21,5 +21,6 @@ android:id="@+id/download_all" android:icon="@drawable/netflix_download" android:title="@string/batch_download" - app:showAsAction="collapseActionView|ifRoom" /> + app:showAsAction="collapseActionView|ifRoom" + android:visible="false"/> \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index d0df339b..fafb6968 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -111,6 +111,27 @@ app:argType="boolean" /> + + + + + + Livestream NSFW Video + Music + Audio Book + Media Source error Remote error Renderer error @@ -617,7 +620,7 @@ View community repositories Public list Uppercase all subtitles - Download all plugins from this repository? + Warning: CloudStream 3 does not take any responsibility for using third-party extensions and does not provide any support for them! %s (Disabled) Tracks Audio tracks @@ -668,6 +671,8 @@ Yes No OK + Dismiss + Open repository Disable Battery optimization To ensure uninterrupted downloads and notifications for subscribed TV shows, CloudStream needs permission to run in background. By pressing "OK", you\'ll be directed to App info. @@ -775,11 +780,8 @@ Password/PIN Authentication Biometric authentication is not supported on this device Unlock the app with Fingerprint, Face ID, PIN, Pattern and Password. - This screen was closed due to multiple failed attempts. Please restart the application. + After a few failed attempts, the prompt will close. Simply restart the app to try again. Your CloudStream data has been backed up now. Although the possibility of this is very low, all devices can behave differently. In the rare case, that you get locked out from accessing the app, clear the app data completely and restore from a backup. We are very sorry for any inconvenience arising from this. - Music - Audio Book - Media Reset CloudStream Wiki Visit %s on your smartphone or computer and enter the above code diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fc2d0f86..2968a1b2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 30 17:11:15 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From b9746c2b17be02f2d33c12d20907cb9301a8815c Mon Sep 17 00:00:00 2001 From: "imgbot[bot]" <31301654+imgbot[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 19:03:55 +0200 Subject: [PATCH 376/441] [ImgBot] Optimize images (#1144) /app/src/main/res/drawable/example_qr.png -- 45.27kb -> 1.28kb (97.17%) Signed-off-by: ImgBotApp Co-authored-by: ImgBotApp --- app/src/main/res/drawable/example_qr.png | Bin 46354 -> 1313 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/src/main/res/drawable/example_qr.png b/app/src/main/res/drawable/example_qr.png index 18decbac49533dcd2890ac8112305b19d05cfa11..764cb9660e4edb5e3957576e4e0f24ed3b412d6d 100644 GIT binary patch literal 1313 zcmeAS@N?(olHy`uVBq!ia0y~yVEzxnj6eZ~eU*;CffP%+qpu?a!^VE@KZ&eBzCyA` zkS_y6l^O#>Lkk1LFQ8Dv3kHT#0|tgy2@DKYGZ+}e3+C(!v;j&mC3(BMFfiWj5?%u2 zv6p!Iy0X7u6Xw$p*=CmM3^ZHQ)5S5Q;?~={j(N8Y1XwR{?s;*^{X<~QmM0mOUHuPI z#TsvNbVyB-@(P=<$l-6ApoE}l-Mu=VlN$fG<$)auG!_O7{?>L*oi%-5U5#CBUES{= zH8bRWy64%8pZWUQ|Nm4gm@*h`(oifFAJVW=Z{uRV$o5NrSzr=SxR> zpnaFoEqmbS5IOt838g7Vnb7sgGo1O`|F`G(oC#~V(6zl|Uh~dy=CApy+TVwsek<4~ zf8z<7{tx$=+pfB63vG3GFYqB6zA=?atMO=t^`LPXFxcWq7i3-p7u$9I5}hUK=Q) z+tZQ7?HT-6ZOREX6D2dggA(rz(N(O--dbN`?PB(N8t3O0H19mv$herX=h?IgpXUko zU2S-emlP+YI-x}y!-S*af&2g6an(31*mri7@iYv1HIwjD9O^iZEs9gx|GcEoD?PS-&EcidXet;Aq~nsMYMnOS+P^OQvi-L^ z#h`qP1c2VC*{4*wcgjIc$@O=eT%CfLk5&61DF89$q&Li2-_a4PEaRMbq(~aw83*_d z#C&dgHsxI1vBx*mRZJ6pilQ07uv>Ui$+5*#)^~d>;&PC3P%)N6S7H5a#+O4|{IZSZ zZY#^6N%r$fgng^JRnM`)-QpD=JDO}m(RbVB_3P0@8S?fs9;*!tnzmqn98gNN#5JNM zC9x#cD!C{XNHG{07@6oAnCTi?h8UVy8Jk-fn`s*uSQ!|^y#8;Cq9HdwB{QuOw+7v9 R?u{VJJzf1=);T3K0RSlGT`~Xw literal 46354 zcmeFa3pmtk_cxv~gJ$H^FvwvhVn_xlr-X5?90nDoCgqfBqm !=O#FGnJfjN;>Qk zwbM?{6~dm*X&0jqB_x$8nRnfz_S617`}zHz|MmR;*L%J1e_z*r_S3by=lfmvTI*hG z-D`c;`W|q1b)17&#?P8HYmT#%oyV+Mq5||k2{HIDM=`N$;QvH+c{tK%o$pi`nl+0& z%h}GxYwyP4XRmiEudie?UVl-4qD`-GBW|E?e9W5qQTa>mqQgKmZV{6RXm#bkj4%CnK)EzRLly=+DLiP1^x^Y7~ z?X&uR6@BxZDRJeVS5`-|jQML=AN;zd66g*(f>7;+>Jk{M6l+T9{*ilAU)(%)PODuz zF-;49doOkR%WqBW;`bk4MRmq)4Z72}i_?FK&I^*E`g6twi+w-o$n0H-#Zy+#TGOgi z!8HrvV@T^AiQ`2txLwYBt6p^YEZ7?dEJ^JA2VbAsyWf2}XbJBbCDQ>B5RXqG z>JvLzq>z(Wcxm+I_b(YPakG*3JaK)X10xj}Z#_rInkB2V#W580@c5Fjr!VMy+TNly z2KZWPpRLP0&v(QLgZEwAy7hRQ#3b$uS;F*WN0DXd=oO}Dj~oHK0$1|)EB08e_53TQEEkP_JjB>=M(FkQJ{2=?EW}+Bt^cgW?;L-*p2s;wo&U9 zlo#7Syo*{Z!%LIrbiT`BsdXm);)aKSjW;SIk@uhu|G7B6smE}zE_y9?lFqNY^Gjy* z+Oz5(cg;TiV&}*7RzmdNn=i)~Q{eeEb203g$_g6Zv?l9(?9n*Kcp4|JU(mC!PkWiH z2<5$9nDk@`A=;F@-60tQxG@hb{^S+ma?ak+qcf|mLhtR8rj~Yg2n72-vD6GO!qBlt z!<~Aay^m3!u!d4$FMKRht5jCTV{7!r<-Ru3Gy%6;24cgW!2@sf2xi`E}foJ7y0=DP;9blpBjoztqxijO(=Y@7fCrW@bEW_rI^^FsfB`D zAI5%nqm_0SJWpA^$wpdw&hQNse7dH7TRgYzH^*r)ky`iZBXP1nIaoWL9E#$H;Vf&W zY-4QEbH!~7-J=PJa}A|io@#50q<{*KnNG8yS77eFiq{Oo_!cU~nVp0Wdf-IMTTY`BQ`m3I? zm@9%`3sWMS?!Qbx0=XwaToLNWB!*3H{ozZ^mKn0+woBK2nV`RVqqBcdRAFL_Tfb2f zsYlYc#im75v3;R3JVHN{PmJ51=o%a8Fw=B@ zrV-WBcauJEOs-Kdre$=dzVUtb=gC3IUE|Jpetm8%ql&_e7)d$QBeFD!lPTvBgPmm2 z;GYoX$DJA_=EKD5!9qg|XWqH#)m%}Pz}?<4kqqBts!tJW3THob-C*~(Q%t;d!8`SlA0ZE=gl@4lm&Wn^EnPDy@|mO)F_ zESw_63pOXyyVB_I#xM5yYiskL_m9_oPL7owXV-mDpWh^Sz|=aKL}dCmD=H@;CbSfX zd6SF}q+u}mO8A^U7q@APjKN`-+UieqzINL>YvRDg8h&wM(-XeXBR^Nr@=WfQ#AI|Sf&#HA=9djWh2MX; zdGj8%=&(6tD77{?upv;toii95ER!{m;3l2n;Q zV(})TxBE_Wy@>Gy6Fv)hdM5uuzjd_Qf?Kj#3WMA*c;1an><`^*8mJY8eG%S+eWIb` zIOKxr#+kPhCYG(p(9l}ME$W>FDk-x;$9Q$wMeZMpxiztRW?ik$wR^8ljn-0_NwK{b zzNEB_#l1h~tV6j$QZh^y>jggJE2%0EaW2n`R`oEQKUQE;}FhMEbvS5_O~^{8g53g+lAH=yX@ z!qlUkWXnT`U!*z{B7#kU%FRxgwm6YVS|F8kooY-|U+KDM=V97LeZjTR77~RyxlKSZ z0yzhGtZ;g3?~q<(gZRwS!>8=F$*QK3xyOqNox3Oc>RzOFcK4;71ql|P!`b<%-Oi=q z&ChkzBPn0%&D8x~c*C-Aj06ty8Bf-&YVWj_&dBz`E$zui3QTSO^C@lfcF3oHe%nsei{g`G2ZzsLNU@BtfR_b& zgOpx^8>sQzIr+|V1CmpVGci)Q)kFmck(5SBU~Wc6VTgk<-$)t zl#R!|w`WQATV;7naA&_}QSpS5cw0Y2q$v?B6HCs#ZnlSYqLxK8HH^{H%A(;H!*0&6 znLZ+*YxM=nc;sj5rZ$N%HYI3TcD)Aj4x}HJ9&gAJtkd7|GJI{M(FzDuZ~GW7&CPt3 zbU#~SY3Oi;euZ^b#^(&FZCNc;K3SauYjp%M4su@_>-eL3lDDtTT|ax5*tC+9ND51L zZ&f{A`z#@tXsn}-Us2ue&b1YplQTS6RF7j4eY?7z;Vd=5CCm-bRyNPlY(G5}gk^Xu zDj6h;>+Id7$XeCaWN$M&zjE!@J@`eEd`}1U?-xZD*3~#d>*6KQL1daZSI@`eQaY75o{hUOGmK ztrUDWoXPduA*S@;YCrx7=tSlN_}ge|yR~$F`gmVY);}@&edAKX`D}VS`iSCuEaE z=iA=60Ga^27ivPlg?KTSJrnM{;>;{yKIol$kJq%VKelZng^XV;aT?5XbeJ{KyRdZ; z0q3Hyv$CYU%NZY?ZEwB2CFMGXU(9REFd;-!hh6GE{EvF)7EK7|`a4SF^zmjsWA=Vx z(SuJPAUD#I^i-05X08V=T&Uuu+A*rC54C<`jN2^V3r^K|7lLrCM0I%X?zm6| z??M0FA_rMo{kArEe-b#|Y3akYZ6p=^aWs@BjmqO~ZRFW( zMtzZisEhds0Zcx*#Qch{%{(v$mA}H@O0$&)i?SF6Q!)R<=>OT4T$qYHOfhzD%qy4* zB7T1gf4f2pmLuU7_zG>_j1qwyfY?Y{DiuR8z8X+e*h+WZ!!fgP zwvn!qXc6aEtR*8m@iq{)c75p4^o3fd%%Z6vj~~^{{Wi$h`}q;WsO|3Xwv?H-y>t%! zYG3Rr+Jsu+F}$`8j2Pi~X0DCXZu%Lrl(=02E@#63LP^sTO`{ddTg2U_w_UxIe`~|D zRw_h9w4fW0w<8VRGXHVS#in2DF~DmJ^K&lqOZpOH-C=F%)Tom41VJ!RS7o3#>biYF zyV7J385sp$2ZoS;PQQ@BEsg!>a~cp7Nt(!n1jqO1)XvY(zg|(Tl-J=WVH6+@Y>sH# z_;nBLXB)y!799CJM*_SSGC$3CUqPsY7w+I`r90Q3qHWQZmtR_#;)4a^9xu!Uu?+96 zDoknYo(L6kOLp(w-wzz<-oV9m9Y4|?&{GZ9-aNb`b{+r_~_q|3npYS_{(}w4h$q*W{hi5?dhqM775OEx-}8} z7z92O?%73flwujYVgqC!Rq#2@;5_!Md2`#Qdw8^D+!sC?=Rsp9~dHk_{JbL<0=CfxDgmg^x3ef2(DRR z!|{Bu$KMX_uC?fHLYcc0jIpazf3u;ndvB`FF zBINuxXWn*TOECG?Q<74pbU&!4HdN@Cigw+W=OWq&D}?-=ae%HyNm{aC$MLP3BZ#-2Qm3w((15rf z|3l(_0e9b;3xd}wf(Qz!)%Tps-139OwyIXSM?#i5ORG)gSCrMdQn&jmudKoV6?$mWlHkJ*5 zj=!o3?YSZp5Emi=aJsFzaFm77UDWt!Yn-+cJqwAI?QdxQHZ|vmPcQ(m%d>E3{9}H6G*<+8fOV#16T)&IvD0Kh+7OqEh9!k zkp1(g{{U8b_USy+WHDV8LmX4G;Y7^u5Ed2AL=wY#Zqpyl`&{4(LW}K{p5CpH4EYNB znUaJjnxJOHpOtA}Yj;;h*9$di5JFyKL?;BK5P)izEaQLd69zd*|LB#9sy7Tx@Npv@ zNcrafXT+UD!OM8yEfO0{UKZo~kOPUhFMA$l`6ndb!k-@C6wHxju`bC(af&H2A#iph zFT+KIWJEDZgrL{URmE0RJ|mk&OXmG{eFvOb_pf&4sJ7rACPG|bPOx>FtwVWm$9l(M zP_rM`96WY93Zkb^!@qxAB-EscAFg7Q5p59B{^6#3oRhhl$lFkS(lefC&tqGC96-wV z9}@Qv-5jE-UZgc@PH<=3+V^7J**-@^eIJr37p@@$$X`Hw#AtzV@|%6l|C86$)y^nN zz&oyLAlZ(+?EVD|3KiYr3$Yfrvjo?rqO#y>MPN=Q#^tjqrC=&w1s5)L2Gz1Zd-L}F zWjM=K`#!w+tS}RYUhzetK~z;?QYVbC~cpeum}UEM%*`|n!3sZ`~ukKJy*t=+#-}ZhzKsQ#6SS>*auXY zrd|wbcPYc_!7xTPczbm7z^8bLi09~4gn@yFI||-3!prY41``|prSdVDsifTvih+Y< za%mVNCg;AN|DMAvlOS2}|EzG!f#u81OY>q619y~G*Z1~$OuQ9{3)4}Wlv01YIU-S4~oRUU&sQ$K}7k~s1FZu(DU&# z9173Z(uggaR!VG#@{g+J_w1SBBXUM%w(o{zsly|Uoe)XH@n!6-opz@-*r$L3!*M`C z9^BDQx&8_4+mdiOLE%6NkOxJu*T-&5^O)M_2saIE#YHIWv?upUr8KIth&5(u)-RvQ zr@uCQDLoxv=L6AF3EtG-&My;->KQ!}45tOLTT|zN5H$W-2!3zUp3oiM2D%&{=tXss zW1^@|lA`v#wD8G7Y1IWKASG~v+i7fX&lK~zJ2`O?RJ~ve6Y*(MV2R*(@!Yc;M%GLd z%)KB|%8}}*axAR4MzB~3v1z|85g17k`;L*ekj3FyXWpUZRAjKDo(?W za%AizWhv-UhDz9-Z!sW#@#AN0aW*NJX(0rq8@WpYa#DneK`B&@QH}n7AypAAY!ejV zy3gs{TL!v0#zuD=dV=IHN%ab_g*&49=DtKN9mtG;hrPC*PaAZH{cSTh{!J~RaYTPu z4>3}jff%XyJtNt+XW~U&6ueP_z#qkK=4T9?MMP?=_xlWkgK7MAljPzE85xi2a!Z$2 z-=ElOwOt^52M(x&(V2K#2cnoWS(u8JVS6k4gQe;07%3%&qGkdrwVBX~ z9*>8FHo>&srt5c_G*lNH<)MOVi8+ur zu}$C)1@x!3WLb{{W)M5EWKEanhrT|MjCG%%XO%V{8_-`UVNL$kAXb|F ze5DMNHF-C-|Hm<9c-|G1fk4kQESS1bZ9TL)dY5B5O?;o(;O&MDv7}nf4P81LMH8wh znbiQ=s%q_g$*woY9LU`H)M7;SW9y5J%O{Omj@j%N-Tb9^Yy~{{#ZV0*stW`Bw%oYhvh#lk7 z<3c49mH~yxhQm>SbprOm({P(47`>H~NQaG_Jt8oizHW#e2i~CM;@5SgrDfpsXf4ev z$p*>R&mlv3Yx3hwnHG&q37we-q31ClXM;1WBhRHJ)GJ~RK!~FLqGz5h`H=PeQeidY zD`8u7|0ZmPvww5!|K~dPiq2QxQa=x1C{bAaEnl@#(wuDXbqTb@jo6P@LKD+4guwWE zwFH*QTZG%;sbSRHtik~g6M*Fiq%7DaN7eMB>!FmYl~At$s|?X9YyK&;HdBHcqxuBz zxX6+!P<714VhX12A?%1A*dHqeW*NJFNXGqxIcK4u`tkhwTvf~L(nM=WSOvpUnQ}_u z)FVYhX;_~a&$=UP*IRxP5^)w82D9Pk!smNSPzAT1-@YVcs)}jbn6k599nztZ#+wdn zVc2=w=Wo%Kur7>;;?SlBNr0ei0D?125jA_qd%}~P$-GvpIk1sibnE|04J33*M6v)i z+vh)5$X&^`w7*Hw;+nOL| ztb+o)kl+sz#GP_9gg6?3ZQT>Yk|Cbxdd>AT4H>e5(o>_RjQjLfgalH+vl1D)Qyux}hM_Jv`W;sR}RZvT4$k0dTBHf8EVgz!@dX!xGao$p7i4M&=hzMK&{l z7^v)IXsp7Bxcw_?*pShilU;fMhHW0-EKLsMj?xrF8l=|RL=QcOb z)o^cVo!c~80?HK!N}(+aF6bcI)~T@jcN1r%P~ljZnY9;TmBOThS_$#vZlWN-i@>8pH;!$kgqdSG*QNT~tn`187z=ci)*2$ii=>pb;3**>f+Y^^ zqTsB_g_|O<|LLZ}NWg223`7j*mWrR8#NK~N%`*OIaJ{Arl9Sy{FOZW6{I;H@1b8sF zY3ag*M#k&gWcbr2d5r?a1iEX1;(m2ijLf7crp@XAXyR71ElYHyKvO{q32?v5YbGE_ zR19qElE`0Z7AH;_v?_xgKOU!7yNZx-k5V|io4R&7{GxrliAA)5vpBqH4;Ky%+1=Kf zf5C7wpyYUBBDfNb)cwYCCeyj9Dnj5Q03 zhu`89+5yqFcRX&^9C!#*EDHW~R`G2!9G3CsvCo4bb9EUfPTVbD$5ZT+_R?S=?CzY| zYVB~x-zAG31u$?PZ$A|vCgoDfaGi^#lmsrR*caJf*JQu>Fw2ZUIYMrdQNm6_or~rh zA+^n?gQSYp&*J*0{6t?9$<59$FE>LK zRw=m!E;>rOv9d3K_vuB>c%{|xO<&wM6}`g+h#!Q)U2P_#O8If^wegcZwvRHp$m4Obif%upqJ#ulC<@Q8@8l8L|r{l zTR%*9kmrS20g7bWLJtn2KK>mNGkq~pO5(PKrbJ2Xe83=3c*VQCZ->+llDRr&OIAU4 z)c^hcpbOVSDQAb^(*76nW79Un;Yu|$c7_{zm{1zEg~JH$)Ic%{StI&;B+%=tsil)$ zCF3#MNb)DZ3>tX>Eagu-2TKdd5D>c30|p59-`X{Z@TR?=8ft~xX6ErWrSDJRAjZ&Di4DoN~xT`;{3!}O#kvR`*g6L zhRIqw{$3(dpdai`5=YfIP+Mn`?$8mk>GKG%Ijva}w@ZPpYm-bdk{Gj3#zR>9r=5e4 z74(JLZhi*X{BP|VTxm76y#x%kM2EX6S#LCxoIZYF7BeC#KCRPl(OG_RqV=i1`vy?7 z$b0}z7$0ic=gJ_X@K>U)O?}%}r*V_33F(9SUUdHWtjd3!jO{%e_@iMzWtVR1q&qEW zF|Y$rZHqg?;C{dJKb?$!ch?+&GKn39JaLf{UOcH^RW6CKT$Y!XArp4T>|DSSW`xWj zCr?=mlFOCRuc{t(U&(wh0ja&e5OpI7u2hlDiaDj344mmxX~+#S-S^Cx1f?Zs6W&|{ zdjIJxq3rqhI|r}nUA@XG(j&Zkg6hcG}1}Nfo~U2GpEZEbtfXVRDeklggu#$@QNcIX+rtqJ z|BWLpO&O<32paIdGs)&6Dm*GUC6_Bsvbe5b2cZGYSG{? zm-ene3<}dWCl1_zI=$7&_%xOVD)s;_YB&wj(<8ubT)&{~wT=L?myd0o0NX{9+# z7Lns)N+p;bNb59Q3$F9*21OaYh!6G+r2Rl#D@F+yBLNu7K%^Wo`c(hj zP~z7?yoYj%4Zr8i#X=Au{xb%^ciIAGsmK-s2S4ZRBA;ji9|_}AGf;qll0kBSS?%wc zbI)1R_vwqqtT?yn&`S)eB3rarWn)n8K`jeryZoiKEeKNp7@#@#+cq}j8l8E!z^Oq4 z(so5vG&w%6XNqt-mx-G_MLEkdG-m|JrBzj3!iVVfK~4{WOvIo;4w$^*&H3~HQi3$+ z@H?$gbYUy)t=i?LP+JH%B_6>S;2^*<+SC)l>MWcQ{sVNoDYUyUybi@RDDAI;zZar7 zwR&A@%fDzjHK?Lgtd>5V&ufiKk^Cxjz0OXPhX6E#($)97Xj}AB#9>js`RDmRNw?KO z-L50k=t#wRD95xmKZiWn36FaF23b^}+U531uJi@LmpA|8g43W)uUVE!0I^(rf5nB8 zUrL;PdBsPiRUHDgdpkNUhmPjHr!OkOck3B6k{+dmK!e~pB~iq>K$(a6J%7?Z@<+Rc zW9!X8|4vOkx_cTivv7(4l`}~l&)KhYOb9uv^g68@>g}~rX)=%pkN25mRf243VGP84 zeC-$jL2~pm85Jpjb=;J^ZfH@TE#L$Ko)^zReHYIT9k+jDVuK%niUP|dT(%8eE&UC| z^zq8&9h<`U)Lzv8?mQH(`GD`{Fn%zTHV7HI-$g==E$ktXb_wt;!@ay<`>MKU3#O!S50k{I{#$#|)uWGML=xO^2C;=?9YyQ5UTEzRA zP;n1*I=;gDQ4LfuHiwJh#q`Y)YGCG|G?F-wL5>xkf_;oJfGWp5@J~qpLsp?sh3nR% z&V}-r5LfLC7~v2A1%^37wW8)gnmeGbdMJs8VGOSI4zaYVcL#TWPtoPdwPR)JJ8wtD zGghAJtVL-<8Wg`5m#$iH4mvY~jV_vgc_KJP3}b}Z1tE-ARxNgD5<{wP+2?#_DiRX+ z(tt5-ki2~Nb@>~pf>xLJ+ot@4dRKS3{n{UR-TihsK-AKqwW%oQ&lbys3~)NAKD#`J z2wl0w@(zA#O$Jb+Be)##YBb7j(`CU~ErLJs z4_Sd#7z?yHUi}s8ys8mE_@7-e!Gu7S&c!zg;UJ}ya9aQ!hz&r+IfdlVv>DLVa7T_- z$Gz4N--vMl{IM;ddI6Yc2nt&^0RXZrq4BQnCJ*mC0Fl`^L{&bzQ{)UOf=Xpt{xxXc zfkFtc9^k$Z=@}_D`E9so?YC9lKu6CN@G)da^kzu86i*#n;{MoY&z1bu@0=PDn3%cP zfpTsea<~$w{{cH;*{qD z*zHWFe8U1r8ntzA*S+Ue({;xhg1kWyDRfD31~VGjOaxoAP8hc*-G{h+D}sLQ{qZ?m zGg7o5HawSrYIJYT%zpW zlngCGmgYp^l*MLzV7dIy!XSYU(#&syN zE6xjVLZxNPuZ=UUYt0hVD@}6SjO9c37`ww=0lbo97TZ}4k};}=V4i+lGbalw@02Cb zJ1j&16%>Ptg9w)KY1;}>QO4rzz)uMM*IyTyPWOITitvkb}^3tW#8oV`;q4Ez;_Twt3M2RJce=VbDhp1n*@?#`(k6=s(z;xr z&^@(naf@?pdA~mx;YlxkT_qX3YeVK`CFuOp8y%RNPG4hMx@_6lSRt=@KypOT7fal{nae-GkSGuv?u>c&7#pVGlHj2H!MS# z&Yn{j^Eg@TeH);QYZ6Y`uKvsKpi~CQ9pfNB-P^y9|578Y$1%Gc_s{jmoqShwQ@9J` zFCzeJh?0dH$ksburx9(T!m8Zbr9X|=vF=7~d;xo{R?pdwDt_|<)^)p-xVi*Ht%n!G z#cZKqp+(D=ivO|>%+AfheqUhaX@dZDmS zXn=qMW&lM;u#lSna*2?yMMIIoJ>_<95HM^aaP#*J z^Kohu;8(niIw)j*@TUzzTFXr4{)72#f;PIx^jdfG-?jnakUzXPDx}rq`tj^G;QXSm zELAxCPb~eLRe!UJxBq{KR;lX>6kf8^8yl%%A}N8vIU!I4qy@@`xj&Rz5sf<1Y#dt4 z*Ew9>YQfLsJ&*MJah5i_!ht;=>W6Ktmw>r3J{xAW)D8{<3QxFLNASbd>*~T_GSt z{fEtbKNKJ;s*pVCb9Aj>Qs?S2V5|BhV8Xc@J++$P0nE5!C9 zSUy{|w^abU&v-(2>c@sa)dPUdINqm$nl7_*Lq*VjDW=pw zJ$=V|306fWU$nSzPBdJ)rM5xikaJO(LKxNGYoxnzP+8~h2SSaP?;+!MT+?=oP#wNMHJfRmSn+toPvoa}r% zp$f^@rlJl}PMrO=JuCOW9bw1IaPl+TX5Y6iksyxy5#wLyLh>(O)UxH(3{3|BU4DX= z!@Lh3EY8pIDIf#~@GA*YW?Pl1@zxC_5yk!T!XF56TxK1IJ-H50UVsq~5E;+bpUREv zX_s$zU!OO7*Z$aaPtSTk6|88fOqew_=JGykjzzGi#!sUCNJj3Ijyr=fLw@X8U?8E?JUuDmlLCiQk6Xtt>~p zxbRkNw8G(m(V0wXXdZ^@anUJp+^%(blwF_dJ}qpyBWZ|qDP7koQEjlOunE$do1T}w zw@#ydaMcrP9l_!`90sAuh7S;7Dm<(rn_>!zN>1xMbSIdg3!4@e)Bz3H=ML66&L z?$wM1VSax1?*|tsW2CmF>|UfzP}5Q2{Id~ADNrM}Zw*gwA8L)W$p}oP@Ztyky=;^x ztfc`02wgUFO@0<%7ke>sv#?*6`B>IuF)`yRY6r_(I>-1BFmiw~woM=iw}w)6!=V+f zRei)uj25cN4uEiaLk(gA4|0k3t4R(0u@LQz)sE@b8xq!G&5FsnkvphP#9% z*B0QKm22ypNt}EdmEha*%5@EjZu}h4h1oSlEMz`xR7D2I=89Spg=kzV%;H)K6~dWb z6HtEU7k=RRP03Hd65UguI_rFInFt7~ZQ~9mcKh426;%l2oXIdcbCki3g$fK!2-vS! zXbI-e>XfbM5-`9KQmHKZV1n9L`@?SZsV{=VFT~Mj^1wWHW(inS!eU}NqrO2>HT153 z3YZ$Q`xraXgTJ=M;&?@VDYCA3C3afp98dHME_6#e$p?Z}iBE*AwTmdE#*$l)ztr(x z;(|{3Vm5SKSeIoos>9s6`%2+@X+xx+F-U|gAoqB~^JaT-k*xa#!}8-rh4`3zi`H`e z7OCU2iO}Q)d766A&VngY<=enymf_CAyuIYKD%<@DX#gxh&yPk;8jI7uy}%vZlVJWF zVHp&f)K;U0orYZFE}uNrfSgh^aE!=7tvkSB?=iO58-fm>d({sQM;#s$hjmOSDpJ zqZ&w%@a1}beTnV*OqtL|0( zsb;gBX(|`P+V7N(3+S!t13wF=tR+++#6NW7vc@lj?TJW5K%K;>bZfHQr2bp4n;cs~_+Xv=|9slbV2SC!bpEIJ6*548zI z_8wOTqRq}75$Jq5)+3xHM>N%iPA1Shs6gX`98;=M5~@u7h(L7AXvyZ@>|Ndq1)iU1 zb)iCzgn9iHM|1_^ynvAAM#GmCYorUU~dMN~@rq@&1iHEgB=>{wsRfu|%#6a`z{=pU; zsk)jk*B5CctLo}% zV+3(O=dIy9EE;~5g zx;;qP>Xj(8l|} ziBu?#MowgAmsS|aSt~xDD?&(=0T|&|a47yt-shqcC0`yuDuT%-pap%_W^b((ZXRg; zcxQAzRDGk-gtey-P~Dc_SqahlNhr~T9j7#)9#mO7KRFwy!7jF)UpjSJkU?H`*r!{l zHHA}g7eq{Ul!yJ=Z}c&d`T2^)4>B?w>_vNktBOlNbI~M52+;&b1lV{;88ed*)+O%7 z(w9+lb`_+&KH?VQcdoE+NsujEr5K8C%4DZY6FmR=5fiTs4rRQcGjwX=TOQltW^3pbz`MN;Sv>-2wGQdtlj0B z6@VjZWPnEU2sHo!8XL>I|~D(Kl(?=nv}?}w}H zUwDtE-(<|gVi(wKmvx^W62U;zOKm-ZGm1bI!3?+Y3mPI7A!HZxVn@`*ro5NE(fwKL z6cW>;b1&VETk6zreI3+`VoK%$CMz=0FtvIYRPg? z!y+_@K(C%eqvrAcgLpKDuubq$IqY6C{?vN?T~?Eqe$S+m6iC?jC*HYk<>AZpEG%UFyZdoL+p@uIN?@5-+ zG2?h1 zQ0LV73#Xg|+`4|hw%%d)j}NZ}j+UHALs-f%2DAvOsJ*|lcq2@G6n#)Pb=;1BtlqNM z3MPM?e2kZ=@QQ<)_F~NShJP@N>TrfbG545VS1;YktW?EN z)#?Xr9pFa#K1zwdS5+|FJ`7;W4@#DxO0WA^nKsb0j4yqWd`|x7>ne-VGwccCc4)){ zKnaO5g((Q11rX9Kc=NSu@wN@8+Ty??ST(aCIEAYy#Y9vd=3^|+$aV#gm5506AW-~h zq#TB34v#v?viD%eOyQ27c3LCgDwsM8snvi(ocze3=qGsv zZI$~45DFXZPn-W|SVfUdshRWXzkL=uEyK|{gLNor#tP25qk>k8{nf25DG*k*u!DRZYOD&u(TlOX%(Cic5J-uts?XT zU@ALvr<2)cdh!G$a{%~b$!DgPHjZpmd`G0|5IZqk=p?KLpL6ih@6(aivN#(eb+$Vr zo?`g5RgWrB{FS*;ehFIpY=#JJ1D+4TL%?+z@*Fezj7LMd>Kk{96%S`pO3hcEEd&e( zj7@^9SLAO8&~?<~35l~_|7n(LXqz*PA2Azpc1@M_dj=pIJ(w#*Fh)I7U19t#Ee#?$-dVyddD@V?b&Ejr(C1Dqr1 zUbv797>o5BtbKM7ui9%M=o=OQCUP6=l*B6a-7cgnK;lQh_JWC`dmJ9gpH(eX)2#f@G17fUSq7g~+Qa?74FN^wfpm_a`q<+Kx4Td6PR^ zC*^X}uP<)BftE*w%!@S0b$Lt^d`e+s3(@I|@#Hh_yjsb$<3va9WG;X~?a8 z%%hg{*=nEMXLGf`(X#7pSzgUfNP9&-KTNgom3R#RA1_cWh2R;L9IYy^xMs1{%;LcH zE;OzOWXm&ny#kOAf(}W$t|B|3M#|Utz*H?^aSFi8Tc^Hgeh3T-lCKV18=3bSA~{Gm z=lzQVMN5oT@@kpSbb4QxsXP4Dikt&TX|K5`V^52W2g-WgI`q3UyqUa(37PMckk@cR zE+NAE@v{#DyF)*j2o@gR*Rd^ie2M9Wnlz#C*5-|WdPJ<@e{#DX=lB-_1d;q)JF&!$ z?@?X1Z!x96Vvl#^R5hUJJ1|Bc*y)EEFkvGNMNjrA>}NYdbp;F8?HhtOoD75hzA#c_ zv|szFx+IemQg6rSar>weA)5&hQQd$svVq?=&?ilR z={v82$~^$&9ON?Qty3(RiaU(VK4qTj$0Z8>yAR5K6m_}Vr=d zf7ZVK(WAxiWp?f}_m0SyX;QT6dT9JX?~l9}4ZtUztO!iw_(-`dscqvA;h*NDwuf3r z91htdXJ(dGm;Q=(h#)PU6C(L+Cm?<1zs&2nQkqY*!Q&(DCo3f1&DFUc%Wygc;}qhd z`F#NJp9s=p@R7|KXF?+dN-vQ(D=yrIGeSJF7cJsk|>qDiy}31$7_U zS=m~f{%VW`6PHVFLM91rVE{Ki66c80({Xh$@^jD0yzL>-<5I($?`68rXUf@uo&~VN z(p7hLuFu|uZ*z4QgONgx>6Jk%KY5(HAma0^SAz5*(D5Longls|?a&s1s8n7w%55B5 z(4wsbrboW=&M})miAlP@PK)r)^qlI$j3@h~OgF0-#`;--Kpw@P8wpDSUHTJs3mo}t zch>gcGS*%5a9z3tdfGOI$?E1M^r+}j7Z(A0$hgHQ82-ACg@UCopkFufZn(f%QUBrY z&%ul*^uQsba&wWZw@Gw`)VdmT;J9`^io@D1vtP%$A5%bm)V;0m{Pie zB|gwD^2f3rO}nw2yxr#tzCbKDulN0lLxldg4T~;B1ptc#+gd*8zlKmjPvOpO6{;2&}>pq@aoIo<3DnSKpGiqr*qn4kYlR@3s+asPn z0<~D;@KhRd+cDVXGvWd3ViEGujF%eTJjVXAS6YP6y(6X{cG14x<#k!d8|bG0?%ka3 zr_U$m$sF&AAJlv>thsZiLY&fr26d8}n(+h0U;8L^*;Pwp?^YmuU z5S@SFA7|(!_^6{t!73QJx6W3Tfqu-+nuY!TS^Jb8^x{Y#?Eg^X5?GkA4)j5=pJW>H zW`$YT{aK-V_BPm;hkdG?%t}wv0syMh@lwy|xdTK_exYQTO;(0tz;8z3yBW9y2b!M`hxz#-guD=rj>ciZuphS%Fsx#h*^XkuKv8(U?|OJW63A0kCjB)r3)f245gMR$0e+YR8C z&jvIUVuGs^^JfM_B=i3zhoz_G|K{-D9R6E}|F#nUjUE1fY$X=I?Ne8RPNA7@+r_5j z;KBp(>Catm?R!^E(}cNoa<~^SN~~{9O5*fy+DyK`9csdKrULRr`X`1#kRO0-(08M7 ziDEetEmQy%LH=gQt0lz_hJ0BqfHG-?GZf}v6ks9*oTQqt#fkAP3?p-Q3e{0$Y`_c) zAJjtkNVtRF(bMiP&D%bJE|wPOc1b7(r$NWMG=O9-XRLGF@eU>lp%P|H6~dLEZxne^;PYWzmllDu$O)y9`qdX3q$5RZk5* z1ft7mTU_rRkLg`{5n`357QWfdvYyauFmnY(gJ|m9wx9v^5J=IUy0!_qh7v+qC{8|H z(l@cv6(S47g~!h(xOap~VPqo|AB@I~;G$_%hy!pVgrKLjj&>tf=b^ykT;@@BE}Dl! z*yt{Nw=On7xFNU$%y{oLOz*G5Rb%1X3NIjjnjJbVwF&^lvtW*y7EIKo2(!yP7dniY zLjxS+MpyVe(=o&8_mu)zw%N-@Z zWUd69z%XQS0C6NNqvu0b1r2bmK5k>(M^#d7d9K3l$&E{hPk#jsH3^_1_iH6};Y$U) zv>Y1#^ZZ^I==@tAZm~qFG9f<-58atfFpf&)@`o~*@hZ5kr`u=ihjM!GSsEc)pvnZ{ zKw!{h!PHCdK}rAcJ=F%SxET!zJ`6w~1bRUq4!9r7cubld8a@6mpAL7D8Bp@h%HvrD zfH7tm;I%KG3V}KLS!019pO*5U#r9@_)}8nfKDdJh{Wij6Zei#q)B#YX9|C1$%vLyv zSDHHZejEUlV2QcoDw{$;`CQh{W5YKrVj1u3{g|3H(3g0;YLl1A zmptsgRQ17?^B}(ms}n}~de((dksr3Y%?K5eyv8tntsZ7M?Y_VC&^kUBQbfaOJ~-AK z+D;G*=zJCwS%3(S>3<@EmWYQh93aSdW3AKPdl$Ye4r-9Z6yKx=70{t6S*Th+U*8%8 z^AP|h$cHaJL3Z^sN;ATN90strCCub(?2yFFcWfQBR(MIB>VBFfxTehOAQ+adt@pwlbfZ?*OV^wrOi zIt`!QfW|C1Wd5AWM|v^1(6(2ozZ8a@1YF3JM#na?Fc$^*jQ*;-QI%_XER&&zP?mAr+)|!Pzy%n! z{h6g^9u4CliuK4ca{ad)#WNe6^+q$a!~@143mm=&CYQrAeYc9CGRZ#da9ZaVclWgM zxPYRtc6ls>>Ut4$6x(YN&Y;*HO}&K#fB7944l@h;N#ZdHHD4)!0jRrE6i&sE9_tx0GgK&j&tHIq!8AX4pt8wLT`-I))hqhQ1yOh*%5#4 z9M=zHj3L&UbifX+3)2z3>Ui_7qO${11c_4e!iBxrX<}C{B2F~RRM5!MTWK zkTj53VG<0q3Zl=a5u0DN3gUWLv9ZKS#S=xF(&^UebKkyx1anNr6=75aXqR&GgfrE-nG8U?*$UOh6pw)%GjxZeq`~)mh5I~2CVjTX z)dz9FDRS}*r|zOm6)3q~zd)bBKDj#6NeJ^rA7@a(hZqahYdPiv43B(g(5gOlt)2l0 z+qeJjNkeuEyadk<{Q(3)U{`ID7y8gvmIj&(W6g`f4tI1y7wKx?Fbq_kD@=nicNS}u z?P{h9RvvyoAD#6Qp$?eoCJD?hz(AEhSe(4YZ|j1_Cj!+W?Kxm{jPO#pwSSyAp?*CV zG8tv}aE{<5&_Qmc_v(40&-Vbp`!A>nji8e8zYPDc_P#tG>imB^)5tJlNG2^A_mQh; zO(Zkpu3Tj+#iE3c{hBQ+p~j36Qc`p{&_xU}3-+#W3Uw>_D-ZSs#>v>gQ_cEjUHWF$oz(tv5B5`Sv~zRR*FMiXa(k5unv> z=)zJ0d#X$(3;b-5)Ni_J0xvS&?PW_f$MxI%{G51$o(fZJC3H8qs#5*IS!OuILQCye zJENnWkNH&{Ssd;8L34pzCH9ge#`Wovbl4Xa^y}0x+67U=>40%iNZf++n*eqjUnA2{ zKNh5H=#EO^o8etB2_Eg7G^g+mISC{&s60HAgvLCDc4-5jh&L7AXxWVBo4QzoZPX>^ zvnWlRCGdNwBhRR30&U_7H%_J@&lT!8FhTR@=dw5NqI0(RaFO?OE|g&)t(KPA>#JWe zQ548ISB@i^GKFTLH2L`K2A&>thBT#8hu?}Wy;Txj7pdM-%Y5^u7S%z9Kw1!+Xz+F4 zhA+stx?&~lCu)2%@=61`nOK%L=EICAu@ZNGeM|_~>~V|Fo*jEFq`@KsDXe&EDq3v# zV-}lO>zqL@Y8x3D!@sM%w^g1WzyES$WafK^n;(6mr%N8dZG)z_yj^WuNN8)2shN2J zE8E|q>c(af%ws+@zF4aD)azV&ozl&Z^{N@OHVXO?<2I*$yChPU=tYjH9xz-YbCmVH zsWUnp;i_Qtrz?~15Nn^AHTh1W#0h%fWTU7V2i2nH4a-CbTvtFg2I#c+Gf zP^<||bps)u18F@TCTZv+#LM&>+MVs?3CrB|5TOik#RZ> z2Qf|jEztB$Q>w!7+hq#?JM#0-QvFzSoGE_X)Ri4j=$+&HPm+bpD+8Z>u#>U4cTO{WP^A!P4Ea z^kmc$6inHz3l2;F1x)%EF#rF+X5tuf+Wi+-Fi&={g){2xl!KL|@u>jr=UxC4IfFNP zvaYbf9P2GSWg9auR5UNac0x2fKe=p{N{LdRHBh z$NS8Dh_~vOH-hU5a9JIu>CMgjgNPO#BJH%e7{&&`YtpMxV zVfr4D8&WON8gLnwG{H(8uolqpc8Li=5xV{Xy|`gJR9mVM0ud9^%v*Ge4)!OAv!@ zOoG8PkYn03ALs$=l;ZPg-`akIo*TJ!(8)Sk%SF-QiU`S#Vy1;tsh6Hdcz)10^g1r* z{oQnj^XJ=(8Nb@Bz$k3hBdKnqZN0u|x=Sz4H+tkn=6gT4)C!^Pxg8_s-*ttB8*{x` z%dm3EhCzB76CT2>#Z>o`6l~-;LwR8slm4}i4+*<;_r?yI`9$CA$1y4Lw9R_gX2*3YWAx>4f{llxu4&&vk}w~>pnHaJyP z*e^rt&mOZ~&bqLSiJRy{jF$Fe1pC&NQg9Y+c|~zS{Y_PuBsA37SCz@~M&O?(cZl(rV>?e3M~FgJlM@Nh6603Ds5- z0={BxWM;?IT7$tiQLl%6ko_5hO~@UksI1-N`New$jfxW_I^Ao9wKNLaRa|5wR+G;` zibHsKcCmo~qrrDhtyRv*wnXw5u%ih#w{`m)1|nYUWroN!@%17t0`mtb2KadAKNPfL&He#}1WKr=C5LH3XwGNj*%U2Tne8D* zyJk2`31igyJHB+?Q!W9-*VEzeKAvk2grB(B zF};CfndyrUKFgD0#-bVl1ufWt-wwy`n#ppQ?@QvkN)vHR#|)+0eAx=2D`L*P&Kr%FRYp zd5jdQsR#gC%Q0So6A1&{owVRCh0IYjAz)5e80^bdi5w5zL#db2xFRW+Pn&di04}Wh zA|U^%fY5~QY18+gEz&BY#o#VgF}64knJaWSQbzCJHhmGRS>((sdpSQXMZVVLO;HGU zq559pS0vN5tPY1>#`oI~8(}Q9DQvG^4=a6PMp?@?yyj5FHqJ}7ULN~EG7M_js1x_E-lw&Q;uI)_XX6+(W7S34r z?K_PN!6u~qQq2$ie)V8Mo)^jP>@Ia8Kd0z}7VibX-`3 zbywZlObyNVXdDSI?)1iO2CU#|#GwWnyo@uZIGCcS9U`tW9QJ#L;8MW2gN0!8L~ zkLt0r_jMoc%#*SC_qSGOj3_nb^$k4{fV`u`UaE(vkvSorz_k;2+VG}kc zJ8-lya!k3^tV5}iZ^vUJOX8keKtttQ__9t;(!Q7v4s78tc9&mA+I zNTd(6ZoaHSze95w=%B)xsaP*t=XBwQ9=qPxN=};`C?o)vVW|60$6Ff|LLADmgw|>R z&w?o@8)>u9(jJJ--!gJT;e#`rtu_*rTeyN0^~4!QFrIG`IsPA#&hLBC{}A3l$U&mx z8yFcIQiE150wP-8Z}n0j;0PK|I1H*x6CDtd69>d0DY-Ed#mq@6RET`>(swvmg>7nx zB(DbiE(KCsnOS(hJhH5Y_>&)P9vP(v?=!b8N1By+zFwkL-WKjyRg79!vq{E14bq3& z>{e}s2m(Q-pa}no%vxULs8j76Bv*%>vft>r_P=x67y~t+wG~!!ZYxWY!hdZmiy9xn z<+y0_6#A?AU$?En0Lx?IgAypF`@29T+Vxhc=Y6C$GzR~LGe2%x@lVJ5b%kA72(dzV z(1Ne6z>^O1=fB@ws)2$1NtKZ&PgFahXM3vk7tv_tK>JtIB8hlP{7=P6y5Hk)mGlr` zr)4P157O$T0*K!e9^)1_i;j)|58(}rvL{$zFHw77hDB;R5D{A2@{h$(K)H0y;5UXG zoezjGMv0S6m)Y={)%Y*^hWFb(B1L-&4>opcEAUZLGqWGQnHi)ZwoaA6FQS<(~vjX`Vw%9&Zmr2px7Csm$1lz{`3%hXMr=gONO`>lK3 zLIpzfiisyt`><;jQC?g2SC!H=v-f7m1soY`Ri5cW3yz?L&hEm$7c*~e9t8g)w<}_a4-w0V4ksZEN~@SjJ0vp?4BWam~g`s%MJJh3;3{`1cn=63hR(PgMcEGlcn<1wl(*{Vn1_KGwQ2z zqeF;pRzEQG^&UMuXuDao9C2G``4IhmMUxd6DQX4^=m52YWggp&r>4SR77x+T|AT`c zMU}+dF~nejW7jo<*qZe7_je60-ijI|-#uzJCHP<>+=c_Exi@c|NcjB}9>VK0TSVvf z^B%oa%zqJGxc<9y&vQHCq6rpKa+1L?RO$EOe)=~8rsQ}&}39(TQM@+a# zo#rcJY5`q&)d7!cmm`?IcJwWG`&V1^g_BZ(UR+m=M;-PG%cp*^jt9 z7iQhrxGf^d#)5R}X#TwQsO$hv!0&9R;?)He!hs+yvYO;WI=f)VZaWSKG)=|C8{@Cf z;Lp1Q4qYYA{tMPVqk&RNF$4vd# zHp#LqCAXsdOd#XH&jWzfzD98YSDhLp1ehBZA?nX*l+s4&3?=eD?}%Q%>gWs=Y^Y2T zsyk3tMuEax?K=v%fSgR){94H!yZLKN;$rCOi!2Q29XCfCs$zr|=D02;e`)YE{>o|a zErx*<8-K5jcQ^!iX{BDs#_pnBf-?+>@!dg$a)W9`a1Fi?=nH|qNYMYg1cB{jt-yN1 zR%{84ehoSKRWhnrKO9Il&OGno1Ld|rl^-T{MiSaCsU%|?_UK)CasL1jko z+5O1ygO!($@5hme>zIjKyawbfs#pQavKs0c zH5wk9t-W7?_Y4gpdSBgfVaE`TZC2%~Q?aRY=LKYv^B%6eYzg%_j$je<=H0RDcXBO} z3gI&gmqso2E&8PscEtB4PwllU7HULnB5C#HRcqLgCGEf)k){Ycv)wPd^9wGI_OKlnjckVeEH5xu^z7feUSJ=PN zBLQOc_z4ok)5+MepkBWN_KZp7WrBY*oOIbhjX?#=6!JX7VwD@@oZ1|>(gufZi!F}R zy);v`mr`mQ_Fr&%OQd%kJW+~L8fxTY{kNcIIN+$oKo3v|mnRjBcijl;`jJV06C+f# z>u_?DuFuuDoW)Y_Oi4dC(j$t%YQ>s?>0#Ps8098gZIEXz7aP)ZzNngi^ih@LTDlc~ z?mRi{#_$dsWjiFg2p&G30)}e>vOlQ9;Wl^J7T+1E6oOpnygGHJUCmIM4OL!P+X_D| z1_`xP@fO~WzsspnB@Zz#lDc`(ow-u!x9ay-pV-xVMm6pc(?b>YkIeNYLuCTlT;K$$0)Jd&$bxuES6F$c3HsqDt}(gwJ!%#2 zM0`|n9?hpX*1It6BIzjxUH=!zE6cPk?*Lt2wr6dqlZup=TxF4PHvB=+&8x9_#QMPmRk9R^@80aJrRyIuy?^0wm($TYauhN)V_l6{3O5d`Ltyi0d9G zHfyt2Gu30~Quza#-)vT2eBM&sV(k7tlie9g6DgMCL6!9VE*$&%c&MIVxJ)`Lk!I7R zy=ffON{H)jO4K#_rN!ran#vD74yYvf41{dt$EJ9)4F5QlhudVI`ZmYjtnY&ZhCZ+q z?vIkxx{}d=MrGB$M2Ot+H-&T-sr&Zh5-pcwFlG=i0Qj6Tyi4H-ty?8ZHE#zH|u8Q%SxB&t^;M zW0?(OL0xNpG4sP+3$rljr`GFJ7taN3&*OMleZ8phf5f?cs3=ZSH3(%AHBSg5LVEB4 z_-0u&`R%K(ww|0y=))e7RTP&G@{+u~4}1A{MZ*z_@8s=Kb$wkZm|ME{ zjO_wxaRH^x;qukBhGwB_Lwb@recAvI8linn(Sn}SUqg~>wiDU3_#SX!Bw^(`kGQ$;FkC}lq5+cXc8>~Y%!iGxs`;7aELoOU z_o6iqa$C7ajUhwyKgD;0$R(K_DwNLl4X|D*l78%I+JcgIGUv82T>!&QYJK>cGs(J= zHaHcXLfTwBRYpjg*9&EZ>|I_Wm4nJplTDX+%F4G1hR}#O#k1}#WK4=;>SS5E(NLw9N~ZuUQIIo{_PFQm~g=DJYjX^zsD|D;e>cXAjZDrXo!XU5%ni+5D2i-i=p zSbaS%(t#W|+@6l*JuDwc0DM!l{BYapK_rNlM`#0Lg{e2XGi9z$x~mWp;l@3cAJ`RU5BLUM!8RtFqjjEx*R_&)eMUr;b}6GrXUW-0BI|M{=G`Ik>Uvy<3Ixa>rS1F6%V(C2j6ykByYx zboo|pNRWq%&vS$1;6zS;x_}s0eMFHWiP>*AB{_qPqxmm zC#9fdA+C#u{cXE`Ghz(My@gdz+J0nj<3Z4C6~Zi6?TG>Q>ZszW+~jp~drtqnR42vM z@z<&~J{@s(urFRPctNLojg23o~n(Knapa$|lc322(=-%8i z_Uv1KGAsmpNOaqbFGG3ai1t~Dne?fBYgW6v%b#HkJs>?$bJZji&fvGxP159Gj~lx< z>TJw-m_{xdN%r9NuEaqtU^!Bp$X=Ui2D0IS25TNyES?f11Urm->IkB955QQXkt&p$ z-CFm|!>E1$Y zHW_p1MecKEr4X)Vl;kezB{gm>6U~hN?ZJl4P+-U6QlHd{gGr>*o|ffyz8FQn(`@V3 zajYRSZvEl4G}{-&fI{=IH>SCLJgztia_J*Yi0KiS6X~Ix_WXLE@XZYc=4XThgPEj3 z#!>|>&PNOOWm|8U`sLs%sJcJ2C!0b3P&nQ23 z7D#+LS};S>8AL;nPs}h~jHv&BBkrZj7k;yJZvv%q2?lwzGfoVh!IfZWKU5PvaQPhv zk#)NrpA34@JYPS;ns&l_KKqM9XI}b3&0&p{?Cl`kcfHm#O#X-{K&$L8Sl?N52 zGwpHf1l`B&dev|B4?QqcSC#(z6M#S`vfVVX7wPn$v&T+uo0!~rMm`z)nS59OL_Rd< zoM}(fQ($c*lizv>os1)=Z!}0bVLoe%r@w29xoobRW_Bu$YM{!6kTF-eVs>k9td>Lo zlv;t^&G!PkPecm_H34ssE@tSR;=8bUN^;GR3GhtnI&yWa=2T1(9u@Z5bV#fmimarE zpAu^aNE`f)5ksWVA=GxWu-lpPNyr;B{v_lOeZF?l4pY>CS#{=eXBdxjep0^@P1hz3 z)@HA;hU`m|BK^VzQ?74Mk~m>IqOE9^NJy)#QHgJ`-P8KweXmXGr*=3`Nr5A*rt7!n;IHZw%>!0_`xN6T zmHpKb!i$q5eokChMJCPz8*}BQx}Y)V>`L-+UVP1UgmF^YqSNH=Bk6A=?znFjsgqY0 z=r;8YKoO^K=G=T}rIC>k$Z2uCmtP$oRe=@_=v5xdDdTp>H6W(j>$w>goC3Qm^K-}i zL;x-a4m#P24y_)b&_88OZT+6K$m z$^0#&4)H%7p!e}{_SM=Rn>kVG8SD6Uj}MM4(@7bLUvdBshN(g+?dFD&)!Q#VGpK-4 zL}Y2fqUom!qta=fqU3Nl{r-S=Q?nCBf|vVNR^+i`XQ7f**8nj3Kbl>ehozVaC+APnm`lUuHJOd;P$)g zP5!9Wy-5GctlALQgJ0J?vR$AQfFkx#}2DG8j9KV zgUo70$T{xIn~p7#d?%M`8aa-(zN{Ll_)eL>6+HG$pd{wFGbwr1-NY8rWZcs4&eh1|LI{b5u=mV_c*U}joLQo zKR8MHyF=a*<{m2RH_?kg%U#dHI@VM{QD|drHl*}E5sG*9>#3EifJ&D}uFZyYnQ*N) zltm2ZfYUoiBfI#`RWBL)nWI|HawjO*4VYRr*o;-8(NBfi*iA6XlBN@AW-&vKQ(%dx zui^`aRJkgv#Q2u9tKW0Ldb&Vyq6pbOHAe9_sj99gwoF4*p-xg{tExc7_7A#wcE z?^oG+@&n80!)D@)8}alVm9>ifAQt>1*oW7|ED3RJ@Yy?=vzw$NJCGltUN`P;juWP! zI+U?mCu7K+>J1JZ5j!VD8<+o$5S^P2ME=5DhI2H65?CEB_^Hel>u5*l?}7_JpRkTR zQvQzx6sm>o0pl|?-kgA1%n2w<<8R;q<%~Y-C9*{kvYKqZ2dn81h3cazPx`_q-F5l{ zke`k)a(EBJ1up}hLz;ee4&{XQCSJwB;^)<${(=Nm|baAxtWV(B0GeKZEe<*A#r+ebq(Is z_SkQI2vh68p0<4#KxfDfEp)v0nMmZ~#-YJ15NlEBs~17cL$>$ow?}kcL9Lu${uI{( zSb&&7Cfl-JwkIBbkNW_%1%{Kgp3+3o`1EG7XH4N!xK!d*1 zSCBPY3JS^L;5Gn#>X)t=xInpR*SJy|T5ed4D!WC1OOKMLiN4climCSV!{CbFPFcNb z_t)<6o28CUYB~SiA-@xW9HAPzaAxmz=n~+eO@XCQ2*DSLF$D0Ru%?b ze7ojr%|RMJIFdfWXNiuHhk1g0?&In_l!+VVxRa6t+1Eb2fQ*@)sLMnZY z)ZwRhM{e>65_WT6onh1{G!Z1yD7~`MBOV?1?2CB|2E*mo? z=cs7LC_?jCfU$F4-S!Sz##R6GlDX0P>SOr>uGI8i=x%`evJP`qsn758MHp0F|IwcnxfZu%+YCFfrvZL!c`IeHq z&dN7;`hLcamngGHS-VYOyidb*vsoj{b8VXZq4tS7Wf&i=MLVt;*w! z^B&3^^)_pWAJYiU=z>L^YstZ|C1;K>9>RmaquRU14DTrPSli>;&S9;5B=v6a+6+E9 z2Uu8OA?;@eE?|H%B1D5$+RTK1=%7H$xj~`@j9o{>uUW kMAI)6KpC)<%tRy}L@sSz@FNfIDb1Vb;^1a~jpon#Z Date: Mon, 24 Jun 2024 12:04:45 -0600 Subject: [PATCH 377/441] Downloads: performance improvements and merge adapters (#1145) --- .../ui/download/DownloadAdapter.kt | 223 ++++++++++++++ .../ui/download/DownloadButtonSetup.kt | 2 - .../ui/download/DownloadChildAdapter.kt | 94 ------ .../ui/download/DownloadChildFragment.kt | 56 ++-- .../ui/download/DownloadFragment.kt | 289 ++++++++---------- .../ui/download/DownloadHeaderAdapter.kt | 149 --------- .../ui/download/DownloadViewModel.kt | 58 ++-- .../cloudstream3/ui/result/EpisodeAdapter.kt | 36 +-- .../ui/result/ResultFragmentPhone.kt | 24 +- .../ui/result/ResultViewModel2.kt | 46 +-- .../cloudstream3/ui/search/SearchHelper.kt | 20 +- .../cloudstream3/utils/VideoDownloadHelper.kt | 12 +- .../res/layout/download_header_episode.xml | 6 +- 13 files changed, 488 insertions(+), 527 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt new file mode 100644 index 00000000..8f496b3c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -0,0 +1,223 @@ +package com.lagradost.cloudstream3.ui.download + +import android.annotation.SuppressLint +import android.text.format.Formatter.formatShortFileSize +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding +import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppUtils.getNameFull +import com.lagradost.cloudstream3.utils.DataStoreHelper +import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual +import com.lagradost.cloudstream3.utils.UIHelper.setImage +import com.lagradost.cloudstream3.utils.VideoDownloadHelper + +const val DOWNLOAD_ACTION_PLAY_FILE = 0 +const val DOWNLOAD_ACTION_DELETE_FILE = 1 +const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2 +const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3 +const val DOWNLOAD_ACTION_DOWNLOAD = 4 +const val DOWNLOAD_ACTION_LONG_CLICK = 5 + +abstract class VisualDownloadCached( + open val currentBytes: Long, + open val totalBytes: Long, + open val data: VideoDownloadHelper.DownloadCached +) { + + // Just to be extra-safe with areContentsTheSame + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is VisualDownloadCached) return false + + if (currentBytes != other.currentBytes) return false + if (totalBytes != other.totalBytes) return false + if (data != other.data) return false + + return true + } + + override fun hashCode(): Int { + var result = currentBytes.hashCode() + result = 31 * result + totalBytes.hashCode() + result = 31 * result + data.hashCode() + return result + } +} + +data class VisualDownloadChildCached( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadEpisodeCached, +): VisualDownloadCached(currentBytes, totalBytes, data) + +data class VisualDownloadHeaderCached( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadHeaderCached, + val child: VideoDownloadHelper.DownloadEpisodeCached?, + val currentOngoingDownloads: Int, + val totalDownloads: Int, +): VisualDownloadCached(currentBytes, totalBytes, data) + +data class DownloadClickEvent( + val action: Int, + val data: VideoDownloadHelper.DownloadEpisodeCached +) + +data class DownloadHeaderClickEvent( + val action: Int, + val data: VideoDownloadHelper.DownloadHeaderCached +) + +class DownloadAdapter( + private val clickCallback: (DownloadHeaderClickEvent) -> Unit, + private val mediaClickCallback: (DownloadClickEvent) -> Unit, +) : ListAdapter(DiffCallback()) { + + companion object { + private const val VIEW_TYPE_HEADER = 0 + private const val VIEW_TYPE_CHILD = 1 + } + + inner class DownloadViewHolder( + private val binding: ViewBinding, + private val clickCallback: (DownloadHeaderClickEvent) -> Unit, + private val mediaClickCallback: (DownloadClickEvent) -> Unit, + ) : RecyclerView.ViewHolder(binding.root) { + + @SuppressLint("SetTextI18n") + fun bind(card: VisualDownloadCached?) { + when (binding) { + is DownloadHeaderEpisodeBinding -> binding.apply { + if (card == null || card !is VisualDownloadHeaderCached) return@apply + val d = card.data + + downloadHeaderPoster.apply { + setImage(d.poster) + setOnClickListener { + clickCallback.invoke(DownloadHeaderClickEvent(1, d)) + } + } + + downloadHeaderTitle.text = d.name + val mbString = formatShortFileSize(itemView.context, card.totalBytes) + + if (card.child != null) { + downloadHeaderGotoChild.isVisible = false + + downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback) + downloadButton.isVisible = true + + episodeHolder.setOnClickListener { + mediaClickCallback.invoke( + DownloadClickEvent( + DOWNLOAD_ACTION_PLAY_FILE, + card.child + ) + ) + } + } else { + downloadButton.isVisible = false + downloadHeaderGotoChild.isVisible = true + + try { + downloadHeaderInfo.text = + downloadHeaderInfo.context.getString(R.string.extra_info_format) + .format( + card.totalDownloads, + if (card.totalDownloads == 1) downloadHeaderInfo.context.getString( + R.string.episode + ) else downloadHeaderInfo.context.getString( + R.string.episodes + ), + mbString + ) + } catch (t: Throwable) { + // You probably formatted incorrectly + downloadHeaderInfo.text = "Error" + logError(t) + } + + episodeHolder.setOnClickListener { + clickCallback.invoke(DownloadHeaderClickEvent(0, d)) + } + } + } + + is DownloadChildEpisodeBinding -> binding.apply { + if (card == null || card !is VisualDownloadChildCached) return@apply + val d = card.data + + val posDur = DataStoreHelper.getViewPos(d.id) + downloadChildEpisodeProgress.apply { + if (posDur != null) { + val visualPos = posDur.fixVisual() + max = (visualPos.duration / 1000).toInt() + progress = (visualPos.position / 1000).toInt() + isVisible = true + } else isVisible = false + } + + downloadButton.setDefaultClickListener(card.data, downloadChildEpisodeTextExtra, mediaClickCallback) + + downloadChildEpisodeText.apply { + text = context.getNameFull(d.name, d.episode, d.season) + isSelected = true // Needed for text repeating + } + + downloadChildEpisodeHolder.setOnClickListener { + mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) + } + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadViewHolder { + val binding = when (viewType) { + VIEW_TYPE_HEADER -> { + DownloadHeaderEpisodeBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + } + VIEW_TYPE_CHILD -> { + DownloadChildEpisodeBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + } + else -> throw IllegalArgumentException("Invalid view type") + } + return DownloadViewHolder(binding, clickCallback, mediaClickCallback) + } + + override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + override fun getItemViewType(position: Int): Int { + val card = getItem(position) + return if (card is VisualDownloadChildCached) VIEW_TYPE_CHILD else VIEW_TYPE_HEADER + } + + class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + return oldItem.data.id == newItem.data.id + } + + override fun areContentsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + return oldItem == newItem + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 10ce67a7..880d5f6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.download -import android.app.Activity import android.content.DialogInterface import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -22,7 +21,6 @@ import com.lagradost.cloudstream3.utils.VideoDownloadManager object DownloadButtonSetup { fun handleDownloadClick(click: DownloadClickEvent) { val id = click.data.id - if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return when (click.action) { DOWNLOAD_ACTION_DELETE_FILE -> { activity?.let { ctx -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt deleted file mode 100644 index 1d7b5a83..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.lagradost.cloudstream3.ui.download - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull -import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual -import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.VideoDownloadHelper - -const val DOWNLOAD_ACTION_PLAY_FILE = 0 -const val DOWNLOAD_ACTION_DELETE_FILE = 1 -const val DOWNLOAD_ACTION_RESUME_DOWNLOAD = 2 -const val DOWNLOAD_ACTION_PAUSE_DOWNLOAD = 3 -const val DOWNLOAD_ACTION_DOWNLOAD = 4 -const val DOWNLOAD_ACTION_LONG_CLICK = 5 - -data class VisualDownloadChildCached( - val currentBytes: Long, - val totalBytes: Long, - val data: VideoDownloadHelper.DownloadEpisodeCached, -) - -data class DownloadClickEvent(val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached) - -class DownloadChildAdapter( - var cardList: List, - private val clickCallback: (DownloadClickEvent) -> Unit, -) : RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return DownloadChildViewHolder( - DownloadChildEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false), - clickCallback - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is DownloadChildViewHolder -> { - holder.bind(cardList[position]) - } - } - } - - override fun getItemCount(): Int { - return cardList.size - } - - class DownloadChildViewHolder - constructor( - val binding: DownloadChildEpisodeBinding, - private val clickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(binding.root) { - - /*private val title: TextView = itemView.download_child_episode_text - private val extraInfo: TextView = itemView.download_child_episode_text_extra - private val holder: CardView = itemView.download_child_episode_holder - private val progressBar: ContentLoadingProgressBar = itemView.download_child_episode_progress - private val progressBarDownload: ContentLoadingProgressBar = itemView.download_child_episode_progress_downloaded - private val downloadImage: ImageView = itemView.download_child_episode_download*/ - - - fun bind(card: VisualDownloadChildCached) { - val d = card.data - - val posDur = getViewPos(d.id) - binding.downloadChildEpisodeProgress.apply { - if (posDur != null) { - val visualPos = posDur.fixVisual() - max = (visualPos.duration / 1000).toInt() - progress = (visualPos.position / 1000).toInt() - visibility = View.VISIBLE - } else { - visibility = View.GONE - } - } - - binding.downloadButton.setDefaultClickListener(card.data, binding.downloadChildEpisodeTextExtra, clickCallback) - - binding.downloadChildEpisodeText.apply { - text = context.getNameFull(d.name, d.episode, d.season) - isSelected = true // is needed for text repeating - } - - - binding.downloadChildEpisodeHolder.setOnClickListener { - clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) - } - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index f54c8698..7734cb08 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick @@ -40,7 +39,8 @@ class DownloadChildFragment : Fragment() { super.onDestroyView() } - var binding: FragmentChildDownloadsBinding? = null + private var binding: FragmentChildDownloadsBinding? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -48,7 +48,7 @@ class DownloadChildFragment : Fragment() { ): View { val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false) binding = localBinding - return localBinding.root//inflater.inflate(R.layout.fragment_child_downloads, container, false) + return localBinding.root } private fun updateList(folder: String) = main { @@ -60,7 +60,11 @@ class DownloadChildFragment : Fragment() { }.mapNotNull { val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(ctx, it.id) ?: return@mapNotNull null - VisualDownloadChildCached(info.fileLength, info.totalBytes, it) + VisualDownloadChildCached( + currentBytes = info.fileLength, + totalBytes = info.totalBytes, + data = it, + ) } }.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 } if (eps.isEmpty()) { @@ -68,9 +72,7 @@ class DownloadChildFragment : Fragment() { return@main } - (binding?.downloadChildList?.adapter as DownloadChildAdapter? ?: return@main).cardList = - eps - binding?.downloadChildList?.adapter?.notifyDataSetChanged() + (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(eps) } } @@ -98,31 +100,39 @@ class DownloadChildFragment : Fragment() { setAppBarNoScrollFlagsOnTV() } - val adapter: RecyclerView.Adapter = - DownloadChildAdapter( - ArrayList(), - ) { click -> - handleDownloadClick(click) + val adapter = DownloadAdapter( + {}, + { downloadClickEvent -> + handleDownloadClick(downloadClickEvent) + if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { + setUpDownloadDeleteListener(folder) + } } + ) + binding?.downloadChildList?.apply { + setHasFixedSize(true) + setItemViewCacheSize(20) + this.adapter = adapter + setLinearListLayout( + isHorizontal = false, + nextRight = FOCUS_SELF, + nextDown = FOCUS_SELF, + ) + } + + updateList(folder) + } + + private fun setUpDownloadDeleteListener(folder: String) { downloadDeleteEventListener = { id: Int -> - val list = (binding?.downloadChildList?.adapter as DownloadChildAdapter?)?.cardList + val list = (binding?.downloadChildList?.adapter as? DownloadAdapter)?.currentList if (list != null) { if (list.any { it.data.id == id }) { updateList(folder) } } } - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } - - binding?.downloadChildList?.adapter = adapter - binding?.downloadChildList?.setLinearListLayout( - isHorizontal = false, - nextDown = FOCUS_SELF, - nextRight = FOCUS_SELF - )//layoutManager = GridLayoutManager(context, 1) - - updateList(folder) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 31790b0f..de2d4f3c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -10,14 +10,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout +import android.widget.TextView import android.widget.Toast +import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding @@ -42,11 +43,9 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV -import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI - const val DOWNLOAD_NAVIGATE_TO = "downloadpage" class DownloadFragment : Fragment() { @@ -63,33 +62,30 @@ class DownloadFragment : Fragment() { private fun setList(list: List) { main { - (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list - binding?.downloadList?.adapter?.notifyDataSetChanged() + (binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(list) } } override fun onDestroyView() { - if (downloadDeleteEventListener != null) { - VideoDownloadManager.downloadDeleteEvent -= downloadDeleteEventListener!! - downloadDeleteEventListener = null + downloadDeleteEventListener?.let { + VideoDownloadManager.downloadDeleteEvent -= it } + downloadDeleteEventListener = null binding = null super.onDestroyView() } - var binding: FragmentDownloadsBinding? = null + private var binding: FragmentDownloadsBinding? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - downloadsViewModel = - ViewModelProvider(this)[DownloadViewModel::class.java] - + ): View { + downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] val localBinding = FragmentDownloadsBinding.inflate(inflater, container, false) binding = localBinding - return localBinding.root//inflater.inflate(R.layout.fragment_downloads, container, false) + return localBinding.root } private var downloadDeleteEventListener: ((Int) -> Unit)? = null @@ -97,7 +93,6 @@ class DownloadFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) hideKeyboard() - binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() observe(downloadsViewModel.noDownloadsText) { @@ -108,176 +103,148 @@ class DownloadFragment : Fragment() { binding?.downloadLoading?.isVisible = false } observe(downloadsViewModel.availableBytes) { - binding?.downloadFreeTxt?.text = - getString(R.string.storage_size_format).format( - getString(R.string.free_storage), - formatShortFileSize(view.context, it) - ) - binding?.downloadFree?.setLayoutWidth(it) + updateStorageInfo(view.context, it, R.string.free_storage, binding?.downloadFreeTxt, binding?.downloadFree) } observe(downloadsViewModel.usedBytes) { - binding?.apply { - downloadUsedTxt.text = - getString(R.string.storage_size_format).format( - getString(R.string.used_storage), - formatShortFileSize(view.context, it) - ) - downloadUsed.setLayoutWidth(it) - downloadStorageAppbar.isVisible = it > 0 - } + updateStorageInfo(view.context, it, R.string.used_storage, binding?.downloadUsedTxt, binding?.downloadUsed) + binding?.downloadStorageAppbar?.isVisible = it > 0 } observe(downloadsViewModel.downloadBytes) { - binding?.apply { - downloadAppTxt.text = - getString(R.string.storage_size_format).format( - getString(R.string.app_storage), - formatShortFileSize(view.context, it) - ) - downloadApp.setLayoutWidth(it) - } + updateStorageInfo(view.context, it, R.string.app_storage, binding?.downloadAppTxt, binding?.downloadApp) } - val adapter: RecyclerView.Adapter = - DownloadHeaderAdapter( - ArrayList(), - { click -> - when (click.action) { - 0 -> { - if (click.data.type.isMovieType()) { - //wont be called - } else { - val folder = DataStore.getFolderName( - DOWNLOAD_EPISODE_CACHE, - click.data.id.toString() - ) - activity?.navigate( - R.id.action_navigation_downloads_to_navigation_download_child, - DownloadChildFragment.newInstance(click.data.name, folder) - ) - } - } - - 1 -> { - (activity as AppCompatActivity?)?.loadResult( - click.data.url, - click.data.apiName - ) - } - } - - }, - { downloadClickEvent -> - if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter - handleDownloadClick(downloadClickEvent) - if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { - context?.let { ctx -> - downloadsViewModel.updateList(ctx) - } - } - } - ) - - downloadDeleteEventListener = { id -> - val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList - if (list != null) { - if (list.any { it.data.id == id }) { - context?.let { ctx -> - setList(ArrayList()) - downloadsViewModel.updateList(ctx) - } + val adapter = DownloadAdapter( + { click -> + handleItemClick(click) + }, + { downloadClickEvent -> + handleDownloadClick(downloadClickEvent) + if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { + setUpDownloadDeleteListener() } } - } - - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + ) binding?.downloadList?.apply { + setHasFixedSize(true) + setItemViewCacheSize(20) this.adapter = adapter setLinearListLayout( isHorizontal = false, nextRight = FOCUS_SELF, nextUp = FOCUS_SELF, - nextDown = FOCUS_SELF + nextDown = FOCUS_SELF, ) - //layoutManager = GridLayoutManager(context, 1) } - // Should be visible in emulator layout - binding?.downloadStreamButton?.isGone = isLayout(TV) - binding?.downloadStreamButton?.setOnClickListener { - val dialog = - Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom) - - val binding = StreamInputBinding.inflate(dialog.layoutInflater) - - dialog.setContentView(binding.root) - - dialog.show() - - // If user has clicked the switch do not interfere - var preventAutoSwitching = false - binding.hlsSwitch.setOnClickListener { - preventAutoSwitching = true - } - - fun activateSwitchOnHls(text: String?) { - binding.hlsSwitch.isChecked = normalSafeApiCall { - URI(text).path?.substringAfterLast(".")?.contains("m3u") - } == true - } - - binding.streamReferer.doOnTextChanged { text, _, _, _ -> - if (!preventAutoSwitching) - activateSwitchOnHls(text?.toString()) - } - - (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt( - 0 - )?.text?.toString()?.let { copy -> - val fixedText = copy.trim() - binding.streamUrl.setText(fixedText) - activateSwitchOnHls(fixedText) - } - - binding.applyBtt.setOnClickListener { - val url = binding.streamUrl.text?.toString() - if (url.isNullOrEmpty()) { - showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT) - } else { - val referer = binding.streamReferer.text?.toString() - - activity?.navigate( - R.id.global_to_navigation_player, - GeneratorPlayer.newInstance( - LinkGenerator( - listOf(BasicLink(url)), - extract = true, - referer = referer, - isM3u8 = binding.hlsSwitch.isChecked - ) - ) - ) - - dialog.dismissSafe(activity) - } - } - - binding.cancelBtt.setOnClickListener { - dialog.dismissSafe(activity) - } + binding?.downloadStreamButton?.apply { + isGone = isLayout(TV) + setOnClickListener { showStreamInputDialog(it.context) } } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> - val dy = scrollY - oldScrollY - if (dy > 0) { //check for scroll down - binding?.downloadStreamButton?.shrink() // hide - } else if (dy < -5) { - binding?.downloadStreamButton?.extend() // show - } + handleScroll(scrollY - oldScrollY) } } downloadsViewModel.updateList(requireContext()) - fixPaddingStatusbar(binding?.downloadRoot) } + + private fun handleItemClick(click: DownloadHeaderClickEvent) { + when (click.action) { + 0 -> { + if (!click.data.type.isMovieType()) { + val folder = DataStore.getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString()) + activity?.navigate( + R.id.action_navigation_downloads_to_navigation_download_child, + DownloadChildFragment.newInstance(click.data.name, folder) + ) + } + } + 1 -> { + (activity as AppCompatActivity?)?.loadResult(click.data.url, click.data.apiName) + } + } + } + + private fun setUpDownloadDeleteListener() { + downloadDeleteEventListener = { id -> + val list = (binding?.downloadList?.adapter as? DownloadAdapter)?.currentList + if (list?.any { it.data.id == id } == true) { + context?.let { downloadsViewModel.updateList(it) } + } + } + downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + } + + private fun updateStorageInfo( + context: Context, + bytes: Long, + @StringRes stringRes: Int, + textView: TextView?, + view: View? + ) { + textView?.text = getString(R.string.storage_size_format).format(getString(stringRes), formatShortFileSize(context, bytes)) + view?.setLayoutWidth(bytes) + } + + private fun showStreamInputDialog(context: Context) { + val dialog = Dialog(context, R.style.AlertDialogCustom) + val binding = StreamInputBinding.inflate(dialog.layoutInflater) + dialog.setContentView(binding.root) + dialog.show() + + var preventAutoSwitching = false + binding.hlsSwitch.setOnClickListener { preventAutoSwitching = true } + + binding.streamReferer.doOnTextChanged { text, _, _, _ -> + if (!preventAutoSwitching) activateSwitchOnHls(text?.toString(), binding) + } + + (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt(0)?.text?.toString()?.let { copy -> + val fixedText = copy.trim() + binding.streamUrl.setText(fixedText) + activateSwitchOnHls(fixedText, binding) + } + + binding.applyBtt.setOnClickListener { + val url = binding.streamUrl.text?.toString() + if (url.isNullOrEmpty()) { + showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT) + } else { + val referer = binding.streamReferer.text?.toString() + activity?.navigate( + R.id.global_to_navigation_player, + GeneratorPlayer.newInstance( + LinkGenerator( + listOf(BasicLink(url)), + extract = true, + referer = referer, + isM3u8 = binding.hlsSwitch.isChecked + ) + ) + ) + dialog.dismissSafe(activity) + } + } + + binding.cancelBtt.setOnClickListener { + dialog.dismissSafe(activity) + } + } + + private fun activateSwitchOnHls(text: String?, binding: StreamInputBinding) { + binding.hlsSwitch.isChecked = normalSafeApiCall { + URI(text).path?.substringAfterLast(".")?.contains("m3u") + } == true + } + + private fun handleScroll(dy: Int) { + if (dy > 0) { + binding?.downloadStreamButton?.shrink() + } else if (dy < -5) { + binding?.downloadStreamButton?.extend() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt deleted file mode 100644 index 65a6441f..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadHeaderAdapter.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.lagradost.cloudstream3.ui.download - -import android.annotation.SuppressLint -import android.text.format.Formatter.formatShortFileSize -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.R -import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.UIHelper.setImage -import com.lagradost.cloudstream3.utils.VideoDownloadHelper -import java.util.* - -data class VisualDownloadHeaderCached( - val currentOngoingDownloads: Int, - val totalDownloads: Int, - val totalBytes: Long, - val currentBytes: Long, - val data: VideoDownloadHelper.DownloadHeaderCached, - val child: VideoDownloadHelper.DownloadEpisodeCached?, -) - -data class DownloadHeaderClickEvent( - val action: Int, - val data: VideoDownloadHelper.DownloadHeaderCached -) - -class DownloadHeaderAdapter( - var cardList: List, - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val movieClickCallback: (DownloadClickEvent) -> Unit, -) : RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return DownloadHeaderViewHolder( - DownloadHeaderEpisodeBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ), - clickCallback, - movieClickCallback - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is DownloadHeaderViewHolder -> { - holder.bind(cardList[position]) - } - } - } - - override fun getItemCount(): Int { - return cardList.size - } - - class DownloadHeaderViewHolder - constructor( - val binding: DownloadHeaderEpisodeBinding, - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val movieClickCallback: (DownloadClickEvent) -> Unit, - ) : RecyclerView.ViewHolder(binding.root) { - - /*private val poster: ImageView? = itemView.download_header_poster - private val title: TextView = itemView.download_header_title - private val extraInfo: TextView = itemView.download_header_info - private val holder: CardView = itemView.episode_holder - - private val downloadBar: ContentLoadingProgressBar = itemView.download_header_progress_downloaded - private val downloadImage: ImageView = itemView.download_header_episode_download - private val normalImage: ImageView = itemView.download_header_goto_child*/ - - @SuppressLint("SetTextI18n") - fun bind(card: VisualDownloadHeaderCached) { - val d = card.data - - binding.downloadHeaderPoster.apply { - setImage(d.poster) - setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(1, d)) - } - } - - binding.apply { - - binding.downloadHeaderTitle.text = d.name - val mbString = formatShortFileSize(itemView.context, card.totalBytes) - - //val isMovie = d.type.isMovieType() - if (card.child != null) { - //downloadHeaderProgressDownloaded.visibility = View.VISIBLE - - // downloadHeaderEpisodeDownload.visibility = View.VISIBLE - binding.downloadHeaderGotoChild.visibility = View.GONE - - downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, movieClickCallback) - downloadButton.isVisible = true - /*setUpButton( - card.currentBytes, - card.totalBytes, - downloadBar, - downloadImage, - extraInfo, - card.child, - movieClickCallback - )*/ - - episodeHolder.setOnClickListener { - movieClickCallback.invoke( - DownloadClickEvent( - DOWNLOAD_ACTION_PLAY_FILE, - card.child - ) - ) - } - } else { - downloadButton.isVisible = false - // downloadHeaderProgressDownloaded.visibility = View.GONE - // downloadHeaderEpisodeDownload.visibility = View.GONE - binding.downloadHeaderGotoChild.visibility = View.VISIBLE - - try { - downloadHeaderInfo.text = - downloadHeaderInfo.context.getString(R.string.extra_info_format).format( - card.totalDownloads, - if (card.totalDownloads == 1) downloadHeaderInfo.context.getString(R.string.episode) else downloadHeaderInfo.context.getString( - R.string.episodes - ), - mbString - ) - } catch (t: Throwable) { - // you probably formatted incorrectly - downloadHeaderInfo.text = "Error" - logError(t) - } - - - episodeHolder.setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(0, d)) - } - } - } - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt index 3a74a715..380430e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt @@ -39,6 +39,8 @@ class DownloadViewModel : ViewModel() { val availableBytes: LiveData = _availableBytes val downloadBytes: LiveData = _downloadBytes + private var previousVisual: List? = null + fun updateList(context: Context) = viewModelScope.launchSafe { val children = withContext(Dispatchers.IO) { val headers = context.getKeys(DOWNLOAD_EPISODE_CACHE) @@ -53,7 +55,6 @@ class DownloadViewModel : ViewModel() { // parentId : downloadsCount val totalDownloads = HashMap() - // Gets all children downloads withContext(Dispatchers.IO) { for (c in children) { @@ -69,7 +70,7 @@ class DownloadViewModel : ViewModel() { } } - val cached = withContext(Dispatchers.IO) { // wont fetch useless keys + val cached = withContext(Dispatchers.IO) { // Won't fetch useless keys totalDownloads.entries.filter { it.value > 0 }.mapNotNull { context.getKey( DOWNLOAD_HEADER_CACHE, @@ -79,7 +80,7 @@ class DownloadViewModel : ViewModel() { } val visual = withContext(Dispatchers.IO) { - cached.mapNotNull { // TODO FIX + cached.mapNotNull { val downloads = totalDownloads[it.id] ?: 0 val bytes = totalBytesUsedByChild[it.id] ?: 0 val currentBytes = currentBytesUsedByChild[it.id] ?: 0 @@ -91,32 +92,37 @@ class DownloadViewModel : ViewModel() { getFolderName(it.id.toString(), it.id.toString()) ) VisualDownloadHeaderCached( - 0, - downloads, - bytes, - currentBytes, - it, - movieEpisode + currentBytes = currentBytes, + totalBytes = bytes, + data = it, + child = movieEpisode, + currentOngoingDownloads = 0, + totalDownloads = downloads, ) }.sortedBy { (it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0) - } // episode sorting by episode, lowest to highest - } - try { - val stat = StatFs(Environment.getExternalStorageDirectory().path) - - val localBytesAvailable = stat.availableBytes//stat.blockSizeLong * stat.blockCountLong - val localTotalBytes = stat.blockSizeLong * stat.blockCountLong - val localDownloadedBytes = visual.sumOf { it.totalBytes } - - _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) - _availableBytes.postValue(localBytesAvailable) - _downloadBytes.postValue(localDownloadedBytes) - } catch (t : Throwable) { - _downloadBytes.postValue(0) - logError(t) + } // Episode sorting by episode, lowest to highest } - _headerCards.postValue(visual) + // Only update list if different from the previous one to prevent duplicate initialization + if (visual != previousVisual) { + previousVisual = visual + + try { + val stat = StatFs(Environment.getExternalStorageDirectory().path) + val localBytesAvailable = stat.availableBytes + val localTotalBytes = stat.blockSizeLong * stat.blockCountLong + val localDownloadedBytes = visual.sumOf { it.totalBytes } + + _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) + _availableBytes.postValue(localBytesAvailable) + _downloadBytes.postValue(localDownloadedBytes) + } catch (t: Throwable) { + _downloadBytes.postValue(0) + logError(t) + } + + _headerCards.postValue(visual) + } } -} +} \ No newline at end of file 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 e4fd0559..62b1fdd1 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 @@ -192,15 +192,15 @@ class EpisodeAdapter( downloadButton.isVisible = hasDownloadSupport downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - card.name, - card.poster, - card.episode, - card.season, - card.id, - card.parentId, - card.rating, - card.description, - System.currentTimeMillis(), + name = card.name, + poster = card.poster, + episode = card.episode, + season = card.season, + id = card.id, + parentId = card.parentId, + rating = card.rating, + description = card.description, + cacheTime = System.currentTimeMillis(), ), null ) { when (it.action) { @@ -343,15 +343,15 @@ class EpisodeAdapter( downloadButton.isVisible = hasDownloadSupport downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - card.name, - card.poster, - card.episode, - card.season, - card.id, - card.parentId, - card.rating, - card.description, - System.currentTimeMillis(), + name = card.name, + poster = card.poster, + episode = card.episode, + season = card.season, + id = card.id, + parentId = card.parentId, + rating = card.rating, + description = card.description, + cacheTime = System.currentTimeMillis(), ), null ) { when (it.action) { 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 fb5160a7..e185e75d 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 @@ -185,8 +185,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { } binding?.resultFullscreenHolder?.isVisible = !isSuccess && isFullScreenPlayer } - - //player_view?.apply { //alpha = 0.0f //ObjectAnimator.ofFloat(player_view, "alpha", 1f).apply { @@ -200,9 +198,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { // fillAfter = true //} //startAnimation(fadeIn) - // } - - + //} } private fun setTrailers(trailers: List?) { @@ -630,15 +626,15 @@ open class ResultFragmentPhone : FullScreenPlayer() { } downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( - ep.name, - ep.poster, - 0, - null, - ep.id, - ep.id, - null, - null, - System.currentTimeMillis(), + name = ep.name, + poster = ep.poster, + episode = 0, + season = null, + id = ep.id, + parentId = ep.id, + rating = null, + description = null, + cacheTime = System.currentTimeMillis(), ), null ) { click -> 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 4285feb1..ac6527de 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 @@ -705,13 +705,13 @@ class ResultViewModel2 : ViewModel() { DOWNLOAD_HEADER_CACHE, parentId.toString(), VideoDownloadHelper.DownloadHeaderCached( - apiName, - url, - currentType, - currentHeaderName, - currentPoster, - parentId, - System.currentTimeMillis(), + apiName = apiName, + url = url, + type = currentType, + name = currentHeaderName, + poster = currentPoster, + id = parentId, + cacheTime = System.currentTimeMillis(), ) ) @@ -722,15 +722,15 @@ class ResultViewModel2 : ViewModel() { ), // 3 deep folder for faster acess episode.id.toString(), VideoDownloadHelper.DownloadEpisodeCached( - episode.name, - episode.poster, - episode.episode, - episode.season, - episode.id, - parentId, - episode.rating, - episode.description, - System.currentTimeMillis(), + name = episode.name, + poster = episode.poster, + episode = episode.episode, + season = episode.season, + id = episode.id, + parentId = parentId, + rating = episode.rating, + description = episode.description, + cacheTime = System.currentTimeMillis(), ) ) @@ -2776,13 +2776,13 @@ class ResultViewModel2 : ViewModel() { DOWNLOAD_HEADER_CACHE, mainId.toString(), VideoDownloadHelper.DownloadHeaderCached( - apiName, - validUrl, - loadResponse.type, - loadResponse.name, - loadResponse.posterUrl, - mainId, - System.currentTimeMillis(), + apiName = apiName, + url = validUrl, + type = loadResponse.type, + name = loadResponse.name, + poster = loadResponse.posterUrl, + id = mainId, + cacheTime = System.currentTimeMillis(), ) ) if (loadTrailers) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt index 5b943105..66423982 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt @@ -25,7 +25,7 @@ object SearchHelper { SEARCH_ACTION_PLAY_FILE -> { if (card is DataStoreHelper.ResumeWatchingResult) { val id = card.id - if(id == null) { + if (id == null) { showToast(R.string.error_invalid_id, Toast.LENGTH_SHORT) } else { if (card.isFromDownload) { @@ -33,15 +33,15 @@ object SearchHelper { DownloadClickEvent( DOWNLOAD_ACTION_PLAY_FILE, VideoDownloadHelper.DownloadEpisodeCached( - card.name, - card.posterUrl, - card.episode ?: 0, - card.season, - id, - card.parentId ?: return, - null, - null, - System.currentTimeMillis() + name = card.name, + poster = card.posterUrl, + episode = card.episode ?: 0, + season = card.season, + id = id, + parentId = card.parentId ?: return, + rating = null, + description = null, + cacheTime = System.currentTimeMillis(), ) ) ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt index d1614bc1..30f66f83 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt @@ -3,17 +3,21 @@ package com.lagradost.cloudstream3.utils import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.TvType object VideoDownloadHelper { + abstract class DownloadCached( + @JsonProperty("id") open val id: Int, + ) + data class DownloadEpisodeCached( @JsonProperty("name") val name: String?, @JsonProperty("poster") val poster: String?, @JsonProperty("episode") val episode: Int, @JsonProperty("season") val season: Int?, - @JsonProperty("id") val id: Int, @JsonProperty("parentId") val parentId: Int, @JsonProperty("rating") val rating: Int?, @JsonProperty("description") val description: String?, @JsonProperty("cacheTime") val cacheTime: Long, - ) + override val id: Int, + ): DownloadCached(id) data class DownloadHeaderCached( @JsonProperty("apiName") val apiName: String, @@ -21,9 +25,9 @@ object VideoDownloadHelper { @JsonProperty("type") val type: TvType, @JsonProperty("name") val name: String, @JsonProperty("poster") val poster: String?, - @JsonProperty("id") val id: Int, @JsonProperty("cacheTime") val cacheTime: Long, - ) + override val id: Int, + ): DownloadCached(id) data class ResumeWatching( @JsonProperty("parentId") val parentId: Int, diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index 21f79ca6..a0b64ce3 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -59,12 +59,12 @@ Date: Mon, 24 Jun 2024 18:05:34 +0000 Subject: [PATCH 378/441] Improve tests (#1142) --- .../com/lagradost/cloudstream3/MainAPI.kt | 2 +- .../lagradost/cloudstream3/plugins/Plugin.kt | 1 + .../cloudstream3/plugins/PluginManager.kt | 3 +- .../ui/settings/testing/TestResultAdapter.kt | 50 ++++- .../ui/settings/testing/TestViewModel.kt | 2 +- .../cloudstream3/utils/ExtractorApi.kt | 2 +- .../cloudstream3/utils/TestingUtils.kt | 186 ++++++++++++------ app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 2 + 9 files changed, 172 insertions(+), 77 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 07a82583..91da2ed0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -622,7 +622,7 @@ abstract class MainAPI { /**Used for testing and can be used to disable the providers if WebView is not available*/ open val usesWebView = false - /** Determines which plugin a given provider is from */ + /** Determines which plugin a given provider is from. This is the full path to the plugin. */ var sourcePlugin: String? = null open val hasMainPage = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index 6b7dc90b..7f08af92 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -67,6 +67,7 @@ abstract class Plugin { * This will contain your resources if you specified requiresResources in gradle */ var resources: Resources? = null + /** Full file path to the plugin. */ var __filename: String? = null /** 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 a30af11c..a5631500 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -18,7 +18,6 @@ import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.removePluginMapping import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider @@ -518,7 +517,7 @@ object PluginManager { return true } - pluginInstance.__filename = fileName + pluginInstance.__filename = file.absolutePath if (manifest.requiresResources) { Log.d(TAG, "Loading resources for ${data.internalName}") // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt index 83480542..023ecb4c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt @@ -2,26 +2,31 @@ package com.lagradost.cloudstream3.ui.settings.testing import android.app.AlertDialog import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ProviderTestItemBinding import com.lagradost.cloudstream3.mvvm.getAllMessages import com.lagradost.cloudstream3.mvvm.getStackTracePretty +import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.TestingUtils +import java.io.File class TestResultAdapter(override val items: MutableList>) : AppUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProviderTestViewHolder( - ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent,false) + ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) //LayoutInflater.from(parent.context) // .inflate(R.layout.provider_test_item, parent, false), ) @@ -36,7 +41,8 @@ class TestResultAdapter(override val items: MutableList } + + api.sourcePlugin?.let { path -> + val pluginFile = File(path) + // Cannot delete a deleted plugin + if (!pluginFile.exists()) return@let + + builder.setNegativeButton(R.string.delete_plugin) { _, _ -> + ioSafe { + val success = PluginManager.deletePlugin(pluginFile) + + runOnMainThread { + if (success) { + showToast(R.string.plugin_deleted, Toast.LENGTH_SHORT) + } else { + showToast(R.string.error, Toast.LENGTH_SHORT) + } + } + } + } + } + + builder.show() } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt index 9e6f8a06..818f1fd7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -95,7 +95,7 @@ class TestViewModel : ViewModel() { providers.clear() updateProgress() - TestingUtils.getDeferredProviderTests(scope ?: return, apis, ::println) { api, result -> + TestingUtils.getDeferredProviderTests(scope ?: return, apis) { api, result -> addProvider(api, result) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 1302453a..12b8837a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -1015,7 +1015,7 @@ abstract class ExtractorApi { abstract val mainUrl: String abstract val requiresReferer: Boolean - /** Determines which plugin a given extractor is from */ + /** Determines which plugin a given provider is from. This is the full path to the plugin. */ var sourcePlugin: String? = null //suspend fun getSafeUrl(url: String, referer: String? = null): List? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index dd973538..5e2b2bc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -13,16 +13,55 @@ object TestingUtils { } } - class TestResultSearch(val results: List) : TestResult(true) - class TestResultLoad(val extractorData: String) : TestResult(true) + class Logger { + enum class LogLevel { + Normal, + Warning, + Error; + } - class TestResultProvider(success: Boolean, val log: String, val exception: Throwable?) : + data class Message(val level: LogLevel, val message: String) { + override fun toString(): String { + val level = when (this.level) { + LogLevel.Normal -> "" + LogLevel.Warning -> "Warning: " + LogLevel.Error -> "Error: " + } + return "$level$message" + } + } + + private val messageLog = mutableListOf() + + fun getRawLog(): List = messageLog + + fun log(message: String) { + messageLog.add(Message(LogLevel.Normal, message)) + } + + fun warn(message: String) { + messageLog.add(Message(LogLevel.Warning, message)) + } + + fun error(message: String) { + messageLog.add(Message(LogLevel.Error, message)) + } + } + + class TestResultList(val results: List) : TestResult(true) + class TestResultLoad(val extractorData: String, val shouldLoadLinks: Boolean) : TestResult(true) + + class TestResultProvider( + success: Boolean, + val log: List, + val exception: Throwable? + ) : TestResult(success) @Throws(AssertionError::class, CancellationException::class) suspend fun testHomepage( api: MainAPI, - logger: (String) -> Unit + logger: Logger ): TestResult { if (api.hasMainPage) { try { @@ -31,22 +70,33 @@ object TestingUtils { api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages)) when { homepage == null -> { - logger.invoke("Homepage provider ${api.name} did not correctly load homepage!") + logger.error("Provider ${api.name} did not correctly load homepage!") } + homepage.items.isEmpty() -> { - logger.invoke("Homepage provider ${api.name} does not contain any items!") + logger.warn("Provider ${api.name} does not contain any homepage rows!") } + homepage.items.any { it.list.isEmpty() } -> { - logger.invoke("Homepage provider ${api.name} does not have any items on result!") + logger.warn("Provider ${api.name} does not have any items in a homepage row!") } } + val homePageList = homepage?.items?.flatMap { it.list } ?: emptyList() + return TestResultList(homePageList) } catch (e: Throwable) { - if (e is NotImplementedError) { - Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") - } else if (e is CancellationException) { - throw e + when (e) { + is NotImplementedError -> { + Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented") + } + + is CancellationException -> { + throw e + } + + else -> { + e.message?.let { logger.warn("Exception thrown when loading homepage: \"$it\"") } + } } - logError(e) } } return TestResult.Pass @@ -54,11 +104,13 @@ object TestingUtils { @Throws(AssertionError::class, CancellationException::class) private suspend fun testSearch( - api: MainAPI + api: MainAPI, + testQueries: List, + logger: Logger, ): TestResult { - val searchQueries = listOf("over", "iron", "guy") - val searchResults = searchQueries.firstNotNullOfOrNull { query -> + val searchResults = testQueries.firstNotNullOfOrNull { query -> try { + logger.log("Searching for: $query") api.search(query).takeIf { !it.isNullOrEmpty() } } catch (e: Throwable) { if (e is NotImplementedError) { @@ -72,12 +124,11 @@ object TestingUtils { } return if (searchResults.isNullOrEmpty()) { - Assert.fail("Api ${api.name} did not return any valid search responses") + Assert.fail("Api ${api.name} did not return any search responses") TestResult.Fail // Should not be reached } else { - TestResultSearch(searchResults) + TestResultList(searchResults) } - } @@ -85,31 +136,27 @@ object TestingUtils { private suspend fun testLoad( api: MainAPI, result: SearchResponse, - logger: (String) -> Unit + logger: Logger ): TestResult { try { - Assert.assertEquals( - "Invalid apiName on SearchResponse on ${api.name}", - result.apiName, - api.name - ) + if (result.apiName != api.name) { + logger.warn("Wrong apiName on SearchResponse: ${api.name} != ${result.apiName}") + } val loadResponse = api.load(result.url) if (loadResponse == null) { - logger.invoke("Returned null loadResponse on ${result.url} on ${api.name}") + logger.error("Returned null loadResponse on ${result.url} on ${api.name}") return TestResult.Fail } - Assert.assertEquals( - "Invalid apiName on LoadResponse on ${api.name}", - loadResponse.apiName, - result.apiName - ) - Assert.assertTrue( - "Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}", - api.supportedTypes.contains(loadResponse.type) - ) + if (loadResponse.apiName != api.name) { + logger.warn("Wrong apiName on LoadResponse: ${api.name} != ${loadResponse.apiName}") + } + + if (!api.supportedTypes.contains(loadResponse.type)) { + logger.warn("Api ${api.name} on load does not contain any of the supportedTypes: ${loadResponse.type}") + } val url = when (loadResponse) { is AnimeLoadResponse -> { @@ -117,39 +164,43 @@ object TestingUtils { loadResponse.episodes.keys.isEmpty() || loadResponse.episodes.keys.any { loadResponse.episodes[it].isNullOrEmpty() } if (gotNoEpisodes) { - logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}") + logger.error("Api ${api.name} got no episodes on ${loadResponse.url}") return TestResult.Fail } (loadResponse.episodes[loadResponse.episodes.keys.firstOrNull()])?.firstOrNull()?.data } + is MovieLoadResponse -> { val gotNoEpisodes = loadResponse.dataUrl.isBlank() if (gotNoEpisodes) { - logger.invoke("Api ${api.name} got no movie on ${loadResponse.url}") + logger.error("Api ${api.name} got no movie on ${loadResponse.url}") return TestResult.Fail } loadResponse.dataUrl } + is TvSeriesLoadResponse -> { val gotNoEpisodes = loadResponse.episodes.isEmpty() if (gotNoEpisodes) { - logger.invoke("Api ${api.name} got no episodes on ${loadResponse.url}") + logger.error("Api ${api.name} got no episodes on ${loadResponse.url}") return TestResult.Fail } loadResponse.episodes.firstOrNull()?.data } + is LiveStreamLoadResponse -> { loadResponse.dataUrl } + else -> { - logger.invoke("Unknown load response: ${loadResponse.javaClass.name}") + logger.error("Unknown load response: ${loadResponse.javaClass.name}") return TestResult.Fail } } ?: return TestResult.Fail - return TestResultLoad(url) + return TestResultLoad(url, loadResponse.type != TvType.CustomMedia) // val loadTest = testLoadResponse(api, load, logger) // if (loadTest is TestResultLoad) { @@ -174,7 +225,7 @@ object TestingUtils { private suspend fun testLinkLoading( api: MainAPI, url: String?, - logger: (String) -> Unit + logger: Logger ): TestResult { Assert.assertNotNull("Api ${api.name} has invalid url on episode", url) if (url == null) return TestResult.Fail // Should never trigger @@ -182,7 +233,7 @@ object TestingUtils { var linksLoaded = 0 try { val success = api.loadLinks(url, false, {}) { link -> - logger.invoke("Video loaded: ${link.name}") + logger.log("Video loaded: ${link.name}") Assert.assertTrue( "Api ${api.name} returns link with invalid url ${link.url}", link.url.length > 4 @@ -190,7 +241,7 @@ object TestingUtils { linksLoaded++ } if (success) { - logger.invoke("Links loaded: $linksLoaded") + logger.log("Links loaded: $linksLoaded") return TestResult(linksLoaded > 0) } else { Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded") @@ -200,8 +251,9 @@ object TestingUtils { is NotImplementedError -> { Assert.fail("Provider has not implemented loadLinks()") } + else -> { - logger.invoke("Failed link loading on ${api.name} using data: $url") + logger.error("Failed link loading on ${api.name} using data: $url") throw e } } @@ -212,53 +264,57 @@ object TestingUtils { fun getDeferredProviderTests( scope: CoroutineScope, providers: Array, - logger: (String) -> Unit, callback: (MainAPI, TestResultProvider) -> Unit ) { providers.forEach { api -> scope.launch { - var log = "" - fun addToLog(string: String) { - log += string + "\n" - logger.invoke(string) - } - fun getLog(): String { - return log.removeSuffix("\n") - } + val logger = Logger() val result = try { - addToLog("Trying ${api.name}") + logger.log("Trying ${api.name}") // Test Homepage - val homepage = testHomepage(api, logger).success - Assert.assertTrue("Homepage failed to load", homepage) + val homepage = testHomepage(api, logger) + Assert.assertTrue("Homepage failed to load", homepage.success) + val homePageList = (homepage as? TestResultList)?.results ?: emptyList() // Test Search Results - val searchResults = testSearch(api) + val searchQueries = + // Use the first 3 home page results as queries since they are guaranteed to exist + (homePageList.take(3).map { it.name } + + // If home page is sparse then use generic search queries + listOf("over", "iron", "guy")).take(3) + + val searchResults = testSearch(api, searchQueries, logger) Assert.assertTrue("Failed to get search results", searchResults.success) - searchResults as TestResultSearch + searchResults as TestResultList // Test Load and LoadLinks // Only try the first 3 search results to prevent spamming val success = searchResults.results.take(3).any { searchResponse -> - addToLog("Testing search result: ${searchResponse.url}") - val loadResponse = testLoad(api, searchResponse, ::addToLog) + logger.log("Testing search result: ${searchResponse.url}") + val loadResponse = testLoad(api, searchResponse, logger) if (loadResponse !is TestResultLoad) { false } else { - testLinkLoading(api, loadResponse.extractorData, ::addToLog).success + if (loadResponse.shouldLoadLinks) { + testLinkLoading(api, loadResponse.extractorData, logger).success + } else { + logger.log("Skipping link loading test") + true + } } } if (success) { - logger.invoke("Success ${api.name}") - TestResultProvider(true, getLog(), null) + logger.log("Success ${api.name}") + TestResultProvider(true, logger.getRawLog(), null) } else { - logger.invoke("Error ${api.name}") - TestResultProvider(false, getLog(), null) + logger.error("Link loading failed") + TestResultProvider(false, logger.getRawLog(), null) } } catch (e: Throwable) { - TestResultProvider(false, getLog(), e) + TestResultProvider(false, logger.getRawLog(), e) } callback.invoke(api, result) } diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7c9ccebe..a37dfad2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -88,4 +88,5 @@ #48E484 #ea596e + #FF9800 \ 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 b39006ad..f577d6e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -304,6 +304,7 @@ Start Failed Passed + Warning Resume -30 +30 @@ -609,6 +610,7 @@ plugins This will also delete all repository plugins Delete repository + Delete plugin Download the list of sites you want to use Downloaded: %d Disabled: %d From 0d40b5ebe3f6a88b2408149e4e44e3de8a8dfe91 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Mon, 24 Jun 2024 21:03:09 +0200 Subject: [PATCH 379/441] Translations update from Hosted Weblate (#1042) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aaditya Bhandari Co-authored-by: Adrian Hermida Co-authored-by: Akhlak Ur Rahman Co-authored-by: Alexander Svärd Co-authored-by: Anarchydr Co-authored-by: Andre Costa Co-authored-by: Antonio N Co-authored-by: Azgar Co-authored-by: Colgrave Co-authored-by: Dan Co-authored-by: Eji-san Co-authored-by: Ettore Atalan Co-authored-by: Evgeniy Khramov <65224669+thejenja@users.noreply.github.com> Co-authored-by: FUTURE Co-authored-by: Fikri Akbar Co-authored-by: Fjuro Co-authored-by: Huzaifah Asif Co-authored-by: Itsmechinmoy Co-authored-by: Jose Delvani Co-authored-by: Konstantinos Tranoudis Co-authored-by: Krisna A. Prayoga Co-authored-by: Luna712 <142361265+Luna712@users.noreply.github.com> Co-authored-by: Marian Turba Co-authored-by: Massimo Pissarello Co-authored-by: Matthaiks Co-authored-by: Milo Ivir Co-authored-by: Mæve Rey Co-authored-by: Naga Co-authored-by: Nicoara Alex Co-authored-by: Nuno Ferreira Co-authored-by: Only1337 Co-authored-by: Pizza Party Co-authored-by: Putra Iskandar Co-authored-by: Qareen Skoll Co-authored-by: Rex_sa Co-authored-by: SeMih Budur Co-authored-by: Semih Co-authored-by: Sufyan Zahoor Jutt Co-authored-by: Waheed Nazir Co-authored-by: Walter H Co-authored-by: Wei-Cheng Yeh (IID) Co-authored-by: amir Co-authored-by: bittin1ddc447d824349b2 Co-authored-by: gallegonovato Co-authored-by: hugoalh Co-authored-by: ngocanhtve Co-authored-by: programutox Co-authored-by: rwi Co-authored-by: stojkovskistefan Co-authored-by: streaming s Co-authored-by: tuan041 Co-authored-by: user0020 <855309c256@gmail.com> Co-authored-by: ΣΤΑΥΡΟΣ ΔΑΛΙΑΚΟΠΟΥΛΟΣ Co-authored-by: Сергей (MrSabin) Co-authored-by: தமிழ்நேரம் Co-authored-by: 电棍老板 Co-authored-by: 구병우 --- app/src/main/res/values-ajp/strings.xml | 64 +- app/src/main/res/values-ar/strings.xml | 28 +- app/src/main/res/values-as/strings.xml | 624 ++++++++++++++++++ app/src/main/res/values-bg/strings.xml | 4 +- app/src/main/res/values-bn/strings.xml | 138 +++- app/src/main/res/values-bp/strings.xml | 23 +- app/src/main/res/values-cs/strings.xml | 23 +- app/src/main/res/values-de/strings.xml | 16 +- app/src/main/res/values-el/strings.xml | 109 ++- app/src/main/res/values-es/strings.xml | 41 +- app/src/main/res/values-fr/strings.xml | 33 +- app/src/main/res/values-hi/strings.xml | 19 +- app/src/main/res/values-hr/strings.xml | 414 ++++++------ app/src/main/res/values-hu/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 13 +- app/src/main/res/values-it/strings.xml | 25 +- app/src/main/res/values-iw/strings.xml | 4 +- app/src/main/res/values-ja/strings.xml | 2 +- app/src/main/res/values-ko/strings.xml | 115 +++- app/src/main/res/values-lv/strings.xml | 4 +- app/src/main/res/values-mk/strings.xml | 71 +- app/src/main/res/values-ml/strings.xml | 4 +- app/src/main/res/values-ms/strings.xml | 4 +- app/src/main/res/values-my/strings.xml | 4 +- app/src/main/res/values-ne/strings.xml | 49 +- app/src/main/res/values-nl/strings.xml | 4 +- app/src/main/res/values-no/strings.xml | 4 +- app/src/main/res/values-pl/strings.xml | 23 +- app/src/main/res/values-pt/strings.xml | 10 +- app/src/main/res/values-ro/strings.xml | 102 ++- app/src/main/res/values-ru/strings.xml | 10 +- app/src/main/res/values-sk/strings.xml | 26 +- app/src/main/res/values-so/strings.xml | 4 +- app/src/main/res/values-sv/strings.xml | 54 +- app/src/main/res/values-ta/strings.xml | 604 +++++++++++++++-- app/src/main/res/values-tr/strings.xml | 97 +-- app/src/main/res/values-uk/strings.xml | 23 +- app/src/main/res/values-ur/strings.xml | 151 +++-- app/src/main/res/values-vi/strings.xml | 32 +- app/src/main/res/values-zh-rTW/strings.xml | 84 ++- app/src/main/res/values-zh/strings.xml | 24 +- fastlane/metadata/android/as/changelogs/2.txt | 1 + .../metadata/android/as/full_description.txt | 10 + .../metadata/android/as/short_description.txt | 1 + fastlane/metadata/android/as/title.txt | 1 + .../android/el-GR/short_description.txt | 2 +- .../metadata/android/id/full_description.txt | 10 +- fastlane/metadata/android/ur/changelogs/2.txt | 2 +- .../metadata/android/ur/full_description.txt | 12 +- .../metadata/android/ur/short_description.txt | 2 +- .../metadata/android/zh-TW/changelogs/2.txt | 1 + .../android/zh-TW/full_description.txt | 10 + .../android/zh-TW/short_description.txt | 1 + fastlane/metadata/android/zh-TW/title.txt | 1 + 54 files changed, 2554 insertions(+), 587 deletions(-) create mode 100644 app/src/main/res/values-as/strings.xml create mode 100644 fastlane/metadata/android/as/changelogs/2.txt create mode 100644 fastlane/metadata/android/as/full_description.txt create mode 100644 fastlane/metadata/android/as/short_description.txt create mode 100644 fastlane/metadata/android/as/title.txt create mode 100644 fastlane/metadata/android/zh-TW/changelogs/2.txt create mode 100644 fastlane/metadata/android/zh-TW/full_description.txt create mode 100644 fastlane/metadata/android/zh-TW/short_description.txt create mode 100644 fastlane/metadata/android/zh-TW/title.txt diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index 734d5644..554fae9c 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -76,7 +76,7 @@ سَتِنگز الترجمة لون العلبة عمول ستريم للتورنت - تلقائيًا نَزِل كل الإضافات من الريپويات يلي نزادِت. + تلقائيًا نَزِل كل الإضافات من الريپويات يللي نزادِت. محي بلش في تِلِفونات ما فيا تعوز الطريقة الجديدة لتجديد الآپات. جربو \"الطريقة القديمة\" إذا ما عم تنزل التجديدات. @@ -90,7 +90,7 @@ متصفح الوَب كبوس مرتين على اليمين أو الشمال حتى تقرب أو ترَجِع الڤيديو ما نلاقى وصف الأحداث - الحلقة يلي بَعدا + الحلقة يللي بَعدها فرجي تجديدات الآپ رفّ آپ من نفس المطورين للروايات الخفيفة، بدل من الڤيديوات @@ -218,7 +218,7 @@ بث مباشر المشغل مبين - مدة التقديم مشكلة مش متوقع بمشغل الڤيديو (Unexpected player error) - بسبب أعطال إذا نحط على مستوى عالي كتير على الأجهزة يلي ما بتساع كتير، متل تلفزيون \"أندرويد\". + بسبِب أعطال إذا نحط على مستوى عالي كتير على الأجهزة يللي ما بتساع كتير، متل تلفزيون \"أندرويد\". شي غير أفي هيدا التجديد نسوخ الرابط @@ -244,7 +244,7 @@ طول التخزين المتوقت حلقة \"كروم كاست\" دراما آسيوية - بسبب أعطال إذا نحط على مستوى عالي كتير على الأجهزة يلي ذِكرتا زغيرة، متل تلفزيون \"أندرويد\". + بسبِب أعطال إذا نحط على مستوى عالي كتير على الأجهزة يللي ذِكرتها زغيرة، متل تلفزيون \"أندرويد\". مشكلة بالمصدر التخزين الموقت للڤيديو على الديسك فلم وثائقي @@ -283,7 +283,7 @@ إشارات المرجعية بَلَش التنزيل فتّو على الأكونت \"%s\" - وقِف الإعلان الأتوماتيكي عن المشاكل يلي بالآپ + وقِف الإعلان الأتوماتيكي عن المشاكل يللي بالآپ محل عنوان الپوستر الشكل %1$d ساعة %2$d ديقة @@ -314,7 +314,7 @@ مدبلج أوتوماتيك عدلو الأكونت - الأرقام السرية يلي نحطت مش صحيحة. جرب مرة أخرى. + الأرقام السرية يللي نحطت مش صحيحة. جرب مرة أخرى. الشكل المعمول للتلفزيون نلغى التنزيل أكونت @@ -366,9 +366,9 @@ زِدت %s المفضلين \"%s\" نزاد ع المفضل - العشوائي يلي بعدو + العشوائي يللي بعده خيال - عم نجدِد المثلثلات يلي مشتركينلا + عم نجدِد المثلثلات يللي مشتركينلها مبين إنو في عنصر متل هيدا موجود بالمكتبة عندكن: \n \n%s @@ -481,7 +481,7 @@ نزلو المصادر بالجملة فتاح بـ راينيگ - محي الريپو كمان بمحي الإضافات يلي في + محي الريپو كمان بمحي الإضافات يللي فيه اللغة شترك نمحت الإضافة @@ -490,7 +490,7 @@ الإضافات شيل الإعلانات من الترجمة رفّكن فاضي ☹ -\nفوتو على أكونت فيا رفّ الڤيديوات يلي حضرينا أو زيدو ڤيديوات بالرفّ المحلي. +\nفوتو على أكونت فيا رفّ الڤيديوات يللي حضرينها أو زيدو ڤيديوات بال رفّ المحلي. إسم الريپوزيتاري الجودات بيانات مش صالحة @@ -530,13 +530,13 @@ Blu-ray %d/10 النص كبير كتير. ما فينا ننسخو. - عدد الإضافيات يلي تجددت: %d + عدد الإضافيات يللي تجددت: %d رح يتجدد الآپ وقتا تطلعو مِنو پلاي ليست \"ايش أل أس\" زيد ريپوزيتوري علم إنو حضرتو شو بَدَك تشوف - شيل المعلومات يلي محطوطة بالترجمة ليلي عندن فقد سمعي + شيل المعلومات يللي محطوطة بال ترجمة ل يللي عندهن فقد سمعي ما لقينا ريپو. تأكدو إنو الرابط صح وجربو تعوزو \"ڤي پي أن\" (VPN) فشل الفوت ع الأكونت \"%s\" الحد الأعلى @@ -544,7 +544,7 @@ إضافات ما قدرنا نفتح %s رايتينگ: %s - بدكن تنزلو كل الإضافات من هيدا الريپو؟ + تحزير: \"كلاود ستريم 3\" مش مسؤولة عن الإضافات المش رسمية، وما بتدعمن أبدًا! الحالة محي الريپو مشغل الڤيديو @@ -567,8 +567,8 @@ الجودة عين الافتراضي المرجع (إختياري) - المشغل يلي بـ\"كلود ستريم\" - نزل لايحة المواقع يلي بدك تعوزن + المشغل يللي ب \"كلود ستريم\" + نزل لايحة المواقع يللي بدك تعوزهن حطو الأرقام السرية فَلتِر حسب اللغة المفضلة أكيد بدكون تطلعو؟ @@ -576,13 +576,13 @@ @string/home_play شيلو من لايحة المحتوى الحاضرينو الإعتمادات - فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يلي عندو أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يلي بتفضلوّا. + فيكُن هون تغيرو طريقة ترتيب المصادر. المصدر يللي عنده أولوية أكتر بينحط أعلى بلايحت تنقايت المصدر. إنتو بتنقو الأولوية يإستعمال الأرقام. حطو الرقم الأعلى للمصادر والجودات يللي بتفضلوها. \n -\nمثلًا: -\nإذا المصدر \"أ\" بتفضلوّ، بتعطوّ كتير نقات (مثلًا 8). -\nإذا الجودة 480 ما بتحبوّا، بتعطوّا نقات قليلة (مثلًا 1). +\nمتلًا: +\nإزا المصدر \"أ\" بتفضلوه، بتعطوه كتير نقات (متلًا 8). +\nإزا الجودة 480 ما بتحبوها، بتعطوها نقات قليلة (متلًا 1). \n -\nعلامت المصدر والجودة تبعو بينجمعو مع بعض (8 + 1 = 9). يلي علامتو 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر! +\nعلامت المصدر وال جودة تبعه بينجمعو مع بعض (8 + 1 = 9). يللي علامته 10 أو أعلى، بينحط تلقائيًا، من دون ما ينعمل لود لكل المصادر! حطو الأرقام السرية الحالية صوت حط كبسة لبرم إتجاه الشاشة @@ -603,7 +603,7 @@ قفل بواسطة المقاييس الحيوية رمز/كلمة مرور للمصادقة فتاح التطبيق باستعمال البصمة، آي دي الوج، پِن، النمط، إو الپاسورد. - تسَكرت هيدي الواجهة من ورا محاولات فاشلة عديدة. پليز، سكر الآپ ورجاع فتحه. + بعد كذا محاولة فاشلة، هيدا الشباك رح يسكر. بكل بساطة، سكر الآپ ورجاع فتحه حتى تجرب بعد مرة. %s \nباقي المصادقة البيومترية مش مدعومة ع هالجهاز @@ -621,5 +621,23 @@ موسيقى أوديو بوك الميديا - لتضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يلي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي بـ الباكگروند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". ملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآن بـ الباكگروند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ بـ«الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. - + ت تضمن عدم انقطاع التنزيلات والنوتيفيكايشنات للبرامج التلفزيونية يللي مشتركلها، الآپ \"كلود ستريم\" بده إذن ليمشي ب الباكگراوند. ازا كبست أوكي، رح تتوجه ع صفحة معلومات التطبيق. هونيك، نزال حتى توصل ل «استخدام بطارية التطبيق» \"App battery usage\" وحط استخدام البطارية ع «غير مقيد» \"Unrestricted\". +\nملاحظة إنو هيدا الإذن ما بيعني إنو \"كلود ستريم 3\" رح تستنزف البطارية. ومش رح يشتغل الآپ بال باكگراوند إلّا عند الضرورة، متل لمّا تتلقا نوتيفيكايشن أو تنزل ڤيديو من الريپو الاصلي. فيك ترجع ترد هيدا الستنگ ب «الإعدادات العامة» \"General settings\"، إزا غيرت رأيك. + ريسات + رح ينزل ب %s + الحلقة ال %2$d من الجزء ال%1$d رح تنزل ب + كاست مراية + إف كاست + نقي جهاز الكاست + ويكي \"كلود ستريم\" + أكونتات + سكوريتي + صورة الـ\"كيو آر\" كود + فشلنا ب فتح پِن الجهاز، جرب تفوت ع أكونت محليًا + خلصت مدة الپِن! + بتخلص مدة الپِن ب %1$d دقايق و%2$d ثانية + فوت عال أكونت محليًا + تجاهل + متاح الريپوزيتوري + فتاح %s ع تلفونك أو كمپيوترك، وحط الكود اللي فوق + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 8681398d..e253ed93 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -220,7 +220,7 @@ أوفا تورنت وثائقي - دراما آسيوية + الدراما الآسيوية بث حي +18 فيديو @@ -435,7 +435,7 @@ عرض مستودعات المجتمع قائمة عامة جميع الترجمات حروف كبيرة - تحميل جميع الإضافات من هذا المستودع\? + تحذير: لا يتحمل CloudStream 3 أي مسؤولية عن استخدام ملحقات الطرف الثالث ولا يقدم أي دعم لها! %s (معطل) المسارات مسار الصوت @@ -631,7 +631,7 @@ افتح التطبيق باستخدام بصمة الإصبع ومعرف الوجه ورقم التعريف الشخصي والنمط وكلمة المرور. فتح سحابة البث مصادقة كلمة المرور/رقم التعريف الشخصي - تم إغلاق هذه الشاشة بسبب عدة محاولات فاشلة. الرجاء إعادة تشغيل التطبيق. + بعد عدة محاولات فاشلة، سيتم إغلاق المطالبة. ما عليك سوى إعادة تشغيل التطبيق للمحاولة مرة أخرى. لقد تم الآن نسخ بيانات CloudStream احتياطيًا. على الرغم من أن احتمال حدوث ذلك منخفض جدًا، إلا أن جميع الأجهزة يمكن أن تتصرف بشكل مختلف. في الحالات النادرة، التي يتم فيها منعك من الوصول إلى التطبيق، قم بمسح بيانات التطبيق بالكامل واستعادتها من نسخة احتياطية. نحن نأسف جدًا لأي إزعاج ناتج عن هذا. %s \nمتبقي @@ -646,8 +646,24 @@ غير قادر على فتح معلومات تطبيق CloudStream. كتاب صوتي حسناً - لضمان عدم انقطاع التنزيلات والإشعارات للبرامج التلفزيونية المشتركة، يحتاج CloudStream إلى إذن للتشغيل في الخلفية. بالضغط على موافق، سيتم توجيهك إلى معلومات التطبيق. هناك، انتقل إلى استخدام بطارية التطبيق -\nواضبط استخدام البطارية على غير مقيد. يرجى ملاحظة أن هذا الإذن لا يعني أن CS3 سوف يستنزف البطارية. ولن يعمل إلا في الخلفية عند الضرورة، كما هو الحال عند تلقي الإشعارات أو تنزيل مقاطع الفيديو من الملحقات الرسمية. إذا اخترت الإلغاء، فيمكنك ضبط هذا الإعداد لاحقًا في الإعدادات العامة. + لضمان عدم انقطاع التنزيلات والإشعارات للبرامج التلفزيونية المشتركة، يحتاج CloudStream إلى إذن للتشغيل في الخلفية. بالضغط على موافق، سيتم توجيهك إلى معلومات التطبيق. هناك، انتقل إلى 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 واضبط استخدام البطارية على 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. يرجى ملاحظة أن هذا الإذن لا يعني أن CS3 سوف يستنزف البطارية. ولن يعمل إلا في الخلفية عند الضرورة، كما هو الحال عند تلقي الإشعارات أو تنزيل مقاطع الفيديو من الامتدادات الرسمية. إذا اخترت الإلغاء، فيمكنك ضبط هذا الإعداد لاحقًا في 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. موسيقى الوسائط - + اعادة تعيين + قادم خلال %s + سيتم إصدار الحلقة %1$d من الموسم %2$d في + مرآة البث + بث ف + حدد جهاز البث + CloudStream ويكي + إعدادات الأمان + الحسابات + صورة رمز الاستجابة السريعة + تجاهَل + فتح المستودع + لقد انتهت صلاحية الرمز السري الآن! + تحقق محليا + قم بزيارة %s على هاتفك الذكي أو جهاز الكمبيوتر وأدخل الرمز أعلاه + لا يمكن الحصول على رمز PIN للجهاز، حاول المصادقة المحلية + تنتهي صلاحية الرمز خلال %1$dm %2$ds + \ No newline at end of file diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml new file mode 100644 index 00000000..8d151d71 --- /dev/null +++ b/app/src/main/res/values-as/strings.xml @@ -0,0 +1,624 @@ + + + %1$s এপি %2$d + পোস্টাৰ + এপিস\'দ পোস্টাৰ + মুখ্য পোস্টাৰ + পিছলৈ যাওক + ৰেটিং: %.1f + ছেটিংছ + অনুসন্ধান কৰক… + %s ত অনুসন্ধান কৰক… + ডাউনল\'ড কৰি আছে + পৰামৰ্শ চাওক + প্লেয়াৰৰ গতি + সাবটেক্সটৰ ৰং + এজেৰ ধৰণ + চালনালৈ অবন্ধ কৰক + প্লট + লগ ক্যাট চাওক + লগ + বেকআপ ত্ৰুটি %s + অনুসন্ধান + গ্ৰন্থাগাৰ + হিসাব আৰু সুৰক্ষা + উন্নয়ন আৰু বেকআপ + স্বয়ংক্ৰিয়ভাৱে প্লাগইন ডাউনল\'ড কৰক + প্লাগইন ডাউনল\'ড ম’ড নিৰ্বাচন কৰক + নিষ্ক্ৰিয়ত আৰম্ভকৰ্তাৰ আদৰ্শ সংগ্ৰহসমূহতৰ প্ৰয়াস হৈছে, আইন্সটল স্বয়ংক্ৰিয়ভাৱে যোগ কৰা হয়। + কাৰ্টুনসমূহ + এনিমে + টৰেণ্টসমূহ + OVA + এছিয়ান ড্ৰামা + লাইভস্ট্ৰিম + NSFW + অন্যান্য + এপত বাজাওক + লিংকসমূহ পুনঃ ল\'ড কৰক + সাবটাইটেল ডাউনল\'ড কৰক + HD চিহ্ন + শিৰোনাম + কোনো উন্নীতকৰণ পোৱা নাই + উন্নীতকৰণ পৰীক্ষা কৰক + আৰম্ভ কৰক না + ভিডিঅ\' প্লেয়াৰৰ শিৰোনামৰ সীমা + বিদ্যমান চাবৰ এটা ক্লোন যোগ কৰক, একটু ভিন্ন urlৰ সৈতে + লিংকসমূহ + আউটলৈন + স্বয়ংক্ৰিয় + password123 + ব্যৱহাৰকাৰীনাম + hello@world.com + 127.0.0.1 + লগ আউট + সমন্বয় সংখ্যা + %d / 10 + /?? + /%d + %s প্ৰমাণিত + %s ত লগ ইন কৰিব পৰা নহয় + অবৈধ ডেটা + অবৈধ url + ত্ৰুটি + সাবটাইটেলৰ পৰা ক্যাপশনসমূহ দূৰ কৰক + উল্লেখক (ঐচ্ছিক) + পৰৱৰ্তী + প্ৰদাতাৰ ভাষা পছন্দ কৰক + গভেয়ান ডাউনলোড + সেটআপ এক্সটেনশনসমূহ + ডাউনলোড হৈছিল: %d + ক্লিপবোৰ্ড অনুমতি দিবলৈ ত্ৰুটি, অনুগ্ৰহ কৰি পুনৰ চেষ্টা কৰক। + বৰ্ণানুক্রমিক (জ থকা অ পৰা অ) + %d প্ৰকাৰ মুকলো! + সাবস্ক্ৰাইব কৰক + সাবস্ক্ৰাইব পৰা মুকা + প্ৰ’ফাইল %d + পিন এন্টাৰ কৰক + একাউন্ট সম্পাদনা কৰক + মিডিয়া + পুনৰায় ছেট কৰক + অভিনেতা: %s + দেখা হৈ আছে + অবিৰাম দেখিব + সাবটাইটেল + চিত্ৰমূল্যৰ সিষ্টেম ব্যৱহাৰ কৰক + বেকআপ অনুমতি অনুপলব্ধ। অনুগ্ৰহ কৰি পুনৰায় চেষ্টা কৰক। + অল্প + এচডি + সমাপ্ত + সাৰদাৰী ভাণ্ডা + অব্যাহত্‌ + VLCত বাজাওক + বেটাৰী অপটিমাইজেশন অসমৰ্থ + সাবস্ক্ৰাইব কৰা বস্তুসমূহ + মান + যোগ কৰক + পুনৰ্স্থাপন কৰক + ক্লাউডষ্ট্ৰীম অনলক কৰক + বায়োমেট্ৰিক ছেটিংসমূহ + পাছৱাৰ্ড/PIN পুনৰ্প্ৰমাণৰ + সংগীত + অডিঅ’ বুক + পৰৱৰ্তী পৰ্ব %d + পৰৱৰ্তী মৌচৰা %1$d পৰ্ব %2$d + %1$dd %2$dh %3$dm + %1$dh %2$dm + %dm + পোস্টাৰ + প্ৰদাতা সলনি কৰক + পূৰ্বালোচনাৰ পৃষ্ঠপুতী + পৰৱৰ্তী যিনিমূলক + গতি (%.2fx) + পৰৱৰ্তী পৰ্ব + %d মিনিট + ক্লাউডষ্ট্ৰীম + মূল + কোনো ডাটা নাই + অধিক বিকল্পসমূহ + শেয়াৰ কৰক + ব্ৰাউজাৰত খোলক + ব্ৰাউজাৰ + দেখিব প্লেন কৰা হৈছে + টৰেণ্ট ষ্ট্ৰীম কৰক + সংযোগ পুনঃচেষ্টা কৰক… + পিছলৈ যাওক + উৎস বাছনি কৰক + ডাউনলোড হোৱা + ডাউনলোড থমা কৰা হৈছে + ডাউনলোড আৰম্ভ হোৱা + ডাউনলোড বিফল হৈছে + ডাউনলোড বাতিল হৈছে + ডাউনলোড সম্পন্ন হৈছে + আপডেট আৰম্ভ হোৱা + নেটৱৰ্ক ষ্ট্ৰীম + লিংক লোড কৰা বিফল + লিংক পুনৰাবৃত্তি হোৱা + অভ্যন্তৰীণ ষ্ট’ৰেজ + ডাব + ছাবটাইটেল + ফাইল মুছি দিব + ফাইল প্লে কৰক + স্বয়ংক্ৰিয় বাগৰ প্ৰতিবেদন নিষ্ক্ৰিয় কৰক + ডাউনলোড পুনৰাৰম্ভ কৰক + ডাউনলোড থমা কৰক + অধিক তথ্য + লুকুৱা কৰক + খেলা + তথ্য + অপশনটো সংগ্ৰহ বাদ কৰক + সংগ্ৰহৰ তলত যোগ কৰক + সংগ্ৰহৰ ফিল্টাৰ কৰক + সংগ্ৰহ ত্ৰুটি + কপি কৰক + বন্ধ কৰক + পৰিষ্কাৰ কৰক + সংৰক্ষণ কৰক + ভৰসৰ্বক্ষণৰ নাম আৰু URL + প্ৰতিলিপি কৰা হৈছে! + নতুন সদস্যতা + অন্যান্য এক্সটেনছনসমূহত অনুসন্ধান কৰক + সাবটাইটেল ছেটিংছ + আউটলাইনৰ ৰং + পৃষ্ঠপুতীৰ ৰং + উইণ্ডোৰ ৰং + সাবটাইটেলৰ উঁচুতি + ফন্ট + ফন্ট আকাৰ + প্ৰকাৰসমূহে অনুসন্ধান কৰক + সৰব সাধনসমূহে অনুসন্ধান কৰক + %d ডেভল\'পাৰ পৰা বেনেন দিয়া হৈছে + কোনো বেনেন দিয়া নাই + স্বয়ংক্ৰিয় ভাষা বাছনি কৰক + ভাষা ডাউনলোড কৰক + ছাবটাইটেলৰ ভাষা + ডিফল্টৰ পুনৰাৰম্ভ কৰিবলৈ ধাৰণ কৰক + এই প্ৰদাতাৰ সঠিকভাবে কাম কৰিবলৈ এটা VPN প্ৰয়োজন হবে + এই প্ৰদাতা এটা ট\'ৰেণ্ট হৈছে, এটা VPN পৰামৰ্শ দিয়া হল + ফন্ট ইম্প\'ৰ্ট কৰিবলৈ %sত ত থকা প্ৰতিষ্ঠান কৰক + অধিক তথ্য + \@string/home_play + মেটাডাটা ঠিকানাৰ সৈতে প্ৰদান কৰা নাই, যদি এটা ঠিকানাৰ মেটাডাটা নাই তেন্তি ভিডিঅ\' ল\'ড হোৱা নাই। + কোনো বিবৰণ পোৱা নাই + কোনো বিৱৰণ পোৱা নাই + প্লেয়াৰৰ আকাৰ বটাম + কৃষ্টি বোৰ্ডাৰ প্ৰস্তুতি কৰক + ছবি-মাধ্যমে ছবি + অন্য এপ্‌সমূহৰ ওপৰত এটা সন্নিহিত প্লেয়াৰত অবিৰত প্লে কৰা হৈছে + প্লেয়াৰৰ ছাবটাইটেল ছেটিংছ + ক্ৰ\'মকাস্ট ছাবটাইটেল + ক্ৰ\'মকাস্ট ছাবটাইটেল ছেটিংছ + প্লেবেক গতি + প্লেয়াৰত গতিৰ বিকল্প যোগ কৰে + সোধক হওৱাৰ বাবে স্বাইপ কৰক + ভিডিঅ\' ত আপোনাৰ অৱস্থান নিয়ন্ত্ৰণ কৰিবলৈ পাছত থকা দিশত স্বাইপ কৰক + ছেটিংছ সলনী কৰিবলৈ স্বাইপ কৰক + প্ৰকাৰ বা আওতাৰ স্থানত ওপৰত অথবা তলত স্বাইপ কৰক উজ্জ্বলতা অথবা ভলিউম সলনী সলন কৰিবলৈ + স্বয়ংক্ৰিয় পৰবৰ্তী সংযোগ + বর্তমান এটা শেষ হোৱা সময় পৰবৰ্তী সংযোগ আৰম্ভ কৰা + দ্বিগুন টেপ কৰি সন্ধান কৰক + দ্বিগুন টেপ কৰি থম কৰক + প্লেয়াৰ সন্ধান পৰিমাণ (ছেকেন্ড) + অগ্ৰগামী বা পিছত সন্ধান কৰিবলৈ সোহদৰ বা বাঁয়া দিকত দুইবাৰ টেপ কৰক + থমবা নিছবা পৰিমাণ দুইবাৰ টেপ কৰক + অ্যাপ প্লেয়াৰত সিষ্টেমৰ আলোক অভিলাই ব্যৱহাৰ কৰিবলৈ বিষ্টা উলঙ্ঘনস্থিতি ব্যৱহাৰ কৰক + এপিস\'ড সংমিলিত কৰা + আপোনাৰ বৰ্তমান পৰ্বৰ প্ৰগতি স্বয়ংক্ৰিয়ভাবে সমতলীয়া কৰা + বেকআপৰ তথ্য পুনৰায় স্থাপন কৰক + তথ্য বেকআপ কৰক + বেকআপ সংখ্যা + ফাইল %sৰ পৰা তথ্য পুনৰায় স্থাপন কৰা নহয় + তথ্য সংৰক্ষিত হৈছে + তথ্য + বেকআপ ফাইল ল\'ড হৈছে + সুধাৰি অনুসন্ধান + প্ৰদানকাৰীৰ পৰা সন্ধান ফলাফল সোমোৱা + কেৱল দুৰ্ঘটনাত ডাটা পঠাৱ + কোনো ডাটা পঠাৱ নহয় + অনুপ্রেৰণাৰ অধ্যাপক দেখাওঁক + ট্ৰেলাৰ দেখাওঁক + Kitsuৰ পোষ্টাৰ দেখাওঁক + অনুসন্ধান ফলাফলত নিৰ্বাচিত ভিডিঅ’ গুণত্ব লুকাওক + স্বয়ংক্ৰিয় প্লাগইন উন্নয়ন + এপ্লিকেশন উন্নয়ন দেখাওঁক + সেটআপ প্ৰক্ৰিয়া পুনৰাবৃত্তি কৰক + প্ৰি-ৰিলিজ আপডেট কৰক + APK Installer + কিছু ফ’নসমূহ নতুন পেকেজ ইনষ্টলাৰ সমৰ্থন কৰে নাই। আপডেট ইনষ্টল নহয় পৰা পৰীক্ষা কৰক লেগেচি বিকল্প প্ৰয়োগ কৰক। + গিটহাব + একেই ডেভল\'পাৰকৰ লাইট নভেল এপ্‌ + ডিসকৰ্ডত যোগদান কৰক + কোনো লিংক পোৱা নাই + ডেভল\'পাৰকলৈ এখন বিনম্রতা দাওঁক + দিয়া বিনম্রতা + এপ্‌ ভাষা + এই প্ৰদাতাৰ কোনো চ্ৰোমকাছ্ট সমৰ্থন নাই + লিংক ক্লিপবৰ্ডত প্ৰতিলিপি কৰা হৈছে + এপিস\'ড বাজাওঁক + দুঃখিত, অ্যাপ্লিকেশন সংঘটিত হৈছে। অজ্ঞাত বাগ ৰিপ’ৰ্ট ডেভেলপাৰসমূহলৈ পঠাওক + ঋতু + ডিফ’ল্ট মান পুনৰাবৃত্তি কৰক + %1$s %2$d%3$s + কোনো ঋতু নাই + এপিস\'ড + এপিস\'ডসমূহ + %1$d-%2$d + %1$d %2$s + %sত আগবাঢ়িছে + ঋতু + এপিস\' + কোনো এপিস\'ড পোৱা নাই + ফাইল মচি দিব + মচি দিব + বাতিল কৰক + অধিপ্ত কৰক + আৰম্ভ কৰক + অসফল + সফল + আবাৰ আৰম্ভ কৰক + -৩০ + +৩০ + এইটো স্থায়ীভাৱে %s মচি দিব +\nআপুনি নিশ্চিত? + %dm +\nবাকি আছে + %s +\nবাকি আছে + চলিত আছে + সম্পন্ন হৈছে + অবস্থা + বছৰ + ৰেটিং + সময় + চাইট + সাৰাংশ + অপেক্ষাৰত + সাবটাইটেল নাই + ডিফ’ল্ট + মুক্ত + ব্যৱহাৰ কৰা + এপ্‌ স্টোৰেজ + চলচ্চিত্ৰসমূহ + টিভি চলচ্চিত্ৰসমূহ + ডকুমেণ্টাৰিসমূহ + এছিয়ান ড্ৰামা + লাইভস্ট্ৰিমসমূহ + এনএসএফডবলিউ + অন্যান্য + চলচ্চিত্ৰ + সিৰিজ + কাৰ্টুন + অ্যানিমে + OVA + টৰেণ্ট + ডকুমেণ্টাৰী + উৎস ত্ৰুটি + দূৰবৰ্তী ত্ৰুটি + ৰেন্ডাৰাৰ ত্ৰুটি + ডাউনল\'ড ত্ৰুটি, সংৰক্ষণ অনুমতিসমূহ পৰীক্ষা কৰক + অপ্ৰত্যাশিত প্লেয়াৰ ত্ৰুটি + চ্ৰোমকাছ্ট এপিস\'ড + চ্ৰোমকাছ্ট আইনমিৰৰ + আইনমিৰ আদৰণি + ডাব চিহ্ন + %sত বাজাওক + ব্ৰাউজাৰত বাজাওক + লিংক প্ৰতিলিপি কৰক + স্বয়ংক্ৰিয় ডাউনল\'ড + আদৰণি ডাউনল\'ড কৰক + উপশিৰোনিৰ চিহ্ন + প\'ষ্টাৰৰ UI উপাদানসমূহ ট\'গল কৰক + ল\'ক + ৰিসাইজ + উৎস + আপডেট + পছন্দসই দেখাৰ গুণত্ব (WiFi) + অপ স্কিপ কৰক + এই আপডেটটো প্ৰস্থান কৰক + পছন্দসই দেখাৰ গুণত্ব (মোবাইল ডেটা) + ভিডিঅ\' প্লেয়াৰৰ আৰুণিমা + ভিডিঅ\' বাফাৰৰ আকাৰ + ভিডিঅ\' বাফাৰৰ লম্বা + ডিস্কত ভিডিঅ\' কেচ কৰক + ভিডিঅ\' আৰু চিত্ৰ কেচ মুক্ত কৰক + প্লেয়াৰ দৃশ্যমান হ\'লে ব্যৱহাৰ কৰা সন্ধান পৰিমাণ + প্লেয়াৰ লুকুৱাওলৈ - সন্ধান পৰিমাণ + প্লেয়াৰ দেখা হোৱালৈ - সন্ধান পৰিমাণ + প্লেয়াৰ লুকুৱা হ\'লে ব্যৱহাৰ কৰা সন্ধান পৰিমাণ + যদি স্তন্যপান উচ্চত নিৰ্ধাৰিত হ\'লে, অন্যত্ৰ সঞ্চয় স্থানত সন্দেহ ঘটিব। উদাহৰণস্বৰূপ, এছিয়ান টিভি। + DNS ওভাৰ HTTPS + যদি স্তন্যপান উচ্চত নিৰ্ধাৰিত হ\'লে, অন্যত্ৰ মেম\'ৰী সন্ধানৰ যন্ত্ৰসমূহত সন্দেহ ঘটিব। উদাহৰণস্বৰূপ, এছিয়ান টিভি। + আইএছপি ব্লক পাৰ কৰিবলৈ কাৰ্যকাৰী + চাব চাওঁক + GitHub প্ৰক্সি + GitHub প্ৰাপ্য নাই। jsDelivr প্ৰক্সি অন কৰা হচ্ছে… + jsDelivr ব্যৱহাৰ কৰি সোধ গুগল url ব্লক অনলাইন কৰক। কিছুদিনৰ মাজত আপডেট ল\'ওক দিব পাৰে। + চাব আঁতৰাওক + ডাউনল\'ড পাথ + NGINX চাৰ্ভাৰ url + ডাব কৰা/সাব কৰা অ্যানিমে প্ৰদৰ্শন কৰক + স্ক্ৰীনলৈ সজাব কৰক + পৰবৰ্তী কৰক + অকৃত্রিম নোটিশ + আইএছপি পাৰ কৰা + জুম কৰক + এপ্ আপডেটসমূহ + বেকাপ + এক্সটেনচনসমূহ + ক্ৰিয়াসমূহ + কেচ + এণ্ড্ৰোইড টিভি + হাস্য + প্লেয়াৰৰ বৈশিষ্ট্য + উপশিৰোতা + লে\'আ\'উট + ডিফ’ল্টসমূহ + দেখুৱাই + বৈশিষ্ট্যসমূহ + সাধাৰণ + যিতা বুটাম + হোমপেজ আৰু লাইব্ৰেৰিত যিতা বুটাম দেখোৱা + সুপালৈক ভাষাসমূহ + এপ্ লে\'আ\'উট + পছন্দসই মিডিয়া + সুবিধা দিয়া সময় NSFW সক্ষম কৰক + উপশিৰোতা ক\'ডিং + প্ৰদাতা + প্ৰদাতা পৰীক্ষা + সকলো এক্সটেনচনসমূহ পৰীক্ষা কৰক + এই পৰীক্ষা কেৱল উন্নীতকাৰীসমূহলৈ পৰমিট আৰু বাৰ্তা কৰা হৈছে আৰু কেইকোনো এক্সটেনচনৰ কাৰ্যক্ষম বা অকাৰ্যক্ষমতা প্ৰমাণিত নহয়। + টিভি লে\'আ\'উট + এমুলেটৰ লে\'আ\'উট + প্ৰাথমিক ৰং + এপ্ থীম + প\'ষ্টাৰ শিৰোনাম অৱস্থান + ফোন লে\'আ\'উট + প\'ষ্টাৰ তলত শিৰোনাম দিয়ক + নতুন সাইটৰ নাম + https://example.com + ভাষা ক\'ড (en) + %1$s %2$s + একাউন্ট + লগ ইন + একাউন্ট পৰিবৰ্তন কৰক + একাউন্ট যোগ কৰক + একাউন্ট সৃষ্টি কৰক + ট্ৰেকিং যোগ কৰক + কোনোবিলাক + সাধাৰিত + %s যোগ কৰা হৈছে + সিঙ্ক + অক্ষম কৰক + সকল + অধিকতম + আউটলাইন + ডিপ্ৰেছড + ছাড়া + 1000 মিলিছেকেন্ড + উঁচুহোৱা + উপশিৰোতা সিংক + উপশিৰোতা দেলাই + যদি উপশিৰোতা %d মিলিছেকেন্ড পূৰ্ববৰ্তী দেখা যায় তেন্ত এইটো ব্যৱহাৰ কৰক + কোনো উপশিৰোতা বিলম্ব নাই + যদি উপশিৰোতা %d মিলিছেকেন্ড পূৰ্ববৰ্তী দেখা যায় না তেন্ত এইটো ব্যৱহাৰ কৰক + দ্রুত মাৰেল ভুঁটি সোমালী কুকুৰা ধীৰে + অনুমোদিত + %s লোড কৰা হৈছে + ফাইলৰ পৰা লোড কৰক + ইন্টাৰনেটৰ পৰা লোড কৰক + পছৱা + ডাউনলোড কৰা ফাইল + উৎস + প্ৰধান + যাত্রা + শীঘ্ৰই আসিব… + সহায়ক + ক্যাম + ক্যাম + ক্যাম + এইচকিউ + এইচডি + টিএছ + ব্লু-ৰে + ডব + টিসি + ডিভিডি + ৪কে + ইউএচডি + এইচডিৰ + এসডিৰ + ওয়েব + পোষ্টাৰ ছবি + প্লেয়াৰ + দৰ্শনীয়তা আৰু শীৰ্ষক + শীৰ্ষক + দৰ্শনীয়তা + অবৈধ আইডি + পছন্দৰ মিডিয়া ভাষাৰ দ্বাৰা ফিল্টাৰ কৰক + সাবটাইটেলত ব্লোট দূৰ কৰক + অতিৰিক্ত + ট্ৰেলাৰ + ক্ৰেশ ৰিপ\'টিং + https://example.com/example.mp4 + পূৰ্বৱৰ্তী + ছেটআপ প্ৰস্থান কৰক + আপোনাৰ ডিভাইচ অনুযায়ী এপ্পৰ প্ৰস্তুতি সলনি কৰক + আপুনি কি চাব + এক্সটেনছনসমূহ + প্ৰায়ণশাল যোগ কৰক + প্ৰায়ণশালৰ নাম + প্ৰায়ণশাল URL + প্লাগইন লোড হোৱা হৈছে + প্লাগইন ডাউনলোড হোৱা হৈছে + প্লাগইন মুছিলো + %s লোড কৰিব পৰা নহয় + ১৮+ + %1$d %2$s ডাউনলোড আৰম্ভ কৰা হ’ল… + %1$d %2$s ডাউনলোড কৰা হ’ল + সকলো %s ইতিমধ্যে ডাউনলোড কৰা হৈছে + ৰিপ’চ্যুটৰীত কোনো প্লাগইন পোৱা নাই + প্লাগইন + প্লাগইনসমূহ + ৰিপ’চ্যুটৰি পোৱা নাই, URL চেক কৰক আৰু VPN চেক কৰক + এইটো আৰুও ৰিপ’চ্যুটৰীত সকলো প্লাগইন মুছিব + ৰিপ’চ্যুটৰি মুছিব + %d প্লাগইন আপডেট কৰা হ’ল + নিষ্ক্ৰিয় কৰা হ’ল: %d + ডাউনলোড হোৱা নাই: %d + ক্লাউডস্ট্ৰীমত ডিফ’ল্টত কোনো বেছি সাইট ইনষ্টল কৰা নাই। আপোনাৰ এটি পৰিৱেশনত থকা সাইটসমূহ ইনষ্টল কৰিব লাগিব। +\n +\nআমাৰ ডিসক’ৰ্ডত যোগদান কৰক অথবা অনলাইনত সন্ধান কৰক। + সম্প্রদায়ৰ প্ৰায়ণশালসমূহ চাওক + সকলো সাবটাইটেলৰ মাজতে আপাৰ আকাৰত লিখক + এই প্ৰায়ণশালৰ সকলো প্লাগিন ডাউনলোড কৰিব? + %s (অক্ষম কৰা আছে) + পথসমূহ + অডিঅ’ পথসমূহ + ভিডিঅ’ পথসমূহ + প্ৰযোগ সলনি কৰিবলৈ এপ্‌লিকেছন পুনৰ আৰম্ভ কৰক। + পুনৰ আৰম্ভ কৰক + নিৰাপদ প্ৰণামী চালু + এপ্প সমস্থ একটি দ্বিঘাতৰ ফলে সমস্থ এক্সটেনছন নিষ্ক্ৰিয় কৰা হৈছিল যিতে আপোনাক কিবা সমস্যা আছে তা চেনা নিব পাৰে। + দ্বিঘাতৰ তথ্য চাওক + ৰেটিং: %s + বিৱৰণ + সংস্কৰণ + অৱস্থা + আকাৰ + লেখক + সমৰ্থিত + ভাষা + শীৰ্ষকত প্লাগিনটো ইনষ্টল কৰক + এপ্‌লিকেছন পোৱা নহয় + সকলো ভাষা + %s অপচাৰ কৰক + ওপনিং + শেষ + পুনৰাবৃত্তি + HLS প্লেলিষ্ট + পছন্দৰ ভিডিঅ’ প্লেয়াৰ + আইণ্টাৰনেল প্লেয়াৰ + MPV + Web ভিডিঅ’ কাষ্ট + Fcast + Web ব্ৰাউজাৰ + কাষ্ট ডিভাইচ নিৰ্বাচন কৰক + মিছিণ শেষ + মিছিণ আৰম্ভ + ইতিহাস মুকা + শ্ৰেয়া + প্ৰস্তাবনা + ইতিহাস + ডাটাবেছত প্ৰবেশ/শেষৰ বাবে পপাপ দেখাওক + বেছি পাঠ্য। ক্লিপব’ৰ্ডত সংৰক্ষণ কৰিব নোৱা যায়। + কপি কৰিবলৈ ত্ৰুটি, অনুগ্ৰহ কৰি লগকেট কপি কৰি অ্যাপ সমৰ্থনকৰ্তাৰ সৈতে যোগাযোগ কৰক। + চাওক হিচাপে চিহ্নিত কৰক + চাওক হিচাপৰ পৰা মুকা + হয় + নহয় + ঠিক আছে + আপুনি নিশ্চিত হৈছে যে আপুনি প্ৰস্থান কৰিব বিচাৰে? + অ্যাপ্‌ৰ বেটেৰি ব্যৱহাৰ ইতিমধ্যে অসীমিত হ’লে + সাবস্ক্ৰাইব কৰা TV সেৰাৰ বাবে অবিচ্ছিন্ন ডাউনলোড আৰু বিজ্ঞাপনৰ বাবে নোটিফিকেশনৰ বাবে, ক্লাউডষ্ট্ৰিমৰ অনুমতি প্ৰয়াপ্ত কৰিবলৈ পৃষ্ঠভূমিত চলকৰ বাবে অনুমতি প্ৰয়াপ্ত কৰিবলৈ লাগে। ঠিক আছে টিপিবলৈ, আপুনি এপ তথ্যলৈ দীঘল হৈ যাব। তত্ত্বাবধানে, ইয়াৰ ব্যৱহাৰ ক্লাউডষ্ট্ৰিমক আপোনাৰ বেটাৰী সেক্ষাৰ কৰিব নাই অৰু। ইয়া শুধু প্ৰয়োজনীয় হোৱা সময়ত বেক্গ্‌গ্‌ত অতিৰিক্ত কাৰ্য কৰিবলৈ অপাৰে, যেনে নোটিফিকেশন সোধা আৰু আধিকাৰিক প্ৰস্তাবনাৰ ভিডিঅ’স ডাউনলোড কৰিবলৈ। যদি আপুনি বাতিল কৰিব বাচন কৰিছে, আপুনি পৰেন্তু সেটিং ইয়াৰ পিছতে ক্রোয়জবলৈ চাব পাৰে জেনে গওঁক সেটিংছ। + ক্লাউডষ্ট্ৰীমৰ অ্যাপ্‌ তথ্য খোলাত অসমৰ্থ + অ্যাপ্‌ৰ নতুন সংস্কৰণ ইনষ্টল কৰিব পৰা নাই + অ্যাপ্‌ আপডেট ডাউনলোড হৈছে… + অ্যাপ্‌ আপডেট ইনষ্টল হৈছে… + পুৰণৰূপ + পেকেজ ইনষ্টলাৰ + অ্যাপ্‌ প্ৰস্থানত আপডেট হৈ যাব + ক্ৰমৰ অনুযায়ী + সাজোৱা + ৰেটিং (উচ্চ থকাৰ পৰা নিম্ন) + ৰেটিং (নিম্ন থকাৰ পৰা উচ্চ) + আপডেট হোৱা (নতুনৰ পৰা পুৰণৰূপ) + আপডেট হোৱা (পুৰণৰূপৰ পৰা নতুন) + বৰ্ণমালা (এ থকা জে পৰা জে) + গোটি বাছক কৰক + সৈতে খোলক + আপোনাৰ গোটিটো খালি আছে :( +\nগোটিত প্ৰৱেশ কৰিবলৈ এখনকা একাউণ্টত লগিন কৰক নাইবা আপোনাৰ স্থানীয় গোটিত চইক যোগ কৰক। + এই তালিকাটো খালি আছে। আৰু এটা অন্য এটা তালিকাত স্থানান্তৰ কৰিব চেষ্টা কৰক। + নিৰাপত ম’ড ফাইল পোৱা গৈছে! +\nফাইল মুকা হোৱা পৰা প্ৰাৰম্ভত কোনো এক্সটেনচন লোড কৰা হ’ব নহয়। + পুনৰ প্ৰয়াণ কৰক + সাবস্ক্ৰাইব কৰা প্ৰদর্শন আপডেট হৈ আছে + %s সাবস্ক্ৰাইব কৰা হ\'ল + %s পৰা সাবস্ক্ৰাইব কৰা হ\'ল + ওয়াইফাই + মোবাইল ডাটা + ডিফ’ল্ট ঠিকনা কৰক + ব্যৱহাৰ কৰক + সম্পাদনা কৰক + প্ৰ’ফাইলসমূহ + সাহায্য + ইয়াত আপুনি সৃষ্টিগত উৎসসমূহ কেমানে সাজোৱা পৰিবৰ্তন কৰিব পাৰে। যদি এটা ভিডিঅ’ এটা উচ্চ অগ্ৰাধিকাৰ থাকে তেনেহলে ই উচ্চ মানৰ উৎস বাছনি তথ্যত প্ৰদৰ্শিত হ\'ব। উৎসৰ প্ৰাথমিকতা আৰু মানৰ প্ৰাথমিকতাৰ যোগফল ভিডিঅ’ৰ প্ৰাথমিকতা হ’ব। +\n +\nউৎস A: 3 +\nগুণগত মান B: 7 +\n10 এৰি মানৰ এটা সংযুক্ত ভিডিঅ’ প্ৰাথমিকতা থাকিব। +\n +\nনোট: যদি যোগফল 10 বা তাতো অধিক হ\'ব তেনেহলে প্লেয়াৰ আটোমেটিকলি সেই লিংক লোড হোৱা সময়ত লোড কৰিব নোৱা হ\'ব! + প্ৰ’ফাইল পৃষ্ঠভূমি + ইউআই সঠিকভাৱে সৃষ্টি কৰা নাই, এটা মুখ্য বুগ আৰু অধীৰ অনুমতি দিয়া হোৱা উচিত এইটো পৰ্যন্ত প্ৰতিবেদন কৰক %s + আপুনি ইতিমধ্যে ভোট দিয়া আছে + প্ৰিয়মূল্য বিষয়বস্তুসমূহ + %s প্ৰিয়মূল্য বিষয়বস্তুসমূহত যোগ কৰা হ\'ল + %s প্ৰিয়মূল্য বিষয়বস্তুসমূহৰ পৰা আঁতৰাই কৰা হ\'ল + প্ৰিয়মূল্য বিষয়বস্তুসমূহত যোগ কৰক + প্ৰিয়মূল্য বিষয়বস্তুসমূহৰ পৰা আঁতৰাই কৰক + সম্ভাৱ্য ডুপ্লিকেট পোৱা গৈছে + সকলোত প্ৰতিস্থাপন কৰক + আপোনাৰ গোটিত এটা সম্ভাব্য ডুপ্লিকেট আইটেম অলপ অস্তিত্বত আছে: \'%s\' +\n +\nআপুনি ই আইটেমটো সংযোগ কৰিব বিচাৰে নে, বৰং ইতিমধ্যে থকা আইটেমটো সংস্কৰণ কৰিব নে, নাইবা অ্যাকছনটো বাতিল কৰিব? + আপোনাৰ গোটিত এক সম্ভাব্য ডুপ্লিকেট আইটেম অনেক অস্তিত্বত আছে: +\n +\n%s +\n +\nআপুনি ই আইটেমটো সংযোগ কৰিব বিচাৰে নে, বৰং ইতিমধ্যে থকা আইটেমটোসমূহ সংস্কৰণ কৰিব নে, নাইবা অ্যাকছনটো বাতিল কৰিব? + %sৰ বাবে PIN এন্টাৰ কৰক + বৰ্তমান পিন এন্টাৰ কৰক + প্ৰ’ফাইল লক কৰক + পিন + অশুদ্ধ পিন। অনুগ্ৰহ কৰি পুনৰ চেষ্টা কৰক। + পিনটো 4 বৰ্ণ থাকিব লাগিব + এটা একাউন্ট নিৰ্বাচন কৰক + একাউন্টসমূহ পৰিচালনা কৰক + প্ৰস্থানত একাউন্ট নিৰ্বাচন পাছ দিব + ডিফ’ল্ট একাউন্ট ব্যৱহাৰ কৰক + ঘূৰাওক + %sৰ হিচাপে লগ ইন কৰা হৈছে + স্ক্ৰীনৰ অৰিএণ্টেশ্বনৰ বাবে ট’গল বুটাম প্ৰদৰ্শন কৰক + ভিডিঅ’ৰ অৰিএণ্টেশ্বনৰ ভিত্তিত স্বয়ংক্ৰিয় স্ক্ৰীনৰ অৰিএণ্টেশ্বন প্ৰবণ কৰক + স্বয়ংক্ৰিয় ঘূৰাওক + প্ৰিয়মূল্যহীন + এই ডিভাইচত বায়োমেট্ৰিক পুনৰ্প্ৰমাণৰ সমৰ্থন কৰা নাই + আঙুলি ছাঁচ ব্যৱহাৰ কৰি, মুখৰ চিত্ৰ, PIN, প্ৰণালী আৰু পাছৱাৰ্ডৰ সৈতে অ্যাপ্‌ আনলক কৰক। + প্ৰিয়মূল্য + এই স্ক্ৰীন একাধিক অসফল চেষ্টাৰ কাৰণে বন্ধ হৈছিল। অনুগ্ৰহ কৰি অ্যাপ্লিকেশ্বন পুনৰ্‌ আৰম্ভ কৰক। + আপোনাৰ CloudStream ডাটা এতিয়া বেকআপ কৰা হৈছে। হৈচঁদিক এই সম্ভাবনা বেছি নাই, সকলো ডিভাইচত বিভিন্নভাৱে আচৰণ কৰিব পাৰে। যদিচয় আপুনি এপ্পটো প্ৰৱেশ কৰাৰ বন্ধ হৈ যাওৱাৰ অভাবতে, অ্যাপ্‌ৰ তথ্য পূৰ্ণভাৱে মচলা কৰক আৰু এক বেকআপৰ পৰা প্ৰতিস্থাপন কৰক। এই বিষয়ত উদ্ভাবিত যোৱা অসুবিধাৰ বাবে আমি অত্যন্ত দুঃখী। + ডাউনলোড কৰক + এপ্‌টোক আৰম্ভ কৰাৰ পাছত নতুন উন্নয়নসমূহ স্বয়ংক্ৰিয়ভাৱে খোঁজি পাওক। + পূৰ্ণ মুক্তি প্ৰাপ্ত কৰিবলৈ প্ৰি-ৰিলিজ উন্নয়ন খোঁজি পাওক। + একেই ডেভল\'পাৰকৰ অনিমে এপ্‌ + নতুন আপডেট পাইছো! +\n%1$s -> %2$s + ফিলাৰ + CloudStream ৰ জৰিয়তে খেলোৱা + সন্ধান + ডাউনলোডসমূহ + টেগসমূহ + লোডিং এৰি যাওক + লোড হৈ আছে… + ধৰি ৰখা হৈ আছে + সম্পন্ন হৈ গ\'ল + এৰি দিয়া হৈছে + পুনৰাবৃত্তি কৰা হৈ আছে + চলচ্চিত্ৰ খেলাওক + ট্ৰেলাৰ খেলাওক + লাইভষ্ট্ৰীম খেলাওক + ছাবটাইটেল বাছনি কৰক + পৰ্ব খেলাওক + প্ৰয়োগ কৰক + \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 2be08369..5a104444 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -422,7 +422,7 @@ Вижте хранилищата на общността Публичен списък Всички субтитри с главни букви - Изтегляне на всички добавки от това хранилище\? + Изтегляне на всички добавки от това хранилище? %s (Деактивиран) Потоци Аудио потоци @@ -601,4 +601,4 @@ Покажи предложения Добавя опция за промяна на скоростта в плеъра Този тест е направен за програмисти и не проверява работата на никакви добавки. - + \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 867dd4ed..ccd9e433 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -56,7 +56,7 @@ ডাউনলোড শুরু ডাউনলোড বাদ ডাউনলোড শেষ - স্ট্রিম + নেটওয়ার্ক স্ট্রিম লিংক লোডিং ব্যর্থ ডাব সাব @@ -119,7 +119,7 @@ ক্রোমক্যাস্ট এ সাবটাইটেল সমূহের সেটিংস কালো প্রান্ত অপসারণ করুন অনুসন্ধান করুন - অ্যাকাউন্টসমূহ + অ্যাকাউন্টসমূহ এবং নিরাপত্তা কোনো উপাত্ত পাঠাবে না বিরতি দিতে মাঝে দুইবার চাপুন সিস্টেম এর উজ্জ্বলতা ব্যবহার করুন @@ -143,7 +143,7 @@ পোস্টার @string/home_play আগাতে ডবল ট্যাপ করুন - আইজেনগ্রাভি মোড + প্লেব্যাক এর গতি আপডেট শুরু হয়েছে ব্রাউজার লগ @@ -229,4 +229,134 @@ আপনার বর্তমান পর্বের অগ্রগতি স্বয়ংক্রিয়ভাবে সিঙ্ক করুন প্লাগইন ডাউনলোড ফিল্টার করতে মোড নির্বাচন করুন লিঙ্ক পুনরায় লোড হয়েছে - + সুইচ অ্যাকাউন্ট + ব্রাউজারে প্লে করুন + দাবিত্যাগ + এশিয়ান ড্রামা + সোর্স + এক্সটেনশন + লিংকস + এনএসএফডব্লিউ + ডাউনলোড মিরর + অপ্রত্যাশিত প্লেয়ার এর সমস্যা + সাবটাইটেল ডাউনলোড করুন + রিপোজিটরির নাম এবং ইউ আর এল + কপি করা হয়েছে! + অ্যান্ড্রয়েড টিভির মতো, কম মেমরির ডিভাইসে খুব বেশি সেট করা হলে সমস্যা করবে। + ক্লোন সাইট + প্লেয়ারের ফিচার + MAL AniList TMDB IMDB Kitsu Trakt %1$s%2$s + অ্যাপ থিম + রিকমেন্ডেশনগুলো দেখাও + প্লেয়ারে গতির বিকল্প যোগ কর + %1$s %2$d%3$s + কার্টুন + এনিমে + পোস্টারে ইউ আই উপাদান টগল করুন + আপডেট চেক করুন + রিসাইজ + ওপেনিং স্কিপ করুন + ডিক্সের ভিডিও ক্যাশ + ভিডিও এবং ইমেজ ক্যাশ পরিস্কার করুন + অ্যান্ড্রয়েড টিভির মতো, কম মেমরির ডিভাইসে খুব বেশি সেট করা হলে ক্র্যাশ করবে + ডিএনএস ওভার এইচটিটিপিএস + একটি ভিন্ন URL সহ একটি বিদ্যমান সাইটের, একটি ক্লোন যোগ করুন + স্ক্রিনে ফিট করুন + ক্যাশ + লেআউট + টিভি লেআউট + ব্যবহারকারীর নাম + %1$d-%2$d + NewSiteName + ডিফল্ট + OVA + ওভিয়ে + টরেন্ট + এপিসোড ক্রোমকাস্ট করুন + প্লে হচ্ছে %s সময়ের মধ্যে + লিঙ্ক কপি করুন + স্বয়ংক্রিয় ডাউনলোড + টাইটেল + প্লেয়ার দেখা যাচ্ছে - সিকের পরিমাণ + রিমুভ সাইট + NGINX সার্ভারের ইউআরএল + আইএসপি বাইপাস + অ্যান্ড্রয়েড টিভি + সমর্থিত এক্সটেনশনগুলিতে NSFW সক্ষম করুন + সাবটাইটেল এনকোডিং + দেখার ধরন + ফিচার সমূহ + হোমপেজ এবং লাইব্রেরিতে এলোমেলো বোতাম দেখান + প্রদানকারী + প্রদানকারী পরীক্ষা + স্বয়ংক্রিয় + ফোন লেআউট + পোস্টার শিরোনামের অবস্থান + পোস্টারের নীচে শিরোনাম রাখুন + প্রবেশ করুন + অ্যাকাউন্ট তৈরি করা + একাউন্ট যোগ করা + যখন প্লেয়ার হিডেন থাকবে তখন সিকের পরিমান + ক্রোমকাস্ট মিরর + এক্সটেনশন ভাষা + লেআউট + সাবটাইটেল + অ্যাকশন + ভিডিও প্লেয়ারের টাইটেল এ সর্বোচ্চ ক্যারেক্টার + ডকুমেন্টারি + অ্যাপ লেআউট + ভিডিও + বেকাপ + E + S + hello@world.com + https://example.com + নতুন এপিসোডের নোটিফিকেশন + অন্য এক্সটেনশনের মধ্যে খুঁজুন + কোন আপডেট পাওয়া যায়নি + password123 + আসছে %s সময়ের মধ্যে + বাতিল করুন + %s +\nঅবশিষ্ট + লাইভ স্ট্রিম + সোর্স সমস্যা + রিমোট সমস্যা + রেন্ডারের সমস্যা + ডাউনলোডের সমস্যা, স্ট্রোরেজদের পারমিশন চেক করুন + অ্যাপ এ প্লে করুন + লিংক রিলোড করুন + কোয়ালিটি লেবেল + সাব লেবেল + ডাব লেবেল + লক + আর দেখাবেন না + এই আপডেট স্কিপ করুন + আপডেট + ওয়াইফাই তে যে কোয়ালিটিতে দেখতে চান + মোবাইল ডাটায় যে কোয়ালিটিতে দেখতে চান + ভিডিও প্লেয়ারে রেজুলেশন + ভিডিও বাফার সাইজ + ভিডিও বাফার লেনথ + যখন আইএসপি ব্লক করবে তখন কার্যকরী + গিডহাব প্রক্সি + ডাউনলোডের পথ + ডাব/সাব এনিমে দেখান + স্ট্রেচ + বড় করুন + অ্যাপ আপডেট + গ্যাসচার + ডিফল্ট + সাধারণ + এলোমেলো বোতাম + পছন্দের মিডিয়া + সমস্ত এক্সটেনশন পরীক্ষা করুন + এই পরীক্ষাটি শুধুমাত্র ডেভেলপারদের জন্য এবং কোন এক্সটেনশনের কাজ যাচাই বা অস্বীকার করে না। + এমুলেটর লেআউট + প্রাইমারি রং + 127.0.0.1 + Language code (en) + অ্যাকাউন্ট + প্রস্থান + %1$d%2$s + \ No newline at end of file diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 40847edf..3042fa21 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -422,7 +422,7 @@ Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas - Transferir todos os plugins deste repositório\? + Atenção: CloudStream 3 não assume nenhuma responsabilidade pelo uso de extensões de terceiros e não fornece nenhum suporte para elas! %s (Desativado) Reproduzir automaticamente próximo episódio Começa o próximo episódio quando o atual termina @@ -622,7 +622,7 @@ Autenticação de Senha/PIN A autenticação biométrica não é compatível com este dispositivo Desbloquear o aplicativo com impressão digital, ID facial, PIN, padrão e senha. - Esta tela foi fechada devido a diversas tentativas malsucedidas. Por favor reinicie o aplicativo. + Após algumas tentativas fracassadas, o prompt será fechado. Basta reiniciar o aplicativo para tentar novamente. %s \nrestante(s) Favorito @@ -639,4 +639,21 @@ Música Áudio-livro Mídia - + Redefinir + Próximos em %s + Temporada %1$d Episódio %2$d será lançado em + Fcast + Selecione o dispositivo de transmissão + Espelhar transmissão + CloudStream Wiki + Segurança + Contas + Autenticação local + Imagem do código QR + Descartar + Abrir repositório + Acesse %s em seu smartphone ou computador e digite o código acima + Não é possível obter o código PIN do dispositivo, tente a autenticação local + O código PIN expirou! + O código expira em %1$dm %2$ds + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 0a8cf997..e1c51874 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -489,7 +489,7 @@ Přidat klon existujícího webu s jinou adresou URL https://example.com Kód jazyka (cs) - Stáhnout všechny doplňky z tohoto repozitáře\? + Varování: CloudStream 3 nenese žádnou zodpovědnost za používání rozšíření třetích stran a neposkytuje pro ně žádnou podporu! %s (zakázáno) Stopy NSFW @@ -623,7 +623,7 @@ Ověření heslem/PINem Biometrické ověření není na tomto zařízení podporováno Odemkněte aplikaci otiskem prstu, obličejem, PINem, gestem nebo heslem. - Tato obrazovka byla po několika nezdařilých pokusech uzavřena. Restartujte prosím aplikaci. + Po několika nezdařilých pokusech se okno zavře. Pro opětovný pokus restartujte aplikaci. Vaše data z aplikace CloudStream byla nyní zálohována. Ačkoli je tato možnost velmi malá, různá zařízení se mohou chovat různě. Ve výjimečném případě, že se vám přístup k aplikaci zablokuje, data aplikace zcela vymažte a obnovte je ze zálohy. Velmi se omlouváme za případné nepříjemnosti z toho plynoucí. Odebrat z oblíbených %s @@ -641,4 +641,21 @@ Zakažte optimalizace baterie Aby bylo zajištěno nepřetržité stahování a upozornění na odebírané seriály, potřebuje aplikace CloudStream povolení ke spuštění na pozadí. Stisknutím tlačítka OK budete přesměrováni na informace o aplikaci. Tam přejděte na položku Využití baterie aplikací a nastavte možnost Využití baterie na hodnotu Neomezené. Upozorňujeme, že toto povolení neznamená, že CS3 bude vybíjet baterii. Na pozadí bude pracovat pouze v případě potřeby, například při přijímání oznámení nebo stahování videí z oficiálních rozšíření. Pokud se rozhodnete toto nastavení zrušit, můžete jej později upravit v Obecných nastaveních. Audiokniha - + Resetovat + Vychází %s + Epizoda %2$d ze série %1$d bude vydána za + Vysílat zrcadlení + Fcast + Vyberte zařízení k vysílání + CloudStream Wiki + Zabezpečení + Obrázek QR kódu + Zavřít + Otevřít repozitář + Navštivte %s na vašem zařízení nebo počítači a zadejte kód výše + Nepodařilo se získat PIN kód, zkuste místní ověření + Kód vyprší za %1$dm %2$ds + Účty + Lokální ověření + PIN kód vypršel! + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5a871217..d111ed68 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -151,7 +151,7 @@ Speicherberechtigungen fehlen. Bitte erneut versuchen. Suche Konten und Sicherheit - Updates und Datensicherung + Aktualisierungen und Datensicherung Info Erweiterte Suche Liefert die Suchergebnisse getrennt nach Anbietern @@ -416,12 +416,12 @@ Community-Repositories anzeigen Öffentliche Liste Alle Untertitel in Großbuchstaben - Alle Plugins aus diesem Repository herunterladen\? + Alle Plugins aus diesem Repository herunterladen? %s (Deaktiviert) Spuren Audiospuren Videospuren - Bei Neustart anwenden + Starte die App neu, um die Änderungen zu sehen. Abgesicherter Modus aktiviert Alle Erweiterungen wurden aufgrund eines Absturzes deaktiviert, damit Sie diejenige finden können, welche Probleme verursacht. Absturzinfo ansehen @@ -607,4 +607,12 @@ Beim kopieren ist ein Fehler aufgetreten, bitte kopieren sie logical und wenden sich an den Support. Fehler beim zugriff auf die Zwischenablage, bitte erneut versuchen. Repository Name und URL - + OK + Akku-Optimierung deaktivieren + Musik + Hörbuch + Medien + Zurücksetzen + Akkuverbrauch der App ist bereits auf unbeschränkt eingestellt + CloudStreams App-Info kann nicht geöffnet werden. + \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index a539f374..dbf03fb8 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -91,7 +91,7 @@ Ρυθμίσεις υποτίτλων του προγράμματος αναπαραγωγής Υπότιτλοι για Chromecast Ρυθμίσεις υποτίτλων για Chromecast - Eigengravy Mode + Ταχύτητα Προβολής Σύρετε για αναζήτηση Σύρετε από πλευρά σε πλευρά για να ελέγξετε το σημείο του βίντεο στο οποίο βρίσκεστε Σύρετε για να αλλάξετε ρυθμίσεις @@ -130,7 +130,7 @@ Τα δεδομένα αποθηκεύτηκαν Δεν έχει δοθεί άδεια για πρόσβαση στον αποθηκευτικό χώρο. Παρακαλώ προσπαθήστε ξανά. Σφάλμα δημιουργίας αντιγράφων ασφαλείας %s - Λογαριασμοί + Λογαριασμοί και Ασφάλεια Ενημερώσεις και αντίγραφα ασφαλείας Εμφάνιση filler επεισοδίου για άνιμε Εμφάνιση trailer @@ -201,7 +201,7 @@ Επαναφόρτωση συνδέσμων Λήψη υποτίτλων Ποιότητα - Dub + Ετικέτα Dub Sub Τίτλος Εναλλαγή γραφικών στοιχείων στην αφίσα @@ -233,11 +233,11 @@ Αποποίηση ευθυνών Γενικά Κουμπί τυχαίας δράσης - Εμφάνιση κουμπιού τυχαίας δράσης στην Αρχική Οθόνη + Εμφάνιση κουμπιού τυχαίας προβολής στην Αρχική Οθόνη Γλώσσες παρόχων Διάταξη εφαρμογής Προτιμώμενα μέσα - Ενεργοποίηση NSFW σε υποστηριζόμενους παρόχους + Ενεργοποίηση ακατάλληλου περιεχομένου σε υποστηριζόμενους παρόχους Κωδικοποίηση υποτίτλων Πάροχοι Διάταξη @@ -302,8 +302,8 @@ Φιλτράρισμα ανά την προτεινόμενη γλώσσα του μέσου Έξτρα Τρέιλερ - Σύνδεσμος για stream - Παραπομπή + Https://Παράδειγμα.com/Παράδειγμα.mp4 + Παραπομπή (προαιρετική) Επόμενο Παρακολούθηση βίντεο σε αυτή την γλώσσα Προηγούμενο @@ -334,8 +334,6 @@ Ενημερώθηκαν %d πρόσθετα Το CloudStream δεν έχει προεγκατεστημένους ιστότοπους. Πρέπει να εγκαταστήσετε ιστότοπους μέσω ορισμένων αποθετηρίων. \n -\nΛόγω ενός χαζού DMCA takedown από μέρους των Sky UK Limited 🤮 δεν μπορούμε να προσθέσουμε απευθείας σύνδεσμο προς τα προαναφερόμενα αποθετήρια εντός της εφαρμογής. -\n \nΒρείτε μας στο Discord ή ψάξτε στο διαδίκτυο. Προβολή αποθετηρίων κοινότητας Δημόσια λίστα @@ -345,7 +343,7 @@ Κομμάτια Ηχητικά κομμάτια Κομμάτια βίντεο - Εφαρμογή στην επανεκκίνηση + Κάντε επανεκκίνηση της εφαρμογής για να δείτε τις αλλαγές. Η ασφαλής λειτουργία ενεργοποιήθηκε Όλα τα extensions απενεργοποιήθηκαν , ώστε να μπορέσετε να διαπιστώσετε ποιο από αυτά προκάλεσε τη κατάρρευση. Προβολή πληροφορίας κατάρρευσης @@ -392,7 +390,7 @@ Αυτόματη ενημέρωση plugin Αυτόματη λήψη plugin DNS μέσω HTTPS - παράδειγμα.com + https://παράδειγμα.com HQ TS TC @@ -412,7 +410,7 @@ NSFW Chromecast mirror Σύνδεσμος NGINX σέρβερ - ΟΚουλΙστότοποςΜου + ΝεοΟνομαΙστοτοπου /\?\? /%d %d / 10 @@ -434,7 +432,7 @@ Δεν βρέθηκε ενημέρωση Έλεγχος για ενημέρωση κωδικός123 - ΤοΚουλΨευδώνυμοΜου + ΤοΚουλΟνομαΜου γειασου@κόσμε.com Η γρήγορη, καφέ αλεπού πηδάει πάνω από τον τεμπέλη σκύλο / The quick brown fox jumps over the lazy dog Cam @@ -521,7 +519,7 @@ \nΣΗΜΕΙΩΣΗ: Εάν το άθροισμα είναι 10 ή περισσότερο, η συσκευή αναπαραγωγής θα παραλείψει αυτόματα τη φόρτωση όταν φορτωθεί αυτός ο σύνδεσμος! Δοκιμή παρόχου Προτιμώμενη ποιότητας παρακολούθησης (Δεδομένα τηλεφώνου) - Διακομιστής μεσολάβησης raw.githubusercontent.com + Διακομιστής μεσολάβησης GitHub Android TV Ενημέρωση εγγεγραμμένων εκπομπών Έγινε εγγραφή σε %s @@ -546,4 +544,85 @@ Επιλέξτε κατάσταση για φιλτράρισμα επεκτάσεων για λήψη Απενεργοποιημένο Τέλος - + Συχνότητα δημιουργίας αντιγράφων ασφαλείας + Οι σύνδεσμοι επαναφορτώθηκαν + αντιγράφηκε! + Αναζήτηση σε άλλες επεκτάσεις + Ειδοποίηση για νέο επεισόδιο + Εισαγωγή Κωδικού + Όνομα \"αποθήκης\" και λινκ + Εμφάνιση προτάσεων + Προσθήκη στα Αγαπημένα + Εγγραφή + Αντικατάσταση Όλων + Χρησιμοποίηση Βασικού λογαριασμού + Αφαίρεση από τα Αγαπημένα + Κρυμμένο Πλέιερ - Δευτερόλεπτα Σκιπ + Δευτερόλεπτα Σκιπ όταν ο αναπαραγωγέας είναι κρυφός + Εντάξει + Απενεργοποιήση της εξοικονόμησης της μπαταρίας + Έχετε ήδη ψηφίσει + Φαίνεται πως ένα πιθανό αντίγραφο βρίσκεται στη βιβλιοθήκη σας: \'%s.\' +\n +\nΘα επιθυμούσατε να το προσθέσετε, να το αντικαταστήσετε, ή να ακυρώσετε την ενέργεια; + Εισαγωγή Τρέχον Κωδικού + Κλείδωμα Προφίλ + Ξεκλείδωμα Cloudstream + ΚωδικόςPIN Αυθεντικότητας + Προσθέτει επιλογή ταχύτητας στον αναπαραγωγέα + Σύνδεση ως %s + Περιστροφή + Απεγγραφή + Διαχείριση λογαριασμών + Ενεργοποίηση αυτόματης περιστροφής οθόνης αναλόγως του βίντεο + Αυτόματη περιστροφή + Σεζόν %1$dΕπεισόδιο%2$d θα κυκλοφορήσει + Δευτερόλεπτα Σκιπ όταν φαίνεται ο αναπαραγωγέας (πλειερ) + Δοκιμή όλων των παροχών + Αυτό το τεστ προορίζεται μόνο για τους προγραμματιστές και δε επαληθείει ούτε απορρίπτει την λειτουργία οποιουδήποτε παρόχου. + Fcast + Επιλογή συσκευής για αναμετάδοση + Πρόβλημα στην πρόσβαση στο Clipboard, Παρακαλώ προσπαθήστε ξανά. + Πρόβλημα στην αντιγραφή , Παρακαλούμε αντιγράψτε το logcat και επικοινωνήστε με την υποστήριξη. + Η χρήση μπαταρίας έχει ήδη τεθεί χωρίς περιορισμό + Αδύνατο άνοιγμα των στοιχείων της εφαρμογής Cloudstream. + Αγαπημένα + %s προστέθηκε στα αγαπημένα + %s αφαιρέθηκε από τα αγαπημένα + Πιθανό αντίγραφο βρέθηκε + Προσθήκη + Αντικατάσταση + Πιθανά διπλά αρχεία βρέθηκαν στην βιβλιοθήκη: +\n +\n%s +\n +\nΘα επιθυμούσατε να προσθέσετε αυτό το αρχείο ούτως η άλλως, να αντικαταστήσετε τα ήδη υπάρχοντα, ή να ακυρώσετε την ενέργεια? + Εισαγωγή Κωδικού για %s + Κωδικός + Εσφαλμένος Κωδικός. Προσπαθήστε ξανά. + Ο κωδικός να περιέχει 4 χαρακτήρες + Επιλογή λογαριασμού + Αφαίρεση από αγαπημένα + Κλείδωμα με βιομετρικά + Έρχεται σε %s + Εμφάνιση Πλέιερ - Δευτερόλεπτα Σκιπ + Παράκαμψη απαγόρευσης από raw github URLs χρησιμοποιώντας jsDelivr. Μπορεί να καθυστερήσει τις ενημερώσεις για μερικές μέρες. + Εμφάνιση κουμπιού για περιστροφή οθόνης + Αγαπημένο + %s +\nαπομένουν + Βιομετρική αυθεντικοποίηση δεν υποστηρίζεται από τη συσκευή + Καστ ταινίας + Για να σιγουρέψουμε πως οι λήψεις ταινιών και οι ειδοποιήσεις για σειρές στο Cloudstream δεν έχουν πρόβλημα, το Cloudstream χρειάζεται άδεια να τρέχει στο παρασκήνιο. Πατώντας ΟΚ θα μεταφερθείτε στις λεπτομέρειες εφαρμογής, από κει πηγαίνετε στην Χρήση μπαταρίας από εφαρμογές και θέσετε την χρήση σε μη περιορισμένη. Να έχετε στο νου σας πως το Cloudstream δε θα καταναλώσει την μπαταρία σας. Απλά θα λειτουργήσει μόνο όταν χρειάζεται, όπως για την ειδοποίηση για ανερχόμενες σειρές ή της λήψεις σας μέσω των παροχών. Άμα θέλετε να ακυρώσετε, μπορείτε να αλλάξετε αυτή τη ρύθμιση μέσω των γενικών ρυθμίσεων. + Ξεκλείδωμα εφαρμογής με δακτυλικό αποτύπωμα, Face ID, PIN, Μοτίβο και Κωδικό. + Η οθόνη έκλεισε λόγω πολλαπλών ανεπιτυχών ενεργειών. Κάντε επανεκκίνηση της εφαρμογής. + Επεξεργασία λογαριασμού + Παράλειψη επιλογής λογαριασμού στην εκκίνηση της εφαρμογής + Μουσική + Ακουστικό Βιβλίο + Μέσα + Επαναφορά + Τα δεδομένα σας στο CloudStream έχουν κάνει back up. Αν και η πιθανότητα είναι πολύ χαμηλή, όλες οι συσκευές συμπεριφέρονται διαφορετικά. Στη σπάνια περίπτωση, που απαγορευτεί η πρόσβασή σας από την εφαρμογή, διαγράψτε τα δεδομένα εφαρμογής και επαναφέρετέ τα από ένα ήδη υπάρχον backup. Συγνώμη για οποιαδήποτε ταλαιπωρία. + Λογαριασμοί + Ασφάλεια + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 20484cd9..011762ba 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -176,7 +176,7 @@ Tipo de Borde Elevación de Subtítulo Buscar usando proveedores - Continúa la reproducción en un reproductor miniatura encima de otras aplicaciones + Continúa la reproducción en una imagen pequeña encima de otras aplicaciones Botón de cambio de tamaño del reproductor Eliminar bordes negros Seleccionar idioma automáticamente @@ -232,7 +232,7 @@ Mostrar los resultados de la búsqueda por proveedor Solo envíar los datos si la App se cierra / falla inesperadamente No enviar datos - Mostrar los trailers + Mostrar avances Mostrar pósters de Kitsu Actualizar a las versiones preliminares Buscar actualizaciones preliminares (beta) en lugar de solo versiones completas (stable releases) @@ -289,7 +289,7 @@ CloudStream no tiene sitios instalados por defecto. Necesitas instalar los sitios desde los repositorios. \n \nÚnase a nuestro Discord o busque en línea. - ¿Descargar todos los plugins de este repositorio? + Advertencia: ¡CloudStream 3 no asume ninguna responsabilidad por el uso de extensiones de terceros y no brinda ningún soporte para ellas! Mostrar actualizaciones de la aplicación Instalador de APK Algunos dispositivos no soportan el nuevo instalador de paquetes. Pruebe la opción antigua (legacy) si las actualizaciones no se instalan. @@ -339,7 +339,7 @@ Alternar elementos de la interfaz de usuario en el póster No se encontró ninguna actualización General - Color primario + Color principal Tema de la aplicación hola@mundo.com %1$s %2$s @@ -449,13 +449,13 @@ Descarga por lotes plugin plugins - Actualizados %d plugins + %d plugins actualizados Ver repositorios de la comunidad Lista pública Pistas Pistas de audio Pistas de video - Modo seguro ON + Modo seguro activado Ver información de fallos Puntaje:%s Versión @@ -483,7 +483,7 @@ Si La aplicación se actualizará al salir Actualización iniciada - Complemento descargado + Plugin descargado Quitar de visto Ordenar por Ordenar @@ -512,7 +512,7 @@ Registro Empezar Aprobado - Prueba del proveedor + Verificar al proveedor Reiniciar Suscrito Suscrito a %s @@ -545,7 +545,7 @@ La interfaz de usuario no se ha podido crear correctamente, se trata de un GRAN BUG y debe ser reportado inmediatamente %s Seleccionar modo para filtrar los plugins descargados Deshabilitar - No se encontraron complementos en el repositorio + No se encontraron plugins en el repositorio Repositorio no encontrado, comprueba la URL y prueba la VPN Ya has votado Frecuencia de la copia de seguridad @@ -599,7 +599,7 @@ Desbloquea la aplicación con huella dactilar, Face ID, PIN, patrón y contraseña. Desbloquear CloudStream La autenticación biométrica no es compatible con este dispositivo - Esta pantalla se cerró después de algunos intentos fallidos. Reinicie la aplicación. + Después de algunos intentos fallidos, el mensaje se cerrará. Simplemente reinicie la aplicación para volver a intentarlo. Ahora se ha realizado una copia de seguridad de sus datos de CloudStream. Aunque la posibilidad de que esto ocurra es muy baja, todos los dispositivos pueden comportarse de forma diferente. En el raro caso de que no puedas acceder a la aplicación, borra completamente los datos de la aplicación y restaura desde una copia de seguridad. Sentimos mucho las molestias que esto pueda ocasionarte. Favorito No favorito @@ -616,5 +616,22 @@ No se puede abrir la información de la aplicación CloudStream. Media Audiolibro - Para garantizar descargas y notificaciones ininterrumpidas para programas de televisión suscritos, CloudStream necesita permiso para ejecutarse en segundo plano. Al presionar OK, se le dirigirá a información de la aplicación. Allí, desplácese hasta Uso de la batería de la aplicación y establezca el uso de la batería en Sin restricciones. Tenga en cuenta que este permiso no significa que CS3 agotará su batería. Solo funcionará en segundo plano cuando sea necesario, como cuando reciba notificaciones o descargue videos de extensiones oficiales. Si decide cancelar, puede ajustar esta configuración más adelante en los ajustes generales. - + Para garantizar descargas y notificaciones ininterrumpidas para programas de televisión suscritos, CloudStream necesita permiso para ejecutarse en segundo plano. Al presionar OK, se le dirigirá a información de la aplicación. Allí, desplácese hasta 𝙐𝙨𝙤 𝙙𝙚 𝙡𝙖 𝙗𝙖𝙩𝙚𝙧í𝙖 𝙙𝙚 𝙡𝙖 𝙖𝙥𝙡𝙞𝙘𝙖𝙘𝙞ó𝙣 y establezca el uso de la batería en 𝙎𝙞𝙣 𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙘𝙞𝙤𝙣𝙚𝙨. Tenga en cuenta que este permiso no significa que CS3 agotará su batería. Solo funcionará en segundo plano cuando sea necesario, como cuando reciba notificaciones o descargue videos de extensiones oficiales. Si decide cancelar, puede ajustar esta configuración más adelante en 𝙡𝙤𝙨 𝙖𝙟𝙪𝙨𝙩𝙚𝙨 𝙜𝙚𝙣𝙚𝙧𝙖𝙡𝙚𝙨. + Reset + Próximamente en %s + La temporada %1$d y el episodio %2$d se estrenarán en + Seleccionar el dispositivo para transmitir + Fcast + Espejo de transmisión + Wiki de CloudStream + Seguridad + Cuentas + Autenticación local + Imagen del código QR + Descartar + Repositorio abierto + Visita %s en tu smartphone o ordenador e introduce el código anterior + ¡El código PIN ya ha caducado! + El código caduca en %1$d mín y %2$d s + No puedo obtener el código PIN del dispositivo; intente con la autenticación local + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 77c3db15..91d23b61 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -280,7 +280,7 @@ Erreur de sauvegarde %s Recherche Comptes et Sécurité - Mises à jour et sauvegarde + Mises à jour et Sauvegarde Info Recherche avancée Vous donne les résultats de la recherche séparés par fournisseur @@ -419,7 +419,7 @@ Télécharger la liste de sites que vous voulez utiliser Téléchargé : %d Pistes vidéo - Appliqué au redémarrage + Redémarrez l\'application pour voir les changements. Toutes les extensions ont été désactivé à cause d\'un crash pour vous aider à trouver l\'extension causant le problème. Mode sans échec activé Taille @@ -446,7 +446,7 @@ Désactivé : %d Non téléchargé : %d %d plugins mis-à-jour - Télécharger tous les plugins de ce repository \? + Télécharger tous les plugins de ce repository ? %s (Désactivé) Pistes Pistes audio @@ -595,4 +595,29 @@ Ce test est destiné uniquement aux développeurs et ne vérifie ni n\'empêche le fonctionnement d\'aucune extension. Copié ! Nom du dépôt et adresse internet - + Favori + Vos données CloudStream viennent d\'être sauvegardées. Bien que cette éventualité soit très faible, tous les appareils peuvent se comporter différemment. Dans le rare cas où l\'accès à l\'application est bloqué, effacez complètement les données de l\'application et restaurez à partir d\'une sauvegarde. Nous sommes sincèrement désolés pour les désagréments occasionnés par cette situation. + Désactiver l\'optimisation de la batterie + Impossible d\'ouvrir les informations de l\'application CloudStream. + Déverrouiller CloudStream + Musique + %s +\nrestants + Erreur d\'accès au presse-papiers, veuillez réessayer. + OK + L\'authentification biométrique n\'est pas prise en charge sur cet appareil + Livre Audio + Mot de passe/Code PIN + Erreur de copie, Veuillez copier le logcat et contacter le support de l\'application. + Déverrouiller l\'appli avec l\'empreinte digitale, l\'identification faciale, le code PIN, le motif et le mot de passe. + Cet écran a été fermé en raison de plusieurs tentatives infructueuses. Veuillez relancer l\'application. + Pour garantir des téléchargements ininterrompus et des notifications pour les émissions de télévision auxquelles vous êtes abonné, CloudStream a besoin d\'une autorisation pour fonctionner en arrière-plan. En appuyant sur OK, vous serez dirigé vers App info. Faites défiler jusqu\'à 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 et réglez l\'utilisation de la batterie sur 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Veuillez noter que cette autorisation ne signifie pas que CS3 épuisera votre batterie. Il ne fonctionnera en arrière-plan que lorsque cela sera nécessaire, par exemple lors de la réception de notifications ou du téléchargement de vidéos à partir d\'extensions officielles. Si vous choisissez d\'annuler, vous pouvez ajuster ce paramètre ultérieurement dans 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. + L\'utilisation de la batterie de l\'application est déjà réglée sur illimitée + Supprimer des favoris + Média + Réinitialiser + À venir dans %s + Verrouillage biométrique + Sélectionnez un appareil de diffusion + Saison %1$d Episode %2$d sera publié dans + \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 8ce224b3..b16292ba 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -192,4 +192,21 @@ लिंक पुन्ह खुली वर्तमान पिन दर्ज करें नेटवर्क स्ट्रीम - + साफ़ करें + उपशीर्षक सेटिंग्स + अक्षर का माप + बंद करें + रिपॉजिटरी का नाम और यूआरएल + कॉपी! + सहेजें + नये एपिसोड की अधिसूचना + अन्य एक्सटेंशन में खोजें + सुझाव दिखाएं + पृष्ठभूमि का रंग + रूपरेखा प्रकार + अक्षर का रंग + बॉक्स का रंग + रूपरेखा रंग + उपशीर्षक ऊंचाई + अक्षर शैली + \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index ea6a80eb..6e35506d 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -11,41 +11,41 @@ %d %.1f/10.0 %d - %1$s Ep %2$d - Cast: %s - Epizoda %d će izaći - %1$dd %2$dh %3$dm - %1$dh %2$dm - %dm + %1$s epizoda %2$d + Glumačka postava: %s + Epizoda %d će izaći za + %1$dd %2$dh %3$dmin + %1$dh %2$dmin + %dmin Poster Poster - Episode Poster - Main Poster - Next Random - Go back - Change Provider - Preview Background + Poster epizode + Glavni poster + Sljedeće slučajno odabrano + Idi natrag + Promijeni pružatelja usluge + Pregled slike pozadine - Brzina (%.2fx) + Brzina (%.2f×) Ocjena: %.1f - Pronađeno novo ažuriranje! + Pronađeno je novo ažuriranje! \n%1$s -> %2$s Umetak %d min CloudStream - Reproduciraj s CloudStream-om + Reproduciraj s CloudStreamom Početna stranica - Pretraži + Traži Preuzimanja Postavke - Pretraži… - Pretraži %s… + Traži … + Traži %s … Nema podataka - Više postavki + Više opcija Sljedeća epizoda Žanrovi - Podijeli + Dijeli Otvori u pregledniku Preskoči učitavanje Učitavanje … @@ -53,35 +53,35 @@ Na čekanju Dovršeno Ispušteno - Planiram pogledati - Ponovno gledam - Pokreni Film + Planiram gledati + Ponovo gledam + Pokreni film Pokreni LiveStream Pokreni Torrent Izvori Titlovi - Ponovno pokušaj povezivanje… + Ponovni pokušaj povezivanja … Idi natrag - Pokreni Epizodu + Pokreni epizodu Preuzmi Preuzeto - Trenutno preuzimam + Preuzimanje u tijeku Preuzimanje pauzirano Preuzimanje započeto - Preuzimanje nije uspjelo + Preuzimanje neuspjelo Preuzimanje otkazano Preuzimanje dovršeno Mrežni stream - Pogreška pri učitavanju veza - Unutarnja pohrana - Dub - Sub + Pogreška pri učitavanju poveznica + Interna pohrana + Sinkronizacija + Titlovi Izbriši datoteku Otvori datoteku Nastavi preuzimanje Pauziraj preuzimanje - Onemogući automatsko izvješćivanje o bugovima + Onemogući automatsko izvješćivanje o greškama Više informacija Sakrij Pokreni @@ -93,99 +93,99 @@ Primijeni Kopiraj Zatvori - Očisti + Izbriši Spremi Brzina playera Postavke titlova Boja teksta - Boja obruba - Pozadinska boja + Boja konture + Boja pozadine Boja prozora - Tip ruba + Vrsta ruba Visina titlova Font Veličina fonta - Pretraži s uslugama - Pretraži s tipovima - %d banana dano developerima + Traži koristeći pružatelje usluga + Traži koristeći vrste + %d banana dano programerima Nisi dao ni jednu bananu Automatski odabir jezika Preuzmi jezike Jezik titlova - Držite za vraćanje na zadane postavke - Uvezi fontove tako da ih postavite u %s - Nastavite s gledanjem + Pritisni za vraćanje na zadane postavke + Uvezi fontove postavljanjem u %s + Nastavi gledati Ukloni Više informacija @string/home_play - Za ispravan rad ovog pružatelja usluga može biti potreban VPN + Za ispravan rad ovog pružatelja usluga je možda potreban VPN Ovaj pružatelj usluga je torrent, preporučuje se VPN - Stranica ne daje metapodatke, učitavanje videozapisa neće uspjeti ako ne postoji na stranici. + Stranica ne sadrži metapodatke. Učitavanje videa neće uspjeti ako ne postoje na stranici. Opis - Plot nije pronađen + Radnja nije pronađena Opis nije pronađen - Prikaži LogMačku 🐈 - Picture-in-picture - Nastavlja reprodukciju u minijaturnom playeru povrh drugih aplikacija - Gumb za promjenu veličine playera - Uklaja crne rubove + Prikaži Logcat 🐈 + Slika u slici + Nastavlja reprodukciju u minijaturnom playeru ispred drugih aplikacija + Gumb za mijenjenje veličine playera + Ukloni crne rubove Titlovi Postavke titlova playera - Chromecast Titlovi + Chromecast titlovi Postavke Chromecast titlova Brzina reprodukcije - Prijeđi prstom za traženje - Prijeđite prstom ulijevo ili udesno kako biste kontrolirali player - Klizni za promjenu postavki - Kliznite prstom ulijevo ili udesno za promjenu svjetline ili glasnoće - Automatski započni sljedeću epizodu - Započne sljedeću epizodu kad trenutna završi - Dodirni dvaput za traženje + Klizni prstom za pomicanje + Klizni prstom ulijevo ili udesno za postavljanje pozicije videa + Klizni prstom za mijenjanje postavki + Klizni prstom prema gore ili dolje na lijevoj ili desnoj strani za mijenjanje svjetline ili glasnoće + Automatski pokreni sljedeću epizodu + Pokreni sljedeću epizodu kada trenutačna epizoda završi + Dodirni dvaput za pomicanje Dodirni dvaput za pauziranje - Iznos preskakanja u playeru (Sekunde) - Dvaput dodirni desnu ili lijevu stranu ekrana za pomicanje naprijed ili natrag - Dodirnite dvaput u sredinu zaslona za pauziranje - Koristi svijetlinu u sustavu + Količina pomicanja u playeru (sekunde) + Dodirni dvaput desnu ili lijevu stranu za pomicanje prema naprijed ili natrag + Dodirni dvaput u sredinu za pauziranje + Koristi svijetlinu sustava Koristi svjetlinu sustava u playeru aplikacija umjesto tamnog preklopa Ažuriraj napredak gledanja Automatski sinkronizira vaš trenutni napredak u filmu ili epizodi - Vraćanje podataka iz sigurnosne kopije + Obnovi podatke iz sigurnosne kopije Sigurnosno kopiranje podataka - Učitana datoteka sigurnosne kopije - Vraćanje podataka iz datoteke nije uspjelo %s + Datoteka sigurnosne kopije je učitana + Obnavljanje podataka iz datoteke %s nije uspjelo Podaci pohranjeni Nedostaju dozvole za pohranu, pokušaj ponovo. Pogreška pri sigurnosnom kopiranju %s - Pretraži + Traži Računi i sigurnost - Ažuriranja i sigurnosne kopije + Ažuriranja i sigurnosna kopija Informacije - Napredno pretraživanje - Daje rezultate pretraživanja odvojene prema pružatelju usluga + Napredna pretraga + Daje rezultate pretrage odvojene prema pružatelju usluga Šalje samo podatke o padovima aplikacije Ne šalje podatke Prikaži dodatnu epizodu za anime Prikaži trailere Prikaži postere iz Kitsua - Sakrij odabranu kvalitetu videozapisa u rezultatima pretraživanja + Sakrij odabranu kvalitetu videa u rezultatima pretrage Automatsko ažuriranje dodataka Prikaži ažuriranja aplikacije Automatski traži nova ažuriranja nakon pokretanja aplikacije. Ažuriranje na predizdanja - Tražite ažuriranja prije izdanja umjesto samo potpunih izdanja + Tražite ažuriranja predizdanja umjesto samo potpunih izdanja Github - Aplikacija za romane od istih developera - Anime aplikacija od istih developera - Uđi u naš Discord - Daj bananu developerima - Dana banana + Aplikacija za romane od istih programera + Anime aplikacija od istih programera + Pridruži se Discordu + Daj bananu programerima + Dane banane Jezik aplikacije Ovaj pružatelj usluga nema podršku za Chromecast - Nisu pronađene veze - Veza je kopirana u međuspremnik + Nisu pronađene poveznice + Poveznica je kopirana u međuspremnik Pokreni epizodu Vrati na zadanu vrijednost - Nažalost, aplikacija se srušila. Anonimno izvješće o bugu bit će poslano developerima + Nažalost se aplikacija srušila. Anonimno izvješće o grešci će se poslati programerima Sezona Nema sezone Epizoda @@ -197,14 +197,14 @@ Nisu pronađene epizode Izbriši datoteku Izbriši - Poništi + Odustani Pauziraj Nastavi - -30 + −30 +30 Ovo će trajno izbrisati %s \nJeste li sigurni\? - %dm + %dmin \npreostalo U tijeku Završeno @@ -244,11 +244,11 @@ Livestream NSFW Video - Greška u izvoru - Pogreška remote-a - Pogreška renderera + Pogreška u izvoru + Pogreška eksternog računala + Pogreška u prikazu Neočekivana pogreška playera - Pogreška preuzimanja, provjeri dozvole za pohranu + Pogreška tijekom preuzimanja, provjeri dozvole za pohranu Chromecast epizoda Chromecast mirror Pokreni u aplikaciji @@ -257,11 +257,11 @@ Kopiraj poveznicu Automatsko preuzimanje Preuzmi zrcalo - Ponovno učitaj poveznice + Ponovo učitaj poveznice Preuzmi titlove - Oznaka kvalitete - Oznaka sinkronizacije - Oznaka titlova + Oznaka za kvalitetu + Oznaka za sinkronizaciju + Oznaka za titlove Naslov Uključi/isključi elemente korisničkog sučelja na posteru Nije pronađeno ažuriranje @@ -270,38 +270,38 @@ Promijeni veličinu Izvor Preskoči OP - Ne prikazuj više + Nemoj više prikazivati Preskoči ovo ažuriranje Ažuriraj - Preferirana kvaliteta streama + Preferirana kvaliteta gledanja (WiFi) Maksimalni broj znakova u naslovu video playera Rezolucija video playera - Veličina video međuspremnika - Duljina video međuspremnika - Video predmemorija na disku - Očisti predmemoriju videa i slika - Izazvat će nasumična rušenja ako se postavi previsoko. Nemojte mijenjati ako imate malu količinu RAM-a kao što je Android TV ili stari telefon. - Može uzrokovati probleme na sustavima s malo prostora za pohranu kao što su Android TV uređaji ako postavite previsoko. + Veličina međuspremnika videa + Duljina međuspremnika videa + Predmemorija videa na disku + Izbriši predmemoriju videa i slika + Uzrokuje rušenje aplikacije ako se postavi previsoko na uređajima s malom količinom RAM-a kao što je Android TV. + Uzrokuje probleme ako se postavi previsoko na uređajima s malom količinom memorije kao što je Android TV. DNS preko HTTPS-a Korisno za zaobilaženje blokada ISP-a Kloniraj web stranicu Ukloni web stranicu Dodajte klon postojeće web-lokacije s drugim url-om - Put preuzimanja + Putanja preuzimanja NGINX server URL Prikaži sinkronizirani anime ili s titlovima - Prilagodi zaslonu + Prilagodi veličini ekrana Rastegni Zoom - Obavijest + Pravna obavijest Any legal issues regarding the content on this application should be taken up with the actual file hosts and providers themselves as we are not affiliated with them. In case of copyright infringement, please directly contact the responsible parties or the streaming websites. The app is purely for educational and personal use. CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface. It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk. Općenito - Random gumb - Prikaži gumb za slučajni odabir reprodukcija na početnoj stranici i biblioteci + Gumb za slučajni odabir + Prikaži gumb za slučajni odabir na početnoj stranici i biblioteci Jezici proširenja Izgled aplikacije Preferirani mediji - Omogućava NSFW na podržanim proširenjima + Omogući NSFW na podržanim proširenjima Kodiranje titlova Pružatelji usluga Raspored @@ -320,60 +320,60 @@ 127.0.0.1 NovoImeStranice https://primjer.com - Šifra jezika (en) + Šifra jezika (hr) %1$s %2$s račun Odjava Prijava Promijeni račun Dodaj račun - Napravi račun - Dodaj tracking + Stvori račun + Dodaj praćenje Dodano %s Sinkroniziraj Ocijenjeno %d / 10 /\?\? /%d - Ovjereno%s + %s ovjeren Nije moguće prijaviti se na %s Nijedan - Normal + Normalno Sve - Maksimalno - Minimalno - Obrub - Depresivno + Maks. + Min. + Kontura + Udubljeno Sjena - Podignuto + Izdignuto Sinkroniziraj titlove 1000 ms Kašnjenje titlova - Koristi ovo ako su titlovi prikazani %d ms prerano + Koristi ovo ako se titlovi prikazuju %d ms prerano Koristite ovo ako se titlovi prikazuju %d ms prekasno - Nema kašnjenja titlova + Bez kašnjenja titlova - The quick brown fox jumps over the lazy dog + Gojazni đačić s biciklom drži hmelj i finu vatu u džepu nošnje Preporučeno Učitano %s - Učitaj datoteku titlova - Učitaj sa interneta + Učitaj iz datoteke + Učitaj s interneta Preuzeta datoteka - Glavno - Podupiranje - Pozadina + Glavni + Sporedni + Statist Izvor - Random - Dolazi uskoro… - Cam - Cam - Cam + Slučajno + Dolazi uskoro … + Kamera + Kamera + Kamera HQ HD TS @@ -392,59 +392,59 @@ Rezolucija i naslov Naslov Rezolucija - ID je nevažeći + Nevažeći ID Nevažeći podaci - URL je nevažeći - Greška - Ukloni CC iz titlova - Ukloni reklame iz titlova - Filtriraj po željenom jeziku medija - Extras + Nevažeći URL + Pogreška + Ukloni titlove za gluhe osobe iz titlova + Ukloni nepotrebne elemente iz titlova (npr. oglase) + Filtriraj po preferiranom jeziku medija + Dodatni sadržaji Trailer https://primjer.com/primjer.mp4 - Referent (nije obavezno) + Referent (opcionalno) Sljedeće - Gledaj videozapise na ovim jezicima + Gledaj videa na ovim jezicima Prethodno Preskoči postavljanje - Promijeni izgled aplikacije kako bi odgovarao vašem uređaju + Promijeni izgled aplikacije kako bi odgovarao tvom uređaju Izvještavanje o rušenju - Što želite vidjeti + Što želiš vidjeti Gotovo - Ekstenzije - Dodaj repository - Ime repositorya - URL spremišta (repositorija) + Proširenja + Dodaj repozitorij + Ime repozitorija + URL repozitorija Dodatak učitan Dodatak izbrisan Nije moguće učitati %s 18+ - Započeto preuzimanje %1$d %2$s… + Započeto preuzimanje %1$d %2$s … Preuzeto %1$d %2$s - Sve %s je već preuzeto + Sve %s već preuzeto Skupno preuzimanje dodatak dodaci Ovo će također izbrisati sve dodatke repozitorija - Izbriši repository - Preuzmi popis stranica koje želite koristiti + Izbriši repozitorij + Preuzmi popis stranica koje želiš koristiti Preuzeto: %d Onemogućeno: %d Nepreuzeto: %d - CloudStream nema instalirane web stranice prema zadanim postavkama. Morate instalirati stranice iz repozitorija. + CloudStream standardno nema instalirane web stranice. Stranice morate instalirati iz repozitorija. \n \nPridružite se našem Discordu ili tražite online. - Pregledajte repozitorije zajednice + Prikaži repozitorije zajednice Javni popis - Svi titlovi pisani velikim slovima - Preuzeti sve dodatke iz ovog repozitorija\? - %s (Onemogućeno) - Zapis + Koristi velika slova za sve titlove + Preuzeti sve dodatke iz ovog repozitorija? + %s (onemogućeno) + Zapisi Audio zapis Video zapis - Primjenjuje se na ponovnom pokretanju - Sigurnosni način rada omogućen - Sve su ekstenzije isključene zbog rušenja aplikacije kako biste lakše pronašli ono koje uzrokuje probleme. + Za prikaz promjena ponovo pokreni aplikaciju. + Sigurnosni način rada uključen + Sva proširenja su isključena zbog rušenja aplikacije kako bi se pronašlo proširenje koje uzrokuje probleme. Pogledajte podatke o padu Ocjena: %s Opis @@ -454,38 +454,38 @@ Autori Podržano Jezik - HLS Playlista + HLS playlista Automatski instaliraj dodatke Zasluge Automatski instaliraj sve neinstalirane dodatke iz dodanih repozitorija. Preferirani video player Interni player - Prvo instalirajte ekstenziju + Najprije instaliraj proširenje VLC MPV - Web Video Cast + Emitiranje na webu Aplikacija nije pronađena Svi jezici Previše teksta. Nije moguće spremiti u međuspremnik. Označi kao gledano - Prikazuje skočni prozor za preskakanje početka ili završetka medija + Prikaži skočni prozor za uvod/kraj Da - Preuzimanje ažuriranja aplikacije… + Preuzimanje ažuriranja aplikacije … Jeste li sigurni da želite izaći\? Ne - Instaliranje ažuriranja aplikacije… + Instaliranje ažuriranja aplikacije … Nije moguće instalirati novu verziju aplikacije - Ažurirano %d dodataka - Mješoviti početak + Ažurirani dodaci: %d + Mješoviti uvod Uvod - Linkovi - Pokreni Trailer + Poveznice + Pokreni trailer Ponovi postupak postavljanja - Neki telefoni ne podržavaju novi program za instaliranje paketa. Isprobaj naslijeđenu opciju ako se ažuriranja ne instaliraju. + Neki telefoni ne podržavaju novi program za instaliranje paketa. Pokušaj sa starijom opcijom ako se ažuriranja ne instaliraju. Instalator APK-a Ažuriranja aplikacije Sigurnosna kopija - Ekstenzije + Proširenja Radnje Predmemorija Geste @@ -493,21 +493,21 @@ Titlovi Raspored Zadane postavke - Izgled + Izgledi Značajke Web preglednik Preskoči %s - Završetak - Zaključak - Mješoviti završetak - Obriši povijest + Kraj + Sažetak + Mješoviti kraj + Izbriši povijest Povijest Legacy - Otvaranje + Uvod PackageInstaller %1$s %2$d%3$s Aktualiziranje započeto - Program če se aktualizirati tijekom zatvaranja programa + Aplikacija će se aktualizirati tijekom zatvaranja Dodatak preuzet Ukloni iz pogledanog Preglednik @@ -525,19 +525,19 @@ Vaša je biblioteka prazna :( \nPrijavite se na račun biblioteke ili dodajte filmove / serije u svoju lokalnu biblioteku. Ova je lista prazna. Pokušajte se prebaciti na jednu drugu listu. - Pronađena datoteka sigurnog načina rada! -\nNe učitavaju se ekstenzije pri pokretanju dok se datoteka ne ukloni. - Prikazan player- iznos preskakanja - Količina preskakanja koja se koristi kada je player vidljiv - Player skriven - Količina preskakanja - Količina preskakanja koja se koristi kada je player skriven + Pronađena je datoteka sigurnog načina rada! +\nProširenja se ne učitavaju tijekom pokretanja dok se datoteka ne ukloni. + Prikazan player – Količina pomicanja + Količina pomicanja koja se koristi kada je player vidljiv + Player skriven – Količina pomicanja + Količina pomicanja koja se koristi kada je player skriven Android TV - Prošlo - Restart + Uspjelo + Pokreni ponovo Log - Početak - Neuspješno - Stop + Pokreni + Neuspjelo + Prekini Test pružatelja usluga Ažuriranje pretplaćenih emisija Epizoda %d izbačena! @@ -549,7 +549,7 @@ GitHub Proxy Neuspješno dohvaćanje GitHuba. Uključuje se jsdelivr proxy … Zaobilazi blokiranje neobrađenih GitHub URL-ova koristeći jsDelivr. Može uzrokovati kašnjenje ažuriranja nekoliko dana. - Preferirana kvaliteta gledanja (podatkovna mobilna mreža) + Preferirana kvaliteta gledanja (mobilni podaci) Profil %d Wi-Fi Mobilni podaci @@ -560,20 +560,20 @@ Pomoć Kvalitete Pozadina profila - Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA GREŠKA i treba se odmah prijaviti %s + Nije bilo moguće ispravno izraditi korisničko sučelje. Ovo je ZNAČAJNA POGREŠKA i treba se odmah prijaviti %s Odaberi modus za filtriranje preuzimanja dodataka Onemogući U repozitoriju nisu pronađeni dodaci - Repozitorij nije pronađen, provjerite URL i pokušajte koristiti VPN - Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je video prioritet. + Repozitorij nije pronađen. Provjeri URL i pokušaj VPN + Ovdje možete promijeniti način na koji su izvori poredani. Ako video ima viši prioritet, pojavit će se više u odabiru izvora. Zbroj prioriteta izvora i prioriteta kvalitete je prioritet videa. \n \nIzvor A: 3 \nKvaliteta B: 7 -\nImat će kombinirani prioritet videozapisa od 10. +\nImat će kombinirani prioritet videa od 10. \n \nNAPOMENA: Ako je zbroj 10 ili više, video player će automatski preskočiti učitavanje kada se ta poveznica učita! Već si glasao/la - Učestalost rezervne kopije + Učestalost spremanja sigurnosne kopije %s uklonjeno iz favorita Favoriti %s dodano u favorite @@ -606,7 +606,7 @@ Preskoči odabir računa pri pokretanju Upravljanje računima Uredi račun - Linkovi ponovno učitani + Poveznice su ponovo učitane Rotiraj Prikaži gumb za prebacivanje orijentacije zaslona Omogućuje automatsko mijenjanje orijentacije zaslona na temelju orijentacije videa @@ -614,8 +614,8 @@ rotiraj_video_tipka automatski_rotiraj_video_tipka Obavijest za novu epizodu - Pretraži u ostalim proširenjima - Dodaje opciju brzine u playeru + Traži u drugim proširenjima + Dodaje opciju za brzinu u playeru Testiraj sva proširenja Ovaj je test namijenjen samo programerima i ne provjerava niti negira rad bilo kojeg proširenja. Prikaži preporuke @@ -624,9 +624,31 @@ Zaključaj s biometrijskim podatcima %s \npreostalo - Greška u pristupanju međuspremnika. Pokušaj ponovo. + Pogreška pri pristupanju međuspremnika. Pokušaj ponovo. Otključaj CloudStream Lozinka/PIN autentifikacija Ovaj uređaj ne podržava biometrijsku autentifikaciju Ovaj je ekran zatvoren zbog višestrukih neuspjelih pokušaja. Pokrenite aplikaciju ponovo. - + U redu + Deaktiviraj optimizaciju baterije + Audio knjiga + Medij + Korištenje baterije aplikacije već je postavljeno na neograničeno + Neuspjelo otvaranje podataka CloudStream aplikacije. + Favorit + Ukloni iz favorita + Glazba + Obnovi + Otključaj aplikaciju pomoću otiska prsta, ID-a lica, PIN-a, uzorka i lozinke. + Sljedeća u %s + Pogreška pri kopiranju. Kopirajte zapisnik i kontaktirajte podršku aplikacije. + Kako bi se osigurala neometana preuzimanja i obavijesti za pretplaćene TV emisije, CloudStream treba dopuštenje za rad u pozadini. Pritiskom gumba „U redu” bit ćete preusmjereni na informacije o aplikaciji. Tamo odaberite „Korištenje baterije aplikacije” i postavite potrošnju baterije na „Neograničeno”. Imajte na umu da ovo dopuštenje ne znači da će CS3 isprazniti vašu bateriju. Radit će u pozadini samo kada je potrebno, kao što je primanje obavijesti ili preuzimanje videa sa službenih proširenja. Ako odlučite otkazati, ovu postavku možete prilagoditi kasnije u „Opće postavke”. + Vaši CloudStream podaci su sada spremljeni u sigurnosnu kopiju. Iako je vjerojatnost mala, neki se uređaji mogu ponašati drugačije. Ako izgubite pristup aplikaciji, potpuno izbrišite podatke aplikacije i obnovite ih pomoću sigurnosne kopije. Ispričavamo se zbog mogućih neugodnosti. + Sezona %1$d epizoda %2$d izlazi + Cast mirror + Fcast + Odaberite uređaj za emitiranje + CloudStream Wiki + Računi + Sigurnost + \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 5533cdc0..717495a9 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -365,7 +365,7 @@ https://példa.hu/példa.mp4 Nem sikerült betölteni: %s Elkezdődött a(z) %1$d %2$s letöltése… - Töltse le az összes bővítményt ebből a tárolóból\? + Töltse le az összes bővítményt ebből a tárolóból? Biztonságos mód bekapcsolva Méret MPV @@ -592,4 +592,4 @@ A PIN 4 karakter hosszú kell legyen Auto elforgatás Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása - + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index d537a1d5..b570068c 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -428,7 +428,7 @@ Ganti subtitle jadi huruf besar semua Terunduh: %d Tidak terunduh: %d - Unduh semua plugin dari repositori ini\? + Unduh semua plugin dari repositori ini? Semua Umur %s (Tidak aktif) Trek @@ -638,4 +638,13 @@ Buku Audio Media Untuk memastikan unduhan dan pemberitahuan tanpa gangguan untuk acara TV berlangganan, CloudStream memerlukan izin untuk berjalan di latar belakang. Dengan menekan OK, Anda akan diarahkan ke Info aplikasi. Di sana, gulir ke Penggunaan baterai aplikasi dan atur penggunaan baterai ke Tidak Terbatas. Harap dicatat, izin ini tidak berarti CS3 akan menguras baterai Anda. Ini hanya akan beroperasi di latar belakang ketika diperlukan, seperti ketika menerima pemberitahuan atau mengunduh video dari ekstensi resmi. Jika Anda memilih untuk membatalkannya, Anda dapat menyesuaikan pengaturan ini nanti di Pengaturan Umum. - + Mengatur ulang + Musim %1$d Episode %2$d akan dirilis pada + Akan datang di %s + Cermin Cast + Pilih perangkat cast + Fcast + CloudStream Wiki + Keamanan + Akun + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 040b0f31..1341b146 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -239,7 +239,7 @@ Errore del renderer Errore inaspettato nel player video Errore download, controlla i permessi di archiviazione - Chromecast + Episodio Chromecast Mirror Chromecast Riproduci in app Riproduci in %s @@ -427,7 +427,7 @@ Vedi le repository della community Lista pubblica Tutti i sottotitoli in maiuscolo - Scaricare tutti i plugin da questa repository\? + Attenzione: CloudStream 3 non si assume alcuna responsabilità per l\'utilizzo di estensioni di terze parti e non fornisce alcun supporto per esse! %s (Disabilitato) Tracce Traccia audio @@ -619,7 +619,7 @@ Autenticazione con password/PIN L\'autenticazione biometrica non è supportata su questo dispositivo Sblocca app con impronta digitale, Face ID, PIN, sequenza e password. - Questa schermata è stata chiusa a causa di più tentativi falliti. Riavvia l\'app. + Dopo alcuni tentativi falliti, il prompt si chiuderà. Riavvia semplicemente l\'app per riprovare. È stato eseguito il backup dei tuoi dati CloudStream. Sebbene questa possibilità sia molto bassa, tutti i dispositivi possono comportarsi in modo diverso. Nel raro caso in cui ti venga bloccato l\'accesso all\'app, cancella completamente i dati dell\'app e ripristina da un backup. Siamo molto spiacenti per qualsiasi inconveniente derivanti da questo. Non preferito %s @@ -637,4 +637,21 @@ L\'utilizzo della batteria dell\'app è già impostato su \"Senza restrizioni\" Musica Audiolibro - + Reimposta + Prossimamente tra %s + L\'episodio %2$d della stagione %1$d uscirà tra + Mirror cast + Seleziona dispositivo per cast + Fcast + Wiki di CloudStream + Conti + Sicurezza + Autenticazione locale + Immagine codice QR + Respingi + Apri repository + Visita %s sul tuo smartphone o computer e inserisci il codice sopra + Impossibile ottenere il codice PIN del dispositivo, prova l\'autenticazione locale + Il codice PIN è scaduto! + Il codice scadrà tra %1$dm %2$ds + \ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index da2952a0..2af7c967 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -442,7 +442,7 @@ לא ניתן להתקין את הגרסה החדשה של האפליקציה הורדת אצווה תוסף - הורד את כל התוספים ממאגר זה\? + הורד את כל התוספים ממאגר זה? רצועות שמע מסלולים Web Video Cast @@ -550,4 +550,4 @@ \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! - + \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index acb2cfc3..5c80d77e 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -242,4 +242,4 @@ 現在のエピソードが終了したら次のエピソードを開始する 長押しするとデフォルトにリセットされます ダウンロードを再開 - + \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 1a63050a..a8756d83 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -133,7 +133,7 @@ 백업 중 오류 %s 검색 라이브러리 - 계정 + 계정 및 보안 소스별로 구분된 검색 결과를 제공합니다 예고편 보기 Kitsu에서 포스터 보기 @@ -311,7 +311,7 @@ 커뮤니티 저장소 보기 공개 목록 모든 자막 대문자화 - 이 저장소에서 모든 플러그인을 다운로드하시겠습니까\? + 이 저장소에서 모든 플러그인을 다운로드하시겠습니까? %s (사용불가) 저장소 추가 저장소 이름 @@ -338,7 +338,7 @@ 로드된 백업 파일 정보 고급 검색 - 데이터를 보내지 않음 + 데이터를 보내지 않습니다 설정 프로세스 다시 실행 APK 인스톨러 Github @@ -527,4 +527,111 @@ 구독중 구독 %s 구독 취소 %s - + 보안 + 장부 + 리포지토리에서 플러그인을 찾을 수 없습니다 + 복사됨! + 레포지토리 이름 및 URL + 본 테스트는 개발자만을 대상으로 하며, 확장자의 작업을 확인하거나 거부하지 않습니다. + 클라우스스트림 위키 + 다시 기록된 링크 + 백업 빈도 + 즐겨찾기 + QR 이미지 + 모든 확장프로그램 테스트 + 로컬 인증 + 클립보드에 액세스하는 중 오류가 발생했습니다. 다시 시도하십시오. + 취소 + 저장소 열기 + 현재 PIN 입력 + 비디오 방향에 따라 화면 방향을 자동으로 전환합니다 + 장치 PIN 코드를 가져올 수 없습니다, 로컬 인증을 시도하세요 + PIN 코드가 만료되었습니다! + 코드 만료까지 남은 시간: %1$dm %2$ds + 리포지토리를 찾을 수 없습니다. URL을 확인하고 VPN을 시도하십시오 + 이미 투표했습니다 + UI를 올바르게 만들 수 없습니다. 이것은 주요 버그이며 %s 즉시 보고해야 합니다 + 즐겨찾기에 추가 + 와이파이 + 도움 + 품질 + 편집 + 프로필 + 확인 + 배터리 최적화 사용 안 함 + 앱 배터리 사용량이 이미 무제한으로 설정되었습니다 + CloudStream의 App 정보를 열 수 없습니다. + 즐겨찾기에 %s 추가 + 프로필 %d + 프로필 배경 + 대체 + PIN 입력 + PIN + PIN은 4자여야 합니다 + %s으로 로그인 됨 + 시작 시 계정 선택 건너뛰기 + 즐겨찾기 + 즐겨찾기 해제 + 잠금 해제 + 생체 인식으로 잠금 + 음악 + 오디오책 + 자동 회전 + 모바일 데이터 + 사용 불가능 + fcast + 캐스트 장치 선택 + 복사하는 중 오류가 발생했습니다. 로그캣을 복사하고 문의하십시오. + 구독 취소 + 기본값 설정 + 구독 + 사용 + 당신의 라이브러리에 이미 잠재적으로 중복된 항목이 존재합니다: \'%s\'. +\n +\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? + 전부 대체 + 추가 + 즐겨찾기에서 %s 제거 + 당신의 라이브러리에 잠재적으로 중복된 항목이 발견되었습니다: +\n +\n%s +\n +\n이 항목을 그래도 추가하시겠습니까, 기존 항목을 교체하시겠습니까, 아니면 작업을 취소하시겠습니까? + 계정 선택 + 기본 계정 사용 + 회전 + 화면 방향을 전환할 토글 버튼 표시 + 계정 관리 + 프로필 잠금 + 잘못된 PIN입니다. 다시 시도하세요. + 계정 편집 + 미디어 + 비밀번호/PIN 인증 + 이 장치에서는 생체 인식이 지원되지 않습니다 + 지문, 얼굴 ID, PIN, 패턴 또는 비밀번호로 앱을 잠급니다. + 여러 번 실패하면 프롬프트가 닫힙니다. 다시 시도하려면 앱을 다시 시작하세요. + 재설정 + 플러그인 다운로드를 필터링할 모드 선택 + 데이터가 백업되었습니다. 장치에 따라 동작이 다를 수 있으며 앱 접근이 차단될 경우 앱 데이터를 완전히 지우고 백업에서 복원하세요. 이로 인해 발생하는 불편을 사과드립니다. + 스마트폰이나 컴퓨터에서 %s를 방문하여 위의 코드를 입력하세요 + 구독 TV 프로그램에 대한 중단 없는 다운로드 및 알림을 보장하기 위해 CloudStream은 백그라운드에서 실행할 수 있는 권한이 필요합니다. 확인을 누르면 App info로 이동합니다. 거기서 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚로 스크롤하여 배터리 사용량을 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙로 설정합니다. 이 권한은 CS3가 배터리를 소모한다는 의미가 아닙니다. 알림을 받거나 공식 확장에서 동영상을 다운로드하는 등 필요할 때만 백그라운드에서 작동합니다. 취소를 선택한 경우 나중에 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨에서 이 설정을 조정할 수 있습니다. + 여기서 소스의 순서를 변경할 수 있습니다. 비디오의 우선 순위가 높은 경우에는 소스 선택에 더 높게 나타납니다. 소스 우선 순위와 품질 우선 순위의 합이 비디오 우선 순위입니다. +\n +\n참고 A: 3 +\n품질 B: 7 +\n총 비디오 우선 순위는 10입니다. +\n +\n참고: 합이 10 이상이면 해당 링크가 로드되면 플레이어는 자동으로 로드를 건너뜁니다! + 시즌 %1$d 에피소드 %2$d이(가) 출시됩니다 + 다른 확장자에서 검색 + 새로운 에피소드 알림 + 권장 사항 표시 + 플레이어에 속도 옵션을 추가합니다 + %s로 출시 예정 + %s +\n남음 + 잠재적 중복 발견 + %s의 PIN 입력 + 즐겨찾기에서 제거 + 캐스트미러 + \ No newline at end of file diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 49b333e3..7989654a 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -415,7 +415,7 @@ Skatīt kopienas krātuves Publisks saraksts Visi subtitri ar lielajiem burtiem - Vai lejupielādēt visus spraudņus no šīs krātuves\? + Vai lejupielādēt visus spraudņus no šīs krātuves? %s (atspējots) Tracks Audio dziesmas @@ -527,4 +527,4 @@ Abonēto šovu atjaunināšana Abonēts Abonēts %s - + \ No newline at end of file diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index fe82a90b..05fc0900 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -89,7 +89,7 @@ Отстранете ги црните граници Преводи Поставки на плеерот за преводи - Режим на Eigengravy + Брзина на репродукција Повлечете за да барате Повлечете од страна на страна за да ја контролирате вашата позиција во видеото Повлечете за да ги промените поставките @@ -186,7 +186,7 @@ Зумирај Disclaimer Општи поставки - Јазици на провајдерите + Јазици на екстензиите Распоред на апликацијата Претпочитани медиуми Автоматски @@ -239,7 +239,7 @@ Видео Исчисти Положен - MyCoolSite + Име на сајт Неважечки податоци Поддршка Функции на плеерот @@ -250,11 +250,11 @@ Опис Апликацијата ќе се ажурира по излегувањето Отпишана е од %s - прокси raw.githubusercontent.com + GitHub прокси TC Претплатен на %s Преводи - Да се преземат сите приклучоци од ова складиште\? + Да се преземат сите приклучоци од ова складиште? Недостасуваат дозволи за складирање. Обидете се повторно. Зачувај Вчитај од датотека @@ -299,7 +299,7 @@ MPV Инсталатор на пакети ОВА - Ажурирања и резервни копии + Ажурирање и резервна копија Вашата библиотека е празна :( \nНајавете се на корисничка сметка или додадете серии. Не се пронајдени епизоди @@ -337,7 +337,7 @@ Додатоци Прикажи случајно копче на почетната страница и библиотеката Поддржано - Сметки + Сметки и безбедност Вовед Креирај сметка Отстрани од гледаното @@ -349,7 +349,7 @@ Ажурирани %d приклучоци Мешано отворање Екстензии - Овозможете NSFW на поддржани провајдери + Овозможете NSFW на поддржани екстензии Не успеа да стигне до GitHub. Вклучувам jsDelivr прокси… Филтрирајте по претпочитан медиумски јазик @string/home_play @@ -381,7 +381,7 @@ Прикажи постери од Kitsu Дали сте сигурни дека сакате да излезете\? Предизвикува проблеми ако е превисоко поставено на уреди со мал простор за складирање, како што е Android TV. - Користејќи jsDelivr, блокирањето на GitHub може да се заобиколи. Може да ги одложи ажурирањата за неколку дена. + Заобиколете го блокирањето на необработени URL-адреси на github користејќи jsDelivr. Може да предизвика ажурирањата да се одложат за неколку дена. Да Азбучно (Ш до А) WP @@ -398,7 +398,7 @@ Инсталатор на APK Екстензии UHD - Референт + Референт (опционално) Се отвора 127.0.0.1 Ова исто така ќе се избрише сите приклучоци за складиште @@ -409,7 +409,7 @@ Не успеа да ги врати податоците од датотеката %s Не успеа Документарец - Стрим + Мрежен проток %d мин Играј со CloudStream Пушти трејлер @@ -433,7 +433,7 @@ %dm \nпреостанува Видео кеш на дискот - Поврзување до пренос + https://example.com/example.mp4 Готово Додај складиште 18+ @@ -449,7 +449,7 @@ SDR Веб-прелистувач Апликацијата не е пронајдена - MyCoolUsername + Корисничко име Отвори со %1$s %2$d%3$s Повторете го процесот на поставување @@ -480,9 +480,7 @@ Приклучокот е преземен Не може да се вчита %s Преземете ја листата на сајтови што сакате да ги користите - CloudStream нема стандардно инсталирани локации. Треба да ги инсталирате сајтовите од складиштата. -\n -\nПоради отстранување на DMCA без мозок од страна на Sky UK Limited 🤮 не можеме да ја поврземе локацијата на складиштето во апликацијата. + CloudStream нема стандардно инсталирани екстензии. Треба сами да инсталирате екстензии. \n \nПридружете се на нашиот Discord или барајте онлајн. Песни @@ -507,9 +505,9 @@ Завршува Измешан крај HDR - example.com + https://example.com Синхронизирај преводи - Примени при рестартирање + Рестартирајте ја апликацијата за да ги видите промените. Наслов на видео плеер максимални знаци Увезете фонтови ставајќи ги во %s Врати ги податоците од резервна копија @@ -591,4 +589,39 @@ Зачестеност на зачувување на бекап Овозможете автоматско префрлување на ориентацијата на екранот врз основа на видео ориентација Автоматска ротација - + Име и URL на складиштето + копирано! + Тестирај ги сите екстензии + ОК + Користењето на батеријата на апликацијата е веќе поставено на неограничено + Неомилен + Омилен + Заклучување со биометрика + Музика + Известување за нова епизода + Пребарајте во други екстензии + Прикажи препораки + Додава опција за брзина во плеерот + Cast mirror + Овој тест е наменет само за програмери и не ја потврдува или негира работата на која било екстензија. + Не може да се отворат информациите за апликацијата CloudStream. + Лозинка/ПИН автентикација + Отклучете ја апликацијата со отпечаток од прст, ID на лице, PIN, шема и лозинка. + Сега е направена резервна копија на вашите податоци на CloudStream. Иако можноста за ова е многу мала, сите уреди можат да се однесуваат поинаку. Во ретки случаи, кога ќе се заклучите од пристап до апликацијата, целосно исчистете ги податоците на апликацијата и вратете ги од резервна копија. Многу ни е жал за какви било непријатности што произлегуваат од ова. + Ресетирај + Сезона %1$d Епизода %2$d ќе биде објавена за + Fcast + Одбери уред да кастираш + Оневозможи оптимизација на батерија + Отклучи CloudStream + Биометриската автентикација не е поддржана на овој уред + Овој екран беше затворен поради повеќе неуспешни обиди. Ве молиме рестартирајте ја апликацијата. + Медиуми + Претстои во %s + %s +\nпреостанати + За да обезбеди непрекинато преземања и известувања за претплатени ТВ-серии, на CloudStream му треба дозвола да работи во заднина. Со притискање на ОК, ќе бидете упатени до информации за апликацијата. Таму, дојдете до 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и поставете ја употребата на батеријата на Неограничено. Ве молиме имајте предвид, оваа дозвола не значи дека CS3 ќе ви ја испразни батеријата. Ќе работи само во заднина кога е потребно, како на пример при примање известувања или преземање видеа од официјални екстензии. Ако изберете да откажете, може да ја прилагодите оваа поставка подоцна во 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. + Грешка при пристапот до таблата со исечоци, обидете се повторно. + Грешка при копирање, копирајте го logcat и контактирајте со поддршката за апликацијата. + Аудио книга + \ No newline at end of file diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 279f5511..0a0f7bd7 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -167,7 +167,7 @@ ഔചിത്യ വീഡിയോ ക്വാളിറ്റി ചരിത്രം കണ്ടതാണെന്ന് അടയാളപ്പെടുത്തുക - %d ദിവസങ്ങൾ %d മണിക്കൂർ %d മിനിറ്റ് + %1$d ദിവസങ്ങൾ %2$d മണിക്കൂർ %3$d മിനിറ്റ് അധ്യായം%dൽ റിലീസ് ചെയ്യും %1$d മണിക്കൂർ %2$d മിനിറ്റ് %1$sഅധ്യാ%2$d @@ -280,4 +280,4 @@ എഡ്ജ് തരം ഔട്ട്ലൈൻ നിറം പശ്ചാത്തല നിറം - + \ No newline at end of file diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 0c90b0c2..8170a7ff 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -55,4 +55,6 @@ Kongsi Tetapan Tutup - + Ep + cuba + \ No newline at end of file diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index ef796f9f..4bf2a273 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -417,7 +417,7 @@ ဖြည့်စွက်များ ရီပိုစစ်ထရီ ဖြည့်စွက်များအားလုံးကိုဖျက်မည်ဖြစ်သည် ရီပိုစစ်ထရီ ကိုဖျက်ရန် - ဤရီပိုစစ်ထရီမှ ဖြည့်စွက်များအားလုံးကို ဒေါင်းလုဒ်လုပ်မှာလား\? + ဤရီပိုစစ်ထရီမှ ဖြည့်စွက်များအားလုံးကို ဒေါင်းလုဒ်လုပ်မှာလား? %s (ပိတ်ပြီး) ထောက်ပံ့ထားသော ဘာသာစကား @@ -550,4 +550,4 @@ သင်နဂိုတည်းကသတ်မှတ်ပြီး လိုက်ဘရီရွေးချယ်ရန် ဖြင့်ဖွင့်မည် - + \ No newline at end of file diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 1e23f8af..99694e91 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -53,7 +53,7 @@ डाउनलोड रद्द गरियो डाउनलोड भयो अपडेट सुरु - स्ट्रिम + नेटवर्क स्ट्रीम लिङ्क लोड गर्दा त्रुटि भयो लिङ्कहरू रिलोड गरियो भित्री स्टोरेज @@ -70,7 +70,7 @@ बुकमार्कहरू फिल्टर गर्नुहोस् बुकमार्कहरू हटाउनुहोस् - हेरेको स्थिति राख्नुहोस् + हेरेको स्थिति निर्धारण गर्नुहोस् कपी बन्द खाली गर्नुहोस् @@ -85,4 +85,47 @@ स्रोतहरू स्वचालित बग रिपोर्टिङ असक्षम गर्नुहोस् लागू गर्नुहोस् - + साइट ले मेटाडाटा दिएको छैन,मेटाडाटा बिना भिडियो लोड नहुन सक्छ। + प्रकरण %1$d प्रसङ्ग %2$d प्रशारण हुनेवाला छ + प्रोभाईडर उपयोग गरी खोज्नुहोस् + भाषा डाउनलाेड गर्नुहोस् + उपशीर्षकको भाषा + यो प्रोभाईडर torrent हो त्यसैले VPN प्रयाेग गर्नुहुन सिफारिश गरिन्छ + वर्णन + केही विषय भेटिएन + Chromecast को उपशीर्षकहरु + केहीपनि वर्णन भेटिएन + Logcat देखाउनुहोस + Log + Picture-in-picture + अरु एप माथी सानो प्लेयरमा पलेब्याक जारी राख्दछ + प्लेयर स्पीड + उपशीर्षकको सेटिङ + विन्डोको रंग + उपशीर्षक ऊंचाई + अक्षरको नाप + फन्ट + प्रकारको उपयोग गरी खोज्नुहोस् + %d केरा डेभलपर लाई दिइयो + एउटै पनी केरा दिइएन + हेर्न सुचारु राख्नुहोस + हटाउनुहोस् + कालो सीमा हटाउनुहोस + उपशीर्षक + नयाँ प्रसङ्ग को सूचना + अन्य एक्सटेन्सन मा खोज्नुहोस् + सुुझाव हरु + अक्षरको रंग + बाहिरी रेखा को रंग + पृष्ठभूमिको रंग + धारको प्रकार + भाषा अटो छनौट + रिसेट गर्न स्क्रिनमा थिचिराख्नुहोस् + फन्ट देखाउन %s मा राख्नुहोस् + अधिक जानकारी + यो प्रोभाईडर सही ढंगले प्रयोग गर्न VPN प्रयोग गर्नुपर्ने हुन सक्छ + प्लेयर resize गर्ने वटन + प्लेयरको उपशीर्षकको सेटिङ + रिपोजिटरी को नाम र यूआरएल + कपी गरियो! + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index fc537837..8844407a 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -487,7 +487,7 @@ Uitbreidingen Intro Publieke lijst - Alle plugins uit deze repository downloaden\? + Alle plugins uit deze repository downloaden? Beoordeling: %s Alle extensies zijn uitgeschakeld door een crash om u te helpen degene te vinden die problemen veroorzaakt. Bekijk de crash info @@ -608,4 +608,4 @@ Link opnieuw geladen Autoroteer Roteer - + \ No newline at end of file diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 724f4a63..b1168c36 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -335,7 +335,7 @@ Last ned listen over sider du vil bruke Dette vil også slette alle pakkebrønnsprogramtillegg Vis gemenskapspakkebrønner - Last ned alle programtilleggene fra denne pakkebrønnen\? + Last ned alle programtilleggene fra denne pakkebrønnen? %s (avskrudd) Spor Fant ikke programmet @@ -538,4 +538,4 @@ Bruk Hjelp Profilbakgrunn - + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index c61f0104..4980c235 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -400,7 +400,7 @@ Zobacz repozytoria społeczności Publiczna lista Wszystkie napisy wielką literą - Pobrać wszystkie rozszerzenia z tego repozytorium\? + Uwaga: CloudStream 3 nie ponosi żadnej odpowiedzialności za korzystanie z rozszerzeń innych dostawców i nie zapewnia dla nich żadnego wsparcia! %s (Wyłączone) Ścieżki Ścieżki audio @@ -597,7 +597,7 @@ Ten test jest przeznaczony wyłącznie dla programistów i nie weryfikuje ani nie zaprzecza działaniu żadnego rozszerzenia. Zablokuj za pomocą biometrii Uwierzytelnianie hasłem/kodem PIN - Ten ekran został zamknięty z powodu wielu nieudanych prób. Uruchom ponownie aplikację. + Po kilku nieudanych próbach monit zostanie zamknięty. Aby spróbować ponownie, po prostu uruchom ponownie aplikację. Odblokuj CloudStream To urządzenie nie obsługuje uwierzytelniania biometrycznego Odblokuj aplikację za pomocą odcisku palca, identyfikatora twarzy, kodu PIN, wzoru i hasła. @@ -618,4 +618,21 @@ Multimedia Użycie akumulatora przez aplikację jest już ustawione na nieograniczone Aby zapewnić nieprzerwane pobieranie i powiadomienia o subskrybowanych programach telewizyjnych, CloudStream potrzebuje pozwolenia na działanie w tle. Naciskając OK, zostaniesz przekierowany do informacji o aplikacji. Tam przewiń do użycia akumulatora przez aplikację i ustaw je na nieograniczone. Pamiętaj, że to pozwolenie nie oznacza, że CS3 będzie zużywać akumulator. Będzie działać w tle tylko wtedy, gdy będzie to konieczne, na przykład podczas odbierania powiadomień lub pobierania filmów z oficjalnych rozszerzeń. Jeśli zdecydujesz się anulować, możesz dostosować to ustawienie później w ustawieniach głównych. - + Resetuj + Nadchodzące w %s + Odcinek %2$d sezonu %1$d wyjdzie za + Fcast + Wybierz urządzenie do transmisji + Mirror transmisji + Wiki CloudStream + Bezpieczeństwo + Konta + Uwierzytelniaj lokalnie + Obraz kodu QR + Nie można uzyskać kodu PIN urządzenia. Spróbuj uwierzytelnienia lokalnego + Kod PIN stracił ważność! + Kod wygasa za %1$dm %2$ds + Odrzuć + Otwórz repozytorium + Odwiedź %s na swoim smartfonie lub komputerze i wprowadź powyższy kod + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 06e2352c..999ebefb 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -379,7 +379,7 @@ Ver repositórios da comunidade Lista pública Todas as legendas em maiúsculas - Transferir todos os plugins deste repositório\? + Transferir todos os plugins deste repositório? %s (Desativado) Instalador APK %d min @@ -615,4 +615,10 @@ Multimédia Desativar a otimização da bateria Para garantir descarregamentos ininterruptos e notificações de programas de TV subscritos, o CloudStream precisa de permissão para ser executado em segundo plano. Ao premir OK, será direcionado para informações da aplicação. Aí, desloque-se para utilização da bateria da aplicação e defina a utilização da bateria para sem restrições. Tenha em atenção que esta permissão não significa que o CS3 irá esgotar a sua bateria. Este só funcionará em segundo plano quando necessário, como ao receber notificações ou baixar vídeos de extensões oficiais. Se optar por cancelar, pode ajustar esta definição mais tarde em definições gerais. - + Reiniciar + Episódio %1$d Episódio %2$d vai ser lançado em + Por vir em + Fcast + Escolha o dispositivo + Transmitir + \ No newline at end of file diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index d7da44b4..30804c4d 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -59,7 +59,7 @@ Descărcare eșuată Descărcare anulată Descărcare finalizată - Stream + Stream de rețea Eroare la încărcarea linkurilor Stocare internă Dub @@ -142,7 +142,7 @@ Permisiunea de arhivare lipșe, vă rugăm să încercați din nou. Eroare de backup %s Căutare - Conturi și credite + Conturi și Securitate Actualizări și copii de rezervă Informații Căutare avansată @@ -255,8 +255,8 @@ Lungimea buffer-ului video Dimensiunea cache-ului video pe disc Ștergeți memoria cache de imagine și video - Provoacă blocaje dacă este setată la un nivel prea ridicat pe dispozitive cu memorie redusă, cum ar fi Android TV. - Cauzează probleme dacă este setat la un nivel prea ridicat pe dispozitive cu spațiu de stocare redus, cum ar fi Android TV. + Cauzează blocări dacă este setat prea mare pe dispozitive cu memorie redusă, cum ar fi Android TV. + Cauzează probleme dacă este setat prea mare pe dispozitive cu spațiu de stocare redus, cum ar fi Android TV. DNS peste HTTPS Folositor pentru evitarea blocajelor ISP Adaugați site-ul @@ -272,8 +272,8 @@ Orice probleme legale privind conținutul acestei aplicații ar trebui să fie rezolvate cu furnizorii și gazdele actuale de fișiere, întrucât noi nu suntem afiliați cu aceștia. În caz de încălcare a drepturilor de autor, vă rugăm să contactați direct părțile responsabile sau site-urile de streaming. Aplicația este destinată exclusiv utilizării educaționale și personale. CloudStream 3 nu găzduiește niciun fel de conținut în aplicație și nu are niciun control asupra conținutului media care este pus sau retras. CloudStream 3 funcționează ca orice alt motor de căutare, cum ar fi Google. CloudStream 3 nu găzduiește, nu încarcă și nu gestionează niciun videoclip, film sau conținut. Pur și simplu navighează, adună și afișează linkuri într-o interfață convenabilă și ușor de utilizat. Pur și simplu, acesta extrage paginile web ale unor terțe părți care sunt accesibile publicului prin intermediul oricărui browser web obișnuit. Este responsabilitatea utilizatorului de a evita orice acțiune care ar putea încălca legile care guvernează locația sa. Utilizați CloudStream 3 pe propria răspundere. General Aleatoriu - Afișați butonul aleatoriu pe pagina de start și în bibliotecă - Limba furnizorului + Afișează butonul pentru aleatoriu pe Pagina Principală și în Bibliotecă + Limbi ale extensiei Aplicație de prezentare Media preferată Codificarea subtitrărilor @@ -309,7 +309,7 @@ /\?\? /%d %s autentificat/ă - Nu s-a putut autentifica la %s + Nu am putut să mă autentific la %s Nu există Normal @@ -332,7 +332,7 @@ https://en.wikipedia.org/w/index.php?title=Pangram&oldid=225849300 https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog --> - Vând muzică de jazz și haine de bun-gust în New-York și Quebec la preț fix. + Vulpea maro iute sare peste câinele leneș Recomandări A fost încărcat %s Încărcați din fișier @@ -343,7 +343,7 @@ Secundar Sursa Aleatoriu - În curând + În curând… Cam Cam Cam @@ -365,7 +365,7 @@ Titlu și rezoluție Titlu Rezoluție - ID invalid + ID-ul invalid Date invalide Eroare @@ -394,14 +394,14 @@ %1$d %2$s NSFW %1$d-%2$d - Player Afișat - Căutați Suma - Player Ascuns/ă - Căutați Suma + Jucătorul afișat - Cantitatea de căutare + Jucător ascuns - Sumă de căutare Livestream-uri NSFW Eșuat - Suma căutată și utilizată atunci când player-ul este vizibil/ă + Suma de căutare utilizată atunci când jucătorul este vizibil Livestream - Cantitatea de căutare utilizată atunci când playerul este ascuns + Cantitatea de căutare folosită când jucătorul este ascuns Calitatea preferată (Date Mobile) Video Instalator APK @@ -426,11 +426,11 @@ Dezabonat de la %s Nu s-a descărcat: %d Vezi depozite din comunitate - PackageInstaller (Instalare a pachetelor) + Instalator de pachete Stare Nu se poate încărca %s Piste audio - Referent + Referer (opțional) Deschidere Extensii Layout @@ -440,7 +440,8 @@ Autori Raportarea accidentelor Adaugă depozit - Se pare că biblioteca ta este goală :( Conectează-te la un cont de bibliotecă sau adaugă emisiuni în biblioteca ta locală. + Biblioteca ta este goală :( +\nConectați-vă într-un cont de bibliotecă sau adăugați emisiuni la biblioteca locală. Eliminați subtitrările închise din subtitrări Descărcați lista de site-uri pe care doriți să le utilizați Evaluare (Ridicat la Scăzut) @@ -453,7 +454,7 @@ Vezi informații despre accident Deschideți cu Eliminați bloat din subtitrări - Actualizat %d plugin-uri + S-au actualizat %d plugin-uri Evaluare (Scăzut la Ridicat) Terminat Versiune @@ -472,11 +473,11 @@ Sortează Selectați Biblioteca Filtrați în funcție de limba media preferată - Episodul %d lansat! + Episodul %d a fost lansat! Android TV VLC Urmăriți videoclipuri în aceste limbi - Reveniți + Revenire Acțiuni Alfabetic (Z la A) URL invalid @@ -492,7 +493,7 @@ \nNu încarcă nicio extensie la pornire până când fișierul nu este eliminat. Scoateți de la urmărit Actualizat (Vechi la Nou) - Aplică la repornire + Reporniți aplicația pentru a vedea schimbările. Descriere Plugin Descărcat Sunteți sigur că vreți să ieșiți\? @@ -508,16 +509,16 @@ Nu s-a putut instala noua versiune a aplicației Piste Repornește - Activează NSFW la furnizori suportate + Activează conținutul pentru adulți pe extensiile suportate Nu s-a putut ajunge la GitHub. Se activează proxy-ul jsDelivr… Proxy GitHub - Depășește blocarea GitHub folosind jsDelivr. Poate cauza întârzieri de câteva zile la actualizări. + Ocolește blocarea URL-urilor brute de pe GitHub folosind jsDelivr. Poate cauza întârzieri în actualizări cu câteva zile. Următorul Toate %s deja descărcate - S-a descărcat: %d + Descărcat: %d Dezactivat: %d Toate subtitrările cu majuscule - Descărcați toate plugin-urile din acest depozit\? + Descărcați toate plugin-urile din acest depozit? Se actualizează emisiunile abonate Abonat Lista publică @@ -532,7 +533,7 @@ Suportat Playlist HLS Piste video - Arată Afișați pop-up-uri de săritură pentru deschidere/încheiere + Afișează opțiunea de omitere a ferestrelor pop-up pentru început/sfârșit Toate limbile Deschidere mixat Credite @@ -593,4 +594,51 @@ Adaugă o opțiune de viteză la player Favoriți/te Frecvența de backup - + Numele și URL-ul depozitului + Copiat! + Eroare la accesarea Clipboard-ului. Te rog să încerci din nou. + Eroare la copiere. Te rog să copiezi logcat-ul și să contactezi suportul aplicației. + PIN incorect. Te rog să încerci din nou. + PIN + Selectați un cont + Administrați conturile + Editare cont + Conectat ca %s + Rotire + Nefavorite + Deblocați CloudStream + Omiteți selecția contului la pornire + Linkuri reîncărcate + Utilizați contul implicit + Această testare este destinată doar dezvoltatorilor și nu verifică sau respinge funcționarea oricărei extensii. + Pentru a asigura descărcările neîntrerupte și notificările pentru serialele TV la care ești abonat, CloudStream are nevoie de permisiunea de a rula în fundal. Apăsând pe OK, vei fi direcționat către informațiile aplicației. Acolo, derulează la \"App battery usage\" și setează utilizarea bateriei la \"Unrestricted\". Te rog să reții, această permisiune nu înseamnă că CS3 îți va consuma bateria. Va opera în fundal doar când este necesar, cum ar fi atunci când primește notificări sau descarcă videoclipuri din extensiile oficiale. Dacă alegi să anulezi, poți ajusta această setare mai târziu în \"General Settings\". + PIN-ul trebuie să fie format din 4 caractere + Afișează un buton de comutare pentru orientarea ecranului + Autentificare parolă/PIN + Autentificarea biometrică nu este acceptată pe acest dispozitiv + Deblocați aplicația cu amprentă digitală, ID facial, PIN, model și parolă. + Acest ecran a fost închis din cauza mai multor încercări eșuate. Vă rugăm să reporniți aplicația. + Datele dvs. CloudStream au fost salvate acum. Deși posibilitatea acestui lucru este foarte mică, toate dispozitivele se pot comporta diferit. În cazul rar, în care nu aveți acces la aplicație, ștergeți complet datele aplicației și restaurați dintr-o copie de rezervă. Ne pare foarte rău pentru orice neplăcere care decurge din aceasta. + Ok + Dezactivează optimizarea bateriei + Utilizarea bateriei pentru aplicație este deja setată ca fiind nelimitată + Imposibil de deschis informațiile aplicației CloudStream. + Favorite + Muzică + Carte audio + Media + Caută în alte extensii + Testează toate extensiile + Rotire automată + Resetați + Activați comutarea automată a orientării ecranului pe baza orientării video + Blocare cu biometrie + %s +\nrămase + Următorul în %s + CloudStream Wiki + Sezonul %1$d Episod %2$d va fi lansat în + Selectați divece-ul pe care doriți să faceți cast + Cast mirror + Fcast + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index cf456f56..79aa66e1 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -422,6 +422,7 @@ %s (отключено) Далее В CloudStream по умолчанию не установлены сайты. Вам необходимо установить сайты из репозиториев. +\n \nПрисоединяйтесь к нашему Discord-серверу или найдите в интернете. Недопустимые данные Разрешение и название @@ -450,7 +451,7 @@ Все %s уже скачаны Начата загрузка %1$d %2$s… Не скачано: %d - Скачать все плагины из этого репозитория\? + Скачать все плагины из этого репозитория? Включен безопасный режим Скачано: %d Обновлено %d плагинов @@ -616,4 +617,9 @@ Этот экран был закрыт из-за нескольких неудачных попыток. Пожалуйста, перезапустите приложение. Ваши данные в CloudStream были скопированы. Хотя вероятность этого очень мала, все устройства могут вести себя по-разному. В редких случаях, когда доступ к приложению заблокирован, полностью удалите данные приложения и восстановите их из резервной копии. Мы приносим свои извинения за любые неудобства, связанные с этим. Чтобы обеспечить бесперебойную загрузку и получение уведомлений о телепередачах, на которые вы подписаны, CloudStream необходимо разрешение на запуск в фоновом режиме. Нажав OK, вы перейдете к информации о приложении. Там перейдите к разделу 𝘼𝙥𝙥 𝙗𝙖𝙩𝙩𝙚𝙧𝙮 𝙪𝙨𝙖𝙜𝙚 и установите значение \"Использование батареи\" 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙. Пожалуйста, обратите внимание, что это разрешение не означает, что CS3 разрядит вашу батарею. Он будет работать в фоновом режиме только при необходимости, например, при получении уведомлений или загрузке видео с официальных расширений. Если вы решите отменить, вы можете изменить эту настройку позже в 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 𝙎𝙚𝙩𝙩𝙞𝙣𝙜𝙨. - + Сброс + Сезон %1$d Эпизод %2$d выйдет + Выйдет %s + Fcast + Выберите девайс для трансляции + \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index ebaaa2ae..5ba29a00 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -112,7 +112,7 @@ Sledujem Popis sa nenašiel Ďalší náhodný - Stream + Sieťový stream Podržané Zobraziť Logcat 🐈 Protokol @@ -355,4 +355,26 @@ Maximálny počet znakov v názve prehrávača Spôsobuje problémy, ak je nastavená príliš vysoko v zariadeniach s malým ukladacím priestorom, ako je napríklad Android TV. Frekvencia zálohovania - + Toto tiež odstráni všetky doplnky repozitára + Sezóna %1$d Epizóda %2$d bude vydaná za + V repozitári neboli nájdené žiadne doplnky + Sťahuje sa aktualizácia aplikácie… + Inštaluje sa aktualizácia aplikácie… + skopírované! + Názov a URL repozitára + Aktualizujú sa odoberané relácie + Upozornenie na novú epizódu + Repozitár nebol nájdený, skontrolujte URL adresu a skúste VPN + Odkazy sa znovu načítali + Zmazať repozitár + URL adresa repozitára + Verejný zoznam + CloudStream nemá nainštalované žiadne stránky v predvolenom nastavení. Musíte nainštalovať stránky z repozitára. +\n +\nPripojte sa k nášmu Discord alebo vyhľadajte online. + Nepodarilo sa nainštalovať novú verziu aplikácie + Stiahnuť všetky doplnky z tohto repozitára? + Pridať repozitár + Názov repozitára + Zobraziť komunitné repozitáre + \ No newline at end of file diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index 7b0d2870..90198dd5 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -440,7 +440,7 @@ \nSababtuna waa in mar dhexdaas ah na dacweeyeen shirkadda Sky UK Limited🤮, markaa si aan mar dambe taasi u dhicin anagu kuma rakibi karno... \n \nDiscord naga soo qabo ama internetka ka baadh. - Soo deji dhamaan sidkanayaasha reboositarkan\? + Soo deji dhamaan sidkanayaasha reboositarkan? Boodhka Boodhka xalqadda Boodhka weyn @@ -485,4 +485,4 @@ Bilowga Bilow isku qasan Qoraalka dhamaadka - + \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 76508c43..695cbd31 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -33,7 +33,7 @@ Försök ansluta igen… Gå tillbaka @string/result_poster_img_des - Spela Avsnitt + Spela avsnitt Ladda ner Intern lagring Dub @@ -71,7 +71,7 @@ Håll inne för att återställa till standard Fortsätt titta Ta bort - Mer information + Mer info En VPN kan behövas för att den här leverantören ska fungera korrekt Denna leverantör är en torrent, en VPN rekommenderas Beskrivning @@ -162,12 +162,12 @@ Visa inte igen Uppdatera Nerladdningar startad - Nerladdning Misslyckades + Nerladdning misslyckades Nerladdad Laddar ner - Nerladdning Pausad - Nerladdning Avbryten - Nerladdning Färdig + Nerladdning pausad + Nerladdning avbryten + Nerladdning färdig Återupta nerladdning Pausa nerladdning Pausa @@ -204,7 +204,7 @@ Logga ut konto Nerladdningsplats - Tittar på nytt + Ser om Automatisk DNS över HTTPS " " @@ -217,7 +217,7 @@ Dubbeltryck i mitten för att pausa Återställ data från backup Konton och säkerhet - Uppdateringar och backup + Uppdateringar och säkerhetskopiering Automatiska pluginuppdateringar %1$dd %2$dh %3$dm Sök %s… @@ -230,7 +230,7 @@ Autospela nästa episod Spela Trailer Starta nästa episod när nuvarande slutar - Episod %d kommer släppas om + Episod %d kommer att släppas om %d min Visa trailers @string/home_play @@ -366,7 +366,7 @@ Titta på videor på dessa språk Föregående Spår - Uppdatering påbörjad + Uppdatering startad Logg Videospelarens hoppsträcka (Sekunder) Ändra status @@ -436,7 +436,7 @@ All %s har redan laddats ner Ladda ner alla tillägg från den här databasen? Felsäkert läge på - Applicera vid omstart + Starta om appen för att se ändringar. Intern spelare Kamera HD Tillägg nedladdad @@ -460,7 +460,7 @@ Avsnitt %d släppt! Den här listan är tom. Försök byta till en annan. Tillägg borttagen - Tillägg laddade + Tillägg laddad Tillägg Säkerhetskopierings antal Uppdatera visnings förlopp @@ -490,7 +490,7 @@ Web Affischbild Vad vill du se - Lägg till databas + Lägg till tillägg Uppdaterade %d tillägg Nedladdat %1$d %2$s Inaktiverad: %d @@ -524,7 +524,7 @@ Förbikoppla ISP Kamera Kamera - Alla tillägg stängdes av på grund av en krasch för att hjälpa dig hitta den som orsakar problem. + Alla tillägg stängdes av på grund av en krasch för att hjälpa dig hitta det tillägget som orsakar problem. Storlek Författarna Stödd @@ -599,9 +599,31 @@ Lås upp appen med Fingerprint, Face ID, PIN, mönster eller lösenord. Lås upp CloudStream Biometrisk autentisering stöds inte på den här enheten - Detta fönster stängs efter några misslyckade försök. Du måste starta om appen. + Skärmen stängdes av på grund av flera misslyckade försök. Starta om applikationen. Favorit Ta bort från favoriter %s \nkvarstår - + kopierad! + Tilläggs namn och URL + För att säkerställa oavbrutna nedladdningar och aviseringar för prenumererade tv-program behöver CloudStream tillstånd att köras i bakgrunden. Genom att trycka på OK kommer du till App info. Där bläddrar du till appens batterianvändning och ställer in batterianvändningen på obegränsad. Observera att denna behörighet inte betyder att CS3 kommer att tömma ditt batteri. Den fungerar bara i bakgrunden när det behövs, till exempel när du tar emot aviseringar eller laddar ner videor från officiella tillägg. Om du väljer att avbryta kan du ändra denna inställning senare i allmänna inställningar. + Din CloudStream-data har säkerhetskopierats nu. Även om möjligheten till detta är mycket liten, kan alla enheter bete sig olika. I det sällsynta fallet att du blir utelåst från att komma åt appen, rensa appdata helt och återställ från en säkerhetskopia. Vi ber om ursäkt för eventuella besvär som detta uppstår. + Ljudbok + Det gick inte att komma åt urklipp. Försök igen. + OK + Inaktivera batterioptimering + Appens batterianvändning är redan inställd på obegränsad + Det gick inte att öppna CloudStreams appinformation. + Musik + Återställ + Kommer ut om %s + Fel vid kopiering, kopiera logcat och kontakta appsupport. + Media + Fcast + Cast mirror + Säsong %1$d Avsnitt %2$d kommer att släppas om + Välj cast-enhet + CloudStream Wiki + Konton + Säkerhet + \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index e981d05a..4b000304 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -10,17 +10,17 @@ மேலும் விருப்பங்கள் அடுத்த அத்தியாயம் வகைகள் - பகிர் + பங்கு உலாவியில் திற - ஏற்றுவதைத் தவிர் + ஏற்றுவதைத் தவிர்க்கவும் பார்த்து கொண்டிருப்பது - நிறுத்தி வைக்கப்பட்டுள்ளது - நிறைவடைந்தது + ஆன்-ஓல்ட் + முடிந்தது பார்க்கத் திட்டமிடப்பட்டுள்ளது மீண்டும் பார்க்கத் தொடங்கியது - ஸ்ட்ரீம் டோரண்ட் + ச்ட்ரீம் டொரண்ட் வசன வரிகள் - பின் செல் + திரும்பிச் செல்லுங்கள் அத்தியாயத்தை இயக்கு எபிசோட் பதிவிற்கான அனுமதி கொடுக்கவும் பதிவிறக்கப்பட்டது @@ -28,82 +28,82 @@ பதிவிறக்கம் இடைநிறுத்தப்பட்டது பதிவிறக்கம் தொடங்கியது பதிவிறக்கம் தோல்வியடைந்தது - ஸ்ட்ரீம் + பிணையம் ச்ட்ரீம் உள் சேமிப்பு மொழிபெயர்க்கப்பட்டது - கோப்பை நீக்கு - கோப்பை இயக்கவும் - பதிவிறக்கத்தை நிறுத்து + கோப்பை அழி + கோப்பு + இடைநிறுத்தம் பதிவிறக்கம் தானியங்கி பிழை அறிக்கையை முடக்கு - மேலும் தகவல்கள் + மேலும் செய்தி மறை - நீக்கு - நீக்கு - சேமிக்கவும் - உரை வண்ணம் + அகற்று + தெளிவான + சேமி + உரை நிறம் வெளிப்புற நிறம் பின்னணி நிறம் - வசன உயர்வு + வசன உயரம் எழுத்துரு வழங்குபவர்கள் பயன்படுத்தி தேடுங்கள் - வகைகளைப் பயன்படுத்தி தேடவும் + வகைகளைப் பயன்படுத்தி தேடுங்கள் மொழிகளைப் பதிவிறக்கவும் வசன மொழி - தொடர்ந்து பார்க்கவும் + தொடர்ந்து பார்த்துக் கொள்ளுங்கள் முறையாக இயங்க vpn பயன்படுத்தவும் - பிளேயர் அளவை மாற்றும் பொத்தான் - Chromecast வசனங்கள் - அமைப்புகளை மாற்ற ஸ்வைப் செய்யவும் - அடுத்த எபிசோடை தானாக இயக்கவும் - தற்போதைய அத்தியாயம் முடிந்ததும் அடுத்த அத்தியாயத்தைத் தொடங்கவும் - தேடுவதற்கு இருமுறை தட்டவும் - பிளேயரில் தேடுதல் வேகம் - இடைநிறுத்துவதற்கு நடுவில் தட்டவும் + பிளேயர் மறுஅளவிடுதல் பொத்தானை + Chromecast வசன வரிகள் + அமைப்புகளை மாற்ற ச்வைப் செய்யவும் + தன்னியக்க அடுத்த அத்தியாயம் + தற்போதைய ஒன்று முடிவடையும் போது அடுத்த அத்தியாயத்தைத் தொடங்கவும் + தேட இரட்டை தட்டு + வீரர் தொகை (விநாடிகள்) + இடைநிறுத்த நடுவில் இரண்டு முறை தட்டவும் நடிகர்கள்: %s - பின் செல் + திரும்பிச் செல்லுங்கள் அமைப்புகள் ஏற்றுகிறது… கைவிடப்பட்டது பதிவிறக்கம் முடிந்தது இணைப்பை மீண்டும் முயலவும்… - திரைப்படத்தை இயக்கு - லைவ்ஸ்ட்ரீம் இயக்கு + திரைப்படம் திரைப்படம் + லைவ்ச்ட்ரீம் விளையாடுங்கள் டிரெய்லரை இயக்கு - மூலம் + மூலங்கள் இணைப்புகளை ஏற்றுவதில் பிழை - இயக்கு + விளையாடுங்கள் பதிவிறக்கம் ரத்து செய்யப்பட்டது வசன அமைப்புகள் - பதிவிறக்கத்தை மீண்டும் தொடங்கவும் + பதிவிறக்கத்தை மீண்டும் தொடங்குங்கள் புக்மார்க்குகளை வடிகட்டவும் தகவல் - பிளேயர் வேகம் - புக்மார்க்கு - பயன்படுத்து - நகலெடுக்கவும் + பிளேயர் விரைவு + புக்மார்க்குகள் + இடு + நகலெடு மூடு எழுத்துரு அளவு - நீக்கு - மேலும் தகவல்கள் - தானாக மொழியை தேர்ந்தெடு - முன்னோக்கி அல்லது பின்னோக்கி தேட வலது அல்லது இடது பக்கத்தில் இருமுறை தட்டவும் + அகற்று + மேலும் செய்தி + தானாக தேர்ந்தெடுக்கப்பட்ட மொழி + முன்னோக்கி அல்லது பின்னோக்கி தேட வலது அல்லது இடது பக்கத்தில் இரண்டு முறை தட்டவும் மொபைலில் பிரகாசத்தை பயன்படுத்த - இயல்புநிலைக்கு மீட்டமைக்க அழுத்திப் பிடிக்கவும் + இயல்புநிலைக்கு மீட்டமைக்க பிடிக்கவும் முறையாக இயங்க vpn பரிந்துரைக்கப்பட்டது கருப்பு எல்லைகளை அகற்றவும் - விளக்கம் + விவரம் கதை எதுவும் காணப்படவில்லை - விளக்கம் ஏதும் காணப்படவில்லை - படத்தில்-படம் - பிளேயர் வசனங்கள் அமைப்புகள் - Logcat 🐈 காட்டு - பிற பயன்பாடுகளுக்கு மேல் மினியேச்சர் பிளேயரில் பிளேபேக் தொடர்கிறது + எந்த விளக்கமும் கிடைக்கவில்லை + படம்-படம் + பிளேயர் வசன வரிகள் அமைப்புகள் + LOGCAT ஐக் காட்டு + மற்ற பயன்பாடுகளின் மேல் ஒரு மினியேச்சர் பிளேயரில் பிளேபேக்கைத் தொடர்கிறது வசன வரிகள் - வீடியோ பிளேயரில் நேரத்தைக் கட்டுப்படுத்த இடது அல்லது வலதுபுறம் ஸ்வைப் செய்யவும் - பிரகாசம் அல்லது ஒலியளவை மாற்ற இடது அல்லது வலது பக்கத்தில் ஸ்வைப் செய்யவும் - இடைநிறுத்துவதற்கு இருமுறை தட்டவும் - Chromecast வசன அமைப்புகள் - இருண்ட மேலடுக்குக்குப் பதிலாக ஆப் பிளேயரில் சிஸ்டம் பிரகாசத்தைப் பயன்படுத்தவும் + ஒரு வீடியோவில் உங்கள் நிலையைக் கட்டுப்படுத்த பக்கத்திலிருந்து பக்கமாக ச்வைப் செய்யவும் + ஒளி அல்லது அளவை மாற்ற இடது அல்லது வலது பக்கத்தில் மேலே அல்லது கீழே சறுக்கி விடுங்கள் + இடைநிறுத்த இரட்டை தட்டு + Chromecast வசன வரிகள் அமைப்புகள் + இருண்ட மேலடுக்கு பதிலாக ஆப் பிளேயரில் கணினி பிரகாசத்தைப் பயன்படுத்தவும் அத்தியாயம் %d-இன் வெளியீட்டு நேரம் %1$dம %2$dநி %dநி @@ -118,5 +118,501 @@ எபிசோட்டின் போஸ்டர் போஸ்டர் பிரதான போஸ்டர் - %1$s Ep %2$d - + %1$s ep %2$d + %S ஏற்ற முடியவில்லை + %1$dd %2$dh %3$dm + வசன வரிகள் + முடிவு + முடிந்தது + சுயவிவரம் %d + வைஃபை + மொழி குறியீடு (en) + பதிவு + டப் சிட்டை + வாட்ச் முன்னேற்றத்தைப் புதுப்பிக்கவும் + ஏற்றப்பட்ட காப்புப்பிரதி கோப்பு + விரிவாக்க மொழிகள் + கணக்குகள் மற்றும் பாதுகாப்பு + எச்.எல்.எச் பிளேலிச்ட் + கலப்பு முடிவு + கிளவுட்ச்ட்ரீம் + பெனின்கள் எதுவும் கொடுக்கப்படவில்லை + பிளேபேக் விரைவு + தேட ச்வைப் + தரவை காப்புப் பிரதி எடுக்கவும் + மேம்பட்ட தேடல் + செருகுநிரல்களை தானாக பதிவிறக்கவும் + %1$d-%2$d + +30 + இது %s நிரந்தரமாக நீக்கும +\n நீ சொல்வது உறுதியா? + ஆண்டு + எதிர்பாராத பிளேயர் பிழை + பயன்பாட்டில் விளையாடுங்கள் + Chromecast அத்தியாயம் + ஆண்ட்ராய்டு டிவி போன்ற குறைந்த சேமிப்பு இடங்களைக் கொண்ட சாதனங்களில் மிக அதிகமாக அமைக்கப்பட்டால் சிக்கல்களை ஏற்படுத்துகிறது. + செயல்கள் + தலைப்பை சுவரொட்டியின் கீழ் வைக்கவும் + விரைவில் வருகிறது… + வீடியோ தடங்கள் + சிக்கலை ஏற்படுத்தும் ஒரு விபத்து காரணமாக அனைத்து நீட்டிப்புகளும் அணைக்கப்பட்டன. + சொருகி பதிவிறக்கம் செய்யப்பட்டது + பயன்பாடு கிடைக்கவில்லை + பிளேயர் காட்டப்பட்டுள்ளது - தொகையைத் தேடுங்கள் + பயன்பாட்டு புதுப்பிப்பைப் பதிவிறக்குகிறது… + கிட்அப்பை அடைய முடியவில்லை. Jsdelivr ப்ராக்சியை இயக்குதல்… + நிறுத்து + Chromecast கண்ணாடி + Hello@world.com + தோல்வி + /%d + லைவ்ச்ட்ரீம் + களஞ்சிய பெயர் மற்றும் முகவரி + நகலெடுக்கப்பட்டது! + நகலெடுப்பதில் பிழை, தயவுசெய்து LogCat ஐ நகலெடுத்து பயன்பாட்டு ஆதரவை தொடர்பு கொள்ளவும். + கிளிப்போர்டை அணுகுவதில் பிழை, மீண்டும் முயற்சிக்கவும். + அகரவரிசை (A முதல் சட் வரை) + %S க்கு முள் உள்ளிடவும் + தற்போதைய முள் உள்ளிடவும் + முள் + தவறான முள். தயவு செய்து மீண்டும் முயற்சிக்கவும். + தலைப்பு + சொருகி நீக்கப்பட்டது + தளம் + பாதுகாப்பான பயன்முறை + கொடுக்கப்பட்ட பெனீன் + திரைப்படங்கள் + இல்லை + முரண்பாட்டில் சேரவும் + ஆசிய நாடகங்கள் + மதிப்பிடப்பட்டது: %.1 எஃப் + \@string/home_play + செயல்வரம்பு + தேடல் + டிரெய்லர்களைக் காட்டு + இணைப்புகள் எதுவும் கிடைக்கவில்லை + கிளிப்போர்டில் இணைப்பு நகலெடுக்கப்பட்டது + கள் + அனைத்தும் + வசன நேரந்தவறுகை இல்லை + அதிக உரை. கிளிப்போர்டில் சேமிக்க முடியவில்லை. + பார்த்தபடி குறி + மற்றவைகள் + இணைப்புகள் மீண்டும் ஏற்றப்பட்டன + துணை + சாளரம் நிறம் + எழுத்துருக்களை %s இல் வைப்பதன் மூலம் இறக்குமதி செய்யுங்கள் + மேனிலை தரவு தளத்தால் வழங்கப்படவில்லை, தளத்தில் இல்லாவிட்டால் வீடியோ ஏற்றுதல் தோல்வியடையும். + காப்பு அதிர்வெண் + தரவு சேமிக்கப்பட்டது + சேமிப்பக அனுமதிகள் இல்லை. தயவு செய்து மீண்டும் முயற்சிக்கவும். + செயலிழப்புகள் குறித்த தரவை மட்டுமே அனுப்புகிறது + சுவரொட்டியில் இடைமுகம் கூறுகளை மாற்றவும் + மேம்படுத்தல் சோதிக்க + பூட்டு + அனிமேசன் டப்பிங்/துணை + சைகைகள் + விரைவான பழுப்பு நரி சோம்பேறி நாய் மீது குதிக்கிறது + மூலம் + கேம் + சுவரொட்டி படம் + செயலிழப்பு அறிக்கை + பொது பட்டியல் + பதிப்பு + நூலகத்தைத் தேர்ந்தெடுக்கவும் + பதிவு + கணக்கைத் திருத்தவும் + %1$s %2$d %3$s + ரத்துசெய் + இயல்புநிலை + ஓவா + டொரண்ட் + ஆவணப்படம் + ஆசிய நாடகம் + NSFW + மூலம் + விருப்பமான கண்காணிப்பு தகுதி (மொபைல் தரவு) + பெரிதாக்கு + ISP பைபாச் + நீட்டிப்புகள் + பிளேயர் நற்பொருத்தங்கள் + நற்பொருத்தங்கள் + பொது + ஆதரிக்கப்பட்ட நீட்டிப்புகளில் NSFW ஐ இயக்கவும் + முதன்மை நிறம் + பயன்பாட்டு கருப்பொருள் + சுவரொட்டி தலைப்பு இடம் + %கள் அங்கீகரிக்கப்பட்டவை + வசன நேரந்தவறுகை + வசன வரிகள் %d ms மிக விரைவாக காட்டப்பட்டால் இதைப் பயன்படுத்தவும் + பரிந்துரைக்கப்படுகிறது + ஏற்றப்பட்ட %s + கோப்பிலிருந்து ஏற்றவும் + இணையத்திலிருந்து ஏற்றவும் + ஆட்டக்காரர் + தீர்மானம் மற்றும் தலைப்பு + தலைப்பு + வசன வரிகளிலிருந்து மூடிய தலைப்புகளை அகற்றவும் + வசனங்களிலிருந்து வீக்கத்தை அகற்று + முந்தைய + நீட்டிப்புகள் + களஞ்சிய முகவரி + சொருகி ஏற்றப்பட்டது + %1$d %2$s ஐ பதிவிறக்கத் தொடங்கியது… + பதிவிறக்கம் %1$d %2$s + பதிவிறக்கம்: %d + அனைத்து %கள் ஏற்கனவே பதிவிறக்கம் செய்யப்பட்டுள்ளன + முடக்கப்பட்டது: %d + அனைத்து வசன வரிகள் + ஆடியோ தடங்கள் + மறுதொடக்கம் + விவரம் + ஆசிரியர்கள் + வலை வீடியோ நடிகர்கள் + இணைய உலாவி + %S ஐத் தவிர்க்கவும் + மறுபரிசீலனை செய்யுங்கள் + அறிமுகம் + வரலாற்றை அழிக்கவும் + அகரவரிசை (z முதல் A வரை) + உடன் திறந்திருக்கும் + குணங்கள் + கூட்டு + மாற்றவும் + அனைத்தையும் மாற்று + உங்கள் நூலகத்தில் ஏற்கனவே ஒரு நகல் உருப்படி இருப்பதாகத் தெரிகிறது: \'%கள்.\' +\n இந்த உருப்படியை எப்படியும் சேர்க்க விரும்புகிறீர்களா, இருக்கும் ஒன்றை மாற்ற விரும்புகிறீர்களா அல்லது செயலை ரத்து செய்ய விரும்புகிறீர்களா? + கடிகார நிலையை அமைக்கவும் + விளிம்பு வகை + பருவம் இல்லை + அத்தியாயம் + தற்குறிப்பு + -30 + ஒளிதோற்றம் + வீடியோ பிளேயர் தீர்மானம் + வீடியோ இடையக அளவு + நகலி தளம் + அறிவிலிமையம் பதிலாள் + JSdelivr ஐப் பயன்படுத்தி மூல அறிவிலிமையம் முகவரி களின் பைபாச். புதுப்பிப்புகள் சில நாட்களுக்கு தாமதமாகிவிடும். + களஞ்சியம் கிடைக்கவில்லை, முகவரி ஐ சரிபார்த்து VPN ஐ முயற்சிக்கவும் + தொகுதி பதிவிறக்கம் + சொருகு + இந்த களஞ்சியத்திலிருந்து அனைத்து செருகுநிரல்களையும் பதிவிறக்கவா? + மொழி + திரும்பவும் + %S இலிருந்து குழுவிலகப்பட்டது + இது அனைத்து களஞ்சிய செருகுநிரல்களையும் நீக்கிவிடும் + நீங்கள் பயன்படுத்த விரும்பும் தளங்களின் பட்டியலைப் பதிவிறக்கவும் + விருப்பமான வீடியோ பிளேயர் + திறப்பு/முடிவுக்கு ச்கிப் பாப்அப்களைக் காட்டு + பயன்படுத்தவும் + தொகு + NSFW + பிளேயர் மறைக்கப்பட்டுள்ளது - தொகையைத் தேடுங்கள் + Nginx சேவையக முகவரி + பின்னணி + மதிப்பீடு: %கள் + நிச்சயமாக நீங்கள் வெளியேற வேண்டுமா? + வீடியோ மற்றும் பட தற்காலிக சேமிப்பை அழிக்கவும் + பார்த்ததிலிருந்து அகற்று + APK நிறுவி + ஆண்ட்ராய்டு டிவி + அதிகபட்சம் + கேம் + சாதாரண + தடங்கள் + கிதப் + + வேறு முகவரி உடன் ஏற்கனவே இருக்கும் தளத்தின் குளோனைச் சேர்க்கவும் + Https க்கு மேல் dns + உங்கள் தற்போதைய அத்தியாயம் முன்னேற்றத்தை தானாக ஒத்திசைக்கவும் + சுருக்கம் + இந்த வழங்குநருக்கு Chromecast உதவி இல்லை + அடுத்தது + பிழை + பயன்பாட்டு தளவமைப்பு + இயல்புநிலை + டி.எச் + கணக்கு + வசன குறியீட்டு + பயன்பாட்டு மொழி + டிவி தளவமைப்பு + காப்புப்பிரதியிலிருந்து தரவை மீட்டெடுக்கவும் + பிழை %s + புதுப்பிப்புகள் மற்றும் காப்புப்பிரதி + PackactionInstaller + வெளியேறியதும் பயன்பாடு புதுப்பிக்கப்படும் + வரிசைப்படுத்தவும் + புதுப்பிக்கப்பட்டது (பழையது முதல் புதியது) + சுயவிவர பின்னணி + நீங்கள் ஏற்கனவே வாக்களித்து விட்டீர்கள் + பிடித்தவை + பிடித்தவைகளிலிருந்து அகற்று + சாத்தியமான நகல் காணப்படுகிறது + மாறாத + கிளவுட்ச்ட்ரீமைத் திறக்கவும் + பயோமெட்ரிக்சுடன் பூட்டு + ஆடியோ நூல் + புதிய அத்தியாயம் அறிவிப்பு + பிற நீட்டிப்புகளில் தேடுங்கள் + கிட்சுவிலிருந்து சுவரொட்டிகளைக் காட்டு + அத்தியாயம் விளையாடுங்கள் + இயல்புநிலை மதிப்புக்கு மீட்டமைக்கவும் + பருவம் + நிலை + இலவசம் + தொலைக்காட்சி தொடர் + அனிம் + மீண்டும் காட்ட வேண்டாம் + நீட்சி + அனைத்து நீட்டிப்புகளையும் சோதிக்கவும் + இந்த சோதனை டெவலப்பர்களுக்கு மட்டுமே, எந்தவொரு நீட்டிப்பையும் சரிபார்க்கவோ மறுக்கவோ இல்லை. + முன்மாதிரி தளவமைப்பு + பதிவிறக்கம் செய்யப்பட்ட கோப்பு + பகுத்தல் + எம்.பி.வி. + உங்கள் நூலகம் காலியாக உள்ளது :( +\n நூலகக் கணக்கில் உள்நுழைக அல்லது உங்கள் உள்ளக நூலகத்தில் காட்சிகளைச் சேர்க்கவும். + குழுவிலகவும் + சுயவிவரங்கள் + முள் 4 எழுத்துகளாக இருக்க வேண்டும் + ஒரு கணக்கைத் தேர்ந்தெடுக்கவும் + கணக்குகளை நிர்வகிக்கவும் + செருகுநிரல்கள் + தவறான வலைதள முகவரி + தானியங்கி சொருகி புதுப்பிப்புகள் + பதிவிறக்கத்தை வடிகட்ட பயன்முறையைத் தேர்ந்தெடுக்கவும் + மீட்டமை + களஞ்சியத்தில் செருகுநிரல்கள் எதுவும் காணப்படவில்லை + சீசன் %1$d எபிசோட் %2$d வெளியிடப்படும் + புதுப்பிப்பு தொடங்கியது + பரிந்துரைகளைக் காட்டு + டெவ்சுக்கு வழங்கப்பட்ட %d பெனின்கள் + பிளேயரில் வேக விருப்பத்தை சேர்க்கிறது + கோப்பு %s இலிருந்து தரவை மீட்டெடுப்பதில் தோல்வி + நூலகம் + தகவல் + வழங்குநரால் பிரிக்கப்பட்ட தேடல் முடிவுகளை உங்களுக்கு வழங்குகிறது + தேடல் முடிவுகளில் தேர்ந்தெடுக்கப்பட்ட வீடியோ தரத்தை மறைக்கவும் + பயன்பாட்டு புதுப்பிப்புகளைக் காட்டு + பயன்பாட்டைத் தொடங்கிய பின் புதிய புதுப்பிப்புகளைத் தானாகவே தேடுங்கள். + அமைவு செயல்முறை மீண்டும் + முன்நிபந்தனைகளுக்கு புதுப்பிக்கவும் + அதே தேவ்சின் ஒளி நாவல் பயன்பாடு + தேவ்சுக்கு ஒரு பெனீன் கொடுங்கள் + %1$d %2$s + அத்தியாயங்கள் எதுவும் கிடைக்கவில்லை + அழி + கோப்பை அழி + இடைநிறுத்தம் + தொடங்கு + கடந்து சென்றது + %டி.எம +\n மீதமுள்ள + %கள +\n மீதமுள்ள + நடந்து கொண்டிருக்கிறது + காலம் + வரிசையில் + பயன்படுத்தப்பட்டது + பயன்பாடு + ஆவணப்படங்கள் + லைவ்ச்ட்ரீம்கள் + தொடர் + கார்ட்டூன் + அனிம் + பிழையைப் பதிவிறக்குங்கள், சேமிப்பக அனுமதிகளை சரிபார்க்கவும் + வழங்குநரை மாற்றவும் + முன்னோட்டம் பின்னணி + எந்த தரவை அனுப்பவில்லை + அனிமேசுக்கு நிரப்பு அத்தியாயத்தைக் காட்டு + கூடுதல் களஞ்சியங்களிலிருந்து இன்னும் நிறுவப்படாத அனைத்து செருகுநிரல்களையும் தானாக நிறுவவும். + முழு வெளியீடுகளுக்கு பதிலாக மட்டுமே புதுப்பிப்புகளைத் தேடுங்கள் + அதே தேவ்சின் அனிம் பயன்பாடு + அத்தியாயங்கள் + %S இல் வரவிருக்கும் + மன்னிக்கவும், விண்ணப்பம் செயலிழந்தது. ஒரு அநாமதேய பிழை அறிக்கை டெவலப்பர்களுக்கு அனுப்பப்படும் + முடிந்தது + வசன வரிகள் இல்லை + கார்ட்டூன்கள் + டொரண்ட்ச் + படம் + %S இல் விளையாடுங்கள் + மூல பிழை + தொலை பிழை + ரெண்டரர் பிழை + உலாவியில் விளையாடுங்கள் + இணைப்பை நகலெடுக்கவும் + ஆட்டோ பதிவிறக்கம் + கண்ணாடியைப் பதிவிறக்கவும் + இணைப்புகளை மீண்டும் ஏற்றவும் + OP ஐத் தவிர்க்கவும் + இந்த புதுப்பிப்பைத் தவிர்க்கவும் + வசன வரிகள் பதிவிறக்கவும் + துணை சிட்டை + மறுஅளவிடுங்கள் + விருப்பமான கடிகார தகுதி (வைஃபை) + வீடியோ பிளேயர் தலைப்பு மேக்ச் சார்ச் + தளத்தை அகற்று + பாதை பதிவிறக்க + வீடியோ இடையக நீளம் + வட்டில் வீடியோ கேச் + பிளேயர் தெரியும் போது பயன்படுத்தப்படும் தேடல் தொகை + பிளேயர் மறைக்கப்படும்போது பயன்படுத்தப்படும் தேடல் தொகை + ஆண்ட்ராய்டு டிவி போன்ற குறைந்த நினைவகம் கொண்ட சாதனங்களில் மிக அதிகமாக அமைக்கப்பட்டால் செயலிழப்புகளை ஏற்படுத்துகிறது. + ISP தொகுதிகளைத் தவிர்ப்பதற்கு பயனுள்ளதாக இருக்கும் + திரைக்கு பொருந்தும் + மறுப்பு + இணைப்புகள் + பயன்பாட்டு புதுப்பிப்புகள் + காப்புப்பிரதி + கேச் + மனையமைவு + தெரிகிறது + சீரற்ற பொத்தான் + முகப்புப்பக்கம் மற்றும் நூலகத்தில் சீரற்ற பொத்தானைக் காட்டு + வழங்குநர் சோதனை + மனையமைவு + விருப்பமான மீடியா + வழங்குநர்கள் + தானி + தொலைபேசி தளவமைப்பு + கடவுச்சொல் 123 + பயனர்பெயர் + 127.0.0.1 + %1$s %2$s + புகுபதிகை + கணக்கு சேர்க்க + உங்கள் கணக்கை துவங்குங்கள் + கண்காணிப்பைச் சேர்க்கவும் + சேர்க்கப்பட்டது %கள் + ஒத்திசைவு + மதிப்பிடப்பட்டது + %d / 10 + /?? + %S இல் உள்நுழைய முடியவில்லை + முடக்கு + எதுவுமில்லை + மணித்துளி + அவுட்லைன் + மனச்சோர்வு + நிழல் + எழுப்பப்பட்ட + ஒத்திசைவு துணை + வசன வரிகள் காட்டப்பட்டால் இதைப் பயன்படுத்தவும் %d ms மிகவும் தாமதமானது + முக்கிய + துணை + கேம் + டி.சி. + ப்ளூ-ரே + Wp + டிவிடி + 4 கே + எச்.டி. + யுஎச்.டி + எச்.டி.ஆர் + எச்டி + எச்.டி.ஆர் + தவறான ஐடி + தவறான தரவு + விருப்பமான ஊடக மொழியால் வடிகட்டவும் + கூடுதல் + டிரெய்லர் + https://example.com/example.mp4 + குறிப்பாளர் (விரும்பினால்) + இந்த மொழிகளில் வீடியோக்களைப் பாருங்கள் + சமூக களஞ்சியங்களைக் காண்க + %கள் (முடக்கப்பட்டவை) + மாற்றங்களைக் காண பயன்பாட்டை மறுதொடக்கம் செய்யுங்கள். + செயலிழப்பு தகவலைக் காண்க + களஞ்சியத்தை நீக்கு + கிளவுட்ச்ட்ரீமில் இயல்புநிலையாக எந்த தளங்களும் நிறுவப்படவில்லை. நீங்கள் களஞ்சியங்களிலிருந்து தளங்களை நிறுவ வேண்டும். +\n எங்கள் முரண்பாட்டில் சேரவும் அல்லது ஆன்லைனில் தேடுங்கள். + நிலை + அளவு + ஆதரிக்கப்பட்டது + முதலில் நீட்டிப்பை நிறுவவும் + Fcast + காச்ட் சாதனத்தைத் தேர்ந்தெடுக்கவும் + அனைத்து மொழிகளும் + ஆம் + பேட்டரி தேர்வுமுறை முடக்கு + உங்கள் நூலகத்தில் சாத்தியமான நகல் உருப்படிகள் கண்டறியப்பட்டுள்ளன: +\n %கள் +\n இந்த உருப்படியை எப்படியும் சேர்க்க விரும்புகிறீர்களா, ஏற்கனவே உள்ளவற்றை மாற்ற விரும்புகிறீர்களா அல்லது செயலை ரத்து செய்ய விரும்புகிறீர்களா? + முள் உள்ளிடவும் + பூட்டு சுயவிவரம் + %S ஆக உள்நுழைந்துள்ளது + தொடக்கத்தில் கணக்கு தேர்வைத் தவிர்க்கவும் + இயல்புநிலை கணக்கைப் பயன்படுத்தவும் + கடவுச்சொல்/முள் ஏற்பு + இந்த சாதனத்தில் பயோமெட்ரிக் ஏற்பு ஆதரிக்கப்படவில்லை + கைரேகை, முகம் ஐடி, முள், முறை மற்றும் கடவுச்சொல் மூலம் பயன்பாட்டைத் திறக்கவும். + பல தோல்வியுற்ற முயற்சிகள் காரணமாக இந்த திரை மூடப்பட்டது. விண்ணப்பத்தை மறுதொடக்கம் செய்யுங்கள். + காச்ட் மிரர் + புதுப்பிப்பு எதுவும் கிடைக்கவில்லை + செய்தித் பெயர் + https://example.com + கணக்கை மாற்றவும் + 1000 எம்.எச் + சீரற்ற + Hq + அமைப்பைத் தவிர்க்கவும் + உங்கள் சாதனத்திற்கு ஏற்றவாறு பயன்பாட்டின் தோற்றத்தை மாற்றவும் + உனக்கு என்ன பார்க்க வேண்டும் + களஞ்சியத்தைச் சேர்க்கவும் + களஞ்சிய பெயர் + 18+ + பதிவிறக்கம் செய்யப்படவில்லை: %d + புதுப்பிக்கப்பட்டது %d செருகுநிரல்கள் + உள் வீரர் + வி.எல்.சி. + திறப்பு + கலப்பு திறப்பு + வரவு + வரலாறு + சரி + கிளவுட்ச்ட்ரீமின் பயன்பாட்டுத் தகவலைத் திறக்க முடியவில்லை. + %S க்கு குழுசேர்ந்தது + உதவி + %கள் பிடித்தவைகளில் சேர்க்கப்படுகின்றன + பிடித்தவையில் சேர் + சுழற்றுங்கள் + திரை நோக்குநிலைக்கு மாற்று பொத்தானைக் காண்பி + வீடியோ நோக்குநிலையின் அடிப்படையில் திரை நோக்குநிலையின் தானியங்கி மாறுவதை இயக்கவும் + ஆட்டோ சுழலும் + பிடித்த + இசை + ஓவா + தரமான சிட்டை + புதுப்பிப்பு + விடுபதிகை + விரலிடைத் தோல் + சில தொலைபேசிகள் புதிய தொகுப்பு நிறுவியை ஆதரிக்கவில்லை. புதுப்பிப்புகள் நிறுவப்படாவிட்டால் மரபு விருப்பத்தை முயற்சிக்கவும். + சந்தா தொலைக்காட்சி நிகழ்ச்சிகளுக்கான தடையற்ற பதிவிறக்கங்கள் மற்றும் அறிவிப்புகளை உறுதிப்படுத்த, கிளவுட்ச்ட்ரீம் பின்னணியில் இயங்க இசைவு தேவை. சரி என்பதை அழுத்துவதன் மூலம், நீங்கள் பயன்பாட்டுத் தகவலுக்கு அனுப்பப்படுவீர்கள். அங்கு, 𝘼𝙥𝙥 𝘼𝙥𝙥 பெறுநர் க்கு உருட்டி, பேட்டரி பயன்பாட்டை 𝙐𝙣𝙧𝙚𝙨𝙩𝙧𝙞𝙘𝙩𝙚𝙙 என அமைக்கவும். தயவுசெய்து கவனிக்கவும், இந்த இசைவு CS3 உங்கள் பேட்டரியை வெளியேற்றும் என்று அர்த்தமல்ல. அறிவிப்புகளைப் பெறும்போது அல்லது உத்தியோகபூர்வ நீட்டிப்புகளிலிருந்து வீடியோக்களைப் பதிவிறக்குவது போன்ற பின்னணியில் மட்டுமே இது செயல்படும். நீங்கள் ரத்து செய்ய தேர்வுசெய்தால், இந்த அமைப்பை பின்னர் 𝙂𝙚𝙣𝙚𝙧𝙖𝙡 in இல் சரிசெய்யலாம். + பயன்பாட்டு பேட்டரி பயன்பாடு ஏற்கனவே கட்டுப்பாடற்றதாக அமைக்கப்பட்டுள்ளது + பயன்பாட்டு புதுப்பிப்பை நிறுவுகிறது… + பயன்பாட்டின் புதிய பதிப்பை நிறுவ முடியவில்லை + மரபு + வரிசைப்படுத்து + மதிப்பீடு (உயர் முதல் குறைந்த வரை) + மதிப்பீடு (குறைந்த முதல் உயர் வரை) + புதுப்பிக்கப்பட்டது (பழையது புதியது) + இந்த பட்டியல் காலியாக உள்ளது. இன்னொரு இடத்திற்கு மாற முயற்சிக்கவும். + பாதுகாப்பான பயன்முறை கோப்பு கிடைத்தது! +\n கோப்பு அகற்றப்படும் வரை தொடக்கத்தில் எந்த நீட்டிப்புகளையும் ஏற்றவில்லை. + சந்தா காட்சிகளைப் புதுப்பித்தல் + சந்தா + எபிசோட் %d வெளியானது! + மொபைல் தரவு + இயல்புநிலையை அமைக்கவும் + ஆதாரங்கள் எவ்வாறு உத்தரவிடப்படுகின்றன என்பதை இங்கே மாற்றலாம். ஒரு வீடியோவுக்கு அதிக முன்னுரிமை இருந்தால், அது மூல தேர்வில் அதிகமாகத் தோன்றும். மூல முன்னுரிமையின் தொகை மற்றும் தரமான முன்னுரிமை ஆகியவை வீடியோ முன்னுரிமை. +\n சான்று A: 3 +\n தகுதி பி: 7 +\n 10 இன் ஒருங்கிணைந்த வீடியோ முன்னுரிமை இருக்கும். +\n குறிப்பு: தொகை 10 அல்லது அதற்கு மேற்பட்டதாக இருந்தால், அந்த இணைப்பு ஏற்றப்படும்போது பிளேயர் தானாகவே ஏற்றுவதைத் தவிர்க்கும்! + இடைமுகம் ஐ சரியாக உருவாக்க முடியவில்லை, இது ஒரு பெரிய பிழை மற்றும் உடனடியாக %கள் தெரிவிக்க வேண்டும் + %கள் பிடித்தவைகளிலிருந்து அகற்றப்பட்டன + உங்கள் கிளவுட்ச்ட்ரீம் தரவு இப்போது காப்புப் பிரதி எடுக்கப்பட்டுள்ளது. இதன் சாத்தியம் மிகக் குறைவு என்றாலும், எல்லா சாதனங்களும் வித்தியாசமாக நடந்து கொள்ளலாம். அரிய விசயத்தில், பயன்பாட்டை அணுகுவதிலிருந்து நீங்கள் பூட்டப்படுகிறீர்கள், பயன்பாட்டு தரவை முழுவதுமாக அழித்து, காப்புப்பிரதியிலிருந்து மீட்டெடுக்கவும். இதிலிருந்து எழும் ஏதேனும் சிரமத்திற்கு நாங்கள் மிகவும் வருந்துகிறோம். + ஊடகம் + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c3e5959a..bd74194e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -18,10 +18,10 @@ %1$ds %2$dd %dm - Poster + Afiş Afiş - Bölüm Posteri - Ana Poster + Bölüm Afişi + Ana Afiş Sonraki Rastgele @string/play_episode Geri git @@ -62,7 +62,7 @@ Canlı yayını oynat Torrent oynat Kaynaklar - Alt yazılar + Altyazılar Yeniden bağlan… Geri dön Bölümü oynat @@ -80,7 +80,7 @@ Bağlantılar yüklenirken hata oluştu Dahili depolama Dublajlı - Alt yazılı + Altyazılı Dosyayı sil Dosyayı oynat İndirmeyi sürdür @@ -100,13 +100,13 @@ Temizle Kaydet Oynatıcı hızı - Alt yazı ayarları + Altyazı ayarları Yazı rengi Dış hat rengi Arka plan rengi Pencere rengi Kenar tipi - Alt yazı yüksekliği + Altyazı yüksekliği Yazı tipi Yazı boyutu Sağlayıcıları kullanarak ara @@ -116,7 +116,7 @@ Otomatik seçilecek dil İndirilecek diller Altyazı dili - Sıfırlamak için basılı tut + Varsayılana sıfırlamak için basılı tutun Fontları içe aktarmak için %s konumuna yerleştirin İzlemeye devam et Kaldır @@ -133,10 +133,10 @@ İçerik diğer uygulamaların üzerinde küçük bir pencerede oynatılmaya devam eder Oynatıcı yeniden boyutlandırma butonu Siyah sınır çizgilerini kaldır - Alt yazılar - Oynatıcı alt yazı ayarları - Chromecast alt yazıları - Chromecast alt yazı ayarları + Altyazılar + Oynatıcı altyazı ayarları + Chromecast altyazıları + Chromecast altyazı ayarları Oynatma hızı Atlamak için kaydır Zamanı ayarlamak için yanlardan kaydır @@ -220,7 +220,7 @@ Site Özet Sıraya alındı - Alt yazı yok + Altyazı yok Varsayılan Boş Kullanılan @@ -263,16 +263,16 @@ Otomatik indir Şu kaynaktan indir Bağlantıları yenile - Alt yazıları indir + Altyazıları indir Kalite etiketi Dublaj etiketi - Alt yazı etiketi + Altyazı etiketi Başlık show_hd_key show_dub_key show_sub_key show_title_key - Poster üzerindeki öğeler + Afiş üzerindeki öğeleri değiştir Güncelleme bulunamadı Güncellemeleri denetle Kilitle @@ -298,7 +298,7 @@ Farklı bir URL ile mevcut bir sitenin klonunu ekleyin İndirme konumu NGINX sunucu URL\'si - Dublajlı/Alt yazılı animeleri göster + Dublajlı/Altyazılı Anime Gösterimi Ekrana sığdır Uzat Yakınlaştır @@ -307,12 +307,12 @@ Any legal issues regarding the content on this application should be taken up with the actual file hosts and providers themselves as we are not affiliated with them. In case of copyright infringement, please directly contact the responsible parties or the streaming websites. The app is purely for educational and personal use. CloudStream 3 does not host any content on the app, and has no control over what media is put up or taken down. CloudStream 3 functions like any other search engine, such as Google. CloudStream 3 does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface. It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use CloudStream 3 at your own risk. Genel Rastgele İçerik - Anasayfada ve Kütüphanede rastgele düğmesini göster + Ana sayfada ve Kütüphanede rastgele düğmesini göster Uzantı dilleri Uygulama düzeni Tercih edilen medya Desteklenen Uzantılarda NSFW\'yi etkinleştirin - Alt yazı kodlaması + Altyazı kodlaması Sağlayıcılar Düzen Otomatik @@ -321,8 +321,8 @@ Emülatör düzeni Birincil renk Uygulama teması - Poster başlık konumu - Başlığı posterin altına yerleştir + Afiş başlık konumu + Başlığı afişin altına yerleştir anilist_key mal_key @@ -344,7 +344,7 @@ Trakt --> %1$s %2$s - hesabı + hesap Çıkış yap Giriş yap Hesap değiştir @@ -360,7 +360,7 @@ %s başarıyla doğrulandı %s ile giriş yapılamadı - Hiçbiri + Yok Normal Hepsi Maksimum @@ -370,12 +370,12 @@ Çökmüş Gölge Yükseltilmiş - Alt yazı senkronu + Altyazı senkronu 1000 ms - Alt yazı gecikmesi - Alt yazılar %d ms erken görüntüleniyorsa bunu kullanın - Alt yazılar %d ms geç gözüküyorsa bunu kullanın - Alt yazı gecikmesi yok + Altyazı gecikmesi + Altyazılar %d ms erken görüntüleniyorsa bunu kullanın + Altyazılar %d ms geç gözüküyorsa bunu kullanın + Altyazı gecikmesi yok Movie Series From 02b956940a626daaed1ba2af70954ac152324237 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 4 Jul 2024 18:07:01 +0000 Subject: [PATCH 391/441] Port large parts of the API to crossplatform (#1163) --- app/build.gradle.kts | 2 + .../lagradost/cloudstream3/AcraApplication.kt | 4 +- .../lagradost/cloudstream3/CommonActivity.kt | 2 +- .../lagradost/cloudstream3/MainActivity.kt | 24 +- .../cloudstream3/network/CloudflareKiller.kt | 1 + .../cloudstream3/plugins/PluginManager.kt | 2 +- .../services/BackupWorkManager.kt | 2 +- .../services/SubscriptionWorkManager.kt | 4 +- .../syncproviders/AccountManager.kt | 13 +- .../syncproviders/{SyncAPI.kt => SyncApi.kt} | 10 - .../syncproviders/providers/AniListApi.kt | 2 +- .../syncproviders/providers/MALApi.kt | 2 +- .../providers/OpenSubtitlesApi.kt | 1 + .../syncproviders/providers/SimklApi.kt | 55 +--- .../cloudstream3/ui/ControllerActivity.kt | 2 +- .../cloudstream3/ui/WebviewFragment.kt | 2 +- .../cloudstream3/ui/account/AccountHelper.kt | 2 +- .../ui/download/DownloadAdapter.kt | 3 +- .../ui/download/DownloadButtonSetup.kt | 6 +- .../ui/download/DownloadFragment.kt | 3 +- .../cloudstream3/ui/home/HomeFragment.kt | 14 +- .../ui/home/HomeParentItemAdapter.kt | 2 +- .../ui/home/HomeParentItemAdapterPreview.kt | 2 +- .../cloudstream3/ui/home/HomeViewModel.kt | 10 +- .../ui/library/LibraryFragment.kt | 6 +- .../cloudstream3/ui/library/PageAdapter.kt | 4 +- .../ui/player/AbstractPlayerFragment.kt | 6 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 3 +- .../ui/player/DownloadFileGenerator.kt | 1 - .../ui/player/DownloadedPlayerActivity.kt | 4 + .../ui/player/ExtractorLinkGenerator.kt | 1 - .../ui/player/FullScreenPlayer.kt | 2 +- .../cloudstream3/ui/player/GeneratorPlayer.kt | 1 + .../cloudstream3/ui/player/IGenerator.kt | 1 - .../cloudstream3/ui/player/IPlayer.kt | 1 - .../cloudstream3/ui/player/LinkGenerator.kt | 20 +- .../ui/player/OfflinePlaybackHelper.kt | 2 +- .../ui/player/PlayerGeneratorViewModel.kt | 1 - .../ui/player/PreviewGenerator.kt | 3 - .../ui/player/RepoLinkGenerator.kt | 1 - .../player/source_priority/PriorityAdapter.kt | 4 +- .../player/source_priority/ProfilesAdapter.kt | 4 +- .../ui/quicksearch/QuickSearchFragment.kt | 7 +- .../cloudstream3/ui/result/ActorAdaptor.kt | 2 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 2 +- .../cloudstream3/ui/result/ResultFragment.kt | 2 +- .../ui/result/ResultFragmentPhone.kt | 10 +- .../ui/result/ResultFragmentTv.kt | 9 +- .../ui/result/ResultViewModel2.kt | 108 +++++-- .../cloudstream3/ui/result/UiText.kt | 2 +- .../cloudstream3/ui/search/SearchFragment.kt | 14 +- .../cloudstream3/ui/search/SearchHelper.kt | 2 +- .../ui/search/SearchResultBuilder.kt | 2 +- .../ui/settings/SettingsAccount.kt | 6 +- .../ui/settings/SettingsProviders.kt | 6 +- .../settings/extensions/ExtensionsFragment.kt | 4 +- .../ui/settings/extensions/PluginAdapter.kt | 2 +- .../ui/settings/extensions/PluginsFragment.kt | 2 +- .../ui/settings/testing/TestResultAdapter.kt | 4 +- .../ui/settings/testing/TestView.kt | 2 +- .../ui/setup/SetupFragmentProviderLanguage.kt | 2 +- .../utils/{AppUtils.kt => AppContextUtils.kt} | 187 +++++++++-- .../cloudstream3/utils/DataStoreHelper.kt | 2 +- .../cloudstream3/utils/InAppUpdater.kt | 2 +- .../utils/PackageInstallerService.kt | 2 +- .../utils/VideoDownloadManager.kt | 11 +- library/build.gradle.kts | 9 + .../lagradost/api/ContextHelper.android.kt | 20 ++ .../network/WebViewResolver.android.kt | 43 +-- .../kotlin/com/lagradost/api/ContextHelper.kt | 16 + .../com/lagradost/cloudstream3/MainAPI.kt | 290 ++++-------------- .../cloudstream3/extractors/AStreamHub.kt | 2 +- .../cloudstream3/extractors/Acefile.kt | 0 .../cloudstream3/extractors/AsianLoad.kt | 0 .../cloudstream3/extractors/Blogger.kt | 0 .../cloudstream3/extractors/BullStream.kt | 0 .../cloudstream3/extractors/ByteShare.kt | 0 .../lagradost/cloudstream3/extractors/Cda.kt | 0 .../cloudstream3/extractors/Chillx.kt | 0 .../extractors/ContentXExtractor.kt | 2 +- .../cloudstream3/extractors/Dailymotion.kt | 0 .../cloudstream3/extractors/DoodExtractor.kt | 0 .../cloudstream3/extractors/EPlay.kt | 0 .../cloudstream3/extractors/Embedgram.kt | 0 .../extractors/EmturbovidExtractor.kt | 0 .../cloudstream3/extractors/Evolaod.kt | 0 .../cloudstream3/extractors/Fastream.kt | 0 .../cloudstream3/extractors/Filesim.kt | 0 .../cloudstream3/extractors/GMPlayer.kt | 0 .../cloudstream3/extractors/Gdriveplayer.kt | 0 .../cloudstream3/extractors/GenericM3U8.kt | 0 .../cloudstream3/extractors/Gofile.kt | 0 .../extractors/GoodstreamExtractor.kt | 0 .../cloudstream3/extractors/GuardareStream.kt | 0 .../extractors/HDMomPlayerExtractor.kt | 2 +- .../extractors/HDPlayerSystemExtractor.kt | 2 +- .../extractors/HDStreamAbleExtractor.kt | 0 .../extractors/HotlingerExtractor.kt | 0 .../cloudstream3/extractors/Hxfile.kt | 0 .../cloudstream3/extractors/JWPlayer.kt | 0 .../cloudstream3/extractors/Jawcloud.kt | 0 .../cloudstream3/extractors/Jeniusplay.kt | 0 .../cloudstream3/extractors/Krakenfiles.kt | 0 .../cloudstream3/extractors/Linkbox.kt | 0 .../cloudstream3/extractors/M3u8Manifest.kt | 0 .../extractors/MailRuExtractor.kt | 2 +- .../cloudstream3/extractors/Maxstream.kt | 0 .../cloudstream3/extractors/Mediafire.kt | 0 .../cloudstream3/extractors/Minoplres.kt | 0 .../cloudstream3/extractors/MixDrop.kt | 0 .../cloudstream3/extractors/Moviehab.kt | 0 .../cloudstream3/extractors/Mp4Upload.kt | 0 .../cloudstream3/extractors/MultiQuality.kt | 0 .../cloudstream3/extractors/Mvidoo.kt | 0 .../extractors/OdnoklassnikiExtractor.kt | 2 +- .../cloudstream3/extractors/OkRuExtractor.kt | 0 .../cloudstream3/extractors/Okrulink.kt | 0 .../extractors/PeaceMakerstExtractor.kt | 2 +- .../cloudstream3/extractors/Pelisplus.kt | 0 .../extractors/PixelDrainExtractor.kt | 0 .../cloudstream3/extractors/PlayLtXyz.kt | 2 +- .../cloudstream3/extractors/PlayerVoxzer.kt | 0 .../cloudstream3/extractors/Rabbitstream.kt | 0 .../extractors/RapidVidExtractor.kt | 2 +- .../cloudstream3/extractors/SBPlay.kt | 0 .../cloudstream3/extractors/Sendvid.kt | 0 .../extractors/SibNetExtractor.kt | 2 +- .../cloudstream3/extractors/Solidfiles.kt | 0 .../cloudstream3/extractors/StreamSB.kt | 0 .../cloudstream3/extractors/StreamTape.kt | 0 .../extractors/StreamWishExtractor.kt | 0 .../cloudstream3/extractors/Streamhub.kt | 0 .../cloudstream3/extractors/Streamlare.kt | 0 .../cloudstream3/extractors/StreamoUpload.kt | 0 .../cloudstream3/extractors/Streamplay.kt | 0 .../cloudstream3/extractors/Supervideo.kt | 0 .../cloudstream3/extractors/TRsTXExtractor.kt | 2 +- .../cloudstream3/extractors/Tantifilm.kt | 0 .../extractors/TauVideoExtractor.kt | 2 +- .../cloudstream3/extractors/Tomatomatela.kt | 0 .../extractors/UpstreamExtractor.kt | 0 .../cloudstream3/extractors/Uqload.kt | 0 .../cloudstream3/extractors/Userload.kt | 0 .../cloudstream3/extractors/Userscloud.kt | 0 .../cloudstream3/extractors/Uservideo.kt | 0 .../cloudstream3/extractors/Vicloud.kt | 0 .../extractors/VidMoxyExtractor.kt | 2 +- .../extractors/VidSrcExtractor.kt | 0 .../cloudstream3/extractors/VidSrcTo.kt | 142 ++++----- .../extractors/VideoSeyredExtractor.kt | 2 +- .../cloudstream3/extractors/VideoVard.kt | 0 .../cloudstream3/extractors/Vidguard.kt | 4 +- .../extractors/VidhideExtractor.kt | 0 .../cloudstream3/extractors/Vidmoly.kt | 0 .../lagradost/cloudstream3/extractors/Vido.kt | 0 .../cloudstream3/extractors/Vidplay.kt | 0 .../cloudstream3/extractors/Vidstream.kt | 0 .../lagradost/cloudstream3/extractors/Voe.kt | 6 +- .../lagradost/cloudstream3/extractors/Vtbe.kt | 0 .../cloudstream3/extractors/WatchSB.kt | 0 .../cloudstream3/extractors/WcoStream.kt | 0 .../cloudstream3/extractors/Wibufile.kt | 0 .../cloudstream3/extractors/XStreamCdn.kt | 0 .../cloudstream3/extractors/YourUpload.kt | 0 .../extractors/YoutubeExtractor.kt | 0 .../cloudstream3/extractors/Zorofile.kt | 0 .../cloudstream3/extractors/Zplayer.kt | 0 .../extractors/helper/AesHelper.kt | 0 .../extractors/helper/AsianEmbedHelper.kt | 2 +- .../extractors/helper/GogoHelper.kt | 0 .../extractors/helper/NineAnimeHelper.kt | 0 .../extractors/helper/VstreamhubHelper.kt | 0 .../extractors/helper/WcoHelper.kt | 10 +- .../cloudstream3/network/WebViewResolver.kt | 28 ++ .../cloudstream3/syncproviders/SyncAPI.kt | 10 + .../lagradost/cloudstream3/utils/AppUtils.kt | 24 ++ .../cloudstream3/utils/ExtractorApi.kt | 28 +- .../cloudstream3/utils/M3u8Helper.kt | 0 .../cloudstream3/utils/UnshortenUrl.kt | 6 +- .../com/lagradost/api/ContextHelper.jvm.kt | 10 + .../network/WebViewResolver.jvm.kt | 35 +++ 181 files changed, 730 insertions(+), 608 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/syncproviders/{SyncAPI.kt => SyncApi.kt} (97%) rename app/src/main/java/com/lagradost/cloudstream3/utils/{AppUtils.kt => AppContextUtils.kt} (82%) create mode 100644 library/src/androidMain/kotlin/com/lagradost/api/ContextHelper.android.kt rename app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt => library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt (90%) create mode 100644 library/src/commonMain/kotlin/com/lagradost/api/ContextHelper.kt rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/MainAPI.kt (86%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/AStreamHub.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Acefile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/AsianLoad.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Blogger.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/BullStream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/ByteShare.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Cda.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Chillx.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Dailymotion.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/DoodExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/EPlay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Embedgram.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Evolaod.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Fastream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Filesim.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/GMPlayer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/GenericM3U8.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Gofile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/GuardareStream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Hxfile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/JWPlayer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Jawcloud.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Jeniusplay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Krakenfiles.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Linkbox.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Maxstream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Mediafire.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Minoplres.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/MixDrop.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Moviehab.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Mp4Upload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/MultiQuality.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Mvidoo.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Okrulink.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt (99%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Pelisplus.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt (99%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Rabbitstream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/SBPlay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Sendvid.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Solidfiles.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/StreamSB.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/StreamTape.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Streamhub.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Streamlare.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/StreamoUpload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Streamplay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Supervideo.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Tantifilm.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Tomatomatela.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Uqload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Userload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Userscloud.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Uservideo.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vicloud.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VidSrcTo.kt (88%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt (98%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VideoVard.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vidguard.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vidmoly.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vido.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vidplay.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vidstream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Voe.kt (93%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Vtbe.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/WatchSB.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/WcoStream.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Wibufile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/XStreamCdn.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/YourUpload.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Zorofile.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/Zplayer.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt (76%) create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/ExtractorApi.kt (97%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/M3u8Helper.kt (100%) rename {app/src/main/java => library/src/commonMain/kotlin}/com/lagradost/cloudstream3/utils/UnshortenUrl.kt (96%) create mode 100644 library/src/jvmMain/kotlin/com/lagradost/api/ContextHelper.jvm.kt create mode 100644 library/src/jvmMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.jvm.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9c75a90d..ebefa0ea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -263,6 +263,8 @@ tasks.register("copyJar") { // Merge the app classes and the library classes into classes.jar tasks.register("makeJar") { + // Duplicates cause hard to catch errors, better to fail at compile time. + duplicatesStrategy = DuplicatesStrategy.FAIL dependsOn(tasks.getByName("copyJar")) from( zipTree("build/app-classes/classes.jar"), diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 1680d698..598ff540 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -8,13 +8,14 @@ import android.content.Intent import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import com.lagradost.api.setContext import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.plugins.PluginManager 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.AppUtils.openBrowser +import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys @@ -151,6 +152,7 @@ class AcraApplication : Application() { get() = _context?.get() private set(value) { _context = WeakReference(value) + setContext(WeakReference(value)) } fun getKeyClass(path: String, valueType: Class): T? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 82e985db..ba303fef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -36,7 +36,7 @@ import com.lagradost.cloudstream3.ui.player.PlayerEventType import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.settings.Globals.updateTv -import com.lagradost.cloudstream3.utils.AppUtils.isRtl +import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.UIHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 21567e4d..a47e7685 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -56,9 +56,7 @@ import com.google.common.collect.Comparators.min import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.initAll -import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -121,16 +119,18 @@ import com.lagradost.cloudstream3.ui.settings.SettingsGeneral import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions import com.lagradost.cloudstream3.utils.ApkInstaller -import com.lagradost.cloudstream3.utils.AppUtils.html -import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable -import com.lagradost.cloudstream3.utils.AppUtils.isLtr -import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable -import com.lagradost.cloudstream3.utils.AppUtils.isRtl -import com.lagradost.cloudstream3.utils.AppUtils.loadCache -import com.lagradost.cloudstream3.utils.AppUtils.loadRepository -import com.lagradost.cloudstream3.utils.AppUtils.loadResult -import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable +import com.lagradost.cloudstream3.utils.AppContextUtils.isLtr +import com.lagradost.cloudstream3.utils.AppContextUtils.isNetworkAvailable +import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl +import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache +import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository +import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt index c8c385cf..ce2fb3a2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.network +import android.util.Base64 import android.util.Log import android.webkit.CookieManager import androidx.annotation.AnyThread 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 a5631500..6b2b75f2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -14,7 +14,6 @@ import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.google.gson.Gson import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.removePluginMapping import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -33,6 +32,7 @@ import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt index 6ed7a447..4ef841f5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt @@ -10,7 +10,7 @@ import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager import androidx.work.WorkerParameters import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import java.util.concurrent.TimeUnit 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 e2bcd6e1..00c74dff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt @@ -10,13 +10,13 @@ import androidx.core.app.NotificationCompat import androidx.core.net.toUri import androidx.work.* import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index a14f8438..e86d73aa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -3,15 +3,22 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.syncproviders.providers.* import java.util.concurrent.TimeUnit abstract class AccountManager(private val defIndex: Int) : AuthAPI { companion object { - val malApi = MALApi(0) - val aniListApi = AniListApi(0) + val malApi = MALApi(0).also { api -> + LoadResponse.Companion.malIdPrefix = api.idPrefix + } + val aniListApi = AniListApi(0).also { api -> + LoadResponse.Companion.aniListIdPrefix = api.idPrefix + } + val simklApi = SimklApi(0).also { api -> + LoadResponse.Companion.simklIdPrefix = api.idPrefix + } val openSubtitlesApi = OpenSubtitlesApi(0) - val simklApi = SimklApi(0) val addic7ed = Addic7ed() val subDlApi = SubDlApi(0) val localListApi = LocalList() diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt rename to app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt index 045fdc94..878e0cb3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt @@ -2,20 +2,10 @@ package com.lagradost.cloudstream3.syncproviders import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.ui.SyncWatchType -import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiText import me.xdrop.fuzzywuzzy.FuzzySearch -enum class SyncIdName { - Anilist, - MyAnimeList, - Trakt, - Imdb, - Simkl, - LocalList, -} - interface SyncAPI : OAuth2API { /** * Set this to true if the user updates something on the list like watch status or score diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 0551fe6c..8a82cf94 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -17,7 +17,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.splitQuery +import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index 4249f949..24ef7136 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -20,7 +20,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.parseJson -import com.lagradost.cloudstream3.utils.AppUtils.splitQuery +import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import java.net.URL import java.security.SecureRandom diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index 7d0514d1..6412ff1b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager +import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.AppUtils import okhttp3.Interceptor import okhttp3.Response diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 4385fa5e..27975d19 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -12,7 +12,9 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.LoadResponse.Companion.readIdFromString import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SimklSyncServices import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mapper @@ -29,7 +31,6 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.toJson -import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import okhttp3.Interceptor import okhttp3.Response import java.math.BigInteger @@ -184,32 +185,6 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } } - /** - * Set of sync services simkl is compatible with. - * Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id - */ - enum class SyncServices(val originalName: String) { - Simkl("simkl"), - Imdb("imdb"), - Tmdb("tmdb"), - AniList("anilist"), - Mal("mal"), - } - - /** - * The ID string is a way to keep a collection of services in one single ID using a map - * This adds a database service (like imdb) to the string and returns the new string. - */ - fun addIdToString(idString: String?, database: SyncServices, id: String?): String? { - if (id == null) return idString - return (readIdFromString(idString) + mapOf(database to id)).toJson() - } - - /** Read the id string to get all other ids */ - fun readIdFromString(idString: String?): Map { - return tryParseJson(idString) ?: return emptyMap() - } - fun getPosterUrl(poster: String): String { return "https://wsrv.nl/?url=https://simkl.in/posters/${poster}_m.webp" } @@ -361,13 +336,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("anilist") val anilist: String? = null, ) { companion object { - fun fromMap(map: Map): Ids { + fun fromMap(map: Map): Ids { return Ids( - simkl = map[SyncServices.Simkl]?.toIntOrNull(), - imdb = map[SyncServices.Imdb], - tmdb = map[SyncServices.Tmdb], - mal = map[SyncServices.Mal], - anilist = map[SyncServices.AniList] + simkl = map[SimklSyncServices.Simkl]?.toIntOrNull(), + imdb = map[SimklSyncServices.Imdb], + tmdb = map[SimklSyncServices.Tmdb], + mal = map[SimklSyncServices.Mal], + anilist = map[SimklSyncServices.AniList] ) } } @@ -749,13 +724,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("anilist") val anilist: String?, @JsonProperty("traktslug") val traktslug: String? ) { - fun matchesId(database: SyncServices, id: String): Boolean { + fun matchesId(database: SimklSyncServices, id: String): Boolean { return when (database) { - SyncServices.Simkl -> this.simkl == id.toIntOrNull() - SyncServices.AniList -> this.anilist == id - SyncServices.Mal -> this.mal == id - SyncServices.Tmdb -> this.tmdb == id - SyncServices.Imdb -> this.imdb == id + SimklSyncServices.Simkl -> this.simkl == id.toIntOrNull() + SimklSyncServices.AniList -> this.anilist == id + SimklSyncServices.Mal -> this.mal == id + SimklSyncServices.Tmdb -> this.tmdb == id + SimklSyncServices.Imdb -> this.imdb == id } } } @@ -916,7 +891,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ - suspend fun searchByIds(serviceMap: Map): Array? { + suspend fun searchByIds(serviceMap: Map): Array? { if (serviceMap.isEmpty()) return emptyArray() return app.get( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 688363e9..6bafa975 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -23,13 +23,13 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.safeApiCall -import com.lagradost.cloudstream3.sortSubs import com.lagradost.cloudstream3.sortUrls import com.lagradost.cloudstream3.ui.player.LoadType import com.lagradost.cloudstream3.ui.player.RepoLinkGenerator import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment +import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt index 9ed58e2c..15e66b38 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.databinding.FragmentWebviewBinding import com.lagradost.cloudstream3.network.WebViewResolver -import com.lagradost.cloudstream3.utils.AppUtils.loadRepository +import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository class WebviewFragment : Fragment() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt index 1db49e27..d2aca862 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountHelper.kt @@ -27,7 +27,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt index 1132416a..b4a16a66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -13,8 +13,9 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.ui.download.button.DownloadStatusTell -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.UIHelper.setImage diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index 880d5f6c..c8c40e29 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -9,11 +9,11 @@ import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator +import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.ui.player.GeneratorPlayer -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE -import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index d5427cd3..82c5ffb8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -39,7 +39,8 @@ import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.loadResult +import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE import com.lagradost.cloudstream3.utils.DataStore import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe 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 12185cbf..82a92d80 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 @@ -25,8 +25,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.chip.Chip import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding @@ -46,11 +44,13 @@ 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.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable -import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult -import com.lagradost.cloudstream3.utils.AppUtils.ownHide -import com.lagradost.cloudstream3.utils.AppUtils.ownShow -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.isRecyclerScrollable +import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppContextUtils.ownHide +import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event 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 4b0360d7..916cb9ae 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 @@ -22,7 +22,7 @@ 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.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable +import com.lagradost.cloudstream3.utils.AppContextUtils.isRecyclerScrollable class LoadClickCallback( val action: Int = 0, 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 52ec06db..2e98dd1f 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 @@ -16,7 +16,6 @@ import androidx.viewbinding.ViewBinding import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup -import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList @@ -36,6 +35,7 @@ import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST +import com.lagradost.cloudstream3.ui.result.getId import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA 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 a2c7583f..9e70d088 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 @@ -6,9 +6,6 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality -import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia -import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -36,8 +33,11 @@ import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching -import com.lagradost.cloudstream3.utils.AppUtils.loadResult +import com.lagradost.cloudstream3.utils.AppContextUtils.addProgramsToContinueWatching +import com.lagradost.cloudstream3.utils.AppContextUtils.filterHomePageListByFilmQuality +import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.utils.AppContextUtils.filterSearchResultByFilmQuality +import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStoreHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index 90e57ef4..7144de09 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -53,9 +53,9 @@ 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.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.loadResult -import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult -import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity +import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppContextUtils.reduceDragSensitivity import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt index b8feb656..b2de307f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/PageAdapter.kt @@ -16,7 +16,7 @@ import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder -import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.UIHelper.toPx import kotlin.math.roundToInt @@ -26,7 +26,7 @@ class PageAdapter( private val resView: AutofitRecyclerView, val clickCallback: (SearchClickCallback) -> Unit ) : - AppUtils.DiffAdapter(items) { + AppContextUtils.DiffAdapter(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return LibraryItemViewHolder( 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 0865b220..9d838c97 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 @@ -44,8 +44,8 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.unixTimeMs import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment -import com.lagradost.cloudstream3.utils.AppUtils -import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus +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 @@ -258,7 +258,7 @@ abstract class AbstractPlayerFragment( private fun requestAudioFocus() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - activity?.requestLocalAudioFocus(AppUtils.getFocusRequest()) + activity?.requestLocalAudioFocus(AppContextUtils.getFocusRequest()) } } 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 31adbc87..8e322f73 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 @@ -57,14 +57,13 @@ import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle -import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData 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.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File import java.lang.IllegalArgumentException diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index 5585924e..3b242172 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -4,7 +4,6 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.VideoDownloadManager import kotlin.math.max import kotlin.math.min diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 4279b542..92ef279d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -8,10 +8,14 @@ import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.utils.UIHelper.navigate +import com.lagradost.safefile.SafeFile import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri +const val DTAG = "PlayerActivity" + class DownloadedPlayerActivity : AppCompatActivity() { private val dTAG = "DownloadedPlayerAct" diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt index d8d2d537..8255360c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/ExtractorLinkGenerator.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.ui.player import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri class ExtractorLinkGenerator( private val links: List, 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 aa25157b..75a861c0 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 @@ -45,7 +45,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals 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.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute 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 c77f9404..d827d31e 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 @@ -49,6 +49,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.Companion.getAutoSelectLanguageISO639_1 import com.lagradost.cloudstream3.utils.* +import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index c5de1a1c..1e2cf4f5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.ui.player import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.ExtractorUri enum class LoadType { Unknown, 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 0e54e2cb..4bd5c769 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 @@ -6,7 +6,6 @@ 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.ExtractorUri enum class PlayerEventType(val value: Int) { //Stop(-1), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index 02f44eb9..89e3c8de 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -1,13 +1,31 @@ package com.lagradost.cloudstream3.ui.player +import android.net.Uri +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri +import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.unshortenLinkSafe +data class ExtractorUri( + val uri: Uri, + val name: String, + + val basePath: String? = null, + val relativePath: String? = null, + val displayName: String? = null, + + val id: Int? = null, + val parentId: Int? = null, + val episode: Int? = null, + val season: Int? = null, + val headerName: String? = null, + val tvType: TvType? = null, +) + /** * Used to open the player more easily with the LinkGenerator **/ diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt index a52ce160..e6de1266 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt @@ -5,7 +5,7 @@ import android.content.ContentUris import android.net.Uri import androidx.core.content.ContextCompat.getString import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.ExtractorUri +import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.safefile.SafeFile 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 ee44567f..1ba5a29f 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 @@ -15,7 +15,6 @@ 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.ExtractorUri import kotlinx.coroutines.Job import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index fb600ef1..7c78ce63 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -9,14 +9,11 @@ import android.util.Log import androidx.annotation.WorkerThread import androidx.core.graphics.scale import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType -import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper2 import kotlinx.coroutines.CoroutineScope diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt index 0a194785..90bd1ca7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt @@ -7,7 +7,6 @@ import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.ExtractorUri import kotlin.math.max import kotlin.math.min diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt index fb60ccce..1e2c9f67 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt @@ -4,7 +4,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.databinding.PlayerPrioritizeItemBinding -import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppContextUtils data class SourcePriority( val data: T, @@ -13,7 +13,7 @@ data class SourcePriority( ) class PriorityAdapter(override val items: MutableList>) : - AppUtils.DiffAdapter>(items) { + AppContextUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return PriorityViewHolder( PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt index 8153d7a1..b587276f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt @@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.PlayerQualityProfileItemBinding import com.lagradost.cloudstream3.ui.result.UiImage -import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.UIHelper.setImage class ProfilesAdapter( @@ -21,7 +21,7 @@ class ProfilesAdapter( val usedProfile: Int, val clickCallback: (oldIndex: Int?, newIndex: Int) -> Unit, ) : - AppUtils.DiffAdapter( + AppContextUtils.DiffAdapter( items, comparison = { first: QualityDataHelper.QualityProfile, second: QualityDataHelper.QualityProfile -> first.id == second.id 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 85e20d1c..12adc040 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 @@ -17,8 +17,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.bottomsheet.BottomSheetDialog -import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia -import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList @@ -34,12 +32,13 @@ 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.settings.Globals 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.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.ownShow +import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.utils.AppContextUtils.filterSearchResultByFilmQuality +import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount 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 7b743388..61188905 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 @@ -138,7 +138,7 @@ class ActorAdaptor( voiceActorImageHolder.isVisible = false voiceActorName.isVisible = false } else { - voiceActorName.text = actor.voiceActor.name + voiceActorName.text = actor.voiceActor?.name voiceActorImageHolder.isVisible = voiceActorImage.setImage(vaImage) } } 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 62b1fdd1..0a1b777d 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 @@ -21,7 +21,7 @@ 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.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 1d3f5a08..c687eaa0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -3,12 +3,12 @@ package com.lagradost.cloudstream3.ui.result import android.os.Bundle import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos 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 e185e75d..2f297098 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 @@ -29,7 +29,6 @@ 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.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse @@ -57,10 +56,11 @@ 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.utils.AppUtils.getNameFull -import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable -import com.lagradost.cloudstream3.utils.AppUtils.loadCache -import com.lagradost.cloudstream3.utils.AppUtils.openBrowser +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable +import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache +import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser +import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers import com.lagradost.cloudstream3.utils.BatteryOptimizationChecker.openBatteryOptimizationSettings import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog 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 13621cda..a0207060 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 @@ -17,7 +17,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialog -import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse @@ -40,13 +39,13 @@ 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.settings.Globals 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.AppUtils.html -import com.lagradost.cloudstream3.utils.AppUtils.isRtl -import com.lagradost.cloudstream3.utils.AppUtils.loadCache +import com.lagradost.cloudstream3.utils.AppContextUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl +import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache +import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant import com.lagradost.cloudstream3.utils.UIHelper 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 ac6527de..8e8dfe30 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 @@ -18,7 +18,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.apis -import com.lagradost.cloudstream3.APIHolder.getId +import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -27,9 +27,9 @@ import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.getAniListId -import com.lagradost.cloudstream3.LoadResponse.Companion.getImdbId import com.lagradost.cloudstream3.LoadResponse.Companion.getMalId import com.lagradost.cloudstream3.LoadResponse.Companion.isMovie +import com.lagradost.cloudstream3.LoadResponse.Companion.readIdFromString import com.lagradost.cloudstream3.MainActivity.Companion.MPV import com.lagradost.cloudstream3.MainActivity.Companion.MPV_COMPONENT import com.lagradost.cloudstream3.MainActivity.Companion.MPV_PACKAGE @@ -56,10 +56,11 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.* -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull -import com.lagradost.cloudstream3.utils.AppUtils.isAppInstalled -import com.lagradost.cloudstream3.utils.AppUtils.isConnectedToChromecast -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.isAppInstalled +import com.lagradost.cloudstream3.utils.AppContextUtils.isConnectedToChromecast +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.CastHelper.startCast import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWork @@ -301,6 +302,23 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData { ) } +data class ExtractorSubtitleLink( + val name: String, + override val url: String, + override val referer: String, + override val headers: Map = mapOf() +) : IDownloadableMinimum + +fun LoadResponse.getId(): Int { + // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked + return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) + ?: getLoadResponseIdFromUrl(url, apiName) +} + +private fun getLoadResponseIdFromUrl(url: String, apiName: String): Int { + return url.replace(getApiFromNameNull(apiName)?.mainUrl ?: "", "").replace("/", "") + .hashCode() +} data class LinkProgress( val linksLoaded: Int, @@ -856,7 +874,7 @@ class ResultViewModel2 : ViewModel() { loadResponse: LoadResponse? = null, statusChangedCallback: ((statusChanged: Boolean) -> Unit)? = null ) { - val (response,currentId) = loadResponse?.let { load -> + val (response, currentId) = loadResponse?.let { load -> (load to load.getId()) } ?: ((currentResponse ?: return) to (currentId ?: return)) @@ -1140,12 +1158,16 @@ class ResultViewModel2 : ViewModel() { val message = if (duplicateEntries.size == 1) { val list = when (listType) { - LibraryListType.BOOKMARKS -> getResultWatchState(duplicateEntries[0].id ?: 0).stringRes + LibraryListType.BOOKMARKS -> getResultWatchState( + duplicateEntries[0].id ?: 0 + ).stringRes + LibraryListType.FAVORITES -> R.string.favorites_list_name LibraryListType.SUBSCRIPTIONS -> R.string.subscription_list_name } - context.getString(R.string.duplicate_message_single, + context.getString( + R.string.duplicate_message_single, "${normalizeString(duplicateEntries[0].name)} (${context.getString(list)}) — ${duplicateEntries[0].apiName}" ) } else { @@ -1170,9 +1192,11 @@ class ResultViewModel2 : ViewModel() { DialogInterface.BUTTON_POSITIVE -> { checkDuplicatesCallback.invoke(true, emptyList()) } + DialogInterface.BUTTON_NEGATIVE -> { checkDuplicatesCallback.invoke(false, emptyList()) } + DialogInterface.BUTTON_NEUTRAL -> { checkDuplicatesCallback.invoke(true, duplicateEntries.map { it.id }) } @@ -1189,17 +1213,17 @@ class ResultViewModel2 : ViewModel() { private fun getImdbIdFromSyncData(syncData: Map?): String? { return normalSafeApiCall { - SimklApi.readIdFromString( + readIdFromString( syncData?.get(AccountManager.simklApi.idPrefix) - )[SimklApi.Companion.SyncServices.Imdb] + )[SimklSyncServices.Imdb] } } private fun getTMDbIdFromSyncData(syncData: Map?): String? { return normalSafeApiCall { - SimklApi.readIdFromString( + readIdFromString( syncData?.get(AccountManager.simklApi.idPrefix) - )[SimklApi.Companion.SyncServices.Tmdb] + )[SimklSyncServices.Tmdb] } } @@ -1303,7 +1327,8 @@ class ResultViewModel2 : ViewModel() { postPopup( text, links.links.apmap { - val size = it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: "" + val size = + it.getVideoSize()?.let { size -> " " + formatFileSize(context, size) } ?: "" txt("${it.name} ${Qualities.getStringByInt(it.quality)}$size") }) { callback.invoke(links to (it ?: return@postPopup)) @@ -1928,7 +1953,8 @@ class ResultViewModel2 : ViewModel() { .distinct().map { // this actually would be nice if we improved a bit as 3rd season == season 3 == III ect // right now it just removes the dubbed status - it.lowercase().replace(Regex("""\(?[ds]ub(bed)?\)?(\s|$)""") , "").trim() + it.lowercase().replace(Regex("""\(?[ds]ub(bed)?\)?(\s|$)"""), "") + .trim() }, TrackerType.getTypes(this.type), this.year @@ -2276,7 +2302,7 @@ class ResultViewModel2 : ViewModel() { private suspend fun postSuccessful( loadResponse: LoadResponse, - mainId : Int, + mainId: Int, apiRepository: APIRepository, updateEpisodes: Boolean, updateFillers: Boolean, @@ -2292,7 +2318,11 @@ class ResultViewModel2 : ViewModel() { postEpisodes(loadResponse, mainId, updateFillers) } - private suspend fun postEpisodes(loadResponse: LoadResponse, mainId : Int, updateFillers: Boolean) { + private suspend fun postEpisodes( + loadResponse: LoadResponse, + mainId: Int, + updateFillers: Boolean + ) { _episodes.postValue(Resource.Loading()) if (updateFillers && loadResponse is AnimeLoadResponse) { @@ -2313,7 +2343,12 @@ class ResultViewModel2 : ViewModel() { ?: 0) val totalIndex = - i.season?.let { season -> loadResponse.getTotalEpisodeIndex(episode, season) } + i.season?.let { season -> + loadResponse.getTotalEpisodeIndex( + episode, + season + ) + } if (!existingEpisodes.contains(id)) { existingEpisodes.add(id) @@ -2366,7 +2401,12 @@ class ResultViewModel2 : ViewModel() { loadResponse.seasonNames.getSeason(episode.season) val totalIndex = - episode.season?.let { season -> loadResponse.getTotalEpisodeIndex(episodeIndex, season) } + episode.season?.let { season -> + loadResponse.getTotalEpisodeIndex( + episodeIndex, + season + ) + } val ep = buildResultEpisode( @@ -2546,7 +2586,13 @@ class ResultViewModel2 : ViewModel() { ResumeProgress( progress = (viewPos.position / 1000).toInt(), maxProgress = (viewPos.duration / 1000).toInt(), - txt(R.string.resume_remaining, secondsToReadable(((viewPos.duration - viewPos.position) / 1_000).toInt(), "0 mins")) + txt( + R.string.resume_remaining, + secondsToReadable( + ((viewPos.duration - viewPos.position) / 1_000).toInt(), + "0 mins" + ) + ) ) } @@ -2672,17 +2718,26 @@ class ResultViewModel2 : ViewModel() { override var posterHeaders: Map? = null, override var backgroundPosterUrl: String? = null, override var contentRating: String? = null, - val id : Int?, + val id: Int?, ) : LoadResponse - fun loadSmall(activity: Activity?, searchResponse : SearchResponse) = ioSafe { + fun loadSmall(activity: Activity?, searchResponse: SearchResponse) = ioSafe { val url = searchResponse.url _page.postValue(Resource.Loading(url)) _episodes.postValue(Resource.Loading()) - val api = APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull(searchResponse.url) ?: APIRepository.noneApi + val api = + APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull( + searchResponse.url + ) ?: APIRepository.noneApi val repo = APIRepository(api) - val response = LoadResponseFromSearch(name = searchResponse.name, url = searchResponse.url, apiName = api.name, type = searchResponse.type ?: TvType.Others, - posterUrl = searchResponse.posterUrl, id = searchResponse.id).apply { + val response = LoadResponseFromSearch( + name = searchResponse.name, + url = searchResponse.url, + apiName = api.name, + type = searchResponse.type ?: TvType.Others, + posterUrl = searchResponse.posterUrl, + id = searchResponse.id + ).apply { if (searchResponse is SyncAPI.LibraryItem) { this.plot = searchResponse.plot this.rating = searchResponse.personalRating?.times(100) ?: searchResponse.rating @@ -2701,7 +2756,8 @@ class ResultViewModel2 : ViewModel() { mainId = mainId, apiRepository = repo, updateEpisodes = false, - updateFillers = false) + updateFillers = false + ) } fun load( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index e0762cc5..70919943 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -10,7 +10,7 @@ import androidx.annotation.StringRes import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage sealed class UiText { 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 24e87d30..ef10fcee 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 @@ -24,11 +24,7 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia -import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings -import com.lagradost.cloudstream3.APIHolder.getApiSettings import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AllLanguagesName @@ -58,9 +54,13 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout 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.AppUtils.ownHide -import com.lagradost.cloudstream3.utils.AppUtils.ownShow -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia +import com.lagradost.cloudstream3.utils.AppContextUtils.filterSearchResultByFilmQuality +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.ownHide +import com.lagradost.cloudstream3.utils.AppContextUtils.ownShow +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt index 66423982..ef1b8719 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt @@ -11,7 +11,7 @@ import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.ui.result.START_ACTION_LOAD_EP import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult +import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt index d18c0197..f597132b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -19,7 +19,7 @@ import com.lagradost.cloudstream3.isMovieType import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.SubtitleHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 67a2a15b..15f8735f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -9,8 +9,6 @@ import android.view.inputmethod.EditorInfo import android.widget.TextView import androidx.annotation.UiThread import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmapOrNull import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.FragmentActivity @@ -49,7 +47,7 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.AppUtils.html +import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback import com.lagradost.cloudstream3.utils.BiometricAuthenticator.authCallback @@ -64,9 +62,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.setImage -import com.lagradost.cloudstream3.utils.UIHelper.toPx import qrcode.QRCode -import java.io.ByteArrayOutputStream class SettingsAccount : PreferenceFragmentCompat(), BiometricCallback { companion object { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 7dc73a46..cfb46c39 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -7,19 +7,17 @@ import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings -import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard -import com.lagradost.cloudstream3.utils.UIHelper.navigate class SettingsProviders : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 1364c376..1b487629 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -33,8 +33,8 @@ import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.AppUtils.addRepositoryDialog -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.addRepositoryDialog +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe 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 cab029bb..909c30be 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 @@ -20,7 +20,7 @@ import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.AppUtils.html +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.SubtitleHelper.fromTwoLettersToLanguage 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 3bdcb251..c5319c37 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 @@ -8,7 +8,6 @@ import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R @@ -24,6 +23,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.ui.settings.appLanguages +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.toPx diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt index 023ecb4c..bad58a0e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestResultAdapter.kt @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.databinding.ProviderTestItemBinding import com.lagradost.cloudstream3.mvvm.getAllMessages import com.lagradost.cloudstream3.mvvm.getStackTracePretty import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.utils.AppUtils +import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso @@ -23,7 +23,7 @@ import com.lagradost.cloudstream3.utils.TestingUtils import java.io.File class TestResultAdapter(override val items: MutableList>) : - AppUtils.DiffAdapter>(items) { + AppContextUtils.DiffAdapter>(items) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProviderTestViewHolder( ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt index 26513f4a..eea495a2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestView.kt @@ -13,7 +13,7 @@ import androidx.core.view.isVisible import androidx.core.widget.ContentLoadingProgressBar import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.AppUtils.animateProgressTo +import com.lagradost.cloudstream3.utils.AppContextUtils.animateProgressTo class TestView @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index 59dcc402..c12e9eb8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -11,11 +11,11 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.APIHolder -import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentSetupProviderLanguagesBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt similarity index 82% rename from app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt rename to app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt index 626eca12..f0aae7bc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -51,6 +51,7 @@ import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.wrappers.Wrappers import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent @@ -60,9 +61,9 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WebviewFragment +import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.settings.Globals -import com.lagradost.cloudstream3.ui.settings.extensions.ExtensionsFragment import com.lagradost.cloudstream3.ui.settings.extensions.PluginsFragment import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -79,7 +80,7 @@ import java.io.* import java.net.URL import java.net.URLDecoder -object AppUtils { +object AppContextUtils { fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) { for (i in 0..maxViewTypeId) recycledViewPool.setMaxRecycledViews(i, maxPoolSize) @@ -371,6 +372,168 @@ object AppUtils { } } + fun sortSubs(subs: Set): List { + return subs.sortedBy { it.name } + } + + fun Context.getApiSettings(): HashSet { + //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + + val hashSet = HashSet() + val activeLangs = getApiProviderLangSettings() + val hasUniversal = activeLangs.contains(AllLanguagesName) + hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } + .map { it.name }) + + /*val set = settingsManager.getStringSet( + this.getString(R.string.search_providers_list_key), + hashSet + )?.toHashSet() ?: hashSet + + val list = HashSet() + for (name in set) { + val api = getApiFromNameNull(name) ?: continue + if (activeLangs.contains(api.lang)) { + list.add(name) + } + }*/ + //if (list.isEmpty()) return hashSet + //return list + return hashSet + } + + fun Context.getApiDubstatusSettings(): HashSet { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = HashSet() + hashSet.addAll(DubStatus.values()) + val list = settingsManager.getStringSet( + this.getString(R.string.display_sub_key), + hashSet.map { it.name }.toMutableSet() + ) ?: return hashSet + + val names = DubStatus.values().map { it.name }.toHashSet() + //if(realSet.isEmpty()) return hashSet + + return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet() + } + + fun Context.getApiProviderLangSettings(): HashSet { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = hashSetOf(AllLanguagesName) // def is all languages +// hashSet.add("en") // def is only en + val list = settingsManager.getStringSet( + this.getString(R.string.provider_lang_key), + hashSet + ) + + if (list.isNullOrEmpty()) return hashSet + return list.toHashSet() + } + + fun Context.getApiTypeSettings(): HashSet { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + val hashSet = HashSet() + hashSet.addAll(TvType.values()) + val list = settingsManager.getStringSet( + this.getString(R.string.search_types_list_key), + hashSet.map { it.name }.toMutableSet() + ) + + if (list.isNullOrEmpty()) return hashSet + + val names = TvType.values().map { it.name }.toHashSet() + val realSet = list.filter { names.contains(it) }.map { TvType.valueOf(it) }.toHashSet() + if (realSet.isEmpty()) return hashSet + + return realSet + } + + fun Context.updateHasTrailers() { + LoadResponse.isTrailersEnabled = getHasTrailers() + } + + private fun Context.getHasTrailers(): Boolean { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + return settingsManager.getBoolean(this.getString(R.string.show_trailers_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 + // Trying fixing using classloader fuckery + val oldLoader = Thread.currentThread().contextClassLoader + Thread.currentThread().contextClassLoader = TvType::class.java.classLoader + + val default = TvType.values() + .sorted() + .filter { it != TvType.NSFW } + .map { it.ordinal } + + Thread.currentThread().contextClassLoader = oldLoader + + val defaultSet = default.map { it.toString() }.toSet() + val currentPrefMedia = try { + PreferenceManager.getDefaultSharedPreferences(this) + .getStringSet(this.getString(R.string.prefer_media_type_key), defaultSet) + ?.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null } + } catch (e: Throwable) { + null + } ?: default + val langs = this.getApiProviderLangSettings() + val hasUniversal = langs.contains(AllLanguagesName) + val allApis = synchronized(apis) { + apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } + } + return if (currentPrefMedia.isEmpty()) { + allApis + } else { + // Filter API depending on preferred media type + allApis.filter { api -> api.supportedTypes.any { currentPrefMedia.contains(it.ordinal) } } + } + } + + fun Context.filterSearchResultByFilmQuality(data: List): List { + // Filter results omitting entries with certain quality + if (data.isNotEmpty()) { + val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) + ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) + ?.mapNotNull { entry -> + entry.toIntOrNull() ?: return@mapNotNull null + } ?: listOf() + if (filteredSearchQuality.isNotEmpty()) { + return data.filter { item -> + val searchQualVal = item.quality?.ordinal ?: -1 + //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") + !filteredSearchQuality.contains(searchQualVal) + } + } + } + return data + } + + fun Context.filterHomePageListByFilmQuality(data: HomePageList): HomePageList { + // Filter results omitting entries with certain quality + if (data.list.isNotEmpty()) { + val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) + ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) + ?.mapNotNull { entry -> + entry.toIntOrNull() ?: return@mapNotNull null + } ?: listOf() + if (filteredSearchQuality.isNotEmpty()) { + return HomePageList( + name = data.name, + isHorizontalImages = data.isHorizontalImages, + list = data.list.filter { item -> + val searchQualVal = item.quality?.ordinal ?: -1 + //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") + !filteredSearchQuality.contains(searchQualVal) + } + ) + } + } + return data + } + fun Activity.loadRepository(url: String) { ioSafe { val repo = RepositoryManager.parseRepository(url) ?: return@ioSafe @@ -532,24 +695,6 @@ object AppUtils { return queryPairs } - /** Any object as json string */ - fun Any.toJson(): String { - if (this is String) return this - return mapper.writeValueAsString(this) - } - - inline fun parseJson(value: String): T { - return mapper.readValue(value) - } - - inline fun tryParseJson(value: String?): T? { - return try { - parseJson(value ?: return null) - } catch (_: Exception) { - null - } - } - /**| S1:E2 Hello World * | Episode 2. Hello world * | Hello World @@ -619,7 +764,7 @@ object AppUtils { val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) Kitsu.isEnabled = settingsManager.getBoolean(this.getString(R.string.show_kitsu_posters_key), true) - }catch (t : Throwable) { + } catch (t: Throwable) { logError(t) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 04387d80..43124a53 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.utils import android.content.Context import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.* -import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -18,6 +17,7 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState +import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia import kotlin.reflect.KClass import kotlin.reflect.KProperty 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 d9a31b4e..89bb0031 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -24,7 +24,7 @@ import okio.sink import java.io.File import android.text.TextUtils import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit -import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt index 322547f4..57b98dc2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstallerService.kt @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import kotlinx.coroutines.delay diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 421b09e2..f3cbdaf1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -24,6 +24,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.BuildConfig +import com.lagradost.cloudstream3.IDownloadableMinimum import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType @@ -107,16 +108,6 @@ object VideoDownloadManager { Stop, } - interface IDownloadableMinimum { - val url: String - val referer: String - val headers: Map - } - - fun IDownloadableMinimum.getId(): Int { - return url.hashCode() - } - data class DownloadEpisodeMetadata( @JsonProperty("id") val id: Int, @JsonProperty("mainName") val mainName: String, diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 46da8e84..516e1ee9 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -1,4 +1,5 @@ import com.codingfeline.buildkonfig.compiler.FieldSpec +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { kotlin("multiplatform") @@ -12,6 +13,11 @@ kotlin { androidTarget() jvm() + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + sourceSets { commonMain.dependencies { implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib @@ -19,6 +25,9 @@ kotlin { ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API Level 25 or Less. */ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") + implementation("me.xdrop:fuzzywuzzy:1.4.0") // Match extractors + implementation("org.mozilla:rhino:1.7.15") // run JavaScript + implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") } } } diff --git a/library/src/androidMain/kotlin/com/lagradost/api/ContextHelper.android.kt b/library/src/androidMain/kotlin/com/lagradost/api/ContextHelper.android.kt new file mode 100644 index 00000000..a8472fea --- /dev/null +++ b/library/src/androidMain/kotlin/com/lagradost/api/ContextHelper.android.kt @@ -0,0 +1,20 @@ +package com.lagradost.api + +import android.content.Context +import java.lang.ref.WeakReference + +var ctx: WeakReference? = null + +/** + * Helper function for Android specific context. Not usable in JVM. + * Do not use this unless absolutely necessary. + */ +actual fun getContext(): Any? { + return ctx?.get() +} + +actual fun setContext(context: WeakReference) { + if (context.get() is Context) { + ctx = context as? WeakReference + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt similarity index 90% rename from app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt rename to library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt index 90872d94..0fbc5749 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt +++ b/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt @@ -1,13 +1,12 @@ package com.lagradost.cloudstream3.network import android.annotation.SuppressLint +import android.content.Context import android.net.http.SslError import android.os.Handler import android.os.Looper import android.webkit.* -import com.lagradost.cloudstream3.AcraApplication -import com.lagradost.cloudstream3.AcraApplication.Companion.context -import com.lagradost.cloudstream3.USER_AGENT +import com.lagradost.api.getContext import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.logError @@ -33,40 +32,24 @@ import java.net.URI * @param scriptCallback will be called with the result from custom js * @param timeout close webview after timeout * */ -class WebViewResolver( +actual class WebViewResolver actual constructor( val interceptUrl: Regex, - val additionalUrls: List = emptyList(), - val userAgent: String? = USER_AGENT, - val useOkhttp: Boolean = true, - val script: String? = null, - val scriptCallback: ((String) -> Unit)? = null, - val timeout: Long = DEFAULT_TIMEOUT + val additionalUrls: List, + val userAgent: String?, + val useOkhttp: Boolean, + val script: String?, + val scriptCallback: ((String) -> Unit)?, + val timeout: Long ) : Interceptor { - constructor( - interceptUrl: Regex, - additionalUrls: List = emptyList(), - userAgent: String? = USER_AGENT, - useOkhttp: Boolean = true, - script: String? = null, - scriptCallback: ((String) -> Unit)? = null, - ) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, script, scriptCallback, DEFAULT_TIMEOUT) - - constructor( - interceptUrl: Regex, - additionalUrls: List = emptyList(), - userAgent: String? = USER_AGENT, - useOkhttp: Boolean = true - ) : this(interceptUrl, additionalUrls, userAgent, useOkhttp, null, null, DEFAULT_TIMEOUT) - - companion object { - private const val DEFAULT_TIMEOUT = 60_000L + actual companion object { var webViewUserAgent: String? = null + actual val DEFAULT_TIMEOUT = 60_000L @JvmName("getWebViewUserAgent1") fun getWebViewUserAgent(): String? { - return webViewUserAgent ?: context?.let { ctx -> + return webViewUserAgent ?: (getContext() as? Context)?.let { ctx -> runBlocking { mainWork { WebView(ctx).settings.userAgentString.also { userAgent -> @@ -137,7 +120,7 @@ class WebViewResolver( WebView.setWebContentsDebuggingEnabled(true) try { webView = WebView( - AcraApplication.context + (getContext() as? Context) ?: throw RuntimeException("No base context in WebViewResolver") ).apply { // Bare minimum to bypass captcha diff --git a/library/src/commonMain/kotlin/com/lagradost/api/ContextHelper.kt b/library/src/commonMain/kotlin/com/lagradost/api/ContextHelper.kt new file mode 100644 index 00000000..fb54e3ca --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/api/ContextHelper.kt @@ -0,0 +1,16 @@ +package com.lagradost.api + +import java.lang.ref.WeakReference + +/** + * Set context for android specific code such as webview. + * Does nothing on JVM. + */ +expect fun setContext(context: WeakReference) +/** + * Helper function for Android specific context. + * Do not use this unless absolutely necessary. + * setContext() must be called before this is called. + * @return Context if on android, null if not. + */ +expect fun getContext(): Any? diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt similarity index 86% rename from app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index 91da2ed0..47ef5382 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -1,34 +1,26 @@ package com.lagradost.cloudstream3 -import android.annotation.SuppressLint -import android.content.Context -import android.net.Uri -import android.util.Base64.encodeToString -import androidx.annotation.WorkerThread -import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi import com.lagradost.cloudstream3.syncproviders.SyncIdName -import com.lagradost.cloudstream3.syncproviders.providers.SimklApi -import com.lagradost.cloudstream3.ui.player.SubtitleData -import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.nicehttp.RequestBodyTypes import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.toRequestBody +import java.net.URI import java.text.SimpleDateFormat import java.util.* +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.math.absoluteValue /** @@ -111,17 +103,6 @@ object APIHolder { return null } - private fun getLoadResponseIdFromUrl(url: String, apiName: String): Int { - return url.replace(getApiFromNameNull(apiName)?.mainUrl ?: "", "").replace("/", "") - .hashCode() - } - - fun LoadResponse.getId(): Int { - // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked - return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) - ?: getLoadResponseIdFromUrl(url, apiName) - } - /** * Gets the website captcha token * discovered originally by https://github.com/ahmedgamal17 @@ -137,10 +118,9 @@ object APIHolder { // To get the key suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? { try { - val uri = Uri.parse(url) - val domain = encodeToString( + val uri = URI.create(url) + val domain = base64Encode( (uri.scheme + "://" + uri.host + ":443").encodeToByteArray(), - 0 ).replace("\n", "").replace("=", ".") val vToken = @@ -275,165 +255,6 @@ object APIHolder { return app.post("https://graphql.anilist.co", requestBody = data) .parsedSafe() } - - - fun Context.getApiSettings(): HashSet { - //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - - val hashSet = HashSet() - val activeLangs = getApiProviderLangSettings() - val hasUniversal = activeLangs.contains(AllLanguagesName) - hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } } - .map { it.name }) - - /*val set = settingsManager.getStringSet( - this.getString(R.string.search_providers_list_key), - hashSet - )?.toHashSet() ?: hashSet - - val list = HashSet() - for (name in set) { - val api = getApiFromNameNull(name) ?: continue - if (activeLangs.contains(api.lang)) { - list.add(name) - } - }*/ - //if (list.isEmpty()) return hashSet - //return list - return hashSet - } - - fun Context.getApiDubstatusSettings(): HashSet { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val hashSet = HashSet() - hashSet.addAll(DubStatus.values()) - val list = settingsManager.getStringSet( - this.getString(R.string.display_sub_key), - hashSet.map { it.name }.toMutableSet() - ) ?: return hashSet - - val names = DubStatus.values().map { it.name }.toHashSet() - //if(realSet.isEmpty()) return hashSet - - return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet() - } - - fun Context.getApiProviderLangSettings(): HashSet { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val hashSet = hashSetOf(AllLanguagesName) // def is all languages -// hashSet.add("en") // def is only en - val list = settingsManager.getStringSet( - this.getString(R.string.provider_lang_key), - hashSet - ) - - if (list.isNullOrEmpty()) return hashSet - return list.toHashSet() - } - - fun Context.getApiTypeSettings(): HashSet { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - val hashSet = HashSet() - hashSet.addAll(TvType.values()) - val list = settingsManager.getStringSet( - this.getString(R.string.search_types_list_key), - hashSet.map { it.name }.toMutableSet() - ) - - if (list.isNullOrEmpty()) return hashSet - - val names = TvType.values().map { it.name }.toHashSet() - val realSet = list.filter { names.contains(it) }.map { TvType.valueOf(it) }.toHashSet() - if (realSet.isEmpty()) return hashSet - - return realSet - } - - fun Context.updateHasTrailers() { - LoadResponse.isTrailersEnabled = getHasTrailers() - } - - private fun Context.getHasTrailers(): Boolean { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - return settingsManager.getBoolean(this.getString(R.string.show_trailers_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 - // Trying fixing using classloader fuckery - val oldLoader = Thread.currentThread().contextClassLoader - Thread.currentThread().contextClassLoader = TvType::class.java.classLoader - - val default = TvType.values() - .sorted() - .filter { it != TvType.NSFW } - .map { it.ordinal } - - Thread.currentThread().contextClassLoader = oldLoader - - val defaultSet = default.map { it.toString() }.toSet() - val currentPrefMedia = try { - PreferenceManager.getDefaultSharedPreferences(this) - .getStringSet(this.getString(R.string.prefer_media_type_key), defaultSet) - ?.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null } - } catch (e: Throwable) { - null - } ?: default - val langs = this.getApiProviderLangSettings() - val hasUniversal = langs.contains(AllLanguagesName) - val allApis = synchronized(apis) { - apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) } - } - return if (currentPrefMedia.isEmpty()) { - allApis - } else { - // Filter API depending on preferred media type - allApis.filter { api -> api.supportedTypes.any { currentPrefMedia.contains(it.ordinal) } } - } - } - - fun Context.filterSearchResultByFilmQuality(data: List): List { - // Filter results omitting entries with certain quality - if (data.isNotEmpty()) { - val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) - ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) - ?.mapNotNull { entry -> - entry.toIntOrNull() ?: return@mapNotNull null - } ?: listOf() - if (filteredSearchQuality.isNotEmpty()) { - return data.filter { item -> - val searchQualVal = item.quality?.ordinal ?: -1 - //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") - !filteredSearchQuality.contains(searchQualVal) - } - } - } - return data - } - - fun Context.filterHomePageListByFilmQuality(data: HomePageList): HomePageList { - // Filter results omitting entries with certain quality - if (data.list.isNotEmpty()) { - val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this) - ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf()) - ?.mapNotNull { entry -> - entry.toIntOrNull() ?: return@mapNotNull null - } ?: listOf() - if (filteredSearchQuality.isNotEmpty()) { - return HomePageList( - name = data.name, - isHorizontalImages = data.isHorizontalImages, - list = data.list.filter { item -> - val searchQualVal = item.quality?.ordinal ?: -1 - //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}") - !filteredSearchQuality.contains(searchQualVal) - } - ) - } - } - return data - } } /* @@ -656,7 +477,7 @@ abstract class MainAPI { //emptyList() // open val mainPage = listOf(MainPageData("", "", false)) - @WorkerThread + // @WorkerThread open suspend fun getMainPage( page: Int, request: MainPageRequest, @@ -664,17 +485,17 @@ abstract class MainAPI { throw NotImplementedError() } - @WorkerThread + // @WorkerThread open suspend fun search(query: String): List? { throw NotImplementedError() } - @WorkerThread + // @WorkerThread open suspend fun quickSearch(query: String): List? { throw NotImplementedError() } - @WorkerThread + // @WorkerThread /** * Based on data from search() or getMainPage() it generates a LoadResponse, * basically opening the info page from a link. @@ -692,13 +513,13 @@ abstract class MainAPI { * This function might be updated to include exoplayer timestamps etc in the future * if the need arises. * */ - @WorkerThread + // @WorkerThread open suspend fun extractorVerifierJob(extractorData: String?) { throw NotImplementedError() } /**Callback is fired once a link is found, will return true if method is executed successfully*/ - @WorkerThread + // @WorkerThread open suspend fun loadLinks( data: String, isCasting: Boolean, @@ -723,27 +544,16 @@ abstract class MainAPI { } /** Might need a different implementation for desktop*/ -@SuppressLint("NewApi") fun base64Decode(string: String): String { return String(base64DecodeArray(string), Charsets.ISO_8859_1) } - -@SuppressLint("NewApi") +@OptIn(ExperimentalEncodingApi::class) fun base64DecodeArray(string: String): ByteArray { - return try { - android.util.Base64.decode(string, android.util.Base64.DEFAULT) - } catch (e: Exception) { - Base64.getDecoder().decode(string) - } + return Base64.decode(string) } - -@SuppressLint("NewApi") +@OptIn(ExperimentalEncodingApi::class) fun base64Encode(array: ByteArray): String { - return try { - String(android.util.Base64.encode(array, android.util.Base64.NO_WRAP), Charsets.ISO_8859_1) - } catch (e: Exception) { - String(Base64.getEncoder().encode(array)) - } + return Base64.encode(array) } fun MainAPI.fixUrlNull(url: String?): String? { @@ -779,10 +589,6 @@ fun sortUrls(urls: Set): List { return urls.sortedBy { t -> -t.quality } } -fun sortSubs(subs: Set): List { - return subs.sortedBy { it.name } -} - fun capitalizeString(str: String): String { return capitalizeStringNullable(str) ?: str } @@ -1204,11 +1010,25 @@ interface LoadResponse { var contentRating: String? companion object { - private val malIdPrefix = malApi.idPrefix - private val aniListIdPrefix = aniListApi.idPrefix - private val simklIdPrefix = simklApi.idPrefix + var malIdPrefix = "" //malApi.idPrefix + var aniListIdPrefix = "" //aniListApi.idPrefix + var simklIdPrefix = "" //simklApi.idPrefix var isTrailersEnabled = true + /** + * The ID string is a way to keep a collection of services in one single ID using a map + * This adds a database service (like imdb) to the string and returns the new string. + */ + fun addIdToString(idString: String?, database: SimklSyncServices, id: String?): String? { + if (id == null) return idString + return (readIdFromString(idString) + mapOf(database to id)).toJson() + } + + /** Read the id string to get all other ids */ + fun readIdFromString(idString: String?): Map { + return tryParseJson(idString) ?: return emptyMap() + } + fun LoadResponse.isMovie(): Boolean { return this.type.isMovieType() || this is MovieLoadResponse } @@ -1232,12 +1052,12 @@ interface LoadResponse { * Internal helper function to add simkl ids from other databases. */ private fun LoadResponse.addSimklId( - database: SimklApi.Companion.SyncServices, + database: SimklSyncServices, id: String? ) { normalSafeApiCall { this.syncData[simklIdPrefix] = - SimklApi.addIdToString(this.syncData[simklIdPrefix], database, id.toString()) + addIdToString(this.syncData[simklIdPrefix], database, id.toString()) ?: return@normalSafeApiCall } } @@ -1257,30 +1077,28 @@ interface LoadResponse { fun LoadResponse.getImdbId(): String? { return normalSafeApiCall { - SimklApi.readIdFromString(this.syncData[simklIdPrefix]) - ?.get(SimklApi.Companion.SyncServices.Imdb) + readIdFromString(this.syncData[simklIdPrefix])[SimklSyncServices.Imdb] } } fun LoadResponse.getTMDbId(): String? { return normalSafeApiCall { - SimklApi.readIdFromString(this.syncData[simklIdPrefix]) - ?.get(SimklApi.Companion.SyncServices.Tmdb) + readIdFromString(this.syncData[simklIdPrefix])[SimklSyncServices.Tmdb] } } fun LoadResponse.addMalId(id: Int?) { this.syncData[malIdPrefix] = (id ?: return).toString() - this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString()) + this.addSimklId(SimklSyncServices.Mal, id.toString()) } fun LoadResponse.addAniListId(id: Int?) { this.syncData[aniListIdPrefix] = (id ?: return).toString() - this.addSimklId(SimklApi.Companion.SyncServices.AniList, id.toString()) + this.addSimklId(SimklSyncServices.AniList, id.toString()) } fun LoadResponse.addSimklId(id: Int?) { - this.addSimklId(SimklApi.Companion.SyncServices.Simkl, id.toString()) + this.addSimklId(SimklSyncServices.Simkl, id.toString()) } fun LoadResponse.addImdbUrl(url: String?) { @@ -1362,7 +1180,7 @@ interface LoadResponse { fun LoadResponse.addImdbId(id: String?) { // TODO add imdb sync - this.addSimklId(SimklApi.Companion.SyncServices.Imdb, id) + this.addSimklId(SimklSyncServices.Imdb, id) } fun LoadResponse.addTrackId(id: String?) { @@ -1375,7 +1193,7 @@ interface LoadResponse { fun LoadResponse.addTMDbId(id: String?) { // TODO add TMDb sync - this.addSimklId(SimklApi.Companion.SyncServices.Tmdb, id) + this.addSimklId(SimklSyncServices.Tmdb, id) } fun LoadResponse.addRating(text: String?) { @@ -1466,7 +1284,7 @@ data class NextAiring( constructor( episode: Int, unixTime: Long, - ) : this ( + ) : this( episode, unixTime, null @@ -1929,6 +1747,28 @@ fun MainAPI.newEpisode( return builder } +interface IDownloadableMinimum { + val url: String + val referer: String + val headers: Map +} + +fun IDownloadableMinimum.getId(): Int { + return url.hashCode() +} + +/** + * Set of sync services simkl is compatible with. + * Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id + */ +enum class SimklSyncServices(val originalName: String) { + Simkl("simkl"), + Imdb("imdb"), + Tmdb("tmdb"), + AniList("anilist"), + Mal("mal"), +} + data class TvSeriesLoadResponse( override var name: String, override var url: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt index b0051ba7..23f8dcf4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Acefile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Acefile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/BullStream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/BullStream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByteShare.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByteShare.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt index b7f84af1..27a5c52a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EPlay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/EPlay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EPlay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Embedgram.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Embedgram.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Embedgram.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Embedgram.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/EmturbovidExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GMPlayer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GMPlayer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GenericM3U8.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GenericM3U8.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/GenericM3U8.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GenericM3U8.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GoodstreamExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GuardareStream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GuardareStream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt index 03586386..1f70ce61 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.extractors.helper.AesHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt index 14333d35..8318c3fb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDStreamAbleExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/JWPlayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/JWPlayer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Jawcloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jawcloud.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Jawcloud.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jawcloud.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Krakenfiles.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Krakenfiles.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt index 766c7762..ce742e97 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Maxstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Maxstream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Maxstream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Maxstream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mediafire.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mediafire.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Mediafire.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mediafire.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/MixDrop.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MixDrop.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/MixDrop.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MixDrop.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Moviehab.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Moviehab.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mp4Upload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mp4Upload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mvidoo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Mvidoo.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt index 46f6ad0f..6db0830c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Okrulink.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Okrulink.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Okrulink.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Okrulink.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt similarity index 99% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt index b57449bf..0a005036 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PixelDrainExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt similarity index 99% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt index 2b286abb..a4dc694e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt index a0d830cf..607d2d78 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SBPlay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SBPlay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Sendvid.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Sendvid.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt index a8bcee31..ebd57f9c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Solidfiles.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Solidfiles.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamWishExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamlare.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Streamlare.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamoUpload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/StreamoUpload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Streamplay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt index 645d7c0e..de5ca9a2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt index 2478edc1..157374a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tomatomatela.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tomatomatela.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uqload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uqload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Userload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Userload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userscloud.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userscloud.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt index b963fe56..e57772ce 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt similarity index 88% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt index 2655670d..73857fb3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcTo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -1,70 +1,72 @@ -package com.lagradost.cloudstream3.extractors - -import android.util.Base64 -import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.amap -import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.ExtractorApi -import com.lagradost.cloudstream3.utils.ExtractorLink -import java.net.URLDecoder -import javax.crypto.Cipher -import javax.crypto.spec.SecretKeySpec - -class VidSrcTo : ExtractorApi() { - override val name = "VidSrcTo" - override val mainUrl = "https://vidsrc.to" - override val requiresReferer = true - - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return - val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return - if (res.status != 200) return - res.result?.amap { source -> - try { - val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap - val finalUrl = DecryptUrl(embedRes.result.encUrl) - if(finalUrl.equals(embedRes.result.encUrl)) return@amap - when (source.title) { - "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) - "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) - } - } catch (e: Exception) { - logError(e) - } - } - } - - private fun DecryptUrl(encUrl: String): String { - var data = encUrl.toByteArray() - data = Base64.decode(data, Base64.URL_SAFE) - val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4") - val cipher = Cipher.getInstance("RC4") - cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters) - data = cipher.doFinal(data) - return URLDecoder.decode(data.toString(Charsets.UTF_8), "utf-8") - } - - data class VidsrctoEpisodeSources( - @JsonProperty("status") val status: Int, - @JsonProperty("result") val result: List? - ) - - data class VidsrctoResult( - @JsonProperty("id") val id: String, - @JsonProperty("title") val title: String - ) - - data class VidsrctoEmbedSource( - @JsonProperty("status") val status: Int, - @JsonProperty("result") val result: VidsrctoUrl - ) - - data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) -} +package com.lagradost.cloudstream3.extractors + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.SubtitleFile +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Decode +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import java.net.URLDecoder +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +class VidSrcTo : ExtractorApi() { + override val name = "VidSrcTo" + override val mainUrl = "https://vidsrc.to" + override val requiresReferer = true + + override suspend fun getUrl( + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit + ) { + val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return + val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return + if (res.status != 200) return + res.result?.amap { source -> + try { + val embedRes = app.get("$mainUrl/ajax/embed/source/${source.id}").parsedSafe() ?: return@amap + val finalUrl = DecryptUrl(embedRes.result.encUrl) + if(finalUrl.equals(embedRes.result.encUrl)) return@amap + when (source.title) { + "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) + } + } catch (e: Exception) { + logError(e) + } + } + } + + @OptIn(ExperimentalEncodingApi::class) + private fun DecryptUrl(encUrl: String): String { + val data = Base64.UrlSafe.decode(encUrl) + val rc4Key = SecretKeySpec("WXrUARXb1aDLaZjI".toByteArray(), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.DECRYPT_MODE, rc4Key, cipher.parameters) + val finalData = cipher.doFinal(data) + return URLDecoder.decode(finalData.toString(Charsets.UTF_8), "utf-8") + } + + data class VidsrctoEpisodeSources( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: List? + ) + + data class VidsrctoResult( + @JsonProperty("id") val id: String, + @JsonProperty("title") val title: String + ) + + data class VidsrctoEmbedSource( + @JsonProperty("status") val status: Int, + @JsonProperty("result") val result: VidsrctoUrl + ) + + data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt similarity index 98% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt index 2439b8ad..1161ff66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt @@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoVard.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoVard.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidguard.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidguard.kt index 230a9e1a..c48b683c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidguard.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidguard.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.AppUtils @@ -87,7 +87,7 @@ open class Vidguardto : ExtractorApi() { } Log.d("runJS", "Result: $result") } catch (e: Exception) { - Log.e("runJS", "Error executing JavaScript", e) + Log.e("runJS", "Error executing JavaScript: ${e.message}") } finally { Context.exit() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidhideExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vido.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vido.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vidplay.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Voe.kt similarity index 93% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Voe.kt index 67fd7eea..1d7dee7c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Voe.kt @@ -1,9 +1,9 @@ package com.lagradost.cloudstream3.extractors -import android.util.Base64 import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.base64Decode import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink @@ -58,12 +58,12 @@ open class Voe : ExtractorApi() { videoLinks.add( when { linkRegex.matches(link) -> link - else -> String(Base64.decode(link, Base64.DEFAULT)) + else -> base64Decode(link) } ) } else { val link2 = base64Regex.find(script)?.value ?: return - val decoded = Base64.decode(link2, Base64.DEFAULT).toString() + val decoded = base64Decode(link2) val videoLinkDTO = AppUtils.parseJson(decoded) videoLinkDTO.let { videoLinks.add(it.toString()) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vtbe.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Vtbe.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vtbe.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WatchSB.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WatchSB.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WcoStream.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WcoStream.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Wibufile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Wibufile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/YourUpload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/YourUpload.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Zorofile.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zorofile.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Zorofile.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zorofile.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Zplayer.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zplayer.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/Zplayer.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zplayer.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AesHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt index 0b401c06..bd42424f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt @@ -1,6 +1,6 @@ package com.lagradost.cloudstream3.extractors.helper -import android.util.Log +import com.lagradost.api.Log import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt similarity index 76% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt index 768fa1f6..35aec2b1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt @@ -1,8 +1,6 @@ package com.lagradost.cloudstream3.extractors.helper import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.app class WcoHelper { @@ -30,9 +28,7 @@ class WcoHelper { private suspend fun getKeys() { keys = keys ?: app.get("https://raw.githubusercontent.com/reduplicated/Cloudstream/master/docs/keys.json") - .parsedSafe()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey( - BACKUP_KEY_DATA - ) + .parsedSafe() } suspend fun getWcoKey(): ExternalKeys? { @@ -43,9 +39,7 @@ class WcoHelper { private suspend fun getNewKeys() { newKeys = newKeys ?: app.get("https://raw.githubusercontent.com/chekaslowakiya/BruhFlow/main/keys.json") - .parsedSafe()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey( - BACKUP_KEY_DATA - ) + .parsedSafe() } suspend fun getNewWcoKey(): NewExternalKeys? { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.kt new file mode 100644 index 00000000..8baf2f31 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.kt @@ -0,0 +1,28 @@ +package com.lagradost.cloudstream3.network + +import com.lagradost.cloudstream3.USER_AGENT +import okhttp3.Interceptor + +/** + * When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...) + * @param interceptUrl will stop the WebView when reaching this url. + * @param additionalUrls this will make resolveUsingWebView also return all other requests matching the list of Regex. + * @param userAgent if null then will use the default user agent + * @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare. + * @param script pass custom js to execute + * @param scriptCallback will be called with the result from custom js + * @param timeout close webview after timeout + * */ +expect class WebViewResolver( + interceptUrl: Regex, + additionalUrls: List = emptyList(), + userAgent: String? = USER_AGENT, + useOkhttp: Boolean = true, + script: String? = null, + scriptCallback: ((String) -> Unit)? = null, + timeout: Long = DEFAULT_TIMEOUT +) : Interceptor { + companion object { + val DEFAULT_TIMEOUT: Long + } +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt new file mode 100644 index 00000000..676ac6fe --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt @@ -0,0 +1,10 @@ +package com.lagradost.cloudstream3.syncproviders + +enum class SyncIdName { + Anilist, + MyAnimeList, + Trakt, + Imdb, + Simkl, + LocalList, +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt new file mode 100644 index 00000000..374751a8 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -0,0 +1,24 @@ +package com.lagradost.cloudstream3.utils + +import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.mapper + +object AppUtils { + /** Any object as json string */ + fun Any.toJson(): String { + if (this is String) return this + return mapper.writeValueAsString(this) + } + + inline fun parseJson(value: String): T { + return mapper.readValue(value) + } + + inline fun tryParseJson(value: String?): T? { + return try { + parseJson(value ?: return null) + } catch (_: Exception) { + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt similarity index 97% rename from app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt index ce6e5ecc..566e29f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -1,9 +1,8 @@ package com.lagradost.cloudstream3.utils -import android.net.Uri import com.fasterxml.jackson.annotation.JsonIgnore +import com.lagradost.cloudstream3.IDownloadableMinimum import com.lagradost.cloudstream3.SubtitleFile -import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.extractors.AStreamHub @@ -431,7 +430,7 @@ open class ExtractorLink constructor( /** Used for getExtractorVerifierJob() */ open val extractorData: String? = null, open val type: ExtractorLinkType, -) : VideoDownloadManager.IDownloadableMinimum { +) : IDownloadableMinimum { val isM3u8: Boolean get() = type == ExtractorLinkType.M3U8 val isDash: Boolean get() = type == ExtractorLinkType.DASH @@ -530,29 +529,6 @@ open class ExtractorLink constructor( } } -data class ExtractorUri( - val uri: Uri, - val name: String, - - val basePath: String? = null, - val relativePath: String? = null, - val displayName: String? = null, - - val id: Int? = null, - val parentId: Int? = null, - val episode: Int? = null, - val season: Int? = null, - val headerName: String? = null, - val tvType: TvType? = null, -) - -data class ExtractorSubtitleLink( - val name: String, - override val url: String, - override val referer: String, - override val headers: Map = mapOf() -) : VideoDownloadManager.IDownloadableMinimum - /** * Removes https:// and www. * To match urls regardless of schema, perhaps Uri() can be used? diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt similarity index 100% rename from app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/M3u8Helper.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UnshortenUrl.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt similarity index 96% rename from app/src/main/java/com/lagradost/cloudstream3/utils/UnshortenUrl.kt rename to library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt index 46b232f6..b13e88e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UnshortenUrl.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/UnshortenUrl.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.utils -import android.util.Base64 import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.base64Decode import com.lagradost.nicehttp.NiceResponse @@ -91,13 +90,12 @@ object ShortLink { } val encodedbytearray = encodedUri.map { it.code.toByte() }.toByteArray() var decodedUri = - Base64.decode(encodedbytearray, Base64.DEFAULT).decodeToString().dropLast(16) + base64Decode(encodedbytearray.toString()).dropLast(16) .drop(16) if (Regex("""go\.php\?u=""").find(decodedUri) != null) { decodedUri = - Base64.decode(decodedUri.replace(Regex("""(.*?)u="""), ""), Base64.DEFAULT) - .decodeToString() + base64Decode(decodedUri.replace(Regex("""(.*?)u="""), "")) } return decodedUri diff --git a/library/src/jvmMain/kotlin/com/lagradost/api/ContextHelper.jvm.kt b/library/src/jvmMain/kotlin/com/lagradost/api/ContextHelper.jvm.kt new file mode 100644 index 00000000..a30810b8 --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/api/ContextHelper.jvm.kt @@ -0,0 +1,10 @@ +package com.lagradost.api + +import java.lang.ref.WeakReference + +actual fun getContext(): Any? { + return null +} + +actual fun setContext(context: WeakReference) { +} \ No newline at end of file diff --git a/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.jvm.kt b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.jvm.kt new file mode 100644 index 00000000..6b99ef3b --- /dev/null +++ b/library/src/jvmMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.jvm.kt @@ -0,0 +1,35 @@ +package com.lagradost.cloudstream3.network + +import okhttp3.Interceptor +import okhttp3.Response + +/** + * When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...) + * @param interceptUrl will stop the WebView when reaching this url. + * @param additionalUrls this will make resolveUsingWebView also return all other requests matching the list of Regex. + * @param userAgent if null then will use the default user agent + * @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare. + * @param script pass custom js to execute + * @param scriptCallback will be called with the result from custom js + * @param timeout close webview after timeout + * */ +actual class WebViewResolver actual constructor( + interceptUrl: Regex, + additionalUrls: List, + userAgent: String?, + useOkhttp: Boolean, + script: String?, + scriptCallback: ((String) -> Unit)?, + timeout: Long +) : + Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + return chain.proceed(request) + } + + actual companion object { + actual val DEFAULT_TIMEOUT = 60_000L + } +} From e5c9e96c8347cf31cc7ee25e2490cc70046167c3 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:33:21 +0200 Subject: [PATCH 392/441] fix filesystem --- .../commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt | 5 +++++ .../commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt | 6 ------ 2 files changed, 5 insertions(+), 6 deletions(-) delete mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index 47ef5382..aa08cb59 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -29,6 +29,11 @@ import kotlin.math.absoluteValue **/ const val AllLanguagesName = "universal" +const val USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" + +class ErrorLoadingException(message: String? = null) : Exception(message) + //val baseHeader = mapOf("User-Agent" to USER_AGENT) val mapper = JsonMapper.builder().addModule(kotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt deleted file mode 100644 index 160ff098..00000000 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainApi.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.lagradost.cloudstream3 - -const val USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" - -class ErrorLoadingException(message: String? = null) : Exception(message) \ No newline at end of file From c1b5f5c128859158a5ef022d0f16eb129b963651 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 4 Jul 2024 15:51:07 -0600 Subject: [PATCH 393/441] Fix download button display bug in adapter (#1175) --- .../ui/download/DownloadAdapter.kt | 22 ++++++-- .../ui/download/DownloadFragment.kt | 3 +- .../ui/download/button/PieFetchButton.kt | 50 +++++++++---------- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt index b4a16a66..9a026334 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.ViewGroup +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter @@ -13,9 +14,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull -import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.ui.download.button.DownloadStatusTell +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -135,8 +135,15 @@ class DownloadAdapter( downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes) // We will let the view model handle this downloadButton.doSetProgress = false + downloadButton.progressBar.progressDrawable = + downloadButton.getDrawableFromStatus(status) + ?.let { ContextCompat.getDrawable(downloadButton.context, it) } downloadHeaderInfo.text = formattedSizeString - } else downloadButton.doSetProgress = true + } else { + downloadButton.doSetProgress = true + downloadButton.progressBar.progressDrawable = + ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + } downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback) downloadButton.isVisible = true @@ -197,8 +204,15 @@ class DownloadAdapter( downloadButton.applyMetaData(d.id, card.currentBytes, card.totalBytes) // We will let the view model handle this downloadButton.doSetProgress = false + downloadButton.progressBar.progressDrawable = + downloadButton.getDrawableFromStatus(status) + ?.let { ContextCompat.getDrawable(downloadButton.context, it) } downloadChildEpisodeTextExtra.text = formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes) - } else downloadButton.doSetProgress = true + } else { + downloadButton.doSetProgress = true + downloadButton.progressBar.progressDrawable = + ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + } downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, mediaClickCallback) downloadButton.isVisible = true diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 82c5ffb8..23d546e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -40,7 +40,6 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult -import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE import com.lagradost.cloudstream3.utils.DataStore import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -273,4 +272,4 @@ class DownloadFragment : Fragment() { val selectedVideoUri = result?.data?.data ?: return@registerForActivityResult playUri(activity ?: return@registerForActivityResult, selectedVideoUri) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index a6dc5c56..abc159d0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -1,7 +1,6 @@ package com.lagradost.cloudstream3.ui.download.button import android.content.Context -import android.graphics.drawable.Drawable import android.os.Looper import android.util.AttributeSet import android.util.Log @@ -45,6 +44,8 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : private var iconPaused: Int = 0 private var hideWhenIcon: Boolean = true + var progressDrawable: Int = 0 + var overrideLayout: Int? = null companion object { @@ -115,10 +116,10 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : R.styleable.PieFetchButton_download_icon_complete, R.drawable.download_icon_done ) iconPaused = getResourceId( - R.styleable.PieFetchButton_download_icon_paused, 0//R.drawable.download_icon_pause + R.styleable.PieFetchButton_download_icon_paused, 0 // R.drawable.download_icon_pause ) iconActive = getResourceId( - R.styleable.PieFetchButton_download_icon_active, 0 //R.drawable.download_icon_load + R.styleable.PieFetchButton_download_icon_active, 0 // R.drawable.download_icon_load ) iconWaiting = getResourceId( R.styleable.PieFetchButton_download_icon_waiting, 0 @@ -129,7 +130,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : val fillIndex = getInt(R.styleable.PieFetchButton_download_fill, 0) - val progressDrawable = getResourceId( + progressDrawable = getResourceId( R.styleable.PieFetchButton_download_fill_override, fillArray[fillIndex] ) @@ -170,7 +171,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : if (isZeroBytes) { removeKey(KEY_RESUME_PACKAGES, card.id.toString()) callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card)) - //callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) + // callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data)) } else { val list = arrayListOf( Pair(DOWNLOAD_ACTION_PLAY_FILE, R.string.popup_play_file), @@ -197,7 +198,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : list ) { callback(DownloadClickEvent(itemId, card)) - //callback.invoke(DownloadClickEvent(itemId, data)) + // callback.invoke(DownloadClickEvent(itemId, data)) } } } @@ -205,7 +206,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : view.setOnLongClickListener { callback(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, card)) - //clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data)) + // clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data)) return@setOnLongClickListener true } } @@ -218,7 +219,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : setDefaultClickListener(this, textView, card, callback) } - /*open fun setDefaultClickListener(requestGetter: suspend BaseFetchButton.() -> List) { + /* open fun setDefaultClickListener(requestGetter: suspend BaseFetchButton.() -> List) { this.setOnClickListener { when (this.currentStatus) { null -> { @@ -244,7 +245,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : else -> {} } } - }*/ + } */ @MainThread private fun setStatusInternal(status : DownloadStatusTell?) { @@ -262,7 +263,8 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : progressBarBackground.background = ContextCompat.getDrawable(context, progressDrawable) - val drawable = getDrawableFromStatus(status) + val drawable = + getDrawableFromStatus(status)?.let { ContextCompat.getDrawable(this.context, it) } statusView.setImageDrawable(drawable) val isDrawable = drawable != null @@ -280,12 +282,12 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : override fun setStatus(status: DownloadStatusTell?) { currentStatus = status - // runs on the main thread, but also instant if it already is + // Runs on the main thread, but also instant if it already is if (Looper.myLooper() == Looper.getMainLooper()) { try { setStatusInternal(status) } catch (t : Throwable) { - logError(t) // just in case setStatusInternal throws because thread + logError(t) // Just in case setStatusInternal throws because thread progressBarBackground.post { setStatusInternal(status) } @@ -325,19 +327,13 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } } - open fun getDrawableFromStatus(status: DownloadStatusTell?): Drawable? { - val drawableInt = when (status) { - DownloadStatusTell.IsPaused -> iconPaused - DownloadStatusTell.IsPending -> iconWaiting - DownloadStatusTell.IsDownloading -> iconActive - DownloadStatusTell.IsFailed -> iconError - DownloadStatusTell.IsDone -> iconComplete - DownloadStatusTell.IsStopped -> iconRemoved - null -> iconInit - } - if (drawableInt == 0) { - return null - } - return ContextCompat.getDrawable(this.context, drawableInt) - } + open fun getDrawableFromStatus(status: DownloadStatusTell?): Int? = when (status) { + DownloadStatusTell.IsPaused -> iconPaused + DownloadStatusTell.IsPending -> iconWaiting + DownloadStatusTell.IsDownloading -> iconActive + DownloadStatusTell.IsFailed -> iconError + DownloadStatusTell.IsDone -> iconComplete + DownloadStatusTell.IsStopped -> iconRemoved + else -> iconInit + }.takeIf { it != 0 } } \ No newline at end of file From e1d4a46309f8a979327e5b5486f2f8753f0a0639 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:26:44 +0200 Subject: [PATCH 394/441] bugfix on lib startup --- .../lagradost/cloudstream3/MainActivity.kt | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a47e7685..59f499c5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1112,23 +1112,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa MainAPI.settingsForProvider = settingsForProvider - // Change library icon with logo of current api in sync - libraryViewModel = ViewModelProvider(this)[LibraryViewModel::class.java] - libraryViewModel?.currentApiName?.observe(this) { - val syncAPI = libraryViewModel?.currentSyncApi - Log.i("SYNC_API", "${syncAPI?.name}, ${syncAPI?.idPrefix}") - val icon = if (syncAPI?.idPrefix == localListApi.idPrefix) { - R.drawable.library_icon - } else { - syncAPI?.icon ?: R.drawable.library_icon - } - - binding?.apply { - navRailView.menu.findItem(R.id.navigation_library)?.setIcon(icon) - navView.menu.findItem(R.id.navigation_library)?.setIcon(icon) - } - } - loadThemes(this) updateLocale() super.onCreate(savedInstanceState) @@ -1538,6 +1521,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa logError(e) } } + + // we need to run this after we init all apis, otherwise currentSyncApi will fuck itself + this@MainActivity.runOnUiThread { + // Change library icon with logo of current api in sync + libraryViewModel = ViewModelProvider(this@MainActivity)[LibraryViewModel::class.java] + libraryViewModel?.currentApiName?.observe(this@MainActivity) { + val syncAPI = libraryViewModel?.currentSyncApi + Log.i("SYNC_API", "${syncAPI?.name}, ${syncAPI?.idPrefix}") + val icon = if (syncAPI?.idPrefix == localListApi.idPrefix) { + R.drawable.library_icon + } else { + syncAPI?.icon ?: R.drawable.library_icon + } + + binding?.apply { + navRailView.menu.findItem(R.id.navigation_library)?.setIcon(icon) + navView.menu.findItem(R.id.navigation_library)?.setIcon(icon) + } + } + } } SearchResultBuilder.updateCache(this) From 699a6979a5d6a924859d5dff122de34389a100a7 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 5 Jul 2024 19:04:32 +0300 Subject: [PATCH 395/441] feat(TV UI): Fix clone site focus (#1179) --- app/src/main/res/layout/add_remove_sites.xml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/layout/add_remove_sites.xml b/app/src/main/res/layout/add_remove_sites.xml index 9ef6ad6a..653f607f 100644 --- a/app/src/main/res/layout/add_remove_sites.xml +++ b/app/src/main/res/layout/add_remove_sites.xml @@ -1,19 +1,21 @@ + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + android:id="@+id/add_site" + android:text="@string/add_site_pref" + android:focusable="true" + style="@style/SettingsItem"> + android:id="@+id/remove_site" + android:text="@string/remove_site_pref" + android:focusable="true" + style="@style/SettingsItem" /> \ No newline at end of file From 9b1ac5fc28774585f207acd0a5444cc9d09933b6 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 5 Jul 2024 19:05:32 +0300 Subject: [PATCH 396/441] feat(Trakt): Skip specials season for next airing (#1181) --- .../com/lagradost/cloudstream3/metaproviders/TraktProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 736e05f2..7c375e0a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -238,7 +238,7 @@ open class TraktProvider : MainAPI() { description = episode.overview, ).apply { this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") - if (nextAir == null && this.date != null && this.date!! > unixTimeMS) { + if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) { nextAir = NextAiring( episode = this.episode!!, unixTime = this.date!!.div(1000L), From 145c42f1c8bdbd53a18733786778be6ff9f77d2b Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 5 Jul 2024 19:10:58 +0300 Subject: [PATCH 397/441] feat(UI): Use same Episode holder size (#1180) --- .../cloudstream3/ui/result/EpisodeAdapter.kt | 7 +++---- app/src/main/res/layout/result_episode.xml | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) 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 0a1b777d..ed5e51f1 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 @@ -57,8 +57,7 @@ const val ACTION_PLAY_EPISODE_IN_MPV = 17 const val ACTION_MARK_AS_WATCHED = 18 const val ACTION_FCAST = 19 -const val TV_EP_SIZE_LARGE = 400 -const val TV_EP_SIZE_SMALL = 300 +const val TV_EP_SIZE = 400 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( @@ -181,7 +180,7 @@ class EpisodeAdapter( fun bind(card: ResultEpisode) { localCard = card val setWidth = - if (isLayout(TV or EMULATOR)) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT + if (isLayout(TV or EMULATOR)) TV_EP_SIZE.toPx else ViewGroup.LayoutParams.MATCH_PARENT binding.episodeLinHolder.layoutParams.width = setWidth binding.episodeHolderLarge.layoutParams.width = setWidth @@ -336,7 +335,7 @@ class EpisodeAdapter( fun bind(card: ResultEpisode) { binding.episodeHolder.layoutParams.apply { width = - if (isLayout(TV or EMULATOR)) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT + if (isLayout(TV or EMULATOR)) TV_EP_SIZE.toPx else ViewGroup.LayoutParams.MATCH_PARENT } binding.apply { diff --git a/app/src/main/res/layout/result_episode.xml b/app/src/main/res/layout/result_episode.xml index b56cdb1d..36d60bd6 100644 --- a/app/src/main/res/layout/result_episode.xml +++ b/app/src/main/res/layout/result_episode.xml @@ -90,14 +90,15 @@ android:textColor="?attr/textColor" tools:text="Episode 1" /> - - + + + \ No newline at end of file From e86c926c30d565841d0701adac937d48dc9a8d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Mon, 8 Jul 2024 23:59:02 +0300 Subject: [PATCH 398/441] Extractor: added Pichive & Sobreatsesuyp (#1184) --- .../extractors/HotlingerExtractor.kt | 5 ++ .../extractors/SobreatsesuypExtractor.kt | 56 +++++++++++++++++++ .../cloudstream3/utils/ExtractorApi.kt | 4 ++ 3 files changed, 65 insertions(+) create mode 100644 library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt index db721108..11f8ccaf 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HotlingerExtractor.kt @@ -22,6 +22,11 @@ class FourPlayRu : ContentX() { override var mainUrl = "https://four.playru.net" } +class Pichive : ContentX() { + override var name = "Pichive" + override var mainUrl = "https://pichive.online" +} + class FourPichive : ContentX() { override var name = "FourPichive" override var mainUrl = "https://four.pichive.online" diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt new file mode 100644 index 00000000..91b60dac --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt @@ -0,0 +1,56 @@ +// ! Bu araç @keyiflerolsun tarafından | @KekikAkademi için yazılmıştır. + +package com.lagradost.cloudstream3.extractors + +import android.util.Log +import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.utils.* +import com.fasterxml.jackson.annotation.JsonProperty + +open class Sobreatsesuyp : ExtractorApi() { + override val name = "Sobreatsesuyp" + override val mainUrl = "https://sobreatsesuyp.com" + override val requiresReferer = true + + override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { + val extRef = referer ?: "" + + val videoReq = app.get(url, referer = extRef).text + + val file = Regex("""file\":\"([^\"]+)""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val postLink = "${mainUrl}/" + file.replace("\\", "") + val rawList = app.post(postLink, referer = extRef).parsedSafe>() ?: throw ErrorLoadingException("Post link not found") + + val postJson: List = rawList.drop(1).map { item -> + val mapItem = item as Map<*, *> + SobreatsesuypVideoData( + title = mapItem["title"] as? String, + file = mapItem["file"] as? String + ) + } + Log.d("Kekik_${this.name}", "postJson » ${postJson}") + + for (item in postJson) { + if (item.file == null || item.title == null) continue + + val fileUrl = "${mainUrl}/playlist/${item.file.substring(1)}.txt" + val videoData = app.post(fileUrl, referer = extRef).text + + callback.invoke( + ExtractorLink( + source = this.name, + name = "${this.name} - ${item.title}", + url = videoData, + referer = extRef, + quality = Qualities.Unknown.value, + type = INFER_TYPE + ) + ) + } + } + + data class SobreatsesuypVideoData( + @JsonProperty("title") val title: String? = null, + @JsonProperty("file") val file: String? = null + ) +} \ 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 566e29f0..0df73a0e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -115,6 +115,7 @@ import com.lagradost.cloudstream3.extractors.Hotlinger import com.lagradost.cloudstream3.extractors.FourCX import com.lagradost.cloudstream3.extractors.PlayRu import com.lagradost.cloudstream3.extractors.FourPlayRu +import com.lagradost.cloudstream3.extractors.Pichive import com.lagradost.cloudstream3.extractors.FourPichive import com.lagradost.cloudstream3.extractors.HDMomPlayer import com.lagradost.cloudstream3.extractors.HDPlayerSystem @@ -124,6 +125,7 @@ import com.lagradost.cloudstream3.extractors.HDStreamAble import com.lagradost.cloudstream3.extractors.RapidVid import com.lagradost.cloudstream3.extractors.TRsTX import com.lagradost.cloudstream3.extractors.VidMoxy +import com.lagradost.cloudstream3.extractors.Sobreatsesuyp import com.lagradost.cloudstream3.extractors.PixelDrain import com.lagradost.cloudstream3.extractors.MailRu import com.lagradost.cloudstream3.extractors.Mediafire @@ -734,6 +736,7 @@ val extractorApis: MutableList = arrayListOf( FourCX(), PlayRu(), FourPlayRu(), + Pichive(), FourPichive(), HDMomPlayer(), HDPlayerSystem(), @@ -743,6 +746,7 @@ val extractorApis: MutableList = arrayListOf( RapidVid(), TRsTX(), VidMoxy(), + Sobreatsesuyp(), PixelDrain(), MailRu(), From 8be8e5474647e5aeb31cb1026fe2e350dbf3c139 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:17:25 +0200 Subject: [PATCH 399/441] Fixed log --- .../cloudstream3/extractors/SobreatsesuypExtractor.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt index 91b60dac..c90b22f4 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SobreatsesuypExtractor.kt @@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.extractors -import android.util.Log import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.utils.* import com.fasterxml.jackson.annotation.JsonProperty @@ -28,15 +27,13 @@ open class Sobreatsesuyp : ExtractorApi() { file = mapItem["file"] as? String ) } - Log.d("Kekik_${this.name}", "postJson » ${postJson}") for (item in postJson) { if (item.file == null || item.title == null) continue - val fileUrl = "${mainUrl}/playlist/${item.file.substring(1)}.txt" - val videoData = app.post(fileUrl, referer = extRef).text + val videoData = app.post("${mainUrl}/playlist/${item.file.substring(1)}.txt", referer = extRef).text - callback.invoke( + callback.invoke( ExtractorLink( source = this.name, name = "${this.name} - ${item.title}", From febb843424e0331b63b2e26ad796e797f7267ccc Mon Sep 17 00:00:00 2001 From: RowdyRushya Date: Mon, 15 Jul 2024 08:06:20 -0700 Subject: [PATCH 400/441] Fix VidSrcTo extractor (#1198) --- .../cloudstream3/extractors/Chillx.kt | 29 +++++++++---------- .../cloudstream3/extractors/VidSrcTo.kt | 14 ++++++++- .../cloudstream3/extractors/Vidplay.kt | 27 +++++++++++++---- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt index 26567c7a..dd22efb2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -6,6 +6,7 @@ import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +import kotlin.run class Moviesapi : Chillx() { override val name = "Moviesapi" @@ -28,17 +29,22 @@ open class Chillx : ExtractorApi() { override val requiresReferer = true companion object { + private val keySource = "https://rowdy-avocado.github.io/multi-keys/" + private var key: String? = null - suspend fun fetchKey(): String { - return if (key != null) { - key!! - } else { - val fetch = app.get("https://raw.githubusercontent.com/rushi-chavan/multi-keys/keys/keys.json").parsedSafe()?.key?.get(0) ?: throw ErrorLoadingException("Unable to get key") - key = fetch - key!! - } + private suspend fun fetchKey(): String { + return key + ?: run { + val res = + app.get(keySource).parsedSafe() + ?: throw ErrorLoadingException("Unable to get keys") + key = res.keys.get(0) + res.keys.get(0) + } } + + private data class KeysData(@JsonProperty("chillx") val keys: List) } @Suppress("NAME_SHADOWING") @@ -97,11 +103,4 @@ open class Chillx : ExtractorApi() { it.groupValues[1].toInt(16).toChar().toString() } } - - - - data class Keys( - @JsonProperty("chillx") val key: List - ) - } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt index 73857fb3..578f5fb9 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -26,7 +26,13 @@ class VidSrcTo : ExtractorApi() { callback: (ExtractorLink) -> Unit ) { val mediaId = app.get(url).document.selectFirst("ul.episodes li a")?.attr("data-id") ?: return - val res = app.get("$mainUrl/ajax/embed/episode/$mediaId/sources").parsedSafe() ?: return + val subtitlesLink = "$mainUrl/ajax/embed/episode/$mediaId/subtitles" + val subRes = app.get(subtitlesLink).parsedSafe>() + subRes?.forEach { + if (it.kind.equals("captions")) subtitleCallback.invoke(SubtitleFile(it.label, it.file)) + } + val sourcesLink = "$mainUrl/ajax/embed/episode/$mediaId/sources" + val res = app.get(sourcesLink).parsedSafe() ?: return if (res.status != 200) return res.result?.amap { source -> try { @@ -68,5 +74,11 @@ class VidSrcTo : ExtractorApi() { @JsonProperty("result") val result: VidsrctoUrl ) + data class VidsrctoSubtitles( + @JsonProperty("file") val file: String, + @JsonProperty("label") val label: String, + @JsonProperty("kind") val kind: String + ) + data class VidsrctoUrl(@JsonProperty("url") val encUrl: String) } diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt index cb9eaf1e..6202800f 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.extractors import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.base64Encode @@ -9,6 +10,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec +import kotlin.run // Code found in https://github.com/KillerDogeEmpire/vidplay-keys // special credits to @KillerDogeEmpire for providing key @@ -35,8 +37,25 @@ open class Vidplay : ExtractorApi() { override val name = "Vidplay" override val mainUrl = "https://vidplay.site" override val requiresReferer = true - open val key = - "https://raw.githubusercontent.com/KillerDogeEmpire/vidplay-keys/keys/keys.json" + + companion object { + private val keySource = "https://rowdy-avocado.github.io/multi-keys/" + + private var keys: List? = null + + private suspend fun getKeys(): List { + return keys + ?: run { + val res = + app.get(keySource).parsedSafe() + ?: throw ErrorLoadingException("Unable to get keys") + keys = res.keys + res.keys + } + } + + private data class KeysData(@JsonProperty("vidplay") val keys: List) + } override suspend fun getUrl( url: String, @@ -70,10 +89,6 @@ open class Vidplay : ExtractorApi() { } - private suspend fun getKeys(): List { - return app.get(key).parsed() - } - private suspend fun callFutoken(id: String, url: String): String? { val script = app.get("$mainUrl/futoken", referer = url).text val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null From 694193fa3eeada9388d68be521822ecf4f659bd4 Mon Sep 17 00:00:00 2001 From: IndusAryan <125901294+IndusAryan@users.noreply.github.com> Date: Mon, 15 Jul 2024 20:40:41 +0530 Subject: [PATCH 401/441] refactor(fix): result sync, fix slider theme and trailer fix (#1187) --- app/build.gradle.kts | 24 +- app/src/main/res/layout/result_sync.xml | 397 ++++++++++-------------- 2 files changed, 168 insertions(+), 253 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ebefa0ea..6e439d53 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -157,16 +157,16 @@ dependencies { testImplementation("junit:junit:4.13.2") testImplementation("org.json:json:20240303") androidTestImplementation("androidx.test:core") - implementation("androidx.test.ext:junit-ktx:1.1.5") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation("androidx.test.ext:junit-ktx:1.2.1") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") // Android Core & Lifecycle implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.navigation:navigation-ui-ktx:2.7.7") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.2") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.2") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.3") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3") implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") // Design & UI @@ -182,9 +182,9 @@ dependencies { implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0") // For KSP -> Official Annotation Processors are Not Yet Supported for KSP - ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") - implementation("com.google.guava:guava:33.2.0-android") - implementation("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") + ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") + implementation("com.google.guava:guava:33.2.1-android") + implementation("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") // Media 3 (ExoPlayer) implementation("androidx.media3:media3-ui:1.1.1") @@ -200,9 +200,9 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:592f159") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ - implementation("com.github.albfernandez:juniversalchardet:2.4.0") // Subtitle Decoding + implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding // Crash Reports (AcraApplication.kt) implementation("ch.acra:acra-core:5.11.3") @@ -215,14 +215,14 @@ dependencies { implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview - implementation("io.github.g0dkar:qrcode-kotlin:4.1.1") // QR code for PIN Auth on TV + implementation("io.github.g0dkar:qrcode-kotlin:4.2.0") // QR code for PIN Auth on TV // Extensions & Other Libs implementation("org.mozilla:rhino:1.7.15") // run JavaScript implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9 - implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0") // TMDB API v3 Wrapper Made with RetroFit + implementation("com.uwetrottmann.tmdb2:tmdb-java:2.11.0") // TMDB API v3 Wrapper Made with RetroFit coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API diff --git a/app/src/main/res/layout/result_sync.xml b/app/src/main/res/layout/result_sync.xml index 9cde195c..8b7b33c0 100644 --- a/app/src/main/res/layout/result_sync.xml +++ b/app/src/main/res/layout/result_sync.xml @@ -1,306 +1,221 @@ + android:layout_width="match_parent" + android:layout_height="match_parent"> + android:id="@+id/result_sync_holder" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:visibility="gone" + tools:visibility="visible"> + + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + android:id="@+id/result_sync_names" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:text="MyAnimeList, AniList" + android:textSize="16sp" + android:textStyle="bold" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:visibility="visible"> + android:id="@+id/result_sync_sub_episode" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end|center_vertical" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:padding="10dp" + android:src="@drawable/baseline_remove_24" + app:tint="?attr/textColor" /> + android:id="@+id/result_sync_current_episodes" + style="@style/AppEditStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:inputType="number" + android:textColorHint="?attr/grayTextColor" + android:textSize="20sp" + tools:hint="20" + tools:ignore="LabelFor" /> + android:id="@+id/result_sync_max_episodes" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:paddingBottom="1dp" + android:textColor="?attr/textColor" + android:textSize="20sp" + tools:text="30" /> + android:id="@+id/result_sync_add_episode" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end|center_vertical" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:padding="10dp" + android:src="@drawable/ic_baseline_add_24" + app:tint="?attr/textColor" /> - - + android:id="@+id/result_sync_episodes" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="20dp" + android:layout_gravity="end|center_vertical" + android:indeterminate="false" + android:max="100" + android:padding="10dp" + android:progress="0" + android:progressBackgroundTint="?attr/colorPrimary" + tools:visibility="visible" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:padding="10dp" + android:text="@string/sync_score" + android:textColor="?attr/textColor" + android:textSize="17sp" /> + style="@style/BlackButton" + android:layout_width="wrap_content" + android:layout_height="30dp" + android:layout_gravity="center_vertical" + android:layout_marginStart="0dp" + android:minWidth="0dp" + android:text="7/10" /> + android:id="@+id/result_sync_rating" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="-5dp" + android:layout_marginEnd="-5dp" + app:thumbHeight="20dp" + android:stepSize="1" + android:value="4" + android:valueFrom="0" + android:valueTo="10" + app:labelStyle="@style/BlackLabel" + app:thumbRadius="10dp" + app:tickVisible="false" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingTop="12dp" + android:paddingBottom="12dp" + android:visibility="gone"> + android:id="@+id/home_parent_item_title" + style="@style/WatchHeaderText" + tools:text="Recommended" /> + android:layout_width="30dp" + android:layout_height="match_parent" + android:layout_gravity="end|center_vertical" + android:layout_marginEnd="5dp" + android:contentDescription="@string/home_more_info" + android:src="@drawable/ic_baseline_arrow_forward_24" + app:tint="?attr/textColor" /> + android:id="@+id/result_sync_check" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_rowWeight="1" + tools:listitem="@layout/sort_bottom_single_choice" /> + style="@style/WhiteButton" + android:layout_width="match_parent" + android:layout_marginTop="10dp" + android:text="@string/type_watching" + android:visibility="gone" /> + android:id="@+id/result_sync_set_score" + style="@style/BlackButton" + android:layout_width="match_parent" + android:layout_marginTop="10dp" + android:text="@string/upload_sync" + app:icon="@drawable/baseline_sync_24" /> + android:id="@+id/result_sync_loading_shimmer" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:orientation="vertical" + android:padding="15dp" + app:shimmer_auto_start="true" + app:shimmer_base_alpha="0.2" + app:shimmer_duration="@integer/loading_time" + app:shimmer_highlight_alpha="0.3" + tools:visibility="gone" + tools:ignore="MissingClass"> + + - - + android:layout_height="30dp" /> + android:layout_width="match_parent" + android:layout_height="30dp" /> + android:layout_width="match_parent" + android:layout_height="30dp" /> @@ -313,8 +228,8 @@ + android:layout_width="match_parent" + android:layout_height="30dp" /> From a157115cfac1ef3f3c532198a931873d6cee9097 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Mon, 15 Jul 2024 18:15:59 +0300 Subject: [PATCH 402/441] feat(Subtitles): SubSource subtitles provider (#1199) --- .../syncproviders/AccountManager.kt | 4 +- .../syncproviders/providers/SubSource.kt | 158 ++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index e86d73aa..0259ccad 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -22,6 +22,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { val addic7ed = Addic7ed() val subDlApi = SubDlApi(0) val localListApi = LocalList() + val subSourceApi = SubSourceApi() // used to login via app intent val OAuth2Apis @@ -51,7 +52,8 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { get() = listOf( openSubtitlesApi, addic7ed, - subDlApi + subDlApi, + subSourceApi ) const val appString = "cloudstreamapp" diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt new file mode 100644 index 00000000..0e233ece --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt @@ -0,0 +1,158 @@ +package com.lagradost.cloudstream3.syncproviders.providers + +import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.subtitles.AbstractSubProvider +import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities +import com.lagradost.cloudstream3.subtitles.SubtitleResource +import com.lagradost.cloudstream3.utils.AppUtils.parseJson +import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.SubtitleHelper + +class SubSourceApi : AbstractSubProvider { + override val idPrefix = "subsource" + val name = "SubSource" + + companion object { + const val APIURL = "https://api.subsource.net/api" + const val DOWNLOADENDPOINT = "https://api.subsource.net/api/downloadSub" + } + + override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List? { + + //Only supports Imdb Id search for now + if (query.imdbId == null) return null + val queryLang = SubtitleHelper.fromTwoLettersToLanguage(query.lang!!) + val type = if ((query.seasonNumber ?: 0) > 0) TvType.TvSeries else TvType.Movie + + val searchRes = app.post( + url = "$APIURL/searchMovie", + data = mapOf( + "query" to query.imdbId!! + ) + ).parsedSafe() ?: return null + + val postData = if (type == TvType.TvSeries) { + mapOf( + "langs" to "[]", + "movieName" to searchRes.found.first().linkName, + "season" to "season-${query.seasonNumber}" + ) + } else { + mapOf( + "langs" to "[]", + "movieName" to searchRes.found.first().linkName, + ) + } + + val getMovieRes = app.post( + url = "$APIURL/getMovie", + data = postData + ).parsedSafe().let { + // api doesn't has episode number or lang filtering + if (type == TvType.Movie) { + it?.subs?.filter { sub -> + sub.lang == queryLang + } + } else { + it?.subs?.filter { sub -> + sub.releaseName!!.contains( + String.format( + "E%02d", + query.epNumber + ) + ) && sub.lang == queryLang + } + } + } ?: return null + + return getMovieRes.map { subtitle -> + AbstractSubtitleEntities.SubtitleEntity( + idPrefix = this.idPrefix, + name = subtitle.releaseName!!, + lang = subtitle.lang!!, + data = SubData( + movie = subtitle.linkName!!, + lang = subtitle.lang, + id = subtitle.subId.toString(), + ).toJson(), + type = type, + source = this.name, + epNumber = query.epNumber, + seasonNumber = query.seasonNumber, + isHearingImpaired = subtitle.hi == 1, + ) + } + } + + override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { + + val parsedSub = parseJson(data.data) + + val subRes = app.post( + url = "$APIURL/getSub", + data = mapOf( + "movie" to parsedSub.movie, + "lang" to data.lang, + "id" to parsedSub.id + ) + ).parsedSafe() ?: return + + this.addZipUrl( + "$DOWNLOADENDPOINT/${subRes.sub.downloadToken}" + ) { name, _ -> + name + } + } + + data class ApiSearch( + @JsonProperty("success") val success: Boolean, + @JsonProperty("found") val found: List, + ) + + data class Found( + @JsonProperty("id") val id: Long, + @JsonProperty("title") val title: String, + @JsonProperty("seasons") val seasons: Long, + @JsonProperty("type") val type: String, + @JsonProperty("releaseYear") val releaseYear: Long, + @JsonProperty("linkName") val linkName: String, + ) + + data class ApiResponse( + @JsonProperty("success") val success: Boolean, + @JsonProperty("movie") val movie: Movie, + @JsonProperty("subs") val subs: List, + ) + + data class Movie( + @JsonProperty("id") val id: Long? = null, + @JsonProperty("type") val type: String? = null, + @JsonProperty("year") val year: Long? = null, + @JsonProperty("fullName") val fullName: String? = null, + ) + + data class Sub( + @JsonProperty("hi") val hi: Int? = null, + @JsonProperty("fullLink") val fullLink: String? = null, + @JsonProperty("linkName") val linkName: String? = null, + @JsonProperty("lang") val lang: String? = null, + @JsonProperty("releaseName") val releaseName: String? = null, + @JsonProperty("subId") val subId: Long? = null, + ) + + data class SubData( + @JsonProperty("movie") val movie: String, + @JsonProperty("lang") val lang: String, + @JsonProperty("id") val id: String, + ) + + data class SubTitleLink( + @JsonProperty("sub") val sub: SubToken, + ) + + data class SubToken( + @JsonProperty("downloadToken") val downloadToken: String, + ) +} \ No newline at end of file From 627dd453093f3246afbc606d83fa13284ce16e5e Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Thu, 18 Jul 2024 02:02:35 +0200 Subject: [PATCH 403/441] 0bytes downloads fix --- .../utils/VideoDownloadManager.kt | 42 +++++++++++++++---- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index f3cbdaf1..197bacc6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -546,7 +546,8 @@ object VideoDownloadManager { tryResume: Boolean, ): StreamData { return setupStream( - context.getBasePath().first ?: getDefaultDir(context) ?: throw IOException("Bad config"), + context.getBasePath().first ?: getDefaultDir(context) + ?: throw IOException("Bad config"), name, folder, extension, @@ -945,7 +946,7 @@ object VideoDownloadManager { bufferSize: Int = DEFAULT_BUFFER_SIZE, /** how many bytes bytes it should require to use the parallel downloader instead, * if we download a very small file we don't want it parallel */ - maximumSmallSize : Long = chuckSize * 2 + maximumSmallSize: Long = chuckSize * 2 ): LazyStreamDownloadData { // we don't want to make a separate connection for every 1kb require(chuckSize > 1000) @@ -1028,7 +1029,10 @@ object VideoDownloadManager { tryResume: Boolean, parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, - parallelConnections: Int = 3 + parallelConnections: Int = 3, + /** how many bytes a valid file must be in bytes, + * this should be different for subtitles and video */ + minimumSize: Long = 100 ): DownloadStatus = withContext(Dispatchers.IO) { if (parallelConnections < 1) { return@withContext DOWNLOAD_INVALID_INPUT @@ -1074,6 +1078,13 @@ object VideoDownloadManager { ) ) + if (items.totalLength != null && items.totalLength < minimumSize) { + fileStream.closeQuietly() + metadata.onDelete() + stream.delete() + return@withContext DOWNLOAD_INVALID_INPUT + } + metadata.totalBytes = items.totalLength metadata.type = DownloadType.IsDownloading metadata.setDownloadFileInfoTemplate( @@ -1223,6 +1234,16 @@ object VideoDownloadManager { return@withContext DOWNLOAD_STOPPED } + // in case the head request lies about content-size, + // then we don't want shit output + if (metadata.bytesDownloaded < minimumSize) { + // we need to close before delete + fileStream.closeQuietly() + metadata.onDelete() + stream.delete() + return@withContext DOWNLOAD_INVALID_INPUT + } + metadata.type = DownloadType.IsDone return@withContext DOWNLOAD_SUCCESS } catch (e: IOException) { @@ -1274,6 +1295,7 @@ object VideoDownloadManager { val displayName = getDisplayName(name, extension) val stream = setupStream(baseFile, name, folder, extension, startAt > 0) + if (!stream.resume) startAt = 0 fileStream = stream.open() @@ -1300,6 +1322,7 @@ object VideoDownloadManager { ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() ) ) + val items = M3u8Helper2.hslLazy(listOf(m3u8)) metadata.hlsTotal = items.size @@ -1397,7 +1420,7 @@ object VideoDownloadManager { try { // may cause java.lang.IllegalStateException: Mutex is not locked because of cancelling fileMutex.unlock() - } catch (t : Throwable) { + } catch (t: Throwable) { logError(t) } } @@ -1524,7 +1547,7 @@ object VideoDownloadManager { tryResume: Boolean = false, ): DownloadStatus { // no support for these file formats - if(link.type == ExtractorLinkType.MAGNET || link.type == ExtractorLinkType.TORRENT || link.type == ExtractorLinkType.DASH) { + if (link.type == ExtractorLinkType.MAGNET || link.type == ExtractorLinkType.TORRENT || link.type == ExtractorLinkType.DASH) { return DOWNLOAD_INVALID_INPUT } @@ -1556,7 +1579,7 @@ object VideoDownloadManager { } try { - when(link.type) { + when (link.type) { ExtractorLinkType.M3U8 -> { val startIndex = if (tryResume) { context.getKey( @@ -1576,6 +1599,7 @@ object VideoDownloadManager { callback, parallelConnections = maxConcurrentConnections ) } + ExtractorLinkType.VIDEO -> { return downloadThing( context, @@ -1585,9 +1609,13 @@ object VideoDownloadManager { "mp4", tryResume, ep.id, - callback, parallelConnections = maxConcurrentConnections + callback, + parallelConnections = maxConcurrentConnections, + /** We require at least 10 MB video files */ + minimumSize = (1 shl 20) * 10 ) } + else -> throw IllegalArgumentException("unsuported download type") } } catch (t: Throwable) { From 12de92455960f012bb0298723127061b90932327 Mon Sep 17 00:00:00 2001 From: RowdyRushya Date: Fri, 19 Jul 2024 09:10:34 -0700 Subject: [PATCH 404/441] updating vidplay encryption method (#1202) --- .../cloudstream3/extractors/VidSrcTo.kt | 2 +- .../cloudstream3/extractors/Vidplay.kt | 101 ++++++++---------- 2 files changed, 43 insertions(+), 60 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt index 578f5fb9..e974f23a 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcTo.kt @@ -40,7 +40,7 @@ class VidSrcTo : ExtractorApi() { val finalUrl = DecryptUrl(embedRes.result.encUrl) if(finalUrl.equals(embedRes.result.encUrl)) return@amap when (source.title) { - "Vidplay" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) + "F2Cloud" -> AnyVidplay(finalUrl.substringBefore("/e/")).getUrl(finalUrl, referer, subtitleCallback, callback) "Filemoon" -> FileMoon().getUrl(finalUrl, referer, subtitleCallback, callback) } } catch (e: Exception) { diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt index 6202800f..d7e7ce18 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidplay.kt @@ -4,13 +4,14 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.base64Encode +import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper +import java.net.URLDecoder import javax.crypto.Cipher import javax.crypto.spec.SecretKeySpec -import kotlin.run +import kotlin.io.encoding.Base64 // Code found in https://github.com/KillerDogeEmpire/vidplay-keys // special credits to @KillerDogeEmpire for providing key @@ -33,6 +34,7 @@ class VidplayOnline : Vidplay() { override val mainUrl = "https://vidplay.online" } +@OptIn(kotlin.io.encoding.ExperimentalEncodingApi::class) open class Vidplay : ExtractorApi() { override val name = "Vidplay" override val mainUrl = "https://vidplay.site" @@ -58,83 +60,64 @@ open class Vidplay : ExtractorApi() { } override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit ) { + val myKeys = getKeys() + val domain = url.substringBefore("/e/") val id = url.substringBefore("?").substringAfterLast("/") - val encodeId = encodeId(id, getKeys()) - val mediaUrl = callFutoken(encodeId, url) - val res = app.get( - "$mediaUrl", headers = mapOf( - "Accept" to "application/json, text/javascript, */*; q=0.01", - "X-Requested-With" to "XMLHttpRequest", - ), referer = url - ).parsedSafe()?.result - + val encodedId = encode(id, myKeys.get(0)) + val t = url.substringAfter("t=").substringBefore("&") + val h = encode(id, myKeys.get(1)) + val mediaUrl = "$domain/mediainfo/$encodedId?t=$t&h=$h" + val encodedRes = + app.get("$mediaUrl").parsedSafe()?.result + ?: throw Exception("Unable to fetch link") + val decodedRes = decode(encodedRes, myKeys.get(2)) + val res = tryParseJson(decodedRes) res?.sources?.map { - M3u8Helper.generateM3u8( - this.name, - it.file ?: return@map, - "$mainUrl/" - ).forEach(callback) + M3u8Helper.generateM3u8(this.name, it.file ?: return@map, "$mainUrl/").forEach(callback) } res?.tracks?.filter { it.kind == "captions" }?.map { - subtitleCallback.invoke( - SubtitleFile(it.label ?: return@map, it.file ?: return@map) - ) + subtitleCallback.invoke(SubtitleFile(it.label ?: return@map, it.file ?: return@map)) } - } - private suspend fun callFutoken(id: String, url: String): String? { - val script = app.get("$mainUrl/futoken", referer = url).text - val k = "k='(\\S+)'".toRegex().find(script)?.groupValues?.get(1) ?: return null - val a = mutableListOf(k) - for (i in id.indices) { - a.add((k[i % k.length].code + id[i].code).toString()) - } - return "$mainUrl/mediainfo/${a.joinToString(",")}?${url.substringAfter("?")}" + private fun encode(input: String, key: String): String { + val rc4Key = SecretKeySpec(key.toByteArray(Charsets.UTF_8), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.ENCRYPT_MODE, rc4Key) + val encryptedBytes = cipher.doFinal(input.toByteArray(Charsets.UTF_8)) + return Base64.UrlSafe.encode(encryptedBytes) } - private fun encodeId(id: String, keyList: List): String { - val cipher1 = Cipher.getInstance("RC4") - val cipher2 = Cipher.getInstance("RC4") - cipher1.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(keyList[0].toByteArray(), "RC4"), - cipher1.parameters - ) - cipher2.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(keyList[1].toByteArray(), "RC4"), - cipher2.parameters - ) - var input = id.toByteArray() - input = cipher1.doFinal(input) - input = cipher2.doFinal(input) - return base64Encode(input).replace("/", "_") + fun decode(input: String, key: String): String { + val decodedBytes = Base64.UrlSafe.decode(input) + val rc4Key = SecretKeySpec(key.toByteArray(Charsets.UTF_8), "RC4") + val cipher = Cipher.getInstance("RC4") + cipher.init(Cipher.DECRYPT_MODE, rc4Key) + val decryptedBytes = cipher.doFinal(decodedBytes) + val decodedString = String(decryptedBytes, Charsets.UTF_8) + return URLDecoder.decode(decodedString, "UTF-8") } data class Tracks( - @JsonProperty("file") val file: String? = null, - @JsonProperty("label") val label: String? = null, - @JsonProperty("kind") val kind: String? = null, + @JsonProperty("file") val file: String? = null, + @JsonProperty("label") val label: String? = null, + @JsonProperty("kind") val kind: String? = null, ) data class Sources( - @JsonProperty("file") val file: String? = null, + @JsonProperty("file") val file: String? = null, ) data class Result( - @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), - @JsonProperty("tracks") val tracks: ArrayList? = arrayListOf(), - ) - - data class Response( - @JsonProperty("result") val result: Result? = null, + @JsonProperty("sources") val sources: ArrayList? = arrayListOf(), + @JsonProperty("tracks") val tracks: ArrayList? = arrayListOf(), ) + data class Response(@JsonProperty("result") val result: String? = null) } From 63465ed7a9cfb2121eb91015671872f9a14deefd Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:24:06 +0200 Subject: [PATCH 405/441] fix autohide --- .../lagradost/cloudstream3/ui/player/FullScreenPlayer.kt | 9 +++++++++ .../lagradost/cloudstream3/ui/player/GeneratorPlayer.kt | 1 + 2 files changed, 10 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 75a861c0..97075136 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 @@ -728,6 +728,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private var currentTapIndex = 0 protected fun autoHide() { currentTapIndex++ + delayHide() + } + + override fun playerStatusChanged() { + super.playerStatusChanged() + delayHide() + } + + private fun delayHide() { val index = currentTapIndex playerBinding?.playerHolder?.postDelayed({ if (!isCurrentTouchValid && isShowing && index == currentTapIndex && player.getIsPlaying()) { 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 d827d31e..f6c78b07 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 @@ -158,6 +158,7 @@ class GeneratorPlayer : FullScreenPlayer() { } override fun playerStatusChanged() { + super.playerStatusChanged() if (player.getIsPlaying()) { viewModel.forceClearCache = false } From 073af50f5f09a3ca6b4bc21d0c84ff1bf2264e8f Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:28:36 +0200 Subject: [PATCH 406/441] fixed html plot in preview --- app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 59f499c5..e8cbc4d8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -107,6 +107,7 @@ import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST import com.lagradost.cloudstream3.ui.result.SyncViewModel import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setText +import com.lagradost.cloudstream3.ui.result.setTextHtml import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SearchFragment import com.lagradost.cloudstream3.ui.search.SearchResultBuilder @@ -1404,7 +1405,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa resultviewPreviewMetaDuration.setText(d.durationText) resultviewPreviewMetaRating.setText(d.ratingText) - resultviewPreviewDescription.setText(d.plotText) + resultviewPreviewDescription.setTextHtml(d.plotText) resultviewPreviewPoster.setImage( d.posterImage ?: d.posterBackgroundImage ) From bb8144a52ecd4f6518eaca41bebd7cdf2ce31e1d Mon Sep 17 00:00:00 2001 From: KingLucius Date: Fri, 19 Jul 2024 20:35:29 +0300 Subject: [PATCH 407/441] feat(TV UI): Player's Top controls redesign (#1203) --- .../cloudstream3/ui/player/CS3IPlayer.kt | 4 + .../ui/player/FullScreenPlayer.kt | 72 +++++++-- .../cloudstream3/ui/player/GeneratorPlayer.kt | 19 ++- .../cloudstream3/ui/player/IPlayer.kt | 2 + .../res/drawable/ic_baseline_equalizer_24.xml | 9 ++ .../res/drawable/ic_baseline_replay_24.xml | 9 ++ .../res/drawable/ic_baseline_restart_24.xml | 9 ++ .../drawable/ic_baseline_skip_next_24_big.xml | 10 ++ .../ic_baseline_skip_next_rounded_24.xml | 9 ++ .../main/res/layout/fragment_player_tv.xml | 11 +- .../main/res/layout/player_custom_layout.xml | 119 +++++++++++--- .../res/layout/player_custom_layout_tv.xml | 148 ++++++++++++++---- app/src/main/res/layout/subtitle_offset.xml | 31 ++-- .../main/res/layout/trailer_custom_layout.xml | 123 ++++++++++++--- app/src/main/res/values/strings.xml | 1 + 15 files changed, 467 insertions(+), 109 deletions(-) create mode 100644 app/src/main/res/drawable/ic_baseline_equalizer_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_replay_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_restart_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml create mode 100644 app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml 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 8e322f73..735e4095 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 @@ -912,7 +912,11 @@ class CS3IPlayer : IPlayer { } CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source) + CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source) + + CSPlayerEvent.Restart -> seekTo(0, source) + CSPlayerEvent.NextEpisode -> event( EpisodeSeekEvent( offset = 1, 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 97075136..a75b9899 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 @@ -14,7 +14,13 @@ import android.os.Bundle import android.provider.Settings import android.text.Editable import android.text.format.DateUtils -import android.view.* +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.Surface +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES import android.view.animation.AlphaAnimation import android.view.animation.Animation @@ -58,7 +64,11 @@ import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UserPreferenceDelegate import com.lagradost.cloudstream3.utils.Vector2 -import kotlin.math.* +import kotlin.math.abs +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min +import kotlin.math.round const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking const val MINIMUM_VERTICAL_SWIPE = 2.0f // in percentage @@ -77,7 +87,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected open var isFullScreenPlayer = true protected var playerBinding: PlayerCustomLayoutBinding? = null - private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false) + private var durationMode: Boolean by UserPreferenceDelegate("duration_mode", false) + // state of player UI protected var isShowing = false protected var isLocked = false @@ -243,7 +254,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat() - playerBinding?.apply { playerOpenSource.let { ObjectAnimator.ofFloat(it, "translationY", playerSourceMove).apply { @@ -284,7 +294,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.getCurrentPreferredSubtitle() == null } - private fun restoreOrientationWithSensor(activity: Activity){ + private fun restoreOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation var orientation = 0 when (currentOrientation) { @@ -300,7 +310,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { activity.requestedOrientation = orientation } - private fun toggleOrientationWithSensor(activity: Activity){ + private fun toggleOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation var orientation = 0 when (currentOrientation) { @@ -345,12 +355,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun updateOrientation(ignoreDynamicOrientation: Boolean = false) { activity?.apply { - if(lockRotation) { - if(isLocked) { + if (lockRotation) { + if (isLocked) { lockOrientation(this) - } - else { - if(ignoreDynamicOrientation){ + } else { + if (ignoreDynamicOrientation) { // restore when lock is disabled restoreOrientationWithSensor(this) } else { @@ -958,7 +967,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } else -> { - player.handleEvent(CSPlayerEvent.PlayPauseToggle, PlayerEventSource.UI) + player.handleEvent( + CSPlayerEvent.PlayPauseToggle, + PlayerEventSource.UI + ) } } } else if (doubleTapEnabled && isFullScreenPlayer) { @@ -1234,6 +1246,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { // if nothing has loaded these buttons should not be visible playerBinding?.apply { playerSkipEpisode.isVisible = false + playerGoForward.isVisible = false playerTracksBtt.isVisible = false playerSkipOp.isVisible = false shadowOverlay.isVisible = false @@ -1307,6 +1320,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.handleEvent(CSPlayerEvent.SeekBack) } + PlayerEventType.Restart -> { + player.handleEvent(CSPlayerEvent.Restart) + } + PlayerEventType.ToggleMute -> { player.handleEvent(CSPlayerEvent.ToggleMute) } @@ -1428,6 +1445,25 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } playerBinding?.apply { + + if (isLayout(TV or EMULATOR)) { + mapOf( + playerGoBack to playerGoBackText, + playerRestart to playerRestartText, + playerGoForward to playerGoForwardText + ).forEach { (button, text) -> + button.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + text.isSelected = false + text.isVisible = false + return@setOnFocusChangeListener + } + text.isSelected = true + text.isVisible = true + } + } + } + playerPausePlay.setOnClickListener { autoHide() player.handleEvent(CSPlayerEvent.PlayPauseToggle) @@ -1471,6 +1507,16 @@ open class FullScreenPlayer : AbstractPlayerFragment() { player.handleEvent(CSPlayerEvent.NextEpisode) } + playerGoForward.setOnClickListener { + autoHide() + player.handleEvent(CSPlayerEvent.NextEpisode) + } + + playerRestart.setOnClickListener { + autoHide() + player.handleEvent(CSPlayerEvent.Restart) + } + playerLock.setOnClickListener { autoHide() toggleLock() @@ -1564,7 +1610,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun setRemainingTimeCounter(showRemaining: Boolean) { durationMode = showRemaining - playerBinding?.exoDuration?.isInvisible= showRemaining + playerBinding?.exoDuration?.isInvisible = showRemaining playerBinding?.timeLeft?.isVisible = showRemaining } 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 f6c78b07..1f7cc5bd 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 @@ -44,6 +44,7 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog import com.lagradost.cloudstream3.ui.result.* 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.isLayout import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY @@ -1097,8 +1098,15 @@ class GeneratorPlayer : FullScreenPlayer() { } playerBinding?.playerSkipOp?.isVisible = isOpVisible - playerBinding?.playerSkipEpisode?.isVisible = - !isOpVisible && viewModel.hasNextEpisode() == true + + when { + isLayout(PHONE) -> + playerBinding?.playerSkipEpisode?.isVisible = + !isOpVisible && viewModel.hasNextEpisode() == true + + else -> + playerBinding?.playerGoForward?.isVisible = viewModel.hasNextEpisode() == true + } if (percentage >= PRELOAD_NEXT_EPISODE_PERCENTAGE) { viewModel.preLoadNextLinks() @@ -1254,7 +1262,7 @@ class GeneratorPlayer : FullScreenPlayer() { fun setPlayerDimen(widthHeight: Pair?) { val extra = if (widthHeight != null) { val (width, height) = widthHeight - "${width}x${height}" + "- ${width}x${height}" } else { "" } @@ -1265,7 +1273,7 @@ class GeneratorPlayer : FullScreenPlayer() { 0 -> "" 1 -> extra 2 -> source - 3 -> "$source - $extra" + 3 -> "$source $extra" else -> "" } playerBinding?.playerVideoTitleRez?.apply { @@ -1290,7 +1298,8 @@ class GeneratorPlayer : FullScreenPlayer() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // this is used instead of layout-television to follow the settings and some TV devices are not classified as TV for some reason - layout = if (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player + layout = + if (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java] sync = ViewModelProvider(this)[SyncViewModel::class.java] 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 4bd5c769..5f7161f7 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 @@ -26,6 +26,7 @@ enum class PlayerEventType(val value: Int) { Resize(13), SearchSubtitlesOnline(14), SkipOp(15), + Restart(16), } enum class CSPlayerEvent(val value: Int) { @@ -39,6 +40,7 @@ enum class CSPlayerEvent(val value: Int) { PrevEpisode(6), PlayPauseToggle(7), ToggleMute(8), + Restart(9), } enum class CSPlayerLoading { diff --git a/app/src/main/res/drawable/ic_baseline_equalizer_24.xml b/app/src/main/res/drawable/ic_baseline_equalizer_24.xml new file mode 100644 index 00000000..cd20ad15 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_equalizer_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_replay_24.xml b/app/src/main/res/drawable/ic_baseline_replay_24.xml new file mode 100644 index 00000000..e247aa92 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_replay_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_restart_24.xml b/app/src/main/res/drawable/ic_baseline_restart_24.xml new file mode 100644 index 00000000..aed3a562 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_restart_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml b/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml new file mode 100644 index 00000000..a8c43bbd --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_skip_next_24_big.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml b/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml new file mode 100644 index 00000000..452c4dd9 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_skip_next_rounded_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_player_tv.xml b/app/src/main/res/layout/fragment_player_tv.xml index 07cbb3c3..3c0ac05e 100644 --- a/app/src/main/res/layout/fragment_player_tv.xml +++ b/app/src/main/res/layout/fragment_player_tv.xml @@ -68,7 +68,9 @@ android:layout_height="wrap_content" android:layout_margin="5dp" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toTopOf="parent" + android:visibility="gone" + tools:visibility="visible"> diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 83be8832..be97b978 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -172,27 +172,108 @@ android:id="@+id/player_go_back_holder" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="5dp" + android:layout_margin="20dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - + + + + + + + + + + + + + + + + + + + + + + + - @@ -626,7 +707,7 @@ android:nextFocusLeft="@id/player_sources_btt" android:nextFocusRight="@id/player_skip_op" android:text="@string/tracks" - app:icon="@drawable/ic_baseline_playlist_play_24" /> + app:icon="@drawable/ic_baseline_equalizer_24" /> + 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 d8406b35..98eb58ac 100644 --- a/app/src/main/res/layout/player_custom_layout_tv.xml +++ b/app/src/main/res/layout/player_custom_layout_tv.xml @@ -231,7 +231,7 @@ @@ -240,6 +240,7 @@ android:id="@+id/player_video_title" android:layout_width="match_parent" android:layout_height="wrap_content" + android:textAlignment="viewEnd" android:gravity="end" android:textColor="@color/white" android:textSize="16sp" @@ -250,6 +251,7 @@ android:id="@+id/player_video_title_rez" android:layout_width="match_parent" android:layout_height="wrap_content" + android:textAlignment="viewEnd" android:gravity="end" android:textColor="@color/white" android:textSize="16sp" @@ -285,28 +287,116 @@ android:id="@+id/player_go_back_holder" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="5dp" + android:layout_marginStart="17dp" + android:layout_marginTop="20dp" + android:layout_marginEnd="17dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - + + + + + + + + + + + + + + + + + + + + + + + - @@ -509,6 +599,7 @@ android:layout_height="wrap_content" android:layoutDirection="ltr" android:orientation="horizontal"> + + app:layout_constraintStart_toStartOf="parent" + app:tint="@color/player_button_tv" + tools:ignore="ContentDescription" /> + tools:text="-23:20" /> @@ -672,12 +763,13 @@ android:id="@+id/player_skip_episode" style="@style/VideoButtonTV" android:nextFocusLeft="@id/player_skip_op" - android:nextFocusRight="@id/player_resize_btt" android:nextFocusUp="@id/player_pause_play" android:nextFocusDown="@id/player_resize_btt" android:text="@string/next_episode" - app:icon="@drawable/ic_baseline_skip_next_24" /> + app:icon="@drawable/ic_baseline_skip_next_24" + android:visibility="gone" + tools:visibility="visible"/> + app:icon="@drawable/ic_baseline_equalizer_24" /> diff --git a/app/src/main/res/layout/subtitle_offset.xml b/app/src/main/res/layout/subtitle_offset.xml index d5e303b6..82c24e61 100644 --- a/app/src/main/res/layout/subtitle_offset.xml +++ b/app/src/main/res/layout/subtitle_offset.xml @@ -30,28 +30,27 @@ @@ -67,29 +66,29 @@ diff --git a/app/src/main/res/layout/trailer_custom_layout.xml b/app/src/main/res/layout/trailer_custom_layout.xml index 59104ca7..20b73630 100644 --- a/app/src/main/res/layout/trailer_custom_layout.xml +++ b/app/src/main/res/layout/trailer_custom_layout.xml @@ -137,33 +137,110 @@ - + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e3f788f..e68c22b9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -96,6 +96,7 @@ Next Random @string/play_episode Go back + Play from the Beginning @string/home_change_provider_img_des Change Provider Preview Background From 4c7379c766837ffc11c62d4fa2c4e7aaa8afd5fc Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Sat, 20 Jul 2024 19:14:11 +0200 Subject: [PATCH 408/441] Revert #979 Episode download cache --- .../main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 1d23e503..802c1a64 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -67,8 +67,6 @@ object BackupUtils { OPEN_SUBTITLES_USER_KEY, SUBDL_SUBTITLES_USER_KEY, - DOWNLOAD_EPISODE_CACHE, - "biometric_key", // can lock down users if backup is shared on a incompatible device "nginx_user", // Nginx user key "download_path_key" // No access rights after restore data from backup @@ -266,4 +264,4 @@ object BackupUtils { } editor.apply() } -} \ No newline at end of file +} From 0c418fdf9bd41f6a94e9a8063c48bc4acd44ffb1 Mon Sep 17 00:00:00 2001 From: RowdyRushya Date: Sat, 20 Jul 2024 15:06:04 -0700 Subject: [PATCH 409/441] Updated VidSrc encryption methods (#1205) --- .../extractors/VidSrcExtractor.kt | 301 +++++++++++++----- 1 file changed, 227 insertions(+), 74 deletions(-) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt index a27bf188..5da919e2 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt @@ -3,98 +3,251 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.app -import com.lagradost.cloudstream3.utils.* -import kotlinx.coroutines.delay -import java.net.URI +import com.lagradost.cloudstream3.utils.ExtractorApi +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.Qualities +import com.lagradost.cloudstream3.utils.loadExtractor +import java.util.Base64 class VidSrcExtractor2 : VidSrcExtractor() { - override val mainUrl = "https://vidsrc.me/embed" - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit - ) { - val newUrl = url.lowercase().replace(mainUrl, super.mainUrl) - super.getUrl(newUrl, referer, subtitleCallback, callback) - } + override val mainUrl = "https://vidsrc.me" } open class VidSrcExtractor : ExtractorApi() { override val name = "VidSrc" - private val absoluteUrl = "https://v2.vidsrc.me" - override val mainUrl = "$absoluteUrl/embed" + override val mainUrl = "https://vidsrc.net" + private val apiUrl = "https://vidsrc.stream" override val requiresReferer = false - companion object { - /** Infinite function to validate the vidSrc pass */ - suspend fun validatePass(url: String) { - val uri = URI(url) - val host = uri.host - - // Basically turn https://tm3p.vidsrc.stream/ -> https://vidsrc.stream/ - val referer = host.split(".").let { - val size = it.size - "https://" + it.subList(maxOf(0, size - 2), size).joinToString(".") + "/" - } - - while (true) { - app.get(url, referer = referer) - delay(60_000) - } - } - } - override suspend fun getUrl( - url: String, - referer: String?, - subtitleCallback: (SubtitleFile) -> Unit, - callback: (ExtractorLink) -> Unit + url: String, + referer: String?, + subtitleCallback: (SubtitleFile) -> Unit, + callback: (ExtractorLink) -> Unit ) { val iframedoc = app.get(url).document - val serverslist = - iframedoc.select("div#sources.button_content div#content div#list div").map { - val datahash = it.attr("data-hash") - if (datahash.isNotBlank()) { - val links = try { - app.get( - "$absoluteUrl/srcrcp/$datahash", - referer = "https://rcp.vidsrc.me/" - ).url - } catch (e: Exception) { - "" - } - links - } else "" - } + val srcrcpList = + iframedoc.select("div.serversList > div.server").mapNotNull { + val datahash = it.attr("data-hash") ?: return@mapNotNull null + val rcpLink = "$apiUrl/rcp/$datahash" + val rcpRes = app.get(rcpLink, referer = apiUrl).text + val srcrcpLink = + Regex("src:\\s*'(.*)',").find(rcpRes)?.destructured?.component1() + ?: return@mapNotNull null + "https:$srcrcpLink" + } - serverslist.amap { server -> - val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/") - if (linkfixed.contains("/prorcp")) { - val srcresponse = app.get(server, referer = absoluteUrl).text - val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)") - val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap - val passRegex = Regex("""['"](.*set_pass[^"']*)""") - val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace( - Regex("""^//"""), "https://" - ) + srcrcpList.amap { server -> + val res = app.get(server, referer = apiUrl) + if (res.url.contains("/prorcp")) { + val encodedElement = res.document.select("div#reporting_content+div") + val decodedUrl = + decodeUrl(encodedElement.attr("id"), encodedElement.text()) ?: return@amap callback.invoke( - ExtractorLink( - this.name, - this.name, - srcm3u8, - "https://vidsrc.stream/", - Qualities.Unknown.value, - extractorData = pass, - isM3u8 = true - ) + ExtractorLink( + this.name, + this.name, + decodedUrl, + apiUrl, + Qualities.Unknown.value, + isM3u8 = true + ) ) } else { - loadExtractor(linkfixed, url, subtitleCallback, callback) + loadExtractor(res.url, url, subtitleCallback, callback) } } } -} \ No newline at end of file + private fun decodeUrl(encType: String, url: String): String? { + return when (encType) { + "NdonQLf1Tzyx7bMG" -> bMGyx71TzQLfdonN(url) + "sXnL9MQIry" -> Iry9MQXnLs(url) + "IhWrImMIGL" -> IGLImMhWrI(url) + "xTyBxQyGTA" -> GTAxQyTyBx(url) + "ux8qjPHC66" -> C66jPHx8qu(url) + "eSfH1IRMyL" -> MyL1IRSfHe(url) + "KJHidj7det" -> detdj7JHiK(url) + "o2VSUnjnZl" -> nZlUnj2VSo(url) + "Oi3v1dAlaM" -> laM1dAi3vO(url) + "TsA2KGDGux" -> GuxKGDsA2T(url) + "JoAHUMCLXV" -> LXVUMCoAHJ(url) + else -> null + } + } + + private fun bMGyx71TzQLfdonN(a: String): String { + val b = 3 + val c = mutableListOf() + var d = 0 + while (d < a.length) { + c.add(a.substring(d, minOf(d + b, a.length))) + d += b + } + val e = c.reversed().joinToString("") + return e + } + + private fun Iry9MQXnLs(a: String): String { + val b = "pWB9V)[*4I`nJpp?ozyB~dbr9yt!_n4u" + val d = a.chunked(2).map { it.toInt(16).toChar() }.joinToString("") + var c = "" + for (e in d.indices) { + c += (d[e].code xor b[e % b.length].code).toChar() + } + var e = "" + for (ch in c) { + e += (ch.code - 3).toChar() + } + return String(Base64.getDecoder().decode(e)) + } + + private fun IGLImMhWrI(a: String): String { + val b = a.reversed() + val c = + b + .map { + when (it) { + in 'a'..'m', in 'A'..'M' -> it + 13 + in 'n'..'z', in 'N'..'Z' -> it - 13 + else -> it + } + } + .joinToString("") + val d = c.reversed() + return String(Base64.getDecoder().decode(d)) + } + + private fun GTAxQyTyBx(a: String): String { + val b = a.reversed() + val c = b.filterIndexed { index, _ -> index % 2 == 0 } + return String(Base64.getDecoder().decode(c)) + } + + private fun C66jPHx8qu(a: String): String { + val b = a.reversed() + val c = "X9a(O;FMV2-7VO5x;Ao:dN1NoFs?j," + val d = b.chunked(2).map { it.toInt(16).toChar() }.joinToString("") + var e = "" + for (i in d.indices) { + e += (d[i].code xor c[i % c.length].code).toChar() + } + return e + } + + private fun MyL1IRSfHe(a: String): String { + val b = a.reversed() + val c = b.map { (it.code - 1).toChar() }.joinToString("") + val d = c.chunked(2).map { it.toInt(16).toChar() }.joinToString("") + return d + } + + private fun detdj7JHiK(a: String): String { + val b = a.substring(10, a.length - 16) + val c = "3SAY~#%Y(V%>5d/Yg\"\$G[Lh1rK4a;7ok" + val d = String(Base64.getDecoder().decode(b)) + val e = c.repeat((d.length + c.length - 1) / c.length).substring(0, d.length) + var f = "" + for (i in d.indices) { + f += (d[i].code xor e[i].code).toChar() + } + return f + } + + private fun nZlUnj2VSo(a: String): String { + val b = + mapOf( + 'x' to 'a', + 'y' to 'b', + 'z' to 'c', + 'a' to 'd', + 'b' to 'e', + 'c' to 'f', + 'd' to 'g', + 'e' to 'h', + 'f' to 'i', + 'g' to 'j', + 'h' to 'k', + 'i' to 'l', + 'j' to 'm', + 'k' to 'n', + 'l' to 'o', + 'm' to 'p', + 'n' to 'q', + 'o' to 'r', + 'p' to 's', + 'q' to 't', + 'r' to 'u', + 's' to 'v', + 't' to 'w', + 'u' to 'x', + 'v' to 'y', + 'w' to 'z', + 'X' to 'A', + 'Y' to 'B', + 'Z' to 'C', + 'A' to 'D', + 'B' to 'E', + 'C' to 'F', + 'D' to 'G', + 'E' to 'H', + 'F' to 'I', + 'G' to 'J', + 'H' to 'K', + 'I' to 'L', + 'J' to 'M', + 'K' to 'N', + 'L' to 'O', + 'M' to 'P', + 'N' to 'Q', + 'O' to 'R', + 'P' to 'S', + 'Q' to 'T', + 'R' to 'U', + 'S' to 'V', + 'T' to 'W', + 'U' to 'X', + 'V' to 'Y', + 'W' to 'Z' + ) + return a.map { b[it] ?: it }.joinToString("") + } + + private fun laM1dAi3vO(a: String): String { + val b = a.reversed() + val c = b.replace("-", "+").replace("_", "/") + val d = String(Base64.getDecoder().decode(c)) + var e = "" + val f = 5 + for (ch in d) { + e += (ch.code - f).toChar() + } + return e + } + + private fun GuxKGDsA2T(a: String): String { + val b = a.reversed() + val c = b.replace("-", "+").replace("_", "/") + val d = String(Base64.getDecoder().decode(c)) + var e = "" + val f = 7 + for (ch in d) { + e += (ch.code - f).toChar() + } + return e + } + + private fun LXVUMCoAHJ(a: String): String { + val b = a.reversed() + val c = b.replace("-", "+").replace("_", "/") + val d = String(Base64.getDecoder().decode(c)) + var e = "" + val f = 3 + for (ch in d) { + e += (ch.code - f).toChar() + } + return e + } +} From c8a863e332d0d952568385f438b2a035cca5b816 Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Wed, 24 Jul 2024 22:38:16 +0200 Subject: [PATCH 410/441] Fixed ExampleInstrumentedTest --- .../java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index faacdf50..c7f02baf 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -154,7 +154,7 @@ class ExampleInstrumentedTest { fun providerCorrectHomepage() { runBlocking { getAllProviders().toList().amap { api -> - TestingUtils.testHomepage(api, ::println) + TestingUtils.testHomepage(api, TestingUtils.Logger()) } } println("Done providerCorrectHomepage") @@ -166,7 +166,6 @@ class ExampleInstrumentedTest { TestingUtils.getDeferredProviderTests( this, getAllProviders(), - ::println ) { _, _ -> } } } From dfd127265a066dfec18e797b3b2ddc7bf2ae51ef Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 25 Jul 2024 21:23:31 +0300 Subject: [PATCH 411/441] Trailers Fix (#1213) --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6e439d53..1ad35d89 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -200,7 +200,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:592f159") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:2d36945") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding From e3ff1cf4554bc1bea1333a456c62becb987fd01b Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 25 Jul 2024 21:23:49 +0300 Subject: [PATCH 412/441] feat(UI): Show Episode Runtime (#1207) --- .../metaproviders/TraktProvider.kt | 1 + .../cloudstream3/ui/result/EpisodeAdapter.kt | 15 +++++++-- .../cloudstream3/ui/result/ResultFragment.kt | 3 ++ .../ui/result/ResultViewModel2.kt | 6 ++-- .../main/res/layout/result_episode_large.xml | 30 +++++++++++++---- .../com/lagradost/cloudstream3/MainAPI.kt | 32 +++++++++++++++++-- 6 files changed, 75 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index 7c375e0a..a1b9ff34 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -236,6 +236,7 @@ open class TraktProvider : MainAPI() { posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()), rating = episode.rating?.times(10)?.roundToInt(), description = episode.overview, + runTime = episode.runtime ).apply { this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) { 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 ed5e51f1..06be6bd5 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 @@ -27,7 +27,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper import java.text.DateFormat import java.text.SimpleDateFormat -import java.util.* +import java.util.Date +import java.util.Locale const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 const val ACTION_PLAY_EPISODE_IN_VLC_PLAYER = 2 @@ -58,6 +59,7 @@ const val ACTION_MARK_AS_WATCHED = 18 const val ACTION_FCAST = 19 const val TV_EP_SIZE = 400 + data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( @@ -274,7 +276,10 @@ class EpisodeAdapter( episodeDate.setText( txt( R.string.episode_upcoming_format, - secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "") + secondsToReadable( + card.airDate.minus(unixTimeMS).div(1000).toInt(), + "" + ) ) ) } else { @@ -292,6 +297,12 @@ class EpisodeAdapter( episodeDate.isVisible = false } + episodeRuntime.setText( + txt( + card.runTime?.times(60L)?.toInt()?.let { secondsToReadable(it, "") } + ) + ) + if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index c687eaa0..3eab0c71 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -51,6 +51,7 @@ data class ResultEpisode( /** Sum of all previous season episode counts + episode */ val totalEpisodeIndex: Int? = null, val airDate: Long? = null, + val runTime: Int? = null, ) fun ResultEpisode.getRealPosition(): Long { @@ -87,6 +88,7 @@ fun buildResultEpisode( parentId: Int, totalEpisodeIndex: Int? = null, airDate: Long? = null, + runTime: Int? = null, ): ResultEpisode { val posDur = getViewPos(id) val videoWatchState = getVideoWatchState(id) ?: VideoWatchState.None @@ -111,6 +113,7 @@ fun buildResultEpisode( videoWatchState, totalEpisodeIndex, airDate, + runTime, ) } 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 8e8dfe30..5086426f 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 @@ -2371,7 +2371,8 @@ class ResultViewModel2 : ViewModel() { loadResponse.type, mainId, totalIndex, - airDate = i.date + airDate = i.date, + runTime = i.runTime, ) val season = eps.seasonIndex ?: 0 @@ -2426,7 +2427,8 @@ class ResultViewModel2 : ViewModel() { loadResponse.type, mainId, totalIndex, - airDate = episode.date + airDate = episode.date, + runTime = episode.runTime, ) val season = ep.seasonIndex ?: 0 diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index e5a6881a..935beac1 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -44,7 +44,7 @@ android:nextFocusRight="@id/download_button" android:scaleType="centerCrop" tools:src="@drawable/example_poster" - tools:visibility="invisible"/> + tools:visibility="invisible" /> + tools:visibility="invisible" /> - + android:layout_gravity="start" + android:orientation="horizontal"> + + + + + + + Date: Thu, 25 Jul 2024 20:25:17 +0200 Subject: [PATCH 413/441] Add the option to hide video controls (#1210) --- .../ui/player/FullScreenPlayer.kt | 25 +++++++++++++++++++ .../ui/settings/SettingsPlayer.kt | 6 ++--- app/src/main/res/values-af/strings.xml | 1 + app/src/main/res/values-ajp/strings.xml | 1 + app/src/main/res/values-am/strings.xml | 1 + app/src/main/res/values-ar/strings.xml | 1 + app/src/main/res/values-ars/strings.xml | 1 + app/src/main/res/values-as/strings.xml | 1 + app/src/main/res/values-bg/strings.xml | 1 + app/src/main/res/values-bn/strings.xml | 1 + app/src/main/res/values-bp/strings.xml | 1 + app/src/main/res/values-cs/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-el/strings.xml | 1 + app/src/main/res/values-eo/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fa/strings.xml | 1 + app/src/main/res/values-fil/strings.xml | 4 ++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-gl/strings.xml | 1 + app/src/main/res/values-hi/strings.xml | 1 + app/src/main/res/values-hr/strings.xml | 1 + app/src/main/res/values-hu/strings.xml | 1 + app/src/main/res/values-in/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-iw/strings.xml | 1 + app/src/main/res/values-ja/strings.xml | 1 + app/src/main/res/values-kn/strings.xml | 1 + app/src/main/res/values-ko/strings.xml | 1 + app/src/main/res/values-lt/strings.xml | 1 + app/src/main/res/values-lv/strings.xml | 1 + app/src/main/res/values-mk/strings.xml | 1 + app/src/main/res/values-ml/strings.xml | 1 + app/src/main/res/values-ms/strings.xml | 1 + app/src/main/res/values-mt/strings.xml | 1 + app/src/main/res/values-my/strings.xml | 1 + app/src/main/res/values-ne/strings.xml | 1 + app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-nn/strings.xml | 1 + app/src/main/res/values-no/strings.xml | 1 + app/src/main/res/values-or/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 1 + app/src/main/res/values-qt/strings.xml | 1 + app/src/main/res/values-ro/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sk/strings.xml | 1 + app/src/main/res/values-so/strings.xml | 1 + app/src/main/res/values-sv/strings.xml | 1 + app/src/main/res/values-ta/strings.xml | 1 + app/src/main/res/values-ti/strings.xml | 1 + app/src/main/res/values-tl/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values-ur/strings.xml | 1 + app/src/main/res/values-vi/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values-zh/strings.xml | 1 + app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/settings_player.xml | 6 +++++ 60 files changed, 93 insertions(+), 5 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 a75b9899..ef7d6bc1 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 @@ -25,15 +25,18 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.animation.AnimationUtils +import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.core.graphics.blue import androidx.core.graphics.green import androidx.core.graphics.red +import androidx.core.view.children import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.preference.PreferenceManager +import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.screenHeight @@ -120,6 +123,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { protected var doubleTapPauseEnabled = true protected var playerRotateEnabled = false protected var autoPlayerRotateEnabled = false + private var hideControlsNames = false protected var subtitleDelay set(value) = try { @@ -1419,6 +1423,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() { false ) + hideControlsNames = settingsManager.getBoolean(ctx.getString(R.string.hide_player_control_names_key), false) + val profiles = QualityDataHelper.getProfiles() val type = if (ctx.isUsingMobileData()) QualityDataHelper.QualityProfileType.Data @@ -1439,6 +1445,9 @@ open class FullScreenPlayer : AbstractPlayerFragment() { playerSpeedBtt.isVisible = playBackSpeedEnabled playerResizeBtt.isVisible = playerResizeEnabled playerRotateBtt.isVisible = playerRotateEnabled + if (hideControlsNames) { + hideControlsNames() + } } } catch (e: Exception) { logError(e) @@ -1591,6 +1600,22 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } + private fun PlayerCustomLayoutBinding.hideControlsNames() { + fun iterate(layout: LinearLayout) { + layout.children.forEach { + if (it is MaterialButton) { + it.textSize = 0f + it.iconPadding = 0 + it.iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START + it.setPadding(0,0,0,0) + } else if (it is LinearLayout) { + iterate(it) + } + } + } + iterate(playerLockHolder.parent as LinearLayout) + } + override fun playerDimensionsLoaded(width: Int, height: Int) { isVerticalOrientation = height > width updateOrientation() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 20279cd1..7560d75f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -87,10 +87,6 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } - /*(getPref(R.string.double_tap_seek_time_key) as? SeekBarPreference?)?.let { - - }*/ - getPref(R.string.prefer_limit_title_rez_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.limit_title_rez_pref_names) val prefValues = resources.getIntArray(R.array.limit_title_rez_pref_values) @@ -109,6 +105,8 @@ class SettingsPlayer : PreferenceFragmentCompat() { return@setOnPreferenceClickListener true } + getPref(R.string.hide_player_control_names_key)?.hideOn(TV) + getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener { val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 45e9a1d4..4adafee4 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -106,4 +106,5 @@ Voer lettertipes in deur dit in %s te plaas Rolverdeling: %s Nuwe episode notifikasie + hide_player_control_names_key diff --git a/app/src/main/res/values-ajp/strings.xml b/app/src/main/res/values-ajp/strings.xml index c78b6924..718b5235 100644 --- a/app/src/main/res/values-ajp/strings.xml +++ b/app/src/main/res/values-ajp/strings.xml @@ -640,4 +640,5 @@ تجاهل متاح الريپوزيتوري فتاح %s ع تلفونك أو كمپيوترك، وحط الكود اللي فوق + hide_player_control_names_key diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml index 7fd3274b..26fb84dd 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -108,4 +108,5 @@ ተጨማሪ መረጃ ዓይነቶችን በመጠቀም ይፈልጉ ቅርጸ-ቁምፊዎችን በ%s ውስጥ በማስቀመጥ ያጫኑ + hide_player_control_names_key diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c2ed35cb..e85fee04 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -666,4 +666,5 @@ قم بزيارة %s على هاتفك الذكي أو جهاز الكمبيوتر وأدخل الرمز أعلاه لا يمكن الحصول على رمز PIN للجهاز، حاول المصادقة المحلية تنتهي صلاحية الرمز خلال %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-ars/strings.xml b/app/src/main/res/values-ars/strings.xml index f3811d3d..f028ef5d 100644 --- a/app/src/main/res/values-ars/strings.xml +++ b/app/src/main/res/values-ars/strings.xml @@ -352,4 +352,5 @@ وثائقي موقع عنوان مشغل الفيديو بحد أقصى لعدد الأحرف + hide_player_control_names_key diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml index 7fb0e7bd..dd1b2eed 100644 --- a/app/src/main/res/values-as/strings.xml +++ b/app/src/main/res/values-as/strings.xml @@ -621,4 +621,5 @@ ছাবটাইটেল বাছনি কৰক পৰ্ব খেলাওক প্ৰয়োগ কৰক + hide_player_control_names_key diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 66e29882..89801322 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -601,4 +601,5 @@ Покажи предложения Добавя опция за промяна на скоростта в плеъра Този тест е направен за програмисти и не проверява работата на никакви добавки. + hide_player_control_names_key diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 3500e85a..1a02eebc 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -359,4 +359,5 @@ অ্যাকাউন্ট প্রস্থান %1$d%2$s + hide_player_control_names_key diff --git a/app/src/main/res/values-bp/strings.xml b/app/src/main/res/values-bp/strings.xml index 6dc38cd8..51138312 100644 --- a/app/src/main/res/values-bp/strings.xml +++ b/app/src/main/res/values-bp/strings.xml @@ -656,4 +656,5 @@ Não é possível obter o código PIN do dispositivo, tente a autenticação local O código PIN expirou! O código expira em %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8e40b12b..2f7dcfed 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -658,4 +658,5 @@ Účty Lokální ověření PIN kód vypršel! + hide_player_control_names_key diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ee378ff6..12a68dbc 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -615,4 +615,5 @@ Zurücksetzen Akkuverbrauch der App ist bereits auf unbeschränkt eingestellt CloudStreams App-Info kann nicht geöffnet werden. + hide_player_control_names_key diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e7fa1f6a..269626cb 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -625,4 +625,5 @@ Τα δεδομένα σας στο CloudStream έχουν κάνει back up. Αν και η πιθανότητα είναι πολύ χαμηλή, όλες οι συσκευές συμπεριφέρονται διαφορετικά. Στη σπάνια περίπτωση, που απαγορευτεί η πρόσβασή σας από την εφαρμογή, διαγράψτε τα δεδομένα εφαρμογής και επαναφέρετέ τα από ένα ήδη υπάρχον backup. Συγνώμη για οποιαδήποτε ταλαιπωρία. Λογαριασμοί Ασφάλεια + hide_player_control_names_key diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 275a4bfb..9d3d07bc 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -127,4 +127,5 @@ Elŝutite Elŝutante Elŝuto Malsukcesite + hide_player_control_names_key diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 82f29381..bd281b55 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -634,4 +634,5 @@ ¡El código PIN ya ha caducado! El código caduca en %1$d mín y %2$d s No puedo obtener el código PIN del dispositivo; intente con la autenticación local + hide_player_control_names_key diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index db432a61..86dee8ef 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -191,4 +191,5 @@ پیش‌فرض کارتون تورنت + hide_player_control_names_key diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index 42eba3cc..2189dd75 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -1,2 +1,4 @@ - + + hide_player_control_names_key + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index fa1e1b61..78f3f2a5 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -620,4 +620,5 @@ Verrouillage biométrique Sélectionnez un appareil de diffusion Saison %1$d Episode %2$d sera publié dans + hide_player_control_names_key diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index ae3105cf..d04792f8 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -164,4 +164,5 @@ Selecciona o modo para filtrar a descarga dos complementos Instala automáticamente todos os complementos aínda non instalados dos repositorios engadidos. Mostrar actualizacións da aplicación + hide_player_control_names_key diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index e08a3b8b..bd50953c 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -209,4 +209,5 @@ रूपरेखा रंग उपशीर्षक ऊंचाई अक्षर शैली + hide_player_control_names_key diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 54448e58..90dbee79 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -651,4 +651,5 @@ CloudStream Wiki Računi Sigurnost + hide_player_control_names_key diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index ebaff041..72213b02 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -592,4 +592,5 @@ A PIN 4 karakter hosszú kell legyen Auto elforgatás Az automatikus videó orientáció alapján való képernyő elforgatás bekapcsolása + hide_player_control_names_key diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 951ba417..0edae603 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -647,4 +647,5 @@ CloudStream Wiki Keamanan Akun + hide_player_control_names_key diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ff7ea6bd..8671a73a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -654,4 +654,5 @@ Impossibile ottenere il codice PIN del dispositivo, prova l\'autenticazione locale Il codice PIN è scaduto! Il codice scadrà tra %1$dm %2$ds + hide_player_control_names_key diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 22626f50..1f34f0e1 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -550,4 +550,5 @@ \nיגרמו לעדיפות הסרטון להיות 10. \n \nשימו לב: אם הסכום הוא 10 או יותר, הנגן ידלג על טעינת הסרטון כאשר הלינק נטען! + hide_player_control_names_key diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index acb2cfc3..fb2ca02d 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -242,4 +242,5 @@ 現在のエピソードが終了したら次のエピソードを開始する 長押しするとデフォルトにリセットされます ダウンロードを再開 + hide_player_control_names_key diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index f3fb665d..75f62bcc 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -130,4 +130,5 @@ Brightness ಅಥವಾ volume ಬದಲಾಯಿಸಲು ಎಡ ಅಥವಾ ಬಲಭಾಗದಲ್ಲಿ ಮೇಲಕ್ಕೆ ಅಥವಾ ಕೆಳಕ್ಕೆ ಸ್ಲೈಡ್ ಮಾಡಿ ಈಗಿನ ಎಪಿಸೋಡ್ ಮುಗಿದಾಗ ಮುಂದಿನ ಎಪಿಸೋಡ್ ಅನ್ನು ಪ್ರಾರಂಭಿಸಿ ಸೆಟ್ಟಿಂಗ್‌ಗಳನ್ನು ಬದಲಾಯಿಸಲು ಸ್ವೈಪ್ ಮಾಡಿ + hide_player_control_names_key diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index bda82057..ec570e69 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -634,4 +634,5 @@ %s의 PIN 입력 즐겨찾기에서 제거 캐스트미러 + hide_player_control_names_key diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index f61bcfc0..0cb3addf 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -260,4 +260,5 @@ Ar tikrai norite išeiti\? Pašalinti iš žiūrimų Garso takelis + hide_player_control_names_key diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 566c721d..96272e71 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -527,4 +527,5 @@ Abonēto šovu atjaunināšana Abonēts Abonēts %s + hide_player_control_names_key diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 5e4d5c06..d4023ec4 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -624,4 +624,5 @@ Грешка при пристапот до таблата со исечоци, обидете се повторно. Грешка при копирање, копирајте го logcat и контактирајте со поддршката за апликацијата. Аудио книга + hide_player_control_names_key diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index d97e666c..213d4a00 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -280,4 +280,5 @@ എഡ്ജ് തരം ഔട്ട്ലൈൻ നിറം പശ്ചാത്തല നിറം + hide_player_control_names_key diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index dca98e53..aae74f4e 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -57,4 +57,5 @@ Tutup Ep cuba + hide_player_control_names_key diff --git a/app/src/main/res/values-mt/strings.xml b/app/src/main/res/values-mt/strings.xml index b2c0356a..37da0580 100644 --- a/app/src/main/res/values-mt/strings.xml +++ b/app/src/main/res/values-mt/strings.xml @@ -123,4 +123,5 @@ Bookmarks Neħħi Falla t-tniżżil + hide_player_control_names_key diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 0ebe3c6b..e7007d12 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -550,4 +550,5 @@ သင်နဂိုတည်းကသတ်မှတ်ပြီး လိုက်ဘရီရွေးချယ်ရန် ဖြင့်ဖွင့်မည် + hide_player_control_names_key diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 49cb6cfa..bc0199a1 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -128,4 +128,5 @@ प्लेयरको उपशीर्षकको सेटिङ रिपोजिटरी को नाम र यूआरएल कपी गरियो! + hide_player_control_names_key diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index b685489b..6029f78b 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -608,4 +608,5 @@ Link opnieuw geladen Autoroteer Roteer + hide_player_control_names_key diff --git a/app/src/main/res/values-nn/strings.xml b/app/src/main/res/values-nn/strings.xml index 95c527f9..930841db 100644 --- a/app/src/main/res/values-nn/strings.xml +++ b/app/src/main/res/values-nn/strings.xml @@ -195,4 +195,5 @@ Bilde i bilde Fortsett å sjå Prøv tilkopling på nytt… + hide_player_control_names_key diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 7b013653..115cd2d3 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -538,4 +538,5 @@ Bruk Hjelp Profilbakgrunn + hide_player_control_names_key diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index bdc55780..07fc8a1d 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -159,4 +159,5 @@ କୌଣସି ତଥ୍ୟ ନାହିଁ %1$s ଅ %2$d ଆଦ୍ୟ ବାଦ୍ ଦିଅ + hide_player_control_names_key diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 8e940c61..209c9d8e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -635,4 +635,5 @@ Odrzuć Otwórz repozytorium Odwiedź %s na swoim smartfonie lub komputerze i wprowadź powyższy kod + hide_player_control_names_key diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index ce20a8af..59406383 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -621,4 +621,5 @@ Fcast Escolha o dispositivo Transmitir + hide_player_control_names_key diff --git a/app/src/main/res/values-qt/strings.xml b/app/src/main/res/values-qt/strings.xml index 5de97c7d..258552e2 100644 --- a/app/src/main/res/values-qt/strings.xml +++ b/app/src/main/res/values-qt/strings.xml @@ -247,4 +247,5 @@ oooooh uuaagh @string/home_play oouuhhh ahhooo-ahah + hide_player_control_names_key diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 344eae21..609190cf 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -641,4 +641,5 @@ Selectați divece-ul pe care doriți să faceți cast Cast mirror Fcast + hide_player_control_names_key diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5a9b843e..7f19ac8c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -622,4 +622,5 @@ Выйдет %s Fcast Выберите девайс для трансляции + hide_player_control_names_key diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index a53e1f53..947e2b6d 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -377,4 +377,5 @@ Pridať repozitár Názov repozitára Zobraziť komunitné repozitáre + hide_player_control_names_key diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index c750ea7a..5dc0bc23 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -485,4 +485,5 @@ Bilowga Bilow isku qasan Qoraalka dhamaadka + hide_player_control_names_key diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 04230ab8..dd2dffb9 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -626,4 +626,5 @@ CloudStream Wiki Konton Säkerhet + hide_player_control_names_key diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 9378e400..44729e09 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -615,4 +615,5 @@ %கள் பிடித்தவைகளிலிருந்து அகற்றப்பட்டன உங்கள் கிளவுட்ச்ட்ரீம் தரவு இப்போது காப்புப் பிரதி எடுக்கப்பட்டுள்ளது. இதன் சாத்தியம் மிகக் குறைவு என்றாலும், எல்லா சாதனங்களும் வித்தியாசமாக நடந்து கொள்ளலாம். அரிய விசயத்தில், பயன்பாட்டை அணுகுவதிலிருந்து நீங்கள் பூட்டப்படுகிறீர்கள், பயன்பாட்டு தரவை முழுவதுமாக அழித்து, காப்புப்பிரதியிலிருந்து மீட்டெடுக்கவும். இதிலிருந்து எழும் ஏதேனும் சிரமத்திற்கு நாங்கள் மிகவும் வருந்துகிறோம். ஊடகம் + hide_player_control_names_key diff --git a/app/src/main/res/values-ti/strings.xml b/app/src/main/res/values-ti/strings.xml index 46235bbd..6c154c8d 100644 --- a/app/src/main/res/values-ti/strings.xml +++ b/app/src/main/res/values-ti/strings.xml @@ -3,4 +3,5 @@ %1$s ክፋል %2$d ክፋል %d በ ላይ ይወጣል ተዋሳእቲ፡ %s + hide_player_control_names_key diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index b4308eb7..dd964877 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -265,4 +265,5 @@ Mga Subtitle ng Chromecast Mga setting ng mga subtitle ng Chromecast Maglaro ng Trailer + hide_player_control_names_key diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3273a901..a55750e9 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -681,4 +681,5 @@ Cihaz PIN kodu alınamıyor, yerel kimlik doğrulamayı deneyin PIN kodunun süresi doldu! Kodun süresi %1$dm %2$ds içinde doluyor + hide_player_control_names_key diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f5770e86..fd24274c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -634,4 +634,5 @@ Термін дії коду закінчується через %1$dхв %2$dс Автентифікація по місцю Відхилити + hide_player_control_names_key diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 04cfd381..c87be59c 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -617,4 +617,5 @@ دیگر ایکسٹینشنز میں تلاش کریں سفارشات دکھائیں آپ کے CloudStream ڈیٹا کا اب بیک اپ لیا گیا ہے۔ اگرچہ اس کا امکان بہت کم ہے، لیکن مختلف ڈیوائس مختلف طریقے سے کام کر سکتے ہیں۔ اگر آپ ایپ تک رسائی حاصل کرنے سے قاصر ہیں تو، ایپ کا ڈیٹا مکمل طور پر صاف کریں اور بیک اپ سے بحال کریں۔ اس سے ہونے والی کسی بھی تکلیف کے لیے ہم بہت معذرت خواہ ہیں۔ + hide_player_control_names_key diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 92e088bf..44868647 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -643,4 +643,5 @@ 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 + hide_player_control_names_key diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c50f284c..69eb8741 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -671,4 +671,5 @@ 為了確保下載與通知已訂閱的電視節目的不間斷,CloudStream 需要取得在背景執行的權限。若點選「確定」,將移至「應用程式資訊」,請找到「應用程式電池使用」並將電池用量設置為「無限制」。請注意,取得此權限並不表示 CS3 會明顯增加電池用量,而是只在必要時在背景執行,例如取得通知或使用官方擴充功能下載影片時。若選擇「取消」,您可以稍後在「一般設定」中調整此設定。 CloudStream Wiki 此裝置不支援生物特徵認證 + hide_player_control_names_key diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 97ba24ea..f2db04e2 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -673,4 +673,5 @@ 选择投射设备 %1$d季%2$d集将在 投射镜像 + hide_player_control_names_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e68c22b9..21067fff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -797,4 +797,6 @@ Can\'t get the device PIN code, try local authentication PIN code is now expired ! Code expires in %1$dm %2$ds + hide_player_control_names_key + Hide names of the player\'s controls \ No newline at end of file diff --git a/app/src/main/res/xml/settings_player.xml b/app/src/main/res/xml/settings_player.xml index 5d5b11d0..0039af3a 100644 --- a/app/src/main/res/xml/settings_player.xml +++ b/app/src/main/res/xml/settings_player.xml @@ -37,6 +37,12 @@ android:icon="@drawable/ic_baseline_text_format_24" android:key="@string/prefer_limit_title_rez_key" android:title="@string/limit_title_rez" /> + Date: Thu, 25 Jul 2024 20:26:21 +0200 Subject: [PATCH 414/441] Bump 4.4.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 1ad35d89..2040cf39 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -60,8 +60,8 @@ android { minSdk = 21 targetSdk = 33 /* Android 14 is Fu*ked ^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/ - versionCode = 63 - versionName = "4.3.2" + versionCode = 64 + versionName = "4.4.0" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") From a28ee413680da64d059bdc90510f67b816e62568 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:59:37 -0600 Subject: [PATCH 415/441] Fix for navigation UI bug (#1220) --- .../lagradost/cloudstream3/MainActivity.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index e8cbc4d8..bc2cb88e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -572,6 +572,35 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa binding?.apply { navRailView.isVisible = isNavVisible && landscape navView.isVisible = isNavVisible && !landscape + + /** + * We need to make sure if we return to a sub-fragment, + * the correct navigation item is selected so that it does not + * highlight the wrong one in UI. + */ + when (destination.id) { + in listOf(R.id.navigation_downloads, R.id.navigation_download_child) -> { + navRailView.menu.findItem(R.id.navigation_downloads).isChecked = true + navView.menu.findItem(R.id.navigation_downloads).isChecked = true + } + in listOf( + R.id.navigation_settings, + R.id.navigation_subtitles, + R.id.navigation_chrome_subtitles, + R.id.navigation_settings_player, + R.id.navigation_settings_updates, + R.id.navigation_settings_ui, + R.id.navigation_settings_account, + R.id.navigation_settings_providers, + R.id.navigation_settings_general, + R.id.navigation_settings_extensions, + R.id.navigation_settings_plugins, + R.id.navigation_test_providers + ) -> { + navRailView.menu.findItem(R.id.navigation_settings).isChecked = true + navView.menu.findItem(R.id.navigation_settings).isChecked = true + } + } } } From 0aa48f335a818e0ebf0e1cf045d302a782e79857 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:26:22 -0600 Subject: [PATCH 416/441] Fix subscription icon displaying for movie types in result previews (#1222) --- .../com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5086426f..ce0fbdc5 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 @@ -2163,7 +2163,7 @@ class ResultViewModel2 : ViewModel() { // lets say that we have subscribed, then we must be able to unsubscribe no matter what else if (data != null) { _subscribeStatus.postValue(true) - } + } else _subscribeStatus.postValue(null) } private fun postFavorites(loadResponse: LoadResponse) { @@ -2861,4 +2861,4 @@ class ResultViewModel2 : ViewModel() { } } } -} +} \ No newline at end of file From 04dda008c40bae83ca076c24f2c8f75f6fcdb870 Mon Sep 17 00:00:00 2001 From: epireyn <48213068+epireyn@users.noreply.github.com> Date: Mon, 29 Jul 2024 00:39:04 +0200 Subject: [PATCH 417/441] Clean up and mark questionable code issues (#1209) --- .../lagradost/cloudstream3/AcraApplication.kt | 12 +- .../lagradost/cloudstream3/CommonActivity.kt | 16 +- .../cloudstream3/DownloaderTestImpl.kt | 6 +- .../lagradost/cloudstream3/MainActivity.kt | 49 +++---- .../cloudstream3/NativeCrashHandler.kt | 53 ------- .../metaproviders/SyncRedirector.kt | 6 +- .../metaproviders/TraktProvider.kt | 2 +- .../cloudstream3/network/CloudflareKiller.kt | 6 +- .../cloudstream3/plugins/CloudstreamPlugin.kt | 3 +- .../lagradost/cloudstream3/plugins/Plugin.kt | 10 +- .../cloudstream3/plugins/PluginManager.kt | 34 +++-- .../cloudstream3/plugins/RepositoryManager.kt | 2 +- .../cloudstream3/plugins/VotingApi.kt | 9 +- .../subtitles/AbstractSubProvider.kt | 2 +- .../subtitles/AbstractSubtitleEntities.kt | 1 - .../syncproviders/AccountManager.kt | 12 +- .../syncproviders/providers/Addic7ed.kt | 14 +- .../syncproviders/providers/AniListApi.kt | 40 ++--- .../syncproviders/providers/LocalList.kt | 2 - .../syncproviders/providers/MALApi.kt | 128 ++++++++-------- .../providers/OpenSubtitlesApi.kt | 29 ++-- .../syncproviders/providers/SimklApi.kt | 137 +++++++++--------- .../syncproviders/providers/SubSource.kt | 1 + .../cloudstream3/ui/APIRepository.kt | 6 +- .../lagradost/cloudstream3/ui/BaseAdapter.kt | 2 + .../cloudstream3/ui/ControllerActivity.kt | 2 + .../cloudstream3/ui/CustomRecyclerViews.kt | 4 +- .../cloudstream3/ui/EasterEggMonke.kt | 2 +- .../ui/NonFinalAdapterListUpdateCallback.kt | 2 +- .../lagradost/cloudstream3/ui/WatchType.kt | 4 +- .../cloudstream3/ui/WebviewFragment.kt | 3 + .../ui/download/button/BaseFetchButton.kt | 1 + .../ui/download/button/DownloadButton.kt | 2 +- .../ui/home/HomeChildItemAdapter.kt | 5 +- .../cloudstream3/ui/home/HomeFragment.kt | 3 +- .../ui/home/HomeParentItemAdapter.kt | 13 +- .../ui/home/HomeParentItemAdapterPreview.kt | 7 +- .../cloudstream3/ui/home/HomeViewModel.kt | 10 +- .../ui/library/LibraryFragment.kt | 6 +- .../ui/library/ViewpagerAdapter.kt | 3 +- .../ui/player/AbstractPlayerFragment.kt | 7 +- .../cloudstream3/ui/player/CS3IPlayer.kt | 82 ++--------- .../ui/player/CustomSubtitleDecoderFactory.kt | 7 +- .../ui/player/CustomTextRenderer.kt | 3 +- .../ui/player/DownloadFileGenerator.kt | 2 +- .../ui/player/DownloadedPlayerActivity.kt | 4 - .../ui/player/FullScreenPlayer.kt | 42 +++--- .../cloudstream3/ui/player/GeneratorPlayer.kt | 20 ++- .../cloudstream3/ui/player/IPlayer.kt | 1 - .../cloudstream3/ui/player/LinkGenerator.kt | 1 - .../ui/player/NonFinalTextRenderer.java | 16 +- .../ui/player/OfflinePlaybackHelper.kt | 2 +- .../ui/player/PlayerGeneratorViewModel.kt | 2 +- .../ui/player/PlayerSubtitleHelper.kt | 3 + .../ui/player/PreviewGenerator.kt | 21 ++- .../ui/player/RepoLinkGenerator.kt | 1 + .../player/source_priority/PriorityAdapter.kt | 5 - .../player/source_priority/ProfilesAdapter.kt | 2 - .../source_priority/QualityDataHelper.kt | 7 +- .../source_priority/QualityProfileDialog.kt | 2 +- .../source_priority/SourcePriorityDialog.kt | 2 +- .../cloudstream3/ui/result/ActorAdaptor.kt | 5 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 6 +- .../cloudstream3/ui/result/ImageAdapter.kt | 15 +- .../ui/result/ResultFragmentPhone.kt | 49 ++++--- .../ui/result/ResultFragmentTv.kt | 45 +----- .../ui/result/ResultViewModel2.kt | 2 +- .../cloudstream3/ui/result/SelectAdaptor.kt | 3 +- .../cloudstream3/ui/search/SearchAdaptor.kt | 7 +- .../ui/search/SearchHistoryAdaptor.kt | 8 +- .../ui/search/SearchResultBuilder.kt | 7 +- .../ui/search/SyncSearchViewModel.kt | 4 +- .../ui/settings/AccountAdapter.kt | 7 +- .../ui/settings/SettingsFragment.kt | 1 - .../ui/settings/SettingsGeneral.kt | 2 - .../ui/settings/SettingsPlayer.kt | 9 +- .../ui/settings/SettingsProviders.kt | 2 +- .../ui/settings/SettingsUpdates.kt | 10 +- .../ui/settings/extensions/PluginAdapter.kt | 23 +-- .../ui/settings/extensions/PluginsFragment.kt | 2 +- .../settings/extensions/PluginsViewModel.kt | 1 - .../ui/setup/SetupFragmentMedia.kt | 1 - .../subtitles/ChromecastSubtitlesFragment.kt | 29 ++-- .../ui/subtitles/SubtitlesFragment.kt | 9 +- .../lagradost/cloudstream3/utils/AniSkip.kt | 2 +- .../cloudstream3/utils/AppContextUtils.kt | 5 +- .../cloudstream3/utils/BackupUtils.kt | 36 ++--- .../lagradost/cloudstream3/utils/DataStore.kt | 25 +++- .../utils/DownloadFileWorkManager.kt | 1 - .../cloudstream3/utils/InAppUpdater.kt | 40 ++--- .../cloudstream3/utils/PackageInstaller.kt | 5 +- .../cloudstream3/utils/PowerManagerAPI.kt | 6 +- .../lagradost/cloudstream3/utils/SyncUtil.kt | 8 +- .../lagradost/cloudstream3/utils/UIHelper.kt | 2 +- .../utils/VideoDownloadManager.kt | 8 +- .../cloudstream3/widget/FlowLayout.kt | 2 +- .../cloudstream3/PluginAdapterTest.kt | 16 ++ 97 files changed, 563 insertions(+), 721 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt create mode 100644 app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 598ff540..d6f978fe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -35,6 +35,7 @@ import java.io.File import java.io.FileNotFoundException import java.io.PrintStream import java.lang.ref.WeakReference +import java.util.Locale import kotlin.concurrent.thread import kotlin.system.exitProcess @@ -81,14 +82,8 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) : ACRA.errorReporter.handleException(error) try { PrintStream(errorFile).use { ps -> - ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")) - ps.println( - String.format( - "Fatal exception on thread %s (%d)", - thread.name, - thread.id - ) - ) + ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}") + ps.println("Fatal exception on thread ${thread.name} (${thread.id})") error.printStackTrace(ps) } } catch (ignored: FileNotFoundException) { @@ -106,7 +101,6 @@ class AcraApplication : Application() { override fun onCreate() { super.onCreate() - //NativeCrashHandler.initCrashHandler() ExceptionHandler(filesDir.resolve("last_error")) { val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) startActivity(Intent.makeRestartActivityTask(intent!!.component)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index ba303fef..63912114 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -164,7 +164,7 @@ object CommonActivity { val toast = Toast(act) toast.duration = duration ?: Toast.LENGTH_SHORT toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) - toast.view = binding.root + toast.view = binding.root //fixme Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version. currentToast = toast toast.show() @@ -464,20 +464,6 @@ object CommonActivity { fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) { - //println("Keycode: $keyCode") - //showToast( - // this, - // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}", - // Toast.LENGTH_LONG - //) - - // Tested keycodes on remote: - // KeyEvent.KEYCODE_MEDIA_FAST_FORWARD - // KeyEvent.KEYCODE_MEDIA_REWIND - // KeyEvent.KEYCODE_MENU - // KeyEvent.KEYCODE_MEDIA_NEXT - // KeyEvent.KEYCODE_MEDIA_PREVIOUS - // KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE // 149 keycode_numpad 5 when (keyCode) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt index 934dd58a..8da7ca38 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt @@ -11,7 +11,7 @@ import java.util.concurrent.TimeUnit class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() { - private val client: OkHttpClient + private val client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build() override fun execute(request: Request): Response { val httpMethod: String = request.httpMethod() val url: String = request.url() @@ -74,8 +74,4 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do return instance } } - - init { - client = builder.readTimeout(30, TimeUnit.SECONDS).build() - } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index bc2cb88e..eed69a50 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -82,13 +82,13 @@ import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver import com.lagradost.cloudstream3.services.SubscriptionWorkManager +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_PLAYER +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_REPO +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_SEARCH import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi import com.lagradost.cloudstream3.syncproviders.SyncAPI @@ -347,7 +347,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa println("Repository url: $realUrl") loadRepository(realUrl) return true - } else if (str.contains(appString)) { + } else if (str.contains(APP_STRING)) { for (api in OAuth2Apis) { if (str.contains("/${api.redirectUrl}")) { ioSafe { @@ -377,15 +377,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } // This specific intent is used for the gradle deployWithAdb // https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46 - if (str == "$appString:") { + if (str == "$APP_STRING:") { PluginManager.hotReloadAllLocalPlugins(activity) } - } else if (safeURI(str)?.scheme == appStringRepo) { - val url = str.replaceFirst(appStringRepo, "https") + } else if (safeURI(str)?.scheme == APP_STRING_REPO) { + val url = str.replaceFirst(APP_STRING_REPO, "https") loadRepository(url) return true - } else if (safeURI(str)?.scheme == appStringSearch) { - val query = str.substringAfter("$appStringSearch://") + } else if (safeURI(str)?.scheme == APP_STRING_SEARCH) { + val query = str.substringAfter("$APP_STRING_SEARCH://") nextSearchQuery = try { URLDecoder.decode(query, "UTF-8") @@ -399,7 +399,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa R.id.navigation_search activity?.findViewById(R.id.nav_rail_view)?.selectedItemId = R.id.navigation_search - } else if (safeURI(str)?.scheme == appStringPlayer) { + } else if (safeURI(str)?.scheme == APP_STRING_PLAYER) { val uri = Uri.parse(str) val name = uri.getQueryParameter("name") val url = URLDecoder.decode(uri.authority, "UTF-8") @@ -413,9 +413,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ) ) ) - } else if (safeURI(str)?.scheme == appStringResumeWatching) { + } else if (safeURI(str)?.scheme == APP_STRING_RESUME_WATCHING) { val id = - str.substringAfter("$appStringResumeWatching://").toIntOrNull() + str.substringAfter("$APP_STRING_RESUME_WATCHING://").toIntOrNull() ?: return false ioSafe { val resumeWatchingCard = @@ -469,7 +469,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ) DubStatus.Dubbed else DubStatus.Subbed, null ) } else { - viewModel.loadSmall(this, result) + viewModel.loadSmall(result) } } @@ -605,7 +605,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } //private var mCastSession: CastSession? = null - lateinit var mSessionManager: SessionManager + var mSessionManager: SessionManager? = null private val mSessionManagerListener: SessionManagerListener by lazy { SessionManagerListenerImpl() } private inner class SessionManagerListenerImpl : SessionManagerListener { @@ -645,8 +645,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa setActivityInstance(this) try { if (isCastApiAvailable()) { - //mCastSession = mSessionManager.currentCastSession - mSessionManager.addSessionManagerListener(mSessionManagerListener) + mSessionManager?.addSessionManagerListener(mSessionManagerListener) } } catch (e: Exception) { logError(e) @@ -662,7 +661,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa } try { if (isCastApiAvailable()) { - mSessionManager.removeSessionManagerListener(mSessionManagerListener) + mSessionManager?.removeSessionManagerListener(mSessionManagerListener) //mCastSession = null } } catch (e: Exception) { @@ -766,7 +765,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa list.forEach { custom -> allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } ?.let { - allProviders.add(it.javaClass.newInstance().apply { + allProviders.add(it.javaClass.getDeclaredConstructor().newInstance().apply { name = custom.name lang = custom.lang mainUrl = custom.url.trimEnd('/') @@ -1147,7 +1146,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa super.onCreate(savedInstanceState) try { if (isCastApiAvailable()) { - mSessionManager = CastContext.getSharedInstance(this).sessionManager + CastContext.getSharedInstance(this) {it.run()}.addOnSuccessListener { mSessionManager = it.sessionManager } } } catch (t: Throwable) { logError(t) @@ -1449,13 +1448,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa val value = viewModel.watchStatus.value ?: WatchType.NONE this@MainActivity.showBottomDialog( - WatchType.values().map { getString(it.stringRes) }.toList(), + WatchType.entries.map { getString(it.stringRes) }.toList(), value.ordinal, this@MainActivity.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { viewModel.updateWatchStatus( - WatchType.values()[it], + WatchType.entries[it], this@MainActivity ) } @@ -1465,12 +1464,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa ?: SyncWatchType.NONE this@MainActivity.showBottomDialog( - SyncWatchType.values().map { getString(it.stringRes) }.toList(), + SyncWatchType.entries.map { getString(it.stringRes) }.toList(), value.ordinal, this@MainActivity.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - syncViewModel.setStatus(SyncWatchType.values()[it].internalId) + syncViewModel.setStatus(SyncWatchType.entries[it].internalId) syncViewModel.publishUserData() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt deleted file mode 100644 index 7be90440..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.lagradost.cloudstream3 - -import com.lagradost.cloudstream3.MainActivity.Companion.lastError -import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -object NativeCrashHandler { - // external fun triggerNativeCrash() - /*private external fun initNativeCrashHandler() - private external fun getSignalStatus(): Int - - private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch { - - //launch { - // delay(10000) - // triggerNativeCrash() - //} - - while (true) { - delay(10_000) - val signal = getSignalStatus() - // Signal is initialized to zero - if (signal == 0) continue - - // Do not crash in safe mode! - if (lastError != null) continue - if (checkSafeModeFile()) continue - - AcraApplication.exceptionHandler?.uncaughtException( - Thread.currentThread(), - RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n") - ) - } - } - - fun initCrashHandler() { - try { - System.loadLibrary("native-lib") - initNativeCrashHandler() - } catch (t: Throwable) { - // Make debug crash. - if (BuildConfig.DEBUG) throw t - logError(t) - return - } - - initSignalPolling() - }*/ -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt index 75e96bec..bc646a8d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt @@ -2,15 +2,13 @@ package com.lagradost.cloudstream3.metaproviders import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.SyncIdName object SyncRedirector { - val syncApis = SyncApis private val syncIds = listOf( - SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""), - SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""") + SyncIdName.MyAnimeList to Regex("""myanimelist\.net/anime/(\d+)"""), + SyncIdName.Anilist to Regex("""anilist\.co/anime/(\d+)""") ) suspend fun redirect( diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt index a1b9ff34..addee9a0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt @@ -296,7 +296,7 @@ open class TraktProvider : MainAPI() { return try { val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) val dateTime = dateString?.let { format.parse(it)?.time } ?: return false - APIHolder.unixTimeMS < dateTime + unixTimeMS < dateTime } catch (t: Throwable) { logError(t) false diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt index ce2fb3a2..85a9db5d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.network -import android.util.Base64 import android.util.Log import android.webkit.CookieManager import androidx.annotation.AnyThread @@ -10,7 +9,10 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.nicehttp.Requests.Companion.await import com.lagradost.nicehttp.cookies import kotlinx.coroutines.runBlocking -import okhttp3.* +import okhttp3.Headers +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response import java.net.URI diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt index e89ccfeb..ddf5b286 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt @@ -2,5 +2,4 @@ package com.lagradost.cloudstream3.plugins @Suppress("unused") @Target(AnnotationTarget.CLASS) -annotation class CloudstreamPlugin( -) \ No newline at end of file +annotation class CloudstreamPlugin \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index 7f08af92..fc836587 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -34,7 +34,7 @@ abstract class Plugin { */ fun registerMainAPI(element: MainAPI) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI") - element.sourcePlugin = this.__filename + element.sourcePlugin = this.filename // Race condition causing which would case duplicates if not for distinctBy synchronized(APIHolder.allProviders) { APIHolder.allProviders.add(element) @@ -48,7 +48,7 @@ abstract class Plugin { */ fun registerExtractorAPI(element: ExtractorApi) { Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi") - element.sourcePlugin = this.__filename + element.sourcePlugin = this.filename extractorApis.add(element) } @@ -68,7 +68,11 @@ abstract class Plugin { */ var resources: Resources? = null /** Full file path to the plugin. */ - var __filename: String? = null + @Deprecated("Renamed to `filename` to follow conventions", replaceWith = ReplaceWith("filename")) + var __filename: String? + get() = filename + set(value) {filename = value} + var filename: String? = null /** * This will add a button in the settings allowing you to add custom settings 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 6b2b75f2..bc2a1780 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -1,13 +1,16 @@ package com.lagradost.cloudstream3.plugins +import android.Manifest import android.app.* import android.content.Context +import android.content.pm.PackageManager import android.content.res.AssetManager import android.content.res.Resources import android.os.Build import android.os.Environment import android.util.Log import android.widget.Toast +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.FragmentActivity @@ -163,7 +166,7 @@ object PluginManager { private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins" - public var currentlyLoading: String? = null + var currentlyLoading: String? = null // Maps filepath to plugin val plugins: MutableMap = @@ -339,7 +342,7 @@ object PluginManager { //Omit non-NSFW if mode is set to NSFW only if (mode == AutoDownloadMode.NsfwOnly) { - if (tvtypes.contains(TvType.NSFW.name) == false) { + if (!tvtypes.contains(TvType.NSFW.name)) { return@mapNotNull null } } @@ -504,10 +507,12 @@ object PluginManager { val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also { Log.d(TAG, "No manifest version for ${data.internalName}") } + + @Suppress("UNCHECKED_CAST") val pluginClass: Class<*> = loader.loadClass(manifest.pluginClassName) as Class val pluginInstance: Plugin = - pluginClass.newInstance() as Plugin + pluginClass.getDeclaredConstructor().newInstance() as Plugin // Sets with the proper version setPluginData(data.copy(version = version)) @@ -517,14 +522,16 @@ object PluginManager { return true } - pluginInstance.__filename = file.absolutePath + pluginInstance.filename = file.absolutePath if (manifest.requiresResources) { Log.d(TAG, "Loading resources for ${data.internalName}") // based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk - val assets = AssetManager::class.java.newInstance() + val assets = AssetManager::class.java.getDeclaredConstructor().newInstance() val addAssetPath = AssetManager::class.java.getMethod("addAssetPath", String::class.java) addAssetPath.invoke(assets, file.absolutePath) + + @Suppress("DEPRECATION") pluginInstance.resources = Resources( assets, context.resources.displayMetrics, @@ -566,14 +573,14 @@ object PluginManager { // remove all registered apis synchronized(APIHolder.apis) { - APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach { + APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { removePluginMapping(it) } } synchronized(APIHolder.allProviders) { - APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename } + APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename } } - extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename } + extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename } classLoaders.values.removeIf { v -> v == plugin } @@ -720,9 +727,14 @@ object PluginManager { } val notification = builder.build() - with(NotificationManagerCompat.from(context)) { - // notificationId is a unique int for each notification that you must define - notify((System.currentTimeMillis() / 1000).toInt(), notification) + // notificationId is a unique int for each notification that you must define + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + NotificationManagerCompat.from(context) + .notify((System.currentTimeMillis() / 1000).toInt(), notification) } return notification } catch (e: Exception) { 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 b80a590e..c6ec9df7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -73,7 +73,7 @@ object RepositoryManager { val PREBUILT_REPOSITORIES: Array by lazy { getKey("PREBUILT_REPOSITORIES") ?: emptyArray() } - 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_.-]+)/(.*)$") /* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */ fun convertRawGitUrl(url: String): String { diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt index a45ab5f0..d1b702f4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.sync.withLock object VotingApi { // please do not cheat the votes lol private const val LOGKEY = "VotingApi" - private const val apiDomain = "https://counterapi.com/api" + private const val API_DOMAIN = "https://counterapi.com/api" private fun transformUrl(url: String): String = // dont touch or all votes get reset MessageDigest @@ -49,13 +49,13 @@ object VotingApi { // please do not cheat the votes lol .joinToString("-") private suspend fun readVote(pluginUrl: String): Int { - var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true" + val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true" Log.d(LOGKEY, "Requesting: $url") return app.get(url).parsedSafe()?.value ?: 0 } private suspend fun writeVote(pluginUrl: String): Boolean { - var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}" + val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}" Log.d(LOGKEY, "Requesting: $url") return app.get(url).parsedSafe()?.value != null } @@ -69,8 +69,7 @@ object VotingApi { // please do not cheat the votes lol getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false fun canVote(pluginUrl: String): Boolean { - if (!PluginManager.urlPlugins.contains(pluginUrl)) return false - return true + return PluginManager.urlPlugins.contains(pluginUrl) } private val voteLock = Mutex() diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt index 857fba11..df64caab 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt @@ -59,7 +59,7 @@ class SubtitleResource { return file } - fun unzip(file: File): List> { + private fun unzip(file: File): List> { val entries = mutableListOf>() ZipInputStream(file.inputStream()).use { zipInputStream -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt index ed4ccb74..685b499b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.subtitles -import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.TvType class AbstractSubtitleEntities { diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt index 0259ccad..2e14c3c4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt @@ -56,22 +56,22 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI { subSourceApi ) - const val appString = "cloudstreamapp" - const val appStringRepo = "cloudstreamrepo" - const val appStringPlayer = "cloudstreamplayer" + const val APP_STRING = "cloudstreamapp" + const val APP_STRING_REPO = "cloudstreamrepo" + const val APP_STRING_PLAYER = "cloudstreamplayer" // Instantly start the search given a query - const val appStringSearch = "cloudstreamsearch" + const val APP_STRING_SEARCH = "cloudstreamsearch" // Instantly resume watching a show - const val appStringResumeWatching = "cloudstreamcontinuewatching" + const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching" val unixTime: Long get() = System.currentTimeMillis() / 1000L val unixTimeMs: Long get() = System.currentTimeMillis() - const val maxStale = 60 * 10 + const val MAX_STALE = 60 * 10 fun secondsToReadable(seconds: Int, completedValue: String): String { var secondsLong = seconds.toLong() diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt index 507c5e2a..db467639 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt @@ -18,13 +18,13 @@ class Addic7ed : AbstractSubApi { override fun logOut() {} companion object { - const val host = "https://www.addic7ed.com" + const val HOST = "https://www.addic7ed.com" const val TAG = "ADDIC7ED" } private fun fixUrl(url: String): String { - return if (url.startsWith("/")) host + url - else if (!url.startsWith("http")) "$host/$url" + return if (url.startsWith("/")) HOST + url + else if (!url.startsWith("http")) "$HOST/$url" else url } @@ -62,7 +62,7 @@ class Addic7ed : AbstractSubApi { } val title = queryText.substringBefore("(").trim() - val url = "$host/search.php?search=${title}&Submit=Search" + val url = "$HOST/search.php?search=${title}&Submit=Search" val hostDocument = app.get(url).document var searchResult = "" if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url @@ -74,8 +74,8 @@ class Addic7ed : AbstractSubApi { hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(") ?.substringBefore(",") val doc = app.get( - "$host/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined", - referer = "$host/" + "$HOST/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined", + referer = "$HOST/" ).document doc.select("#season tr:contains($queryLang)").mapNotNull { node -> if (node.selectFirst("td")?.text() @@ -97,7 +97,7 @@ class Addic7ed : AbstractSubApi { val link = fixUrl(node.select("a.buttonDownload").attr("href")) val isHearingImpaired = !node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty() - cleanResources(results, name, link, mapOf("referer" to "$host/"), isHearingImpaired) + cleanResources(results, name, link, mapOf("referer" to "$HOST/"), isHearingImpaired) } return results } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index 8a82cf94..e51d3d65 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -63,7 +63,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleRedirect(url: String): Boolean { val sanitizer = - splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR + splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR val token = sanitizer["access_token"]!! val expiresIn = sanitizer["expires_in"]!! @@ -87,7 +87,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun search(name: String): List? { val data = searchShows(name) ?: return null - return data.data?.Page?.media?.map { + return data.data?.page?.media?.map { SyncAPI.SyncSearchResult( it.title.romaji ?: return null, this.name, @@ -101,7 +101,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getResult(id: String): SyncAPI.SyncResult { val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1) ?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId") - val season = getSeason(internalId).data.Media + val season = getSeason(internalId).data.media return SyncAPI.SyncResult( season.id.toString(), @@ -301,12 +301,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { //println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}") val shows = searchShows(name.replace(blackListRegex, "")) - shows?.data?.Page?.media?.find { + shows?.data?.page?.media?.find { (malId ?: "NONE") == it.idMal.toString() }?.let { return it } val filtered = - shows?.data?.Page?.media?.filter { + shows?.data?.page?.media?.filter { (((it.startDate.year ?: year.toString()) == year.toString() || year == null)) } @@ -496,7 +496,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { val data = postApi(q, true) val d = parseJson(data ?: return null) - val main = d.data?.Media + val main = d.data?.media if (main?.mediaListEntry != null) { return AniListTitleHolder( title = main.title, @@ -536,7 +536,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { headers = mapOf( "Authorization" to "Bearer " + (getAuth() ?: return@suspendSafeApiCall null), - if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" + if (cache) "Cache-Control" to "max-stale=$MAX_STALE" else "Cache-Control" to "no-cache" ), cacheTime = 0, data = mapOf( @@ -647,7 +647,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class Data( - @JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection + @JsonProperty("MediaListCollection") val mediaListCollection: MediaListCollection ) private fun getAniListListCached(): Array? { @@ -659,7 +659,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { if (checkToken()) return null return if (requireLibraryRefresh) { - val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray() + val list = getFullAniListList()?.data?.mediaListCollection?.lists?.toTypedArray() if (list != null) { setKey(ANILIST_CACHED_LIST, list) } @@ -678,7 +678,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { // To fill empty lists when AniList does not return them val baseMap = - AniListStatusType.values().filter { it.value >= 0 }.associate { + AniListStatusType.entries.filter { it.value >= 0 }.associate { it.stringRes to emptyList() } @@ -764,7 +764,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { /** Used to query a saved MediaItem on the list to get the id for removal */ data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null) - data class MediaListItem(@JsonProperty("MediaList") val MediaList: MediaListId? = null) + data class MediaListItem(@JsonProperty("MediaList") val mediaList: MediaListId? = null) data class MediaListId(@JsonProperty("id") val id: Long? = null) private suspend fun postDataAboutId( @@ -787,7 +787,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { """ val response = postApi(idQuery) val listId = - tryParseJson(response)?.data?.MediaList?.id ?: return false + tryParseJson(response)?.data?.mediaList?.id ?: return false """ mutation(${'$'}id: Int = $listId) { DeleteMediaListEntry(id: ${'$'}id) { @@ -836,7 +836,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { val data = postApi(q) if (data.isNullOrBlank()) return null val userData = parseJson(data) - val u = userData.data?.Viewer + val u = userData.data?.viewer val user = AniListUser( u?.id, u?.name, @@ -858,8 +858,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { suspend fun getSeasonRecursive(id: Int) { val season = getSeason(id) seasons.add(season) - if (season.data.Media.format?.startsWith("TV") == true) { - season.data.Media.relations?.edges?.forEach { + if (season.data.media.format?.startsWith("TV") == true) { + season.data.media.relations?.edges?.forEach { if (it.node?.format != null) { if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) { getSeasonRecursive(it.node.id) @@ -878,7 +878,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class SeasonData( - @JsonProperty("Media") val Media: SeasonMedia, + @JsonProperty("Media") val media: SeasonMedia, ) data class SeasonMedia( @@ -1050,7 +1050,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class AniListData( - @JsonProperty("Viewer") val Viewer: AniListViewer?, + @JsonProperty("Viewer") val viewer: AniListViewer?, ) data class AniListRoot( @@ -1090,7 +1090,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class LikeData( - @JsonProperty("Viewer") val Viewer: LikeViewer?, + @JsonProperty("Viewer") val viewer: LikeViewer?, ) data class LikeRoot( @@ -1130,7 +1130,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class GetDataData( - @JsonProperty("Media") val Media: GetDataMedia?, + @JsonProperty("Media") val media: GetDataMedia?, ) data class GetDataRoot( @@ -1163,7 +1163,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ) data class GetSearchPage( - @JsonProperty("Page") val Page: GetSearchData?, + @JsonProperty("Page") val page: GetSearchData?, ) data class GetSearchData( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt index 00f8d00c..f819cd3b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -119,8 +119,6 @@ class LocalList : SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, -// ListSorting.RatingHigh, -// ListSorting.RatingLow, ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index 24ef7136..6046a0f2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -19,14 +19,18 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import java.net.URL import java.security.SecureRandom import java.text.ParseException import java.text.SimpleDateFormat -import java.util.* +import java.time.Instant +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.TimeZone /** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */ const val MAL_MAX_SEARCH_LIMIT = 25 @@ -51,7 +55,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } override fun loginInfo(): AuthAPI.LoginInfo? { - //getMalUser(true)? getKey(accountId, MAL_USER_KEY)?.let { user -> return AuthAPI.LoginInfo( profilePicture = user.picture, @@ -84,7 +87,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { this.name, node.id.toString(), "$mainUrl/anime/${node.id}/", - node.main_picture?.large ?: node.main_picture?.medium + node.mainPicture?.large ?: node.mainPicture?.medium ) } } @@ -178,7 +181,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private fun parseDate(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd")?.parse(string ?: return null)?.time + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(string ?: return null)?.time } catch (e: Exception) { null } @@ -190,7 +193,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { apiName = this.name, syncId = node.id.toString(), url = "$mainUrl/anime/${node.id}", - posterUrl = node.main_picture?.large + posterUrl = node.mainPicture?.large ) } @@ -244,12 +247,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { val internalId = id.toIntOrNull() ?: return null val data = - getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status") + getDataAboutMalId(internalId)?.myListStatus //?: throw ErrorLoadingException("No my_list_status") return SyncAPI.SyncStatus( score = data?.score, - status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)) , + status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)), isFavorite = null, - watchedEpisodes = data?.num_episodes_watched, + watchedEpisodes = data?.numEpisodesWatched, ) } @@ -291,7 +294,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { private fun parseDateLong(string: String?): Long? { return try { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse( + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse( string ?: return null )?.time?.div(1000) } catch (e: Exception) { @@ -302,7 +305,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleRedirect(url: String): Boolean { val sanitizer = - splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR + splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR val state = sanitizer["state"]!! if (state == "RequestID$requestId") { val currentCode = sanitizer["code"]!! @@ -351,9 +354,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { try { if (response != "") { val token = parseJson(response) - setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime)) - setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token) - setKey(accountId, MAL_TOKEN_KEY, token.access_token) + setKey(accountId, MAL_UNIXTIME_KEY, (token.expiresIn + unixTime)) + setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refreshToken) + setKey(accountId, MAL_TOKEN_KEY, token.accessToken) requireLibraryRefresh = true } } catch (e: Exception) { @@ -395,53 +398,53 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class Node( @JsonProperty("id") val id: Int, @JsonProperty("title") val title: String, - @JsonProperty("main_picture") val main_picture: MainPicture?, - @JsonProperty("alternative_titles") val alternative_titles: AlternativeTitles?, - @JsonProperty("media_type") val media_type: String?, - @JsonProperty("num_episodes") val num_episodes: Int?, + @JsonProperty("main_picture") val mainPicture: MainPicture?, + @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?, + @JsonProperty("media_type") val mediaType: String?, + @JsonProperty("num_episodes") val numEpisodes: Int?, @JsonProperty("status") val status: String?, - @JsonProperty("start_date") val start_date: String?, - @JsonProperty("end_date") val end_date: String?, - @JsonProperty("average_episode_duration") val average_episode_duration: Int?, + @JsonProperty("start_date") val startDate: String?, + @JsonProperty("end_date") val endDate: String?, + @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?, @JsonProperty("synopsis") val synopsis: String?, @JsonProperty("mean") val mean: Double?, @JsonProperty("genres") val genres: List?, @JsonProperty("rank") val rank: Int?, @JsonProperty("popularity") val popularity: Int?, - @JsonProperty("num_list_users") val num_list_users: Int?, - @JsonProperty("num_favorites") val num_favorites: Int?, - @JsonProperty("num_scoring_users") val num_scoring_users: Int?, - @JsonProperty("start_season") val start_season: StartSeason?, + @JsonProperty("num_list_users") val numListUsers: Int?, + @JsonProperty("num_favorites") val numFavorites: Int?, + @JsonProperty("num_scoring_users") val numScoringUsers: Int?, + @JsonProperty("start_season") val startSeason: StartSeason?, @JsonProperty("broadcast") val broadcast: Broadcast?, @JsonProperty("nsfw") val nsfw: String?, - @JsonProperty("created_at") val created_at: String?, - @JsonProperty("updated_at") val updated_at: String? + @JsonProperty("created_at") val createdAt: String?, + @JsonProperty("updated_at") val updatedAt: String? ) data class ListStatus( @JsonProperty("status") val status: String?, @JsonProperty("score") val score: Int, - @JsonProperty("num_episodes_watched") val num_episodes_watched: Int, - @JsonProperty("is_rewatching") val is_rewatching: Boolean, - @JsonProperty("updated_at") val updated_at: String, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, + @JsonProperty("is_rewatching") val isRewatching: Boolean, + @JsonProperty("updated_at") val updatedAt: String, ) data class Data( @JsonProperty("node") val node: Node, - @JsonProperty("list_status") val list_status: ListStatus?, + @JsonProperty("list_status") val listStatus: ListStatus?, ) { fun toLibraryItem(): SyncAPI.LibraryItem { return SyncAPI.LibraryItem( this.node.title, "https://myanimelist.net/anime/${this.node.id}/", this.node.id.toString(), - this.list_status?.num_episodes_watched, - this.node.num_episodes, - this.list_status?.score?.times(10), - parseDateLong(this.list_status?.updated_at), + this.listStatus?.numEpisodesWatched, + this.node.numEpisodes, + this.listStatus?.score?.times(10), + parseDateLong(this.listStatus?.updatedAt), "MAL", TvType.Anime, - this.node.main_picture?.large ?: this.node.main_picture?.medium, + this.node.mainPicture?.large ?: this.node.mainPicture?.medium, null, null, plot = this.node.synopsis, @@ -470,8 +473,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ) data class Broadcast( - @JsonProperty("day_of_the_week") val day_of_the_week: String?, - @JsonProperty("start_time") val start_time: String? + @JsonProperty("day_of_the_week") val dayOfTheWeek: String?, + @JsonProperty("start_time") val startTime: String? ) private fun getMalAnimeListCached(): Array? { @@ -491,14 +494,14 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata { val list = getMalAnimeListSmart()?.groupBy { - convertToStatus(it.list_status?.status ?: "").stringRes + convertToStatus(it.listStatus?.status ?: "").stringRes }?.mapValues { group -> group.value.map { it.toLibraryItem() } } ?: emptyMap() // To fill empty lists when MAL does not return them val baseMap = - MalStatusType.values().filter { it.value >= 0 }.associate { + MalStatusType.entries.filter { it.value >= 0 }.associate { it.stringRes to emptyList() } @@ -573,7 +576,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ).text val values = parseJson(res) val titles = - values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) } + values.data.map { MalTitleHolder(it.listStatus, it.node.id, it.node.title) } for (t in titles) { allTitles[t.id] = t } @@ -582,11 +585,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } } - fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { + private fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { // No time remaining if the show has already ended try { endDate?.let { - if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null + if (SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(it) + ?.before(Date.from(Instant.now())) != false + ) return@convertJapanTimeToTimeRemaining null } } catch (e: ParseException) { logError(e) @@ -603,7 +608,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH) val currentYear = currentDate.get(Calendar.YEAR) - val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm") + val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm", Locale.getDefault()) dateFormat.timeZone = TimeZone.getTimeZone("Japan") val parsedDate = dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null @@ -647,13 +652,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { id: Int, status: MalStatusType? = null, score: Int? = null, - num_watched_episodes: Int? = null, + numWatchedEpisodes: Int? = null, ): Boolean { val res = setScoreRequest( id, if (status == null) null else malStatusAsString[maxOf(0, status.value)], score, - num_watched_episodes + numWatchedEpisodes ) return if (res.isNullOrBlank()) { @@ -670,17 +675,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { } } + @Suppress("UNCHECKED_CAST") private suspend fun setScoreRequest( id: Int, status: String? = null, score: Int? = null, - num_watched_episodes: Int? = null, + numWatchedEpisodes: Int? = null, ): String? { val data = mapOf( "status" to status, "score" to score?.toString(), - "num_watched_episodes" to num_watched_episodes?.toString() - ).filter { it.value != null } as Map + "num_watched_episodes" to numWatchedEpisodes?.toString() + ).filterValues { it != null } as Map return app.put( "$apiUrl/v2/anime/$id/my_list_status", @@ -693,10 +699,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class ResponseToken( - @JsonProperty("token_type") val token_type: String, - @JsonProperty("expires_in") val expires_in: Int, - @JsonProperty("access_token") val access_token: String, - @JsonProperty("refresh_token") val refresh_token: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("expires_in") val expiresIn: Int, + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("refresh_token") val refreshToken: String, ) data class MalRoot( @@ -705,7 +711,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class MalDatum( @JsonProperty("node") val node: MalNode, - @JsonProperty("list_status") val list_status: MalStatus, + @JsonProperty("list_status") val listStatus: MalStatus, ) data class MalNode( @@ -722,16 +728,16 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class MalStatus( @JsonProperty("status") val status: String, @JsonProperty("score") val score: Int, - @JsonProperty("num_episodes_watched") val num_episodes_watched: Int, - @JsonProperty("is_rewatching") val is_rewatching: Boolean, - @JsonProperty("updated_at") val updated_at: String, + @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int, + @JsonProperty("is_rewatching") val isRewatching: Boolean, + @JsonProperty("updated_at") val updatedAt: String, ) data class MalUser( @JsonProperty("id") val id: Int, @JsonProperty("name") val name: String, @JsonProperty("location") val location: String, - @JsonProperty("joined_at") val joined_at: String, + @JsonProperty("joined_at") val joinedAt: String, @JsonProperty("picture") val picture: String?, ) @@ -744,9 +750,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { data class SmallMalAnime( @JsonProperty("id") val id: Int, @JsonProperty("title") val title: String?, - @JsonProperty("num_episodes") val num_episodes: Int, - @JsonProperty("my_list_status") val my_list_status: MalStatus?, - @JsonProperty("main_picture") val main_picture: MalMainPicture?, + @JsonProperty("num_episodes") val numEpisodes: Int, + @JsonProperty("my_list_status") val myListStatus: MalStatus?, + @JsonProperty("main_picture") val mainPicture: MalMainPicture?, ) data class MalSearchNode( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt index 6412ff1b..37b95614 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt @@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager -import com.lagradost.cloudstream3.utils.AppContextUtils import com.lagradost.cloudstream3.utils.AppUtils import okhttp3.Interceptor import okhttp3.Response @@ -30,10 +29,10 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi companion object { const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile - const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" - const val host = "https://api.opensubtitles.com/api/v1" + const val API_KEY = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" + const val HOST = "https://api.opensubtitles.com/api/v1" const val TAG = "OPENSUBS" - const val coolDownDuration: Long = 1000L * 30L // CoolDown if 429 error code in ms + const val COOLDOWN_DURATION: Long = 1000L * 30L // CoolDown if 429 error code in ms var currentCoolDown: Long = 0L var currentSession: SubtitleOAuthEntity? = null } @@ -49,7 +48,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi chain.request().newBuilder() .removeHeader("user-agent") .addHeader("user-agent", userAgent) - .addHeader("Api-Key", apiKey) + .addHeader("Api-Key", API_KEY) .build() ) } @@ -66,7 +65,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi } private fun throwGotTooManyRequests() { - currentCoolDown = unixTimeMs + coolDownDuration + currentCoolDown = unixTimeMs + COOLDOWN_DURATION throw ErrorLoadingException("Too many requests") } @@ -115,7 +114,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi private suspend fun initLogin(username: String, password: String): Boolean { //Log.i(TAG, "DATA = [$username] [$password]") val response = app.post( - url = "$host/login", + url = "$HOST/login", headers = mapOf( "Content-Type" to "application/json", ), @@ -134,7 +133,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi SubtitleOAuthEntity( user = username, pass = password, - access_token = token.token ?: run { + accessToken = token.token ?: run { return false }) ) @@ -197,8 +196,8 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val searchQueryUrl = when (imdbId > 0) { //Use imdb_id to search if its valid - true -> "$host/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" - false -> "$host/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" + true -> "$HOST/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" + false -> "$HOST/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" } val req = app.get( @@ -233,7 +232,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber val year = featureDetails?.year ?: query.year val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie - val isHearingImpaired = attr.hearing_impaired ?: false + val isHearingImpaired = attr.hearingImpaired ?: false //Log.i(TAG, "Result id/name => ${item.id} / $name") item.attributes?.files?.forEach { file -> val resultData = file.fileId?.toString() ?: "" @@ -266,11 +265,11 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi throwIfCantDoRequest() val req = app.post( - url = "$host/download", + url = "$HOST/download", headers = mapOf( Pair( "Authorization", - "Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}" + "Bearer ${currentSession?.accessToken ?: throw ErrorLoadingException("No access token active in current session")}" ), Pair("Content-Type", "application/json"), Pair("Accept", "*/*") @@ -299,7 +298,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi data class SubtitleOAuthEntity( var user: String, var pass: String, - var access_token: String, + var accessToken: String, ) data class OAuthToken( @@ -324,7 +323,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi @JsonProperty("url") var url: String? = null, @JsonProperty("files") var files: List? = listOf(), @JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(), - @JsonProperty("hearing_impaired") var hearing_impaired: Boolean? = null, + @JsonProperty("hearing_impaired") var hearingImpaired: Boolean? = null, ) data class ResultFiles( diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index 27975d19..e5db626b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -38,6 +38,7 @@ import java.security.SecureRandom import java.text.SimpleDateFormat import java.time.Instant import java.util.Date +import java.util.Locale import java.util.TimeZone import kotlin.time.Duration import kotlin.time.DurationUnit @@ -144,8 +145,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } companion object { - private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID - private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET + private const val CLIENT_ID: String = BuildConfig.SIMKL_CLIENT_ID + private const val CLIENT_SECRET: String = BuildConfig.SIMKL_CLIENT_SECRET private var lastLoginState = "" const val SIMKL_TOKEN_KEY: String = "simkl_token" @@ -154,10 +155,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time" /** 2014-09-01T09:10:11Z -> 1409562611 */ - private const val simklDateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + private const val SIMKL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'" fun getUnixTime(string: String?): Long? { return try { - SimpleDateFormat(simklDateFormat).apply { + SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply { this.timeZone = TimeZone.getTimeZone("UTC") }.parse( string ?: return null @@ -171,7 +172,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** 1409562611 -> 2014-09-01T09:10:11Z */ fun getDateTime(unixTime: Long?): String? { return try { - SimpleDateFormat(simklDateFormat).apply { + SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply { this.timeZone = TimeZone.getTimeZone("UTC") }.format( Date.from( @@ -208,7 +209,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { companion object { fun fromString(string: String): SimklListStatusType? { - return SimklListStatusType.values().firstOrNull { + return SimklListStatusType.entries.firstOrNull { it.originalName == string } } @@ -219,17 +220,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonInclude(JsonInclude.Include.NON_EMPTY) data class TokenRequest( @JsonProperty("code") val code: String, - @JsonProperty("client_id") val client_id: String = clientId, - @JsonProperty("client_secret") val client_secret: String = clientSecret, - @JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl", - @JsonProperty("grant_type") val grant_type: String = "authorization_code" + @JsonProperty("client_id") val clientId: String = CLIENT_ID, + @JsonProperty("client_secret") val clientSecret: String = CLIENT_SECRET, + @JsonProperty("redirect_uri") val redirectUri: String = "$APP_STRING://simkl", + @JsonProperty("grant_type") val grantType: String = "authorization_code" ) data class TokenResponse( /** No expiration date */ - val access_token: String, - val token_type: String, - val scope: String + @JsonProperty("access_token") val accessToken: String, + @JsonProperty("token_type") val tokenType: String, + @JsonProperty("scope") val scope: String ) // ------------------- @@ -261,15 +262,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { // ------------------- data class ActivitiesResponse( - val all: String?, - val tv_shows: UpdatedAt, - val anime: UpdatedAt, - val movies: UpdatedAt, + @JsonProperty("all") val all: String?, + @JsonProperty("tv_shows") val tvShows: UpdatedAt, + @JsonProperty("anime") val anime: UpdatedAt, + @JsonProperty("movies") val movies: UpdatedAt, ) { data class UpdatedAt( - val all: String?, - val removed_from_list: String?, - val rated_at: String?, + @JsonProperty("all") val all: String?, + @JsonProperty("removed_from_list") val removedFromList: String?, + @JsonProperty("rated_at") val ratedAt: String?, ) } @@ -308,7 +309,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("title") val title: String?, @JsonProperty("year") val year: Int?, @JsonProperty("ids") val ids: Ids?, - @JsonProperty("total_episodes") val total_episodes: Int? = null, + @JsonProperty("total_episodes") val totalEpisodes: Int? = null, @JsonProperty("status") val status: String? = null, @JsonProperty("poster") val poster: String? = null, @JsonProperty("type") val type: String? = null, @@ -540,7 +541,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } debugPrint { "Requesting episodes from $url" } - return app.get(url, params = mapOf("client_id" to clientId)) + return app.get(url, params = mapOf("client_id" to CLIENT_ID)) .parsedSafe>()?.also { val cacheTime = if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value @@ -558,7 +559,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("seasons") seasons: List? = null, @JsonProperty("episodes") episodes: List? = null, @JsonProperty("rating") val rating: Int? = null, - @JsonProperty("rated_at") val rated_at: String? = null, + @JsonProperty("rated_at") val ratedAt: String? = null, ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -567,7 +568,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("year") year: Int?, @JsonProperty("ids") ids: Ids?, @JsonProperty("rating") val rating: Int, - @JsonProperty("rated_at") val rated_at: String? = getDateTime(unixTime) + @JsonProperty("rated_at") val ratedAt: String? = getDateTime(unixTime) ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -576,7 +577,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { @JsonProperty("year") year: Int?, @JsonProperty("ids") ids: Ids?, @JsonProperty("to") val to: String, - @JsonProperty("watched_at") val watched_at: String? = getDateTime(unixTime) + @JsonProperty("watched_at") val watchedAt: String? = getDateTime(unixTime) ) : MediaObject(title, year, ids) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -631,24 +632,24 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } interface Metadata { - val last_watched_at: String? + val lastWatchedAt: String? val status: String? - val user_rating: Int? - val last_watched: String? - val watched_episodes_count: Int? - val total_episodes_count: Int? + val userRating: Int? + val lastWatched: String? + val watchedEpisodesCount: Int? + val totalEpisodesCount: Int? fun getIds(): ShowMetadata.Show.Ids fun toLibraryItem(): SyncAPI.LibraryItem } data class MovieMetadata( - override val last_watched_at: String?, - override val status: String, - override val user_rating: Int?, - override val last_watched: String?, - override val watched_episodes_count: Int?, - override val total_episodes_count: Int?, + @JsonProperty("last_watched_at") override val lastWatchedAt: String?, + @JsonProperty("status") override val status: String, + @JsonProperty("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, val movie: ShowMetadata.Show ) : Metadata { override fun getIds(): ShowMetadata.Show.Ids { @@ -660,10 +661,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.movie.title, "https://simkl.com/tv/${movie.ids.simkl}", movie.ids.simkl.toString(), - this.watched_episodes_count, - this.total_episodes_count, - this.user_rating?.times(10), - getUnixTime(last_watched_at) ?: 0, + this.watchedEpisodesCount, + this.totalEpisodesCount, + this.userRating?.times(10), + getUnixTime(lastWatchedAt) ?: 0, "Simkl", TvType.Movie, this.movie.poster?.let { getPosterUrl(it) }, @@ -675,12 +676,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { } data class ShowMetadata( - @JsonProperty("last_watched_at") override val last_watched_at: String?, + @JsonProperty("last_watched_at") override val lastWatchedAt: String?, @JsonProperty("status") override val status: String, - @JsonProperty("user_rating") override val user_rating: Int?, - @JsonProperty("last_watched") override val last_watched: String?, - @JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?, - @JsonProperty("total_episodes_count") override val total_episodes_count: Int?, + @JsonProperty("user_rating") override val userRating: Int?, + @JsonProperty("last_watched") override val lastWatched: String?, + @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?, + @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?, @JsonProperty("show") val show: Show ) : Metadata { override fun getIds(): Show.Ids { @@ -692,10 +693,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.show.title, "https://simkl.com/tv/${show.ids.simkl}", show.ids.simkl.toString(), - this.watched_episodes_count, - this.total_episodes_count, - this.user_rating?.times(10), - getUnixTime(last_watched_at) ?: 0, + this.watchedEpisodesCount, + this.totalEpisodesCount, + this.userRating?.times(10), + getUnixTime(lastWatchedAt) ?: 0, "Simkl", TvType.Anime, this.show.poster?.let { getPosterUrl(it) }, @@ -749,7 +750,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { chain.request() .newBuilder() .addHeader("Authorization", "Bearer $token") - .addHeader("simkl-api-key", clientId) + .addHeader("simkl-api-key", CLIENT_ID) .build() ) } @@ -810,7 +811,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { val episodeConstructor = SimklEpisodeConstructor( searchResult.ids?.simkl, searchResult.type, - searchResult.total_episodes, + searchResult.totalEpisodes, searchResult.hasEnded() ) @@ -832,12 +833,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ) } ?: return null, - score = foundItem.user_rating, - watchedEpisodes = foundItem.watched_episodes_count, - maxEpisodes = searchResult.total_episodes, + score = foundItem.userRating, + watchedEpisodes = foundItem.watchedEpisodesCount, + maxEpisodes = searchResult.totalEpisodes, episodeConstructor = episodeConstructor, - oldEpisodes = foundItem.watched_episodes_count ?: 0, - oldScore = foundItem.user_rating, + oldEpisodes = foundItem.watchedEpisodesCount ?: 0, + oldScore = foundItem.userRating, oldStatus = foundItem.status ) } else { @@ -845,7 +846,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { status = SyncWatchType.fromInternalId(SimklListStatusType.None.value), score = 0, watchedEpisodes = 0, - maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes, + maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.totalEpisodes, episodeConstructor = episodeConstructor, oldEpisodes = 0, oldStatus = null, @@ -891,12 +892,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { /** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ - suspend fun searchByIds(serviceMap: Map): Array? { + private suspend fun searchByIds(serviceMap: Map): Array? { if (serviceMap.isEmpty()) return emptyArray() return app.get( "$mainUrl/search/id", - params = mapOf("client_id" to clientId) + serviceMap.map { (service, id) -> + params = mapOf("client_id" to CLIENT_ID) + serviceMap.map { (service, id) -> service.originalName to id } ).parsedSafe() @@ -904,14 +905,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun search(name: String): List? { return app.get( - "$mainUrl/search/", params = mapOf("client_id" to clientId, "q" to name) + "$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name) ).parsedSafe>()?.mapNotNull { it.toSyncSearchResult() } } override fun authenticate(activity: FragmentActivity?) { lastLoginState = BigInteger(130, SecureRandom()).toString(32) val url = - "https://simkl.com/oauth/authorize?response_type=code&client_id=$clientId&redirect_uri=$appString://${redirectUrl}&state=$lastLoginState" + "https://simkl.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}&state=$lastLoginState" openBrowser(url, activity) } @@ -961,15 +962,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { val activities = getActivities() val lastCacheUpdate = getKey(accountId, SIMKL_CACHED_LIST_TIME) val lastRemoval = listOf( - activities?.tv_shows?.removed_from_list, - activities?.anime?.removed_from_list, - activities?.movies?.removed_from_list + activities?.tvShows?.removedFromList, + activities?.anime?.removedFromList, + activities?.movies?.removedFromList ).maxOf { getUnixTime(it) ?: -1 } val lastRealUpdate = listOf( - activities?.tv_shows?.all, + activities?.tvShows?.all, activities?.anime?.all, activities?.movies?.all, ).maxOf { @@ -1039,7 +1040,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun getDevicePin(): OAuth2API.PinAuthData? { val pinAuthResp = app.get( - "$mainUrl/oauth/pin?client_id=$clientId&redirect_uri=$appString://${redirectUrl}" + "$mainUrl/oauth/pin?client_id=$CLIENT_ID&redirect_uri=$APP_STRING://${redirectUrl}" ).parsedSafe() ?: return null return OAuth2API.PinAuthData( @@ -1053,7 +1054,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean { val pinAuthResp = app.get( - "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$clientId" + "$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$CLIENT_ID" ).parsedSafe() ?: return false if (pinAuthResp.accessToken != null) { @@ -1088,7 +1089,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ).parsedSafe() ?: return false switchToNewAccount() - setKey(accountId, SIMKL_TOKEN_KEY, token.access_token) + setKey(accountId, SIMKL_TOKEN_KEY, token.accessToken) val user = getUser() if (user == null) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt index 0e233ece..8dad1f88 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SubSource.kt @@ -59,6 +59,7 @@ class SubSourceApi : AbstractSubProvider { it?.subs?.filter { sub -> sub.releaseName!!.contains( String.format( + null, "E%02d", query.epNumber ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt index a075cc2e..9150cfc5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/APIRepository.kt @@ -50,7 +50,7 @@ class APIRepository(val api: MainAPI) { private val cache = threadSafeListOf() private var cacheIndex: Int = 0 - const val cacheSize = 20 + const val CACHE_SIZE = 20 } private fun afterPluginsLoaded(forceReload: Boolean) { @@ -94,9 +94,9 @@ class APIRepository(val api: MainAPI) { val add = SavedLoadResponse(unixTime, response, lookingForHash) synchronized(cache) { - if (cache.size > cacheSize) { + if (cache.size > CACHE_SIZE) { cache[cacheIndex] = add // rolling cache - cacheIndex = (cacheIndex + 1) % cacheSize + cacheIndex = (cacheIndex + 1) % CACHE_SIZE } else { cache.add(add) } 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 d90177f5..e930961c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt @@ -112,6 +112,7 @@ abstract class BaseAdapter< holder.onViewDetachedFromWindow() } + @Suppress("UNCHECKED_CAST") fun save(recyclerView: RecyclerView) { for (child in recyclerView.children) { val holder = @@ -124,6 +125,7 @@ abstract class BaseAdapter< stateViewModel.layoutManagerStates[id]?.clear() } + @Suppress("UNCHECKED_CAST") private fun getState(holder: ViewHolderState): S? = stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index 6bafa975..1eaac505 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -6,6 +6,7 @@ import android.view.Menu import android.view.View.* import android.widget.* import androidx.appcompat.app.AlertDialog +import androidx.media3.common.util.UnstableApi import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.kotlinModule @@ -263,6 +264,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi var isLoadingMore = false + override fun onMediaStatusUpdated() { super.onMediaStatusUpdated() val meta = getCurrentMetaData() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt index 1a9549e1..78ad2a6b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt @@ -8,8 +8,8 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs -class GrdLayoutManager(val context: Context, _spanCount: Int) : - GridLayoutManager(context, _spanCount) { +class GrdLayoutManager(val context: Context, spanCount: Int) : + GridLayoutManager(context, spanCount) { override fun onFocusSearchFailed( focused: View, focusDirection: Int, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt index c7041776..4879d2e0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/EasterEggMonke.kt @@ -51,7 +51,7 @@ class EasterEggMonke : AppCompatActivity() { FrameLayout.LayoutParams.WRAP_CONTENT) binding.frame.addView(newStar) - newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX + newStar.scaleX += Math.random().toFloat() * 1.5f newStar.scaleY = newStar.scaleX starW *= newStar.scaleX starH *= newStar.scaleY diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt index f721401e..12a5ae2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt @@ -15,7 +15,7 @@ open class NonFinalAdapterListUpdateCallback /** * Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter. * - * @param adapter The Adapter to send updates to. + * @param mAdapter The Adapter to send updates to. */(private var mAdapter: RecyclerView.Adapter<*>) : ListUpdateCallback { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt index 9532d1a9..b778ba5a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WatchType.kt @@ -13,7 +13,7 @@ enum class WatchType(val internalId: Int, @StringRes val stringRes: Int, @Drawab NONE(5, R.string.type_none, R.drawable.ic_baseline_add_24); companion object { - fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE + fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE } } @@ -36,6 +36,6 @@ enum class SyncWatchType(val internalId: Int, @StringRes val stringRes: Int, @Dr REWATCHING(5, R.string.type_re_watching, R.drawable.ic_baseline_bookmark_24); companion object { - fun fromInternalId(id: Int?) = values().find { value -> value.internalId == id } ?: NONE + fun fromInternalId(id: Int?) = entries.find { value -> value.internalId == id } ?: NONE } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt index 15e66b38..5e2b97e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/WebviewFragment.kt @@ -8,8 +8,10 @@ import android.webkit.JavascriptInterface import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient +import androidx.annotation.OptIn import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import androidx.media3.common.util.UnstableApi import androidx.navigation.fragment.findNavController import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.USER_AGENT @@ -29,6 +31,7 @@ class WebviewFragment : Fragment() { } binding?.webView?.webViewClient = object : WebViewClient() { + @OptIn(UnstableApi::class) override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest? diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index f10e103e..45132131 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -54,6 +54,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } init { + @Suppress("LeakingThis") resetViewData() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt index d97a4b88..20a44461 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt @@ -13,7 +13,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper class DownloadButton(context: Context, attributeSet: AttributeSet) : PieFetchButton(context, attributeSet) { - var mainText: TextView? = null + private var mainText: TextView? = null override fun onAttachedToWindow() { super.onAttachedToWindow() progressText = findViewById(R.id.result_movie_download_text_precentage) 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 ebed901f..b25486eb 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 @@ -15,7 +15,7 @@ import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout +import com.lagradost.cloudstream3.utils.UIHelper.isBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState(view) { @@ -54,7 +54,7 @@ class HomeChildItemAdapter( var hasNext: Boolean = false override fun onCreateContent(parent: ViewGroup): ViewHolderState { - val expanded = parent.context.IsBottomLayout() + val expanded = parent.context.isBottomLayout() /* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid val root = LayoutInflater.from(parent.context).inflate(layout, parent, false) @@ -133,7 +133,6 @@ class HomeChildItemAdapter( item, position, holder.itemView, - null, // nextFocusBehavior, nextFocusUp, nextFocusDown ) 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 82a92d80..49de2503 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 @@ -17,7 +17,6 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.* import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -234,7 +233,7 @@ class HomeFragment : Fragment() { return bottomSheetDialogBuilder } - fun getPairList( + private fun getPairList( anime: Chip?, cartoons: Chip?, tvs: Chip?, 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 916cb9ae..8bc0aa28 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 @@ -1,6 +1,8 @@ package com.lagradost.cloudstream3.ui.home +import android.os.Build import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -53,12 +55,12 @@ open class ParentItemAdapter( "value", recyclerView?.layoutManager?.onSaveInstanceState() ) - (recyclerView?.adapter as? BaseAdapter<*,*>)?.save(recyclerView) + (recyclerView?.adapter as? BaseAdapter<*, *>)?.save(recyclerView) } override fun restore(state: Bundle) { (binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState( - state.getParcelable("value") + state.getSafeParcelable("value") ) } } @@ -169,4 +171,9 @@ open class ParentItemAdapter( submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) } .toMutableList()) } -} \ No newline at end of file +} + +@Suppress("DEPRECATION") +inline fun Bundle.getSafeParcelable(key: String): T? = + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelable(key) + else getParcelable(key, T::class.java) \ No newline at end of file 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 2e98dd1f..339ef1e1 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 @@ -117,15 +117,12 @@ class HomeParentItemAdapterPreview( } override fun restore(state: Bundle) { - state.getParcelable("resumeRecyclerView")?.let { recycle -> + state.getSafeParcelable("resumeRecyclerView")?.let { recycle -> resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - state.getParcelable("bookmarkRecyclerView")?.let { recycle -> + state.getSafeParcelable("bookmarkRecyclerView")?.let { recycle -> bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - //state.getInt("previewViewpager").let { recycle -> - // previewViewpager.setCurrentItem(recycle,true) - //} } val previewAdapter = HomeScrollAdapter(fragment = fragment) 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 9e70d088..24ca4df2 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 @@ -152,7 +152,7 @@ class HomeViewModel : ViewModel() { } }?.distinctBy { it.first } ?: return@launchSafe - val length = WatchType.values().size + val length = WatchType.entries.size val currentWatchTypes = mutableSetOf() for (watch in watchStatusIds) { @@ -387,7 +387,9 @@ class HomeViewModel : ViewModel() { } is Resource.Failure -> { + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") _page.postValue(data!!) + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") _preview.postValue(data!!) } @@ -397,9 +399,7 @@ class HomeViewModel : ViewModel() { } fun click(callback: SearchClickCallback) { - if (callback.action == SEARCH_ACTION_FOCUSED) { - //focusCallback(callback.card) - } else { + if (callback.action != SEARCH_ACTION_FOCUSED) { SearchHelper.handleSearchClickCallback(callback) } } @@ -516,7 +516,7 @@ class HomeViewModel : ViewModel() { } else { _page.postValue(Resource.Loading()) if (preferredApiName != null) - _apiName.postValue(preferredApiName) + _apiName.postValue(preferredApiName!!) } } else { // if the api is found, then set it to it and save key diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index 7144de09..5b240693 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -600,8 +600,4 @@ class LibraryFragment : Fragment() { } } -class MenuSearchView(context: Context) : SearchView(context) { - override fun onActionViewCollapsed() { - super.onActionViewCollapsed() - } -} \ No newline at end of file +class MenuSearchView(context: Context) : SearchView(context) \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt index cfd22220..0110187f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.BaseAdapter import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.home.getSafeParcelable import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV @@ -32,7 +33,7 @@ class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding) } override fun restore(state: Bundle) { - state.getParcelable("pageRecyclerview")?.let { recycle -> + state.getSafeParcelable("pageRecyclerview")?.let { recycle -> binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle) } } 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 9d838c97..88c34c87 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 @@ -25,6 +25,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.media3.common.PlaybackException +import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import androidx.media3.ui.* @@ -216,7 +217,7 @@ abstract class AbstractPlayerFragment( return } player.handleEvent( - CSPlayerEvent.values()[intent.getIntExtra( + CSPlayerEvent.entries[intent.getIntExtra( EXTRA_CONTROL_TYPE, 0 )], source = PlayerEventSource.UI @@ -603,12 +604,12 @@ abstract class AbstractPlayerFragment( } fun nextResize() { - resizeMode = (resizeMode + 1) % PlayerResize.values().size + resizeMode = (resizeMode + 1) % PlayerResize.entries.size resize(resizeMode, true) } fun resize(resize: Int, showToast: Boolean) { - resize(PlayerResize.values()[resize], showToast) + resize(PlayerResize.entries[resize], showToast) } @SuppressLint("UnsafeOptInUsageError") 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 735e4095..86d67b28 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 @@ -9,7 +9,11 @@ import android.os.Looper import android.util.Log import android.util.Rational import android.widget.FrameLayout -import androidx.media3.common.C.* +import androidx.annotation.OptIn +import androidx.media3.common.C.TIME_UNSET +import androidx.media3.common.C.TRACK_TYPE_AUDIO +import androidx.media3.common.C.TRACK_TYPE_TEXT +import androidx.media3.common.C.TRACK_TYPE_VIDEO import androidx.media3.common.Format import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes @@ -19,9 +23,10 @@ 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.UnstableApi import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.datasource.DataSource -import androidx.media3.datasource.DefaultDataSourceFactory +import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.cache.CacheDataSource @@ -66,7 +71,6 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import java.io.File -import java.lang.IllegalArgumentException import java.util.UUID import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext @@ -84,7 +88,7 @@ const val toleranceBeforeUs = 300_000L * seek position, in microseconds. Must be non-negative. */ const val toleranceAfterUs = 300_000L - +@OptIn(UnstableApi::class) class CS3IPlayer : IPlayer { private var isPlaying = false private var exoPlayer: ExoPlayer? = null @@ -257,7 +261,6 @@ class CS3IPlayer : IPlayer { private var currentSubtitles: SubtitleData? = null - @SuppressLint("UnsafeOptInUsageError") private fun List.getTrack(id: String?): Pair? { if (id == null) return null // This beast of an expression does: @@ -342,7 +345,6 @@ class CS3IPlayer : IPlayer { }.flatten() } - @SuppressLint("UnsafeOptInUsageError") private fun Tracks.Group.getFormats(): List> { return (0 until this.mediaTrackGroup.length).mapNotNull { i -> if (this.isSupported) @@ -371,7 +373,6 @@ class CS3IPlayer : IPlayer { ) } - @SuppressLint("UnsafeOptInUsageError") override fun getVideoTracks(): CurrentTracks { val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList() val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO } @@ -391,7 +392,6 @@ class CS3IPlayer : IPlayer { /** * @return True if the player should be reloaded * */ - @SuppressLint("UnsafeOptInUsageError") override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { Log.i(TAG, "setPreferredSubtitles init $subtitle") currentSubtitles = subtitle @@ -451,7 +451,7 @@ class CS3IPlayer : IPlayer { } ?: false } - var currentSubtitleOffset: Long = 0 + private var currentSubtitleOffset: Long = 0 override fun setSubtitleOffset(offset: Long) { currentSubtitleOffset = offset @@ -459,7 +459,7 @@ class CS3IPlayer : IPlayer { } override fun getSubtitleOffset(): Long { - return currentSubtitleOffset //currentTextRenderer?.getRenderOffsetMs() ?: currentSubtitleOffset + return currentSubtitleOffset } override fun getCurrentPreferredSubtitle(): SubtitleData? { @@ -470,7 +470,6 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") override fun getAspectRatio(): Rational? { return exoPlayer?.videoFormat?.let { format -> Rational(format.width, format.height) @@ -481,14 +480,13 @@ class CS3IPlayer : IPlayer { subtitleHelper.setSubStyle(style) } - @SuppressLint("UnsafeOptInUsageError") override fun saveData() { Log.i(TAG, "saveData") updatedTime() exoPlayer?.let { exo -> playbackPosition = exo.currentPosition - currentWindow = exo.currentWindowIndex + currentWindow = exo.currentMediaItemIndex isPlaying = exo.isPlaying } } @@ -500,7 +498,7 @@ class CS3IPlayer : IPlayer { updatedTime() exoPlayer?.apply { - setPlayWhenReady(false) + playWhenReady = false stop() release() } @@ -563,7 +561,6 @@ class CS3IPlayer : IPlayer { var requestSubtitleUpdate: (() -> Unit)? = null - @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(headers: Map): HttpDataSource.Factory { val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT) return source.apply { @@ -571,7 +568,6 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory { val provider = getApiFromNameNull(link.source) val interceptor = provider?.getVideoInterceptor(link) @@ -604,53 +600,10 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") private fun Context.createOfflineSource(): DataSource.Factory { - return DefaultDataSourceFactory(this, USER_AGENT) + return DefaultDataSource.Factory(this, DefaultHttpDataSource.Factory().setUserAgent(USER_AGENT)) } - /*private fun getSubSources( - onlineSourceFactory: DataSource.Factory?, - offlineSourceFactory: DataSource.Factory?, - subHelper: PlayerSubtitleHelper, - ): Pair, List> { - val activeSubtitles = ArrayList() - val subSources = subHelper.getAllSubtitles().mapNotNull { sub -> - val subConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(sub.url)) - .setMimeType(sub.mimeType) - .setLanguage("_${sub.name}") - .setSelectionFlags(C.SELECTION_FLAG_DEFAULT) - .build() - when (sub.origin) { - SubtitleOrigin.DOWNLOADED_FILE -> { - if (offlineSourceFactory != null) { - activeSubtitles.add(sub) - SingleSampleMediaSource.Factory(offlineSourceFactory) - .createMediaSource(subConfig, C.TIME_UNSET) - } else { - null - } - } - SubtitleOrigin.URL -> { - if (onlineSourceFactory != null) { - activeSubtitles.add(sub) - SingleSampleMediaSource.Factory(onlineSourceFactory) - .createMediaSource(subConfig, C.TIME_UNSET) - } else { - null - } - } - SubtitleOrigin.OPEN_SUBTITLES -> { - // TODO - throw NotImplementedError() - } - } - } - println("SUBSRC: ${subSources.size} activeSubtitles : ${activeSubtitles.size} of ${subHelper.getAllSubtitles().size} ") - return Pair(subSources, activeSubtitles) - }*/ - - @SuppressLint("UnsafeOptInUsageError") private fun getCache(context: Context, cacheSize: Long): SimpleCache? { return try { val databaseProvider = StandaloneDatabaseProvider(context) @@ -682,7 +635,6 @@ class CS3IPlayer : IPlayer { return getMediaItemBuilder(mimeType).setUri(url).build() } - @SuppressLint("UnsafeOptInUsageError") private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector { val trackSelector = DefaultTrackSelector(context) trackSelector.parameters = trackSelector.buildUponParameters() @@ -696,7 +648,6 @@ class CS3IPlayer : IPlayer { var currentTextRenderer: CustomTextRenderer? = null - @SuppressLint("UnsafeOptInUsageError") private fun buildExoPlayer( context: Context, mediaItemSlices: List, @@ -736,7 +687,7 @@ class CS3IPlayer : IPlayer { textRendererOutput, eventHandler.looper, CustomSubtitleDecoderFactory() - ).also { this.currentTextRenderer = it } + ).also { renderer -> this.currentTextRenderer = renderer } currentTextRenderer } else it }.toTypedArray() @@ -1033,7 +984,7 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") + //fixme: Use onPlaybackStateChanged(int) and onPlayWhenReadyChanged(boolean, int) instead. override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { exoPlayer?.let { exo -> event( @@ -1169,7 +1120,6 @@ class CS3IPlayer : IPlayer { private var lastTimeStamps: List = emptyList() - @SuppressLint("UnsafeOptInUsageError") override fun addTimeStamps(timeStamps: List) { lastTimeStamps = timeStamps timeStamps.forEach { timestamp -> @@ -1187,7 +1137,6 @@ class CS3IPlayer : IPlayer { updatedTime(source = PlayerEventSource.Player) } - @SuppressLint("UnsafeOptInUsageError") fun onRenderFirst() { if (hasUsedFirstRender) { // this insures that we only call this once per player load return @@ -1254,7 +1203,6 @@ class CS3IPlayer : IPlayer { } } - @SuppressLint("UnsafeOptInUsageError") private fun getSubSources( onlineSourceFactory: HttpDataSource.Factory?, offlineSourceFactory: DataSource.Factory?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt index 20d093a6..07ce413e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomSubtitleDecoderFactory.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player import android.content.Context import android.util.Log +import androidx.annotation.OptIn import androidx.preference.PreferenceManager import androidx.media3.common.Format import androidx.media3.common.MimeTypes @@ -31,7 +32,7 @@ import java.nio.charset.Charset * @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not * enough to identify the subtitle format. **/ -@UnstableApi +@OptIn(UnstableApi::class) class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { companion object { fun updateForcedEncoding(context: Context) { @@ -72,7 +73,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { RegexOption.IGNORE_CASE ), ) - val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\d\s]*?[])}]\s*""")) + val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\s]*?[])}]\s*""")) //https://emptycharacter.com/ //https://www.fileformat.info/info/unicode/char/200b/index.htm @@ -262,7 +263,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { } /** See https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java */ -@UnstableApi +@OptIn(UnstableApi::class) class CustomSubtitleDecoderFactory : SubtitleDecoderFactory { override fun supportsFormat(format: Format): Boolean { // return SubtitleDecoderFactory.DEFAULT.supportsFormat(format) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt index d6b0735d..f2b863fb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CustomTextRenderer.kt @@ -1,11 +1,12 @@ package com.lagradost.cloudstream3.ui.player import android.os.Looper +import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.text.SubtitleDecoderFactory import androidx.media3.exoplayer.text.TextOutput -@UnstableApi +@OptIn(UnstableApi::class) class CustomTextRenderer( offset: Long, output: TextOutput?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index 3b242172..a8a3106a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -49,7 +49,7 @@ class DownloadFileGenerator( return null } - fun cleanDisplayName(name: String): String { + private fun cleanDisplayName(name: String): String { return name.substringBeforeLast('.').trim() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 92ef279d..4279b542 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -8,14 +8,10 @@ import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.utils.UIHelper.navigate -import com.lagradost.safefile.SafeFile import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri -const val DTAG = "PlayerActivity" - class DownloadedPlayerActivity : AppCompatActivity() { private val dTAG = "DownloadedPlayerAct" 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 ef7d6bc1..b2e80749 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 @@ -25,6 +25,7 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO import android.view.animation.AlphaAnimation import android.view.animation.Animation import android.view.animation.AnimationUtils +import androidx.annotation.OptIn import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.core.graphics.blue @@ -35,6 +36,7 @@ import androidx.core.view.isGone import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged +import androidx.media3.common.util.UnstableApi import androidx.preference.PreferenceManager import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.CommonActivity.keyEventListener @@ -50,7 +52,6 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvid import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout @@ -245,6 +246,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { fadeAnimation.duration = 100 fadeAnimation.fillAfter = true + @OptIn(UnstableApi::class) val sView = subView val sStyle = subStyle if (sView != null && sStyle != null) { @@ -300,42 +302,40 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun restoreOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation - var orientation = 0 - when (currentOrientation) { + val orientation = when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - - Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> - orientation = dynamicOrientation() + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE Configuration.ORIENTATION_PORTRAIT -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + + else -> dynamicOrientation() } activity.requestedOrientation = orientation } private fun toggleOrientationWithSensor(activity: Activity) { val currentOrientation = activity.resources.configuration.orientation - var orientation = 0 - when (currentOrientation) { + val orientation: Int = when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT - - Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> - orientation = dynamicOrientation() + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT Configuration.ORIENTATION_PORTRAIT -> - orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + + else -> dynamicOrientation() } activity.requestedOrientation = orientation } open fun lockOrientation(activity: Activity) { - val display = + @Suppress("DEPRECATION") + val display = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay + else activity.display!! val rotation = display.rotation val currentOrientation = activity.resources.configuration.orientation - var orientation = 0 + val orientation: Int when (currentOrientation) { Configuration.ORIENTATION_LANDSCAPE -> orientation = @@ -344,15 +344,14 @@ open class FullScreenPlayer : AbstractPlayerFragment() { else ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE - Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED -> - orientation = dynamicOrientation() - Configuration.ORIENTATION_PORTRAIT -> orientation = if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT else ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + + else -> orientation = dynamicOrientation() } activity.requestedOrientation = orientation } @@ -1167,6 +1166,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { return true } + @SuppressLint("GestureBackNavigation") private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean { if (hasNavigated) { autoHide() @@ -1581,7 +1581,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } // cs3 is peak media center - setRemainingTimeCounter(durationMode || Globals.isLayout(Globals.TV)) + setRemainingTimeCounter(durationMode || isLayout(TV)) playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ -> updateRemainingTime() } 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 1f7cc5bd..8e8f6bf5 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 @@ -6,6 +6,7 @@ import android.app.Dialog import android.content.Context import android.content.Intent import android.content.res.ColorStateList +import android.os.Build import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -13,6 +14,7 @@ import android.view.View import android.view.ViewGroup import android.widget.* import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.OptIn import androidx.core.animation.addListener import androidx.core.content.ContextCompat import androidx.core.view.isGone @@ -21,6 +23,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager import androidx.media3.common.Format.NO_VALUE import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -63,6 +66,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.safefile.SafeFile import kotlinx.coroutines.Job +import java.io.Serializable import java.util.* import kotlin.math.abs @@ -234,7 +238,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun closestQuality(target: Int?): Qualities { if (target == null) return Qualities.Unknown - return Qualities.values().minBy { abs(it.value - target) } + return Qualities.entries.minBy { abs(it.value - target) } } private fun getLinkPriority( @@ -367,8 +371,6 @@ class GeneratorPlayer : FullScreenPlayer() { binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE binding.subtitleAdapter.adapter = arrayAdapter - val adapter = - binding.subtitleAdapter.adapter as? ArrayAdapter binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ -> currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener @@ -379,8 +381,8 @@ class GeneratorPlayer : FullScreenPlayer() { fun setSubtitlesList(list: List) { currentSubtitles = list - adapter?.clear() - adapter?.addAll(currentSubtitles) + arrayAdapter.clear() + arrayAdapter.addAll(currentSubtitles) } val currentTempMeta = getMetaData() @@ -522,7 +524,7 @@ class GeneratorPlayer : FullScreenPlayer() { //TODO: Set year text from currently loaded movie on Player //dialog.subtitles_search_year?.setText(currentTempMeta.year) } - + @OptIn(UnstableApi::class) private fun openSubPicker() { try { subsPathPicker.launch( @@ -795,7 +797,6 @@ class GeneratorPlayer : FullScreenPlayer() { settingsManager.edit().putString( ctx.getString(R.string.subtitles_encoding_key), prefValues[it] ).apply() - updateForcedEncoding(ctx) dismiss() player.seekTime(-1) // to update subtitles, a dirty trick @@ -1290,7 +1291,7 @@ class GeneratorPlayer : FullScreenPlayer() { private fun unwrapBundle(savedInstanceState: Bundle?) { Log.i(TAG, "unwrapBundle = $savedInstanceState") savedInstanceState?.let { bundle -> - sync.addSyncs(bundle.getSerializable("syncData") as? HashMap?) + sync.addSyncs(bundle.getSafeSerializable>("syncData")) } } @@ -1507,3 +1508,6 @@ class GeneratorPlayer : FullScreenPlayer() { } } } + +@Suppress("DEPRECATION") +inline fun Bundle.getSafeSerializable(key: String) : T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getSerializable(key) as? T else getSerializable(key, T::class.java) \ No newline at end of file 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 5f7161f7..89c6f73b 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 @@ -8,7 +8,6 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink enum class PlayerEventType(val value: Int) { - //Stop(-1), Pause(0), Play(1), SeekForward(2), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt index 89e3c8de..07ea56dd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/LinkGenerator.kt @@ -4,7 +4,6 @@ import android.net.Uri import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.loadExtractor diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java index 3482f21c..232440cc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java @@ -29,6 +29,7 @@ import android.os.Message; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.text.Cue; @@ -66,7 +67,7 @@ import java.util.stream.Collectors; * obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s * is delegated to a {@link TextOutput}. */ -@UnstableApi +@OptIn(markerClass = UnstableApi.class) public class NonFinalTextRenderer extends BaseRenderer implements Callback { private static final String TAG = "TextRenderer"; @@ -74,7 +75,7 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { /** * @param trackType The track type that the renderer handles. One of the {@link C} {@code * TRACK_TYPE_*} constants. - * @param outputHandler + * @param outputHandler todo description */ public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) { super(trackType); @@ -416,13 +417,11 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_UPDATE_OUTPUT: - invokeUpdateOutputInternal((List) msg.obj); - return true; - default: - throw new IllegalStateException(); + if (msg.what == MSG_UPDATE_OUTPUT) { + invokeUpdateOutputInternal((List) msg.obj); + return true; } + throw new IllegalStateException(); } private void invokeUpdateOutputInternal(List cues) { @@ -441,7 +440,6 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback { } ).collect(Collectors.toList()); - output.onCues(fixedCues); output.onCues(new CueGroup(fixedCues, 0L)); } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt index e6de1266..f00f8a61 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/OfflinePlaybackHelper.kt @@ -4,8 +4,8 @@ import android.app.Activity import android.content.ContentUris import android.net.Uri import androidx.core.content.ContextCompat.getString +import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.ui.player.ExtractorUri import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.safefile.SafeFile 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 1ba5a29f..122eaa97 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 @@ -20,7 +20,7 @@ import kotlinx.coroutines.launch class PlayerGeneratorViewModel : ViewModel() { companion object { - val TAG = "PlayViewGen" + const val TAG = "PlayViewGen" } private var generator: IGenerator? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index 25d7e3dd..02a7ee03 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -4,7 +4,9 @@ import android.util.Log import android.util.TypedValue import android.view.ViewGroup import android.widget.FrameLayout +import androidx.annotation.OptIn import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat @@ -47,6 +49,7 @@ data class SubtitleData( } } +@OptIn(UnstableApi::class) class PlayerSubtitleHelper { private var activeSubtitles: Set = emptySet() private var allSubtitles: Set = emptySet() diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index 7c78ce63..2d1feaab 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -239,7 +239,11 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG // generated images 1:1 to idx of hsl private var images: Array = arrayOf() - private val TAG = "PreviewImgM3u8" + companion object { + private const val TAG = "PreviewImgM3u8" + } + + // prefixSum[i] = sum(hsl.ts[0..i].time) // where [0] = 0, [1] = hsl.ts[0].time aka time at start of segment, do [b] - [a] for range a,b @@ -388,13 +392,6 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG logError(t) continue } - - /* - val buffer = hsl.resolveLinkSafe(index) ?: continue - tmpFile?.writeBytes(buffer) - val buff = FileOutputStream(tmpFile) - retriever.setDataSource(buff.fd) - val frame = retriever.getFrameAtTime(0L)*/ } } @@ -412,14 +409,16 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe null } + companion object { + private const val TAG = "PreviewImgMp4" + } + override fun hasPreview(): Boolean { synchronized(images) { return loadedLod >= MIN_LOD } } - val TAG = "PreviewImgMp4" - override fun getPreviewImage(fraction: Float): Bitmap? { synchronized(images) { if (loadedLod < MIN_LOD) { @@ -524,7 +523,7 @@ private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGe val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) Log.i(TAG, "Generating preview for ${fraction * 100}%") val frame = durationUs * fraction - val img = retriever.image(frame.toLong(), params); + val img = retriever.image(frame.toLong(), params) if (!scope.isActive) return if (img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt index 90bd1ca7..6943c641 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/RepoLinkGenerator.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.player import android.util.Log +import androidx.media3.common.util.UnstableApi import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.LoadResponse diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt index 1e2c9f67..ce457740 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/PriorityAdapter.kt @@ -17,7 +17,6 @@ class PriorityAdapter(override val items: MutableList>) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return PriorityViewHolder( PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false), - //LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false) ) } @@ -31,10 +30,6 @@ class PriorityAdapter(override val items: MutableList>) : val binding: PlayerPrioritizeItemBinding, ) : RecyclerView.ViewHolder(binding.root) { fun bind(item: SourcePriority) { - /* val plusButton: ImageView = itemView.add_button - val subtractButton: ImageView = itemView.subtract_button - val priorityText: TextView = itemView.priority_text - val priorityNumber: TextView = itemView.priority_number*/ binding.priorityText.text = item.name fun updatePriority() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt index b587276f..45f6aa66 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/ProfilesAdapter.kt @@ -29,8 +29,6 @@ class ProfilesAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return ProfilesViewHolder( PlayerQualityProfileItemBinding.inflate(LayoutInflater.from(parent.context),parent,false) - //LayoutInflater.from(parent.context) - // .inflate(R.layout.player_quality_profile_item, parent, false) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt index 96249db4..3267efd7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.player.source_priority -import android.content.Context import androidx.annotation.StringRes import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey @@ -104,7 +103,7 @@ object QualityDataHelper { * Must under all circumstances at least return one profile **/ fun getProfiles(): List { - val availableTypes = QualityProfileType.values().toMutableList() + val availableTypes = QualityProfileType.entries.toMutableList() val profiles = (1..PROFILE_COUNT).map { profileNumber -> // Get the real type val type = getQualityProfileType(profileNumber) @@ -140,12 +139,12 @@ object QualityDataHelper { } } - QualityProfileType.values().forEach { + QualityProfileType.entries.forEach { if (it.unique) insertType(profiles, it) } debugAssert({ - !QualityProfileType.values().all { type -> + !QualityProfileType.entries.all { type -> !type.unique || profiles.any { it.type == type } } }, { "All unique quality types do not exist" }) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt index e3629158..0537092c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityProfileDialog.kt @@ -65,7 +65,7 @@ class QualityProfileDialog( setDefaultBtt.setOnClickListener { val currentProfile = getCurrentProfile() ?: return@setOnClickListener - val choices = QualityDataHelper.QualityProfileType.values() + val choices = QualityDataHelper.QualityProfileType.entries .filter { it != QualityDataHelper.QualityProfileType.None } val choiceNames = choices.map { txt(it.stringRes).asString(context) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt index 1b59882e..bc6282af 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -47,7 +47,7 @@ class SourcePriorityDialog( ) qualitiesRecyclerView.adapter = PriorityAdapter( - Qualities.values().mapNotNull { + Qualities.entries.mapNotNull { SourcePriority( it, Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, 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 61188905..0ca326dd 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 @@ -3,8 +3,6 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.annotation.IdRes -import androidx.annotation.LayoutRes import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -70,8 +68,7 @@ class ActorAdaptor( } } - private inner class CardViewHolder - constructor( + private inner class CardViewHolder( val binding: CastItemBinding, private val focusCallback: (View?) -> Unit = {} ) : 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 06be6bd5..d12521b3 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 @@ -169,8 +169,7 @@ class EpisodeAdapter( return cardList.size } - class EpisodeCardViewHolderLarge - constructor( + class EpisodeCardViewHolderLarge( val binding: ResultEpisodeLargeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, @@ -335,8 +334,7 @@ class EpisodeAdapter( } } - class EpisodeCardViewHolderSmall - constructor( + class EpisodeCardViewHolderSmall( val binding: ResultEpisodeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, 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 7b7bae43..eecd6262 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 @@ -8,18 +8,6 @@ import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -/* -class ImageAdapter(context: Context, val resource: Int) : ArrayAdapter(context, resource) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val newConvertView = convertView ?: run { - val mInflater = context - .getSystemService(Activity.LAYOUT_INFLATER_SERVICE) as LayoutInflater - mInflater.inflate(resource, null) - } - getItem(position)?.let { (newConvertView as? ImageView?)?.setImageResource(it) } - return newConvertView - } -}*/ const val IMAGE_CLICK = 0 const val IMAGE_LONG_CLICK = 1 @@ -66,8 +54,7 @@ class ImageAdapter( diffResult.dispatchUpdatesTo(this) } - class ImageViewHolder - constructor(val binding: ResultMiniImageBinding) : + class ImageViewHolder(val binding: ResultMiniImageBinding) : RecyclerView.ViewHolder(binding.root) { fun bind( img: Int, 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 2f297098..f1399e8d 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 @@ -78,11 +78,12 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.VideoDownloadHelper open class ResultFragmentPhone : FullScreenPlayer() { - private val gestureRegionsListener = object : PanelsChildGestureRegionObserver.GestureRegionsListener { - override fun onGestureRegionsUpdate(gestureRegions: List) { - binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + private val gestureRegionsListener = + object : PanelsChildGestureRegionObserver.GestureRegionsListener { + override fun onGestureRegionsUpdate(gestureRegions: List) { + binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) + } } - } protected lateinit var viewModel: ResultViewModel2 protected lateinit var syncModel: SyncViewModel @@ -336,7 +337,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { } - // ===== ===== ===== resultBinding?.apply { @@ -430,16 +430,16 @@ open class ResultFragmentPhone : FullScreenPlayer() { if (newStatus == null) return@toggleSubscriptionStatus val message = if (newStatus) { - // Kinda icky to have this here, but it works. - SubscriptionWorkManager.enqueuePeriodicWork(context) - R.string.subscription_new - } else { - R.string.subscription_deleted - } + // Kinda icky to have this here, but it works. + SubscriptionWorkManager.enqueuePeriodicWork(context) + R.string.subscription_new + } else { + R.string.subscription_deleted + } - val name = (viewModel.page.value as? Resource.Success)?.value?.title - ?: txt(R.string.no_data).asStringNull(context) ?: "" - showToast(txt(message, name), Toast.LENGTH_SHORT) + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + showToast(txt(message, name), Toast.LENGTH_SHORT) } context?.let { openBatteryOptimizationSettings(it) } } @@ -473,8 +473,16 @@ open class ResultFragmentPhone : FullScreenPlayer() { if (act.isCastApiAvailable()) { try { CastButtonFactory.setUpMediaRouteButton(act, this) - val castContext = CastContext.getSharedInstance(act.applicationContext) - isGone = castContext.castState == CastState.NO_DEVICES_AVAILABLE + CastContext.getSharedInstance(act.applicationContext) { + it.run() + }.addOnCompleteListener { + isGone = if (it.isSuccessful) { + it.result.castState == CastState.NO_DEVICES_AVAILABLE + } else { + true + } + + } // this shit leaks for some reason //castContext.addCastStateListener { state -> // media_route_button?.isGone = state == CastState.NO_DEVICES_AVAILABLE @@ -961,12 +969,12 @@ open class ResultFragmentPhone : FullScreenPlayer() { setOnClickListener { fab -> activity?.showBottomDialog( - WatchType.values().map { fab.context.getString(it.stringRes) }.toList(), + WatchType.entries.map { fab.context.getString(it.stringRes) }.toList(), watchType.ordinal, fab.context.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - viewModel.updateWatchStatus(WatchType.values()[it], context) + viewModel.updateWatchStatus(WatchType.entries[it], context) } } } @@ -1046,7 +1054,7 @@ open class ResultFragmentPhone : FullScreenPlayer() { text?.asStringNull(ctx) ?: return@mapNotNull null ) }) { - viewModel.changeDubStatus(DubStatus.values()[itemId]) + viewModel.changeDubStatus(DubStatus.entries[itemId]) } } } @@ -1103,7 +1111,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { override fun onPause() { super.onPause() - PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(gestureRegionsListener) + PanelsChildGestureRegionObserver.Provider.get() + .addGestureRegionsUpdateListener(gestureRegionsListener) } private fun setRecommendations(rec: List?, validApiName: String?) { 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 a0207060..1878f0b8 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 @@ -56,7 +56,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage class ResultFragmentTv : Fragment() { - protected lateinit var viewModel: ResultViewModel2 + private lateinit var viewModel: ResultViewModel2 private var binding: FragmentResultTvBinding? = null override fun onDestroyView() { @@ -418,10 +418,6 @@ class ResultFragmentTv : Fragment() { resultCastItems.layoutManager = object : LinearListLayout(view.context) { - override fun onInterceptFocusSearch(focused: View, direction: Int): View? { - return super.onInterceptFocusSearch(focused, direction) - } - override fun onRequestChildFocus( parent: RecyclerView, state: RecyclerView.State, @@ -649,7 +645,7 @@ class ResultFragmentTv : Fragment() { binding?.apply { - (data as? Resource.Success)?.value?.let { (text, ep) -> + (data as? Resource.Success)?.value?.let { (_, ep) -> resultPlayMovieButton.setOnClickListener { viewModel.handleAction( @@ -817,45 +813,8 @@ class ResultFragmentTv : Fragment() { } } - /* - * Okay so what is this fuckery? - * Basically Android TV will crash if you request a new focus while - * the adapter gets updated. - * - * This means that if you load thumbnails and request a next focus at the same time - * the app will crash without any way to catch it! - * - * How to bypass this? - * This code basically steals the focus for 500ms and puts it in an inescapable view - * then lets out the focus by requesting focus to result_episodes - */ - - val hasEpisodes = - !(resultEpisodes.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty() - /*val focus = activity?.currentFocus - - if (hasEpisodes) { - // Make it impossible to focus anywhere else! - temporaryNoFocus.isFocusable = true - temporaryNoFocus.requestFocus() - }*/ (resultEpisodes.adapter as? EpisodeAdapter)?.updateList(episodes.value) - - /* if (hasEpisodes) main { - - delay(500) - // This might make some people sad as it changes the focus when leaving an episode :( - if(focus?.requestFocus() == true) { - temporaryNoFocus.isFocusable = false - return@main - } - temporaryNoFocus.isFocusable = false - temporaryNoFocus.requestFocus() - } - - if (hasNoFocus()) - binding?.resultEpisodes?.requestFocus()*/ } } } 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 ce0fbdc5..6443a923 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 @@ -2723,7 +2723,7 @@ class ResultViewModel2 : ViewModel() { val id: Int?, ) : LoadResponse - fun loadSmall(activity: Activity?, searchResponse: SearchResponse) = ioSafe { + fun loadSmall(searchResponse: SearchResponse) = ioSafe { val url = searchResponse.url _page.postValue(Resource.Loading(url)) _episodes.postValue(Resource.Loading()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt index 5a23bfc1..8752e275 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt @@ -63,8 +63,7 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter Unit, resView: AutofitRecyclerView diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt index 0a2ecb81..4ef5fa69 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHistoryAdaptor.kt @@ -1,16 +1,11 @@ package com.lagradost.cloudstream3.ui.search import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.databinding.AccountSingleBinding import com.lagradost.cloudstream3.databinding.SearchHistoryItemBinding data class SearchHistoryItem( @@ -63,8 +58,7 @@ class SearchHistoryAdaptor( diffResult.dispatchUpdatesTo(this) } - class CardViewHolder - constructor( + class CardViewHolder( val binding: SearchHistoryItemBinding, private val clickCallback: (SearchHistoryCallback) -> Unit, ) : diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt index f597132b..92575e58 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.search +import android.annotation.SuppressLint import android.content.Context import android.view.View import android.widget.ImageView @@ -37,16 +38,12 @@ object SearchResultBuilder { } } - /** - * @param nextFocusBehavior True if first, False if last, Null if between. - * Used to prevent escaping the adapter horizontally (focus wise). - */ + @SuppressLint("StringFormatInvalid") fun bind( clickCallback: (SearchClickCallback) -> Unit, card: SearchResponse, position: Int, itemView: View, - nextFocusBehavior: Boolean? = null, nextFocusUp: Int? = null, nextFocusDown: Int? = null, colorCallback : ((Palette) -> Unit)? = null diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt index 9e03079f..71077e91 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SyncSearchViewModel.kt @@ -3,11 +3,9 @@ package com.lagradost.cloudstream3.ui.search import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.TvType -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis +//TODO Relevance of this class since it's not used class SyncSearchViewModel { - private val repos = SyncApis - data class SyncSearchResultSearchResponse( override val name: String, override val url: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt index 1dc79dc0..d7bd69f1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/AccountAdapter.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.ui.settings +import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -13,7 +14,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.LoginInfo) class AccountAdapter( - val cardList: List, + private val cardList: List, private val clickCallback: (AccountClickCallback) -> Unit ) : RecyclerView.Adapter() { @@ -42,12 +43,12 @@ class AccountAdapter( return cardList[position].accountIndex.toLong() } - class CardViewHolder - constructor(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) : + class CardViewHolder(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) : RecyclerView.ViewHolder(binding.root) { // private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! // private val accountName: TextView = itemView.findViewById(R.id.account_name)!! + @SuppressLint("StringFormatInvalid") fun bind(card: AuthAPI.LoginInfo) { // just in case name is null account index will show, should never happened binding.accountName.text = card.name ?: "%s %d".format( 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 6ba93c0f..88335eea 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 @@ -26,7 +26,6 @@ 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.isLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index fd61962c..7cb1a848 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -28,10 +28,8 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.EasterEggMonke 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.beneneCount -import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt index 7560d75f..21707ca7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsPlayer.kt @@ -10,7 +10,6 @@ import com.lagradost.cloudstream3.mvvm.logError 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.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getFolderSize import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn @@ -108,7 +107,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { getPref(R.string.hide_player_control_names_key)?.hideOn(TV) getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener { - val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() + val prefValues = Qualities.entries.map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) val prefNames = prefValues.map { Qualities.getStringByInt(it) } @@ -116,7 +115,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { val currentQuality = settingsManager.getInt( getString(R.string.quality_pref_key), - Qualities.values().last().value + Qualities.entries.last().value ) activity?.showBottomDialog( @@ -132,7 +131,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { } getPref(R.string.quality_pref_mobile_data_key)?.setOnPreferenceClickListener { - val prefValues = Qualities.values().map { it.value }.reversed().toMutableList() + val prefValues = Qualities.entries.map { it.value }.reversed().toMutableList() prefValues.remove(Qualities.Unknown.value) val prefNames = prefValues.map { Qualities.getStringByInt(it) } @@ -140,7 +139,7 @@ class SettingsPlayer : PreferenceFragmentCompat() { val currentQuality = settingsManager.getInt( getString(R.string.quality_pref_mobile_data_key), - Qualities.values().last().value + Qualities.entries.last().value ) activity?.showBottomDialog( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index cfb46c39..cb7d25fd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -34,7 +34,7 @@ class SettingsProviders : PreferenceFragmentCompat() { getPref(R.string.display_sub_key)?.setOnPreferenceClickListener { activity?.getApiDubstatusSettings()?.let { current -> - val dublist = DubStatus.values() + val dublist = DubStatus.entries val names = dublist.map { it.name } val currentList = ArrayList() 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 4aaa5e12..260c6674 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 @@ -128,7 +128,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { } binding.saveBtt.setOnClickListener { - val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) + val date = SimpleDateFormat("yyyy_MM_dd_HH_mm", Locale.getDefault()).format(Date(currentTimeMillis())) var fileStream: OutputStream? = null try { fileStream = VideoDownloadManager.setupStream( @@ -169,10 +169,10 @@ class SettingsUpdates : PreferenceFragmentCompat() { prefValues.indexOf(currentInstaller), getString(R.string.apk_installer_settings), true, - {}) { + {}) { num -> try { settingsManager.edit() - .putInt(getString(R.string.apk_installer_key), prefValues[it]) + .putInt(getString(R.string.apk_installer_key), prefValues[num]) .apply() } catch (e: Exception) { logError(e) @@ -209,9 +209,9 @@ class SettingsUpdates : PreferenceFragmentCompat() { prefValues.indexOf(current), getString(R.string.automatic_plugin_download_mode_title), true, - {}) { + {}) { num -> settingsManager.edit() - .putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() + .putInt(getString(R.string.auto_download_plugins_key), prefValues[num]).apply() (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } return@setOnPreferenceClickListener true 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 909c30be..9fb3f282 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 @@ -1,9 +1,11 @@ package com.lagradost.cloudstream3.ui.settings.extensions +import android.annotation.SuppressLint import android.text.format.Formatter.formatShortFileSize import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup +import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible @@ -27,11 +29,10 @@ import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx -import org.junit.Assert -import org.junit.Test import java.text.DecimalFormat import kotlin.math.floor import kotlin.math.log10 +import kotlin.math.pow data class PluginViewData( @@ -95,21 +96,13 @@ class PluginAdapter( } companion object { - private tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { if (current >= max) return max if (current >= target) return current return findClosestBase2(target, current * 2, max) } - @Test - fun testFindClosestBase2() { - Assert.assertEquals(16, findClosestBase2(0)) - Assert.assertEquals(256, findClosestBase2(170)) - Assert.assertEquals(256, findClosestBase2(256)) - Assert.assertEquals(512, findClosestBase2(257)) - Assert.assertEquals(512, findClosestBase2(700)) - } - private val iconSizeExact = 32.toPx private val iconSize by lazy { findClosestBase2(iconSizeExact, 16, 512) @@ -122,10 +115,7 @@ class PluginAdapter( val base = value / 3 return if (value >= 3 && base < suffix.size) { DecimalFormat("#0.00").format( - numValue / Math.pow( - 10.0, - (base * 3).toDouble() - ) + numValue / 10.0.pow((base * 3).toDouble()) ) + suffix[base] } else { DecimalFormat().format(numValue) @@ -136,6 +126,7 @@ class PluginAdapter( inner class PluginViewHolder(val binding: RepositoryItemBinding) : RecyclerView.ViewHolder(binding.root) { + @SuppressLint("SetTextI18n") fun bind( data: PluginViewData, ) { 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 c5319c37..4878049b 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 @@ -190,7 +190,7 @@ class PluginsFragment : Fragment() { bindChips( binding?.tvtypesChipsScroll?.tvtypesChips, emptyList(), - TvType.values().toList(), + TvType.entries.toList(), callback = { list -> pluginViewModel.tvTypes.clear() pluginViewModel.tvTypes.addAll(list.map { it.name }) 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 56014eb4..fd5422b2 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 @@ -10,7 +10,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index f9197213..49a93608 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -10,7 +10,6 @@ import androidx.core.util.forEach import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager -import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index bb9558b8..c76a218e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -15,8 +15,11 @@ import android.widget.Toast import androidx.fragment.app.Fragment import androidx.media3.common.text.Cue import com.fasterxml.jackson.annotation.JsonProperty -import com.google.android.gms.cast.TextTrackStyle -import com.google.android.gms.cast.TextTrackStyle.* +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DEPRESSED +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DROP_SHADOW +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_NONE +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_OUTLINE +import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_RAISED import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent @@ -42,7 +45,7 @@ data class SaveChromeCaptionStyle( @JsonProperty("fontGenericFamily") var fontGenericFamily: Int? = null, @JsonProperty("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent @JsonProperty("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK - @JsonProperty("edgeType") var edgeType: Int = TextTrackStyle.EDGE_TYPE_OUTLINE, + @JsonProperty("edgeType") var edgeType: Int = EDGE_TYPE_OUTLINE, @JsonProperty("foregroundColor") var foregroundColor: Int = Color.WHITE, @JsonProperty("fontScale") var fontScale: Float = 1.05f, @JsonProperty("windowColor") var windowColor: Int = Color.TRANSPARENT, @@ -99,7 +102,7 @@ class ChromecastSubtitlesFragment : Fragment() { } private fun onColorSelected(stuff: Pair) { - context?.setColor(stuff.first, stuff.second) + setColor(stuff.first, stuff.second) if (hide) activity?.hideSystemUI() } @@ -122,7 +125,7 @@ class ChromecastSubtitlesFragment : Fragment() { return if (color == Color.TRANSPARENT) Color.BLACK else color } - private fun Context.setColor(id: Int, color: Int?) { + private fun setColor(id: Int, color: Int?) { val realColor = color ?: getDefColor(id) when (id) { 0 -> state.foregroundColor = realColor @@ -135,7 +138,7 @@ class ChromecastSubtitlesFragment : Fragment() { updateState() } - private fun Context.updateState() { + private fun updateState() { //subtitle_text?.setStyle(fromSaveToStyle(state)) } @@ -173,7 +176,7 @@ class ChromecastSubtitlesFragment : Fragment() { fixPaddingStatusbar(binding?.subsRoot) state = getCurrentSavedStyle() - context?.updateState() + updateState() val isTvSettings = isLayout(TV or EMULATOR) @@ -195,7 +198,7 @@ class ChromecastSubtitlesFragment : Fragment() { } this.setOnLongClickListener { - it.context.setColor(id, null) + setColor(id, null) showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } @@ -247,13 +250,13 @@ class ChromecastSubtitlesFragment : Fragment() { dismissCallback ) { index -> state.edgeType = edgeTypes.map { it.first }[index] - textView.context.updateState() + updateState() } } binding?.subsEdgeType?.setOnLongClickListener { state.edgeType = defaultState.edgeType - it.context.updateState() + updateState() showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } @@ -323,12 +326,12 @@ class ChromecastSubtitlesFragment : Fragment() { dismissCallback ) { index -> state.fontFamily = fontTypes.map { it.first }[index] - textView.context.updateState() + updateState() } } - binding?.subsFont?.setOnLongClickListener { textView -> + binding?.subsFont?.setOnLongClickListener { _ -> state.fontFamily = defaultState.fontFamily - textView.context.updateState() + updateState() showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) return@setOnLongClickListener true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index 1466afed..8821905e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -14,11 +14,13 @@ import android.view.ViewGroup import android.widget.TextView import android.widget.Toast import androidx.annotation.FontRes +import androidx.annotation.OptIn import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager import com.fasterxml.jackson.annotation.JsonProperty import androidx.media3.common.text.Cue +import androidx.media3.common.util.UnstableApi import androidx.media3.ui.CaptionStyleCompat import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.lagradost.cloudstream3.AcraApplication.Companion.getKey @@ -28,7 +30,6 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding -import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStore.setKey @@ -46,7 +47,7 @@ const val SUBTITLE_KEY = "subtitle_settings" const val SUBTITLE_AUTO_SELECT_KEY = "subs_auto_select" const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download" -data class SaveCaptionStyle( +data class SaveCaptionStyle @OptIn(UnstableApi::class) constructor( @JsonProperty("foregroundColor") var foregroundColor: Int, @JsonProperty("backgroundColor") var backgroundColor: Int, @JsonProperty("windowColor") var windowColor: Int, @@ -67,7 +68,7 @@ data class SaveCaptionStyle( const val DEF_SUBS_ELEVATION = 20 -@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) +@OptIn(androidx.media3.common.util.UnstableApi::class) class SubtitlesFragment : Fragment() { companion object { val applyStyleEvent = Event() @@ -167,7 +168,7 @@ class SubtitlesFragment : Fragment() { activity?.hideSystemUI() } - private fun onDialogDismissed(id: Int) { + private fun onDialogDismissed(@Suppress("UNUSED_PARAMETER") id: Int) { if (hide) activity?.hideSystemUI() } 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 e9b69c5b..f0c948a4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AniSkip.kt @@ -83,7 +83,7 @@ object EpisodeSkip { startMs = start, endMs = end ) - }?.let { list -> + }.let { list -> out.addAll(list) } } 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 f0aae7bc..b13de062 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -43,7 +43,6 @@ import androidx.recyclerview.widget.RecyclerView import androidx.tvprovider.media.tv.* import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor import androidx.viewpager2.widget.ViewPager2 -import com.fasterxml.jackson.module.kotlin.readValue import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState import com.google.android.gms.common.ConnectionResult @@ -58,7 +57,7 @@ import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEv import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.plugins.RepositoryManager -import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching +import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WebviewFragment import com.lagradost.cloudstream3.ui.player.SubtitleData @@ -161,7 +160,7 @@ object AppContextUtils { .setTitle(title) .setPosterArtUri(Uri.parse(card.posterUrl)) .setIntentUri(Uri.parse(card.id?.let { - "$appStringResumeWatching://$it" + "$APP_STRING_RESUME_WATCHING://$it" } ?: card.url)) .setInternalProviderId(card.url) .setLastEngagementTimeUtcMillis( 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 802c1a64..b25be59f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -81,12 +81,12 @@ object BackupUtils { // Kinda hack, but I couldn't think of a better way data class BackupVars( - @JsonProperty("_Bool") val _Bool: Map?, - @JsonProperty("_Int") val _Int: Map?, - @JsonProperty("_String") val _String: Map?, - @JsonProperty("_Float") val _Float: Map?, - @JsonProperty("_Long") val _Long: Map?, - @JsonProperty("_StringSet") val _StringSet: Map?>?, + @JsonProperty("_Bool") val bool: Map?, + @JsonProperty("_Int") val int: Map?, + @JsonProperty("_String") val string: Map?, + @JsonProperty("_Float") val float: Map?, + @JsonProperty("_Long") val long: Map?, + @JsonProperty("_StringSet") val stringSet: Map?>?, ) data class BackupFile( @@ -134,21 +134,21 @@ object BackupUtils { ) { if (context == null) return if (restoreSettings) { - context.restoreMap(backupFile.settings._Bool, true) - context.restoreMap(backupFile.settings._Int, true) - context.restoreMap(backupFile.settings._String, true) - context.restoreMap(backupFile.settings._Float, true) - context.restoreMap(backupFile.settings._Long, true) - context.restoreMap(backupFile.settings._StringSet, true) + context.restoreMap(backupFile.settings.bool, true) + context.restoreMap(backupFile.settings.int, true) + context.restoreMap(backupFile.settings.string, true) + context.restoreMap(backupFile.settings.float, true) + context.restoreMap(backupFile.settings.long, true) + context.restoreMap(backupFile.settings.stringSet, true) } if (restoreDataStore) { - context.restoreMap(backupFile.datastore._Bool) - context.restoreMap(backupFile.datastore._Int) - context.restoreMap(backupFile.datastore._String) - context.restoreMap(backupFile.datastore._Float) - context.restoreMap(backupFile.datastore._Long) - context.restoreMap(backupFile.datastore._StringSet) + context.restoreMap(backupFile.datastore.bool) + context.restoreMap(backupFile.datastore.int) + context.restoreMap(backupFile.datastore.string) + context.restoreMap(backupFile.datastore.float) + context.restoreMap(backupFile.datastore.long) + context.restoreMap(backupFile.datastore.stringSet) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index 19c817b9..b5192aae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -56,16 +56,27 @@ data class Editor( ) { /** Always remember to call apply after */ fun setKeyRaw(path: String, value: T) { - when (value) { - is Boolean -> editor.putBoolean(path, value) - is Int -> editor.putInt(path, value) - is String -> editor.putString(path, value) - is Float -> editor.putFloat(path, value) - is Long -> editor.putLong(path, value) - (value as? Set != null) -> editor.putStringSet(path, value as Set) + @Suppress("UNCHECKED_CAST") + if (isStringSet(value)) { + editor.putStringSet(path, value as Set) + } else { + when (value) { + is Boolean -> editor.putBoolean(path, value) + is Int -> editor.putInt(path, value) + is String -> editor.putString(path, value) + is Float -> editor.putFloat(path, value) + is Long -> editor.putLong(path, value) + } } } + private fun isStringSet(value: Any?) : Boolean { + if (value is Set<*>) { + return value.filterIsInstance().size == value.size + } + return false + } + fun apply() { editor.apply() System.gc() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt index 421e4420..c92da214 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DownloadFileWorkManager.kt @@ -7,7 +7,6 @@ import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO 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 89bb0031..59f534ff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -32,26 +32,26 @@ import java.io.InputStreamReader class InAppUpdater { companion object { - const val GITHUB_USER_NAME = "recloudstream" - const val GITHUB_REPO = "cloudstream" + private const val GITHUB_USER_NAME = "recloudstream" + private const val GITHUB_REPO = "cloudstream" - const val LOG_TAG = "InAppUpdater" + private const val LOG_TAG = "InAppUpdater" // === IN APP UPDATER === data class GithubAsset( @JsonProperty("name") val name: String, @JsonProperty("size") val size: Int, // Size bytes - @JsonProperty("browser_download_url") val browser_download_url: String, // download link - @JsonProperty("content_type") val content_type: String, // application/vnd.android.package-archive + @JsonProperty("browser_download_url") val browserDownloadUrl: String, // download link + @JsonProperty("content_type") val contentType: String, // application/vnd.android.package-archive ) data class GithubRelease( - @JsonProperty("tag_name") val tag_name: String, // Version code + @JsonProperty("tag_name") val tagName: String, // Version code @JsonProperty("body") val body: String, // Desc @JsonProperty("assets") val assets: List, - @JsonProperty("target_commitish") val target_commitish: String, // branch + @JsonProperty("target_commitish") val targetCommitish: String, // branch @JsonProperty("prerelease") val prerelease: Boolean, - @JsonProperty("node_id") val node_id: String //Node Id + @JsonProperty("node_id") val nodeId: String //Node Id ) data class GithubObject( @@ -61,7 +61,7 @@ class InAppUpdater { ) data class GithubTag( - @JsonProperty("object") val github_object: GithubObject, + @JsonProperty("object") val githubObject: GithubObject, ) data class Update( @@ -114,7 +114,7 @@ class InAppUpdater { response.filter { rel -> !rel.prerelease }.sortedWith(compareBy { release -> - release.assets.firstOrNull { it.content_type == "application/vnd.android.package-archive" }?.name?.let { it1 -> + release.assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }?.name?.let { it1 -> versionRegex.find( it1 )?.groupValues?.let { @@ -134,7 +134,7 @@ class InAppUpdater { foundAsset?.name?.let { assetName -> val foundVersion = versionRegex.find(assetName) val shouldUpdate = - if (foundAsset.browser_download_url != "" && foundVersion != null) currentVersion?.versionName?.let { versionName -> + if (foundAsset.browserDownloadUrl != "" && foundVersion != null) currentVersion?.versionName?.let { versionName -> versionRegexLocal.find(versionName)?.groupValues?.let { it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() } @@ -146,10 +146,10 @@ class InAppUpdater { return if (foundVersion != null) { Update( shouldUpdate, - foundAsset.browser_download_url, + foundAsset.browserDownloadUrl, foundVersion.groupValues[2], found.body, - found.node_id + found.nodeId ) } else { Update(false, null, null, null, null) @@ -168,33 +168,33 @@ class InAppUpdater { val found = response.lastOrNull { rel -> - rel.prerelease || rel.tag_name == "pre-release" + rel.prerelease || rel.tagName == "pre-release" } val foundAsset = found?.assets?.filter { it -> - it.content_type == "application/vnd.android.package-archive" + it.contentType == "application/vnd.android.package-archive" }?.getOrNull(0) val tagResponse = parseJson(app.get(tagUrl, headers = headers).text) - Log.d(LOG_TAG, "Fetched GitHub tag: ${tagResponse.github_object.sha.take(7)}") + Log.d(LOG_TAG, "Fetched GitHub tag: ${tagResponse.githubObject.sha.take(7)}") val shouldUpdate = (getString(R.string.commit_hash) .trim { c -> c.isWhitespace() } .take(7) != - tagResponse.github_object.sha + tagResponse.githubObject.sha .trim { c -> c.isWhitespace() } .take(7)) return if (foundAsset != null) { Update( shouldUpdate, - foundAsset.browser_download_url, - tagResponse.github_object.sha.take(10), + foundAsset.browserDownloadUrl, + tagResponse.githubObject.sha.take(10), found.body, - found.node_id + found.nodeId ) } else { Update(false, null, null, null, null) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt index bc81a5b9..4b3f02f1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PackageInstaller.kt @@ -11,7 +11,6 @@ import android.os.Build import android.widget.Toast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError -import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.utils.Coroutines.main import java.io.InputStream @@ -57,7 +56,7 @@ class ApkInstaller(private val service: PackageInstallerService) { PackageInstaller.STATUS_FAILURE )) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT) + val userAction = intent.getSafeParcelableExtra(Intent.EXTRA_INTENT) userAction?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(userAction) } @@ -146,3 +145,5 @@ class ApkInstaller(private val service: PackageInstallerService) { } } +@Suppress("DEPRECATION") +inline fun Intent.getSafeParcelableExtra(key: String): T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelableExtra(key) else getParcelableExtra(key, T::class.java) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt index 27609730..0d3da8e7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/PowerManagerAPI.kt @@ -17,8 +17,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -const val packageName = BuildConfig.APPLICATION_ID -const val TAG = "PowerManagerAPI" +private const val PACKAGE_NAME = BuildConfig.APPLICATION_ID +private const val TAG = "PowerManagerAPI" object BatteryOptimizationChecker { @@ -72,7 +72,7 @@ object BatteryOptimizationChecker { val intent = Intent() try { intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - .setData(Uri.fromParts("package", packageName, null)) + .setData(Uri.fromParts("package", PACKAGE_NAME, null)) context.startActivity(intent, Bundle()) } catch (t: Throwable) { Log.e(TAG, "Unable to invoke any intent", t) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index 71d3a1ef..351e77c8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -73,8 +73,8 @@ object SyncUtil { val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text val mapped = parseJson(response) - val overrideMal = mapped?.malId ?: mapped?.Mal?.id ?: mapped?.Anilist?.malId - val overrideAnilist = mapped?.aniId ?: mapped?.Anilist?.id + val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId + val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id if (overrideMal != null) { return overrideMal.toString() to overrideAnilist?.toString() @@ -135,8 +135,8 @@ object SyncUtil { @JsonProperty("createdAt") val createdAt: String?, @JsonProperty("updatedAt") val updatedAt: String?, @JsonProperty("deletedAt") val deletedAt: String?, - @JsonProperty("Mal") val Mal: Mal?, - @JsonProperty("Anilist") val Anilist: Anilist?, + @JsonProperty("Mal") val mal: Mal?, + @JsonProperty("Anilist") val anilist: Anilist?, @JsonProperty("malUrl") val malUrl: String? ) 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 8670de53..ad1b6502 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -553,7 +553,7 @@ object UIHelper { return result } - fun Context?.IsBottomLayout(): Boolean { + fun Context?.isBottomLayout(): Boolean { if (this == null) return true val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) return settingsManager.getBoolean(getString(R.string.bottom_title_key), true) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 197bacc6..a3f6d789 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -293,6 +293,7 @@ object VideoDownloadManager { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) } else { + //fixme Specify a better flag PendingIntent.getActivity(context, 0, intent, 0) } builder.setContentIntent(pendingIntent) @@ -475,10 +476,10 @@ object VideoDownloadManager { } } - private const val reservedChars = "|\\?*<\":>+[]/\'" + private const val RESERVED_CHARS = "|\\?*<\":>+[]/\'" fun sanitizeFilename(name: String, removeSpaces: Boolean = false): String { var tempName = name - for (c in reservedChars) { + for (c in RESERVED_CHARS) { tempName = tempName.replace(c, ' ') } if (removeSpaces) tempName = tempName.replace(" ", "") @@ -1699,7 +1700,7 @@ object VideoDownloadManager { } */ fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? = - getDownloadFileInfo(context, id, removeKeys = true) + getDownloadFileInfo(context, id) private fun DownloadedFileInfo.toFile(context: Context): SafeFile? { return basePathToFile(context, this.basePath)?.gotoDirectory(relativePath) @@ -1709,7 +1710,6 @@ object VideoDownloadManager { private fun getDownloadFileInfo( context: Context, id: Int, - removeKeys: Boolean = false ): DownloadedFileInfoResult? { try { val info = diff --git a/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt index d4725d53..2aea0b8d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/widget/FlowLayout.kt @@ -19,7 +19,7 @@ class FlowLayout : ViewGroup { @SuppressLint("CustomViewStyleable") internal constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) { val t = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout) - itemSpacing = t.getDimensionPixelSize(R.styleable.FlowLayout_Layout_itemSpacing, 0); + itemSpacing = t.getDimensionPixelSize(R.styleable.FlowLayout_Layout_itemSpacing, 0) t.recycle() } diff --git a/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt b/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt new file mode 100644 index 00000000..5dbf4d7c --- /dev/null +++ b/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt @@ -0,0 +1,16 @@ +package com.lagradost.cloudstream3 + +import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter.Companion.findClosestBase2 +import org.junit.Assert +import org.junit.Test + +class PluginAdapterTest { + @Test + fun testFindClosestBase2() { + Assert.assertEquals(16, findClosestBase2(0)) + Assert.assertEquals(256, findClosestBase2(170)) + Assert.assertEquals(256, findClosestBase2(256)) + Assert.assertEquals(512, findClosestBase2(257)) + Assert.assertEquals(512, findClosestBase2(700)) + } +} \ No newline at end of file From 82f8ab489e88145e0f498dc3537d26d59157b7dc Mon Sep 17 00:00:00 2001 From: firelight <147925818+fire-light42@users.noreply.github.com> Date: Mon, 29 Jul 2024 00:58:35 +0200 Subject: [PATCH 418/441] Fix prerelease test function --- .../ui/settings/extensions/PluginAdapter.kt | 17 ++++++++++++++--- .../lagradost/cloudstream3/PluginAdapterTest.kt | 16 ---------------- 2 files changed, 14 insertions(+), 19 deletions(-) delete mode 100644 app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt 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 9fb3f282..d159539d 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 @@ -33,7 +33,8 @@ import java.text.DecimalFormat import kotlin.math.floor import kotlin.math.log10 import kotlin.math.pow - +import org.junit.Test +import org.junit.Assert data class PluginViewData( val plugin: Plugin, @@ -96,13 +97,23 @@ class PluginAdapter( } companion object { - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { + private tailrec fun findClosestBase2(target: Int, current: Int = 16, max: Int = 512): Int { if (current >= max) return max if (current >= target) return current return findClosestBase2(target, current * 2, max) } + // DO NOT MOVE, as running this test will result in ExceptionInInitializerError on prerelease due to static variables using Resources.getSystem() + // this test function is only to show how the function works + @Test + fun testFindClosestBase2() { + Assert.assertEquals(16, findClosestBase2(0)) + Assert.assertEquals(256, findClosestBase2(170)) + Assert.assertEquals(256, findClosestBase2(256)) + Assert.assertEquals(512, findClosestBase2(257)) + Assert.assertEquals(512, findClosestBase2(700)) + } + private val iconSizeExact = 32.toPx private val iconSize by lazy { findClosestBase2(iconSizeExact, 16, 512) diff --git a/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt b/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt deleted file mode 100644 index 5dbf4d7c..00000000 --- a/app/src/test/java/com/lagradost/cloudstream3/PluginAdapterTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.lagradost.cloudstream3 - -import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter.Companion.findClosestBase2 -import org.junit.Assert -import org.junit.Test - -class PluginAdapterTest { - @Test - fun testFindClosestBase2() { - Assert.assertEquals(16, findClosestBase2(0)) - Assert.assertEquals(256, findClosestBase2(170)) - Assert.assertEquals(256, findClosestBase2(256)) - Assert.assertEquals(512, findClosestBase2(257)) - Assert.assertEquals(512, findClosestBase2(700)) - } -} \ No newline at end of file From 150ad5fc9f4c90a8de8c42bd53b190a594606156 Mon Sep 17 00:00:00 2001 From: epireyn <48213068+epireyn@users.noreply.github.com> Date: Mon, 29 Jul 2024 01:00:44 +0200 Subject: [PATCH 419/441] Add sorting by release date (#1206) --- .../cloudstream3/syncproviders/SyncApi.kt | 6 +++++- .../syncproviders/providers/AniListApi.kt | 10 +++++++--- .../syncproviders/providers/LocalList.kt | 5 +++++ .../syncproviders/providers/MALApi.kt | 9 +++++++++ .../syncproviders/providers/SimklApi.kt | 7 ++++++- .../ui/library/LibraryViewModel.kt | 4 +++- .../cloudstream3/utils/DataStoreHelper.kt | 20 +++++++++++++++---- app/src/main/res/values/strings.xml | 2 ++ 8 files changed, 53 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt index 878e0cb3..dcb8bbea 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt @@ -5,6 +5,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiText import me.xdrop.fuzzywuzzy.FuzzySearch +import java.util.Date interface SyncAPI : OAuth2API { /** @@ -124,6 +125,8 @@ interface SyncAPI : OAuth2API { ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed() ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) } ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime } + ListSorting.ReleaseDateNew -> items.sortedByDescending { it.releaseDate } + ListSorting.ReleaseDateOld -> items.sortedBy { it.releaseDate } else -> items } } @@ -158,9 +161,10 @@ interface SyncAPI : OAuth2API { override var posterUrl: String?, override var posterHeaders: Map?, override var quality: SearchQuality?, + val releaseDate: Date?, override var id: Int? = null, val plot : String? = null, val rating: Int? = null, - val tags: List? = null, + val tags: List? = null ) : SearchResponse } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt index e51d3d65..6112c7db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt @@ -16,15 +16,16 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery +import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject +import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear import java.net.URL import java.net.URLEncoder -import java.util.* +import java.util.Locale class AniListApi(index: Int) : AccountManager(index), SyncAPI { override var name = "AniList" @@ -631,8 +632,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ?: this.media.coverImage.medium, null, null, + this.media.seasonYear.toYear(), null, - plot = this.media.description + plot = this.media.description, ) } } @@ -689,6 +691,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt index f819cd3b..0d9a4d13 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -119,6 +119,11 @@ class LocalList : SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, +// ListSorting.RatingHigh, +// ListSorting.RatingLow, + ) ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt index 6046a0f2..08c18653 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt @@ -27,6 +27,7 @@ import java.security.SecureRandom import java.text.ParseException import java.text.SimpleDateFormat import java.time.Instant +import java.time.format.DateTimeFormatter import java.util.Calendar import java.util.Date import java.util.Locale @@ -448,6 +449,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { null, null, plot = this.node.synopsis, + releaseDate = if (this.node.startDate == null) null else try {Date.from( + Instant.from( + DateTimeFormatter.ofPattern(if (this.node.startDate.length == 4) "yyyy" else if (this.node.startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd") + .parse(this.node.startDate) + ) + )} catch (_: RuntimeException) {null} ) } } @@ -512,6 +519,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt index e5db626b..50517f9d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt @@ -31,6 +31,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.toJson +import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear import okhttp3.Interceptor import okhttp3.Response import java.math.BigInteger @@ -670,7 +671,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.movie.poster?.let { getPosterUrl(it) }, null, null, - movie.ids.simkl, + this.movie.year?.toYear(), + movie.ids.simkl ) } } @@ -702,6 +704,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { this.show.poster?.let { getPosterUrl(it) }, null, null, + this.show.year?.toYear(), show.ids.simkl ) } @@ -1027,6 +1030,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI { ListSorting.AlphabeticalZ, ListSorting.UpdatedNew, ListSorting.UpdatedOld, + ListSorting.ReleaseDateNew, + ListSorting.ReleaseDateOld, ListSorting.RatingHigh, ListSorting.RatingLow, ) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index 1bd01c86..6c602e6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -23,6 +23,8 @@ enum class ListSorting(@StringRes val stringRes: Int) { UpdatedOld(R.string.sort_updated_old), AlphabeticalA(R.string.sort_alphabetical_a), AlphabeticalZ(R.string.sort_alphabetical_z), + ReleaseDateNew(R.string.sort_release_date_new), + ReleaseDateOld(R.string.sort_release_date_old), } const val LAST_SYNC_API_KEY = "last_sync_api" @@ -132,4 +134,4 @@ class LibraryViewModel : ViewModel() { MainActivity.reloadLibraryEvent -= ::reloadPages super.onCleared() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 43124a53..2fa5f6a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -2,8 +2,8 @@ package com.lagradost.cloudstream3.utils import android.content.Context import com.fasterxml.jackson.annotation.JsonProperty -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.AcraApplication import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys @@ -11,6 +11,13 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast +import com.lagradost.cloudstream3.DubStatus +import com.lagradost.cloudstream3.EpisodeResponse +import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchQuality +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType @@ -18,6 +25,9 @@ import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState import com.lagradost.cloudstream3.utils.AppContextUtils.filterProviderByPreferredMedia +import java.util.Calendar +import java.util.Date +import java.util.GregorianCalendar import kotlin.reflect.KClass import kotlin.reflect.KProperty @@ -195,6 +205,8 @@ object DataStoreHelper { return this } + fun Int.toYear() : Date = GregorianCalendar.getInstance().also { it.set(Calendar.YEAR, this) }.time + /** * Used to display notifications on new episodes and posters in library. **/ @@ -242,7 +254,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } @@ -273,7 +285,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } @@ -304,7 +316,7 @@ object DataStoreHelper { null, null, latestUpdatedTime, - apiName, type, posterUrl, posterHeaders, quality, this.id, plot = this.plot, rating = this.rating, tags = this.tags + apiName, type, posterUrl, posterHeaders, quality, year?.toYear(), this.id, plot = this.plot, rating = this.rating, tags = this.tags ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21067fff..37a3f993 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -797,6 +797,8 @@ Can\'t get the device PIN code, try local authentication PIN code is now expired ! Code expires in %1$dm %2$ds + Release Date (New to Old) + Release Date (Old to New) hide_player_control_names_key Hide names of the player\'s controls \ No newline at end of file From b2f08847e1dc736aa54a26c84d8f71e8ea83c166 Mon Sep 17 00:00:00 2001 From: epireyn <48213068+epireyn@users.noreply.github.com> Date: Mon, 29 Jul 2024 01:01:45 +0200 Subject: [PATCH 420/441] Add system dark theme (#1208) --- app/src/main/AndroidManifest.xml | 2 +- .../lagradost/cloudstream3/CommonActivity.kt | 28 +++++++++++++++++-- .../lagradost/cloudstream3/MainActivity.kt | 2 ++ .../cloudstream3/ui/settings/SettingsUI.kt | 14 +++++++--- app/src/main/res/values-es/array.xml | 2 ++ app/src/main/res/values-pl/array.xml | 2 ++ app/src/main/res/values-tr/array.xml | 2 ++ app/src/main/res/values-vi/array.xml | 2 ++ app/src/main/res/values/array.xml | 2 ++ 9 files changed, 49 insertions(+), 7 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a23ef725..888be999 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -97,7 +97,7 @@ --> = Build.VERSION_CODES.Q) { + loadThemes(act) + } + } + + private fun mapSystemTheme(act: Activity): Int { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val currentNightMode = + act.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return when (currentNightMode) { + Configuration.UI_MODE_NIGHT_NO -> R.style.LightMode // Night mode is not active, we're using the light theme + else -> R.style.AppTheme // Night mode is active, we're using dark theme + } + } else { + return R.style.AppTheme + } + } + fun loadThemes(act: Activity?) { if (act == null) return val settingsManager = PreferenceManager.getDefaultSharedPreferences(act) val currentTheme = when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) { + "System" -> mapSystemTheme(act) "Black" -> R.style.AppTheme "Light" -> R.style.LightMode "Amoled" -> R.style.AmoledMode @@ -352,8 +376,8 @@ object CommonActivity { currentLook = currentLook.parent as? View ?: break }*/ - private fun View.hasContent() : Boolean { - return isShown && when(this) { + private fun View.hasContent(): Boolean { + return isShown && when (this) { //is RecyclerView -> this.childCount > 0 is ViewGroup -> this.childCount > 0 else -> true diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index eed69a50..b59265ee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -68,6 +68,7 @@ import com.lagradost.cloudstream3.CommonActivity.screenHeight import com.lagradost.cloudstream3.CommonActivity.setActivityInstance import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale +import com.lagradost.cloudstream3.CommonActivity.updateTheme import com.lagradost.cloudstream3.databinding.ActivityMainBinding import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding @@ -484,6 +485,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) updateLocale() // android fucks me by chaining lang when rotating the phone + updateTheme(this) // Update if system theme val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment 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 cc14e761..8c3ad0ad 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 @@ -88,10 +88,9 @@ class SettingsUI : PreferenceFragmentCompat() { getPref(R.string.app_theme_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.themes_names).toMutableList() val prefValues = resources.getStringArray(R.array.themes_names_values).toMutableList() - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less + val removeIncompatible = { text: String -> val toRemove = prefValues - .mapIndexed { idx, s -> if (s.startsWith("Monet")) idx else null } + .mapIndexed { idx, s -> if (s.startsWith(text)) idx else null } .filterNotNull() var offset = 0 toRemove.forEach { idx -> @@ -100,6 +99,12 @@ class SettingsUI : PreferenceFragmentCompat() { offset += 1 } } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less + removeIncompatible("Monet") + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { // Remove system on android 9 and less + removeIncompatible("System") + } val currentLayout = settingsManager.getString(getString(R.string.app_theme_key), prefValues.first()) @@ -123,7 +128,8 @@ class SettingsUI : PreferenceFragmentCompat() { } getPref(R.string.primary_color_key)?.setOnPreferenceClickListener { val prefNames = resources.getStringArray(R.array.themes_overlay_names).toMutableList() - val prefValues = resources.getStringArray(R.array.themes_overlay_names_values).toMutableList() + val prefValues = + resources.getStringArray(R.array.themes_overlay_names_values).toMutableList() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // remove monet on android 11 and less val toRemove = prefValues diff --git a/app/src/main/res/values-es/array.xml b/app/src/main/res/values-es/array.xml index 05d49f98..eb197f43 100644 --- a/app/src/main/res/values-es/array.xml +++ b/app/src/main/res/values-es/array.xml @@ -247,6 +247,7 @@ Gris Amoled Destello + Sistema Material You @@ -254,6 +255,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values-pl/array.xml b/app/src/main/res/values-pl/array.xml index 9f76f423..a43d7bcf 100644 --- a/app/src/main/res/values-pl/array.xml +++ b/app/src/main/res/values-pl/array.xml @@ -256,6 +256,7 @@ Szary Amoled Flashbang + System Material You @@ -263,6 +264,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values-tr/array.xml b/app/src/main/res/values-tr/array.xml index 5c723f72..22a94ebf 100644 --- a/app/src/main/res/values-tr/array.xml +++ b/app/src/main/res/values-tr/array.xml @@ -281,6 +281,7 @@ Gri Amoled Flaş Bombası + Sistem Material You @@ -288,6 +289,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values-vi/array.xml b/app/src/main/res/values-vi/array.xml index aac94100..f363befd 100644 --- a/app/src/main/res/values-vi/array.xml +++ b/app/src/main/res/values-vi/array.xml @@ -248,6 +248,7 @@ Xám Amoled Sáng + Hệ thống Material You @@ -255,6 +256,7 @@ Black Amoled Light + System Monet diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 3be12510..03715faf 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -318,6 +318,7 @@ Gray Amoled Flashbang + System Material You @@ -325,6 +326,7 @@ Black Amoled Light + System Monet From 63e27c2ea5c7c57d41846038678c38654e28e495 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 30 Jul 2024 21:16:11 +0300 Subject: [PATCH 421/441] Fix Trailers on API<33 (#1226) Recent NewPipeExtractor updates pushed minimum sdk to 33 which needs desugar_jdk_libs_nio --- app/build.gradle.kts | 8 ++++---- library/build.gradle.kts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2040cf39..ee6cda6c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -140,7 +140,7 @@ android { abortOnError = false checkReleaseBuilds = false } - + buildFeatures { buildConfig = true } @@ -200,7 +200,7 @@ dependencies { // PlayBack implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs - implementation("com.github.teamnewpipe:NewPipeExtractor:2d36945") /* For Trailers + implementation("com.github.teamnewpipe:NewPipeExtractor:176da72") /* For Trailers ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */ implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding @@ -213,7 +213,7 @@ dependencies { implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors implementation("androidx.tvprovider:tvprovider:1.0.0") implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures - implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication + implementation("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview implementation("io.github.g0dkar:qrcode-kotlin:4.2.0") // QR code for PIN Auth on TV @@ -223,7 +223,7 @@ dependencies { implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9 implementation("com.uwetrottmann.tmdb2:tmdb-java:2.11.0") // TMDB API v3 Wrapper Made with RetroFit - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.0.4") //nio flavor needed for NewPipeExtractor implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API Level 25 or Less. */ diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 516e1ee9..00bc3c14 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -27,7 +27,7 @@ kotlin { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") implementation("me.xdrop:fuzzywuzzy:1.4.0") // Match extractors implementation("org.mozilla:rhino:1.7.15") // run JavaScript - implementation("com.github.teamnewpipe:NewPipeExtractor:fafd471") + implementation("com.github.teamnewpipe:NewPipeExtractor:176da72") } } } From 30adb1cd9d8aa7d538d027fb4e9506cd31224869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Sancak?= Date: Tue, 30 Jul 2024 21:38:51 +0300 Subject: [PATCH 422/441] fixed: Test Search & VidMoxy, RapidVid extractors (#1219) --- .../cloudstream3/utils/TestingUtils.kt | 5 ++- .../extractors/ContentXExtractor.kt | 41 +++++++++---------- .../extractors/HDMomPlayerExtractor.kt | 21 +++++----- .../extractors/HDPlayerSystemExtractor.kt | 23 +++++------ .../extractors/MailRuExtractor.kt | 21 ++++------ .../extractors/OdnoklassnikiExtractor.kt | 18 ++++---- .../extractors/PeaceMakerstExtractor.kt | 37 ++++++++--------- .../extractors/RapidVidExtractor.kt | 37 ++++++++++------- .../extractors/SibNetExtractor.kt | 11 +++-- .../cloudstream3/extractors/TRsTXExtractor.kt | 30 +++++++------- .../extractors/TauVideoExtractor.kt | 11 +++-- .../extractors/VidMoxyExtractor.kt | 37 ++++++++++------- .../extractors/VideoSeyredExtractor.kt | 13 +++--- 13 files changed, 155 insertions(+), 150 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index 5e2b2bc1..049f92fb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -4,6 +4,7 @@ import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.mvvm.logError import kotlinx.coroutines.* import org.junit.Assert +import kotlin.random.Random object TestingUtils { open class TestResult(val success: Boolean) { @@ -280,8 +281,8 @@ object TestingUtils { // Test Search Results val searchQueries = - // Use the first 3 home page results as queries since they are guaranteed to exist - (homePageList.take(3).map { it.name } + + // Use the random 3 home page results as queries since they are guaranteed to exist + (homePageList.shuffled(Random).take(3).map { it.name.split(" ").first() } + // If home page is sparse then use generic search queries listOf("over", "iron", "guy")).take(3) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt index 27a5c52a..13a717b6 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ContentXExtractor.kt @@ -12,53 +12,52 @@ open class ContentX : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - Log.d("Kekik_${this.name}", "url » ${url}") + val extRef = referer ?: "" - val i_source = app.get(url, referer=ext_ref).text - val i_extract = Regex("""window\.openPlayer\('([^']+)'""").find(i_source)!!.groups[1]?.value ?: throw ErrorLoadingException("i_extract is null") + val iSource = app.get(url, referer=extRef).text + val iExtract = Regex("""window\.openPlayer\('([^']+)'""").find(iSource)!!.groups[1]?.value ?: throw ErrorLoadingException("iExtract is null") - val sub_urls = mutableSetOf() - Regex("""\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(i_source).forEach { - val (sub_url, sub_lang) = it.destructured + val subUrls = mutableSetOf() + Regex("""\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(iSource).forEach { + val (subUrl, subLang) = it.destructured - if (sub_url in sub_urls) { return@forEach } - sub_urls.add(sub_url) + if (subUrl in subUrls) { return@forEach } + subUrls.add(subUrl) subtitleCallback.invoke( SubtitleFile( - lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), - url = fixUrl(sub_url.replace("\\", "")) + lang = subLang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(subUrl.replace("\\", "")) ) ) } - val vid_source = app.get("${mainUrl}/source2.php?v=${i_extract}", referer=ext_ref).text - val vid_extract = Regex("""file\":\"([^\"]+)""").find(vid_source)!!.groups[1]?.value ?: throw ErrorLoadingException("vid_extract is null") - val m3u_link = vid_extract.replace("\\", "") + val vidSource = app.get("${mainUrl}/source2.php?v=${iExtract}", referer=extRef).text + val vidExtract = Regex("""file\":\"([^\"]+)""").find(vidSource)!!.groups[1]?.value ?: throw ErrorLoadingException("vidExtract is null") + val m3uLink = vidExtract.replace("\\", "") callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3u_link, + url = m3uLink, referer = url, quality = Qualities.Unknown.value, isM3u8 = true ) ) - val i_dublaj = Regex(""",\"([^']+)\",\"Türkçe""").find(i_source)!!.groups[1]?.value - if (i_dublaj != null) { - val dublaj_source = app.get("${mainUrl}/source2.php?v=${i_dublaj}", referer=ext_ref).text - val dublaj_extract = Regex("""file\":\"([^\"]+)""").find(dublaj_source)!!.groups[1]?.value ?: throw ErrorLoadingException("dublaj_extract is null") - val dublaj_link = dublaj_extract.replace("\\", "") + val iDublaj = Regex(""",\"([^']+)\",\"Türkçe""").find(iSource)!!.groups[1]?.value + if (iDublaj != null) { + val dublajSource = app.get("${mainUrl}/source2.php?v=${iDublaj}", referer=extRef).text + val dublajExtract = Regex("""file\":\"([^\"]+)""").find(dublajSource)!!.groups[1]?.value ?: throw ErrorLoadingException("dublajExtract is null") + val dublajLink = dublajExtract.replace("\\", "") callback.invoke( ExtractorLink( source = "${this.name} Türkçe Dublaj", name = "${this.name} Türkçe Dublaj", - url = dublaj_link, + url = dublajLink, referer = url, quality = Qualities.Unknown.value, isM3u8 = true diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt index 1f70ce61..1152cb4b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDMomPlayerExtractor.kt @@ -16,24 +16,23 @@ open class HDMomPlayer : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val m3u_link:String? - val ext_ref = referer ?: "" - val i_source = app.get(url, referer=ext_ref).text + val m3uLink:String? + val extRef = referer ?: "" + val iSource = app.get(url, referer=extRef).text - val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(i_source)?.groupValues + val bePlayer = Regex("""bePlayer\('([^']+)',\s*'(\{[^\}]+\})'\);""").find(iSource)?.groupValues if (bePlayer != null) { val bePlayerPass = bePlayer.get(1) val bePlayerData = bePlayer.get(2) val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt") - Log.d("Kekik_${this.name}", "encrypted » ${encrypted}") - m3u_link = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1) + m3uLink = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1) } else { - m3u_link = Regex("""file:\"([^\"]+)""").find(i_source)?.groupValues?.get(1) + m3uLink = Regex("""file:\"([^\"]+)""").find(iSource)?.groupValues?.get(1) - val track_str = Regex("""tracks:\[([^\]]+)""").find(i_source)?.groupValues?.get(1) - if (track_str != null) { - val tracks:List = jacksonObjectMapper().readValue("[${track_str}]") + val trackStr = Regex("""tracks:\[([^\]]+)""").find(iSource)?.groupValues?.get(1) + if (trackStr != null) { + val tracks:List = jacksonObjectMapper().readValue("[${trackStr}]") for (track in tracks) { if (track.file == null || track.label == null) continue @@ -53,7 +52,7 @@ open class HDMomPlayer : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = m3u_link ?: throw ErrorLoadingException("m3u link not found"), + url = m3uLink ?: throw ErrorLoadingException("m3u link not found"), referer = url, quality = Qualities.Unknown.value, isM3u8 = true diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt index 8318c3fb..e3cf3aee 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/HDPlayerSystemExtractor.kt @@ -13,37 +13,36 @@ open class HDPlayerSystem : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val vid_id = if (url.contains("video/")) { + val extRef = referer ?: "" + val vidId = if (url.contains("video/")) { url.substringAfter("video/") } else { url.substringAfter("?data=") } - val post_url = "${mainUrl}/player/index.php?data=${vid_id}&do=getVideo" - Log.d("Kekik_${this.name}", "post_url » ${post_url}") + val postUrl = "${mainUrl}/player/index.php?data=${vidId}&do=getVideo" val response = app.post( - post_url, + postUrl, data = mapOf( - "hash" to vid_id, - "r" to ext_ref + "hash" to vidId, + "r" to extRef ), - referer = ext_ref, + referer = extRef, headers = mapOf( "Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With" to "XMLHttpRequest" ) ) - val video_response = response.parsedSafe() ?: throw ErrorLoadingException("failed to parse response") - val m3u_link = video_response.securedLink + val videoResponse = response.parsedSafe() ?: throw ErrorLoadingException("failed to parse response") + val m3uLink = videoResponse.securedLink callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3u_link, - referer = ext_ref, + url = m3uLink, + referer = extRef, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt index ce742e97..07346c70 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MailRuExtractor.kt @@ -13,28 +13,25 @@ open class MailRu : ExtractorApi() { override val requiresReferer = false override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - Log.d("Kekik_${this.name}", "url » ${url}") + val extRef = referer ?: "" - val vid_id = url.substringAfter("video/embed/").trim() - val video_req = app.get("${mainUrl}/+/video/meta/${vid_id}", referer=url) - val video_key = video_req.cookies["video_key"].toString() - Log.d("Kekik_${this.name}", "video_key » ${video_key}") + val vidId = url.substringAfter("video/embed/").trim() + val videoReq = app.get("${mainUrl}/+/video/meta/${vidId}", referer=url) + val videoKey = videoReq.cookies["video_key"].toString() - val video_data = AppUtils.tryParseJson(video_req.text) ?: throw ErrorLoadingException("Video not found") + val videoData = AppUtils.tryParseJson(videoReq.text) ?: throw ErrorLoadingException("Video not found") - for (video in video_data.videos) { - Log.d("Kekik_${this.name}", "video » ${video}") + for (video in videoData.videos) { - val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url + val videoUrl = if (video.url.startsWith("//")) "https:${video.url}" else video.url callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = video_url, + url = videoUrl, referer = url, - headers = mapOf("Cookie" to "video_key=${video_key}"), + headers = mapOf("Cookie" to "video_key=${videoKey}"), quality = getQualityFromName(video.key), isM3u8 = false ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt index 6db0830c..31b3d50b 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/OdnoklassnikiExtractor.kt @@ -13,22 +13,20 @@ open class Odnoklassniki : ExtractorApi() { override val requiresReferer = false override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - Log.d("Kekik_${this.name}", "url » ${url}") + val extRef = referer ?: "" - val user_agent = mapOf("User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36") + val userAgent = mapOf("User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36") - val video_req = app.get(url, headers=user_agent).text.replace("\\"", "\"").replace("\\\\", "\\") + val videoReq = app.get(url, headers=userAgent).text.replace("\\"", "\"").replace("\\\\", "\\") .replace(Regex("\\\\u([0-9A-Fa-f]{4})")) { matchResult -> Integer.parseInt(matchResult.groupValues[1], 16).toChar().toString() } - val videos_str = Regex("""\"videos\":(\[[^\]]*\])""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found") - val videos = AppUtils.tryParseJson>(videos_str) ?: throw ErrorLoadingException("Video not found") + val videosStr = Regex("""\"videos\":(\[[^\]]*\])""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found") + val videos = AppUtils.tryParseJson>(videosStr) ?: throw ErrorLoadingException("Video not found") for (video in videos) { - Log.d("Kekik_${this.name}", "video » ${video}") - val video_url = if (video.url.startsWith("//")) "https:${video.url}" else video.url + val videoUrl = if (video.url.startsWith("//")) "https:${video.url}" else video.url val quality = video.name.uppercase() .replace("MOBILE", "144p") @@ -44,10 +42,10 @@ open class Odnoklassniki : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = video_url, + url = videoUrl, referer = url, quality = getQualityFromName(quality), - headers = user_agent, + headers = userAgent, isM3u8 = false ) ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt index 0a005036..3a5cf727 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PeaceMakerstExtractor.kt @@ -13,39 +13,38 @@ open class PeaceMakerst : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val m3u_link:String? - val ext_ref = referer ?: "" - val post_url = "${url}?do=getVideo" - Log.d("Kekik_${this.name}", "post_url » ${post_url}") + val m3uLink:String? + val extRef = referer ?: "" + val postUrl = "${url}?do=getVideo" val response = app.post( - post_url, + postUrl, data = mapOf( "hash" to url.substringAfter("video/"), - "r" to ext_ref, + "r" to extRef, "s" to "" ), - referer = ext_ref, + referer = extRef, headers = mapOf( "Content-Type" to "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With" to "XMLHttpRequest" ) ) if (response.text.contains("teve2.com.tr\\/embed\\/")) { - val teve2_id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"") - val teve2_response = app.get( - "https://www.teve2.com.tr/action/media/${teve2_id}", - referer = "https://www.teve2.com.tr/embed/${teve2_id}" + val teve2Id = response.text.substringAfter("teve2.com.tr\\/embed\\/").substringBefore("\"") + val teve2Response = app.get( + "https://www.teve2.com.tr/action/media/${teve2Id}", + referer = "https://www.teve2.com.tr/embed/${teve2Id}" ).parsedSafe() ?: throw ErrorLoadingException("teve2 response is null") - m3u_link = teve2_response.media.link.serviceUrl + "//" + teve2_response.media.link.securePath + m3uLink = teve2Response.media.link.serviceUrl + "//" + teve2Response.media.link.securePath } else { - val video_response = response.parsedSafe() ?: throw ErrorLoadingException("peace response is null") - val video_sources = video_response.videoSources - if (video_sources.isNotEmpty()) { - m3u_link = video_sources.lastOrNull()?.file + val videoResponse = response.parsedSafe() ?: throw ErrorLoadingException("peace response is null") + val videoSources = videoResponse.videoSources + if (videoSources.isNotEmpty()) { + m3uLink = videoSources.lastOrNull()?.file } else { - m3u_link = null + m3uLink = null } } @@ -53,8 +52,8 @@ open class PeaceMakerst : ExtractorApi() { ExtractorLink( source = this.name, name = this.name, - url = m3u_link ?: throw ErrorLoadingException("m3u link not found"), - referer = ext_ref, + url = m3uLink ?: throw ErrorLoadingException("m3u link not found"), + referer = extRef, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt index 607d2d78..1088f2e9 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/RapidVidExtractor.kt @@ -12,36 +12,45 @@ open class RapidVid : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_req = app.get(url, referer=ext_ref).text + val extRef = referer ?: "" + val videoReq = app.get(url, referer=extRef).text - val sub_urls = mutableSetOf() - Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach { - val (sub_url, sub_lang) = it.destructured + val subUrls = mutableSetOf() + Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(videoReq).forEach { + val (subUrl, subLang) = it.destructured - if (sub_url in sub_urls) { return@forEach } - sub_urls.add(sub_url) + if (subUrl in subUrls) { return@forEach } + subUrls.add(subUrl) subtitleCallback.invoke( SubtitleFile( - lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), - url = fixUrl(sub_url.replace("\\", "")) + lang = subLang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(subUrl.replace("\\", "")) ) ) } - val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + var extractedValue = Regex("""file": "(.*)",""").find(videoReq)?.groupValues?.get(1) + var decoded: String? = null - val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() - val decoded = String(bytes, Charsets.UTF_8) - Log.d("Kekik_${this.name}", "decoded » ${decoded}") + if (extractedValue != null) { + val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() + decoded = String(bytes, Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } else { + val evalJWSsetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val JWSsetup = getAndUnpack(getAndUnpack(evalJWSsetup)).replace("\\\\", "\\") + extractedValue = Regex("""file":"(.*)","label""").find(JWSsetup)?.groupValues?.get(1)?.replace("\\\\x", "") + + val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() + decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } callback.invoke( ExtractorLink( source = this.name, name = this.name, url = decoded, - referer = ext_ref, + referer = extRef, quality = Qualities.Unknown.value, isM3u8 = true ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt index ebd57f9c..89f731f7 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SibNetExtractor.kt @@ -12,18 +12,17 @@ open class SibNet : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val i_source = app.get(url, referer=ext_ref).text - var m3u_link = Regex("""player.src\(\[\{src: \"([^\"]+)""").find(i_source)?.groupValues?.get(1) ?: throw ErrorLoadingException("m3u link not found") + val extRef = referer ?: "" + val iSource = app.get(url, referer=extRef).text + var m3uLink = Regex("""player.src\(\[\{src: \"([^\"]+)""").find(iSource)?.groupValues?.get(1) ?: throw ErrorLoadingException("m3u link not found") - m3u_link = "${mainUrl}${m3u_link}" - Log.d("Kekik_${this.name}", "m3u_link » ${m3u_link}") + m3uLink = "${mainUrl}${m3uLink}" callback.invoke( ExtractorLink( source = this.name, name = this.name, - url = m3u_link, + url = m3uLink, referer = url, quality = Qualities.Unknown.value, type = INFER_TYPE diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt index de5ca9a2..f2a75b94 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TRsTXExtractor.kt @@ -13,13 +13,13 @@ open class TRsTX : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" + val extRef = referer ?: "" - val video_req = app.get(url, referer=ext_ref).text + val videoReq = app.get(url, referer=extRef).text - val file = Regex("""file\":\"([^\"]+)""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val file = Regex("""file\":\"([^\"]+)""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") val postLink = "${mainUrl}/" + file.replace("\\", "") - val rawList = app.post(postLink, referer=ext_ref).parsedSafe>() ?: throw ErrorLoadingException("Post link not found") + val rawList = app.post(postLink, referer=extRef).parsedSafe>() ?: throw ErrorLoadingException("Post link not found") val postJson: List = rawList.drop(1).map { item -> val mapItem = item as Map<*, *> @@ -28,37 +28,35 @@ open class TRsTX : ExtractorApi() { file = mapItem["file"] as? String ) } - Log.d("Kekik_${this.name}", "postJson » ${postJson}") - val vid_links = mutableSetOf() - val vid_map = mutableListOf>() + val vidLinks = mutableSetOf() + val vidMap = mutableListOf>() for (item in postJson) { if (item.file == null || item.title == null) continue val fileUrl = "${mainUrl}/playlist/" + item.file.substring(1) + ".txt" - val videoData = app.post(fileUrl, referer=ext_ref).text + val videoData = app.post(fileUrl, referer=extRef).text - if (videoData in vid_links) { continue } - vid_links.add(videoData) + if (videoData in vidLinks) { continue } + vidLinks.add(videoData) - vid_map.add(mapOf( + vidMap.add(mapOf( "title" to item.title, "videoData" to videoData )) } - for (mapEntry in vid_map) { - Log.d("Kekik_${this.name}", "mapEntry » ${mapEntry}") + for (mapEntry in vidMap) { val title = mapEntry["title"] ?: continue - val m3u_link = mapEntry["videoData"] ?: continue + val m3uLink = mapEntry["videoData"] ?: continue callback.invoke( ExtractorLink( source = this.name, name = "${this.name} - ${title}", - url = m3u_link, - referer = ext_ref, + url = m3uLink, + referer = extRef, quality = Qualities.Unknown.value, type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt index 157374a3..0893b4de 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/TauVideoExtractor.kt @@ -13,12 +13,11 @@ open class TauVideo : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_key = url.split("/").last() - val video_url = "${mainUrl}/api/video/${video_key}" - Log.d("Kekik_${this.name}", "video_url » ${video_url}") + val extRef = referer ?: "" + val videoKey = url.split("/").last() + val videoUrl = "${mainUrl}/api/video/${videoKey}" - val api = app.get(video_url).parsedSafe() ?: throw ErrorLoadingException("TauVideo") + val api = app.get(videoUrl).parsedSafe() ?: throw ErrorLoadingException("TauVideo") for (video in api.urls) { callback.invoke( @@ -26,7 +25,7 @@ open class TauVideo : ExtractorApi() { source = this.name, name = this.name, url = video.url, - referer = ext_ref, + referer = extRef, quality = getQualityFromName(video.label), type = INFER_TYPE ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt index e57772ce..f7c3dd5e 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VidMoxyExtractor.kt @@ -12,36 +12,45 @@ open class VidMoxy : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_req = app.get(url, referer=ext_ref).text + val extRef = referer ?: "" + val videoReq = app.get(url, referer=extRef).text - val sub_urls = mutableSetOf() - Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(video_req).forEach { - val (sub_url, sub_lang) = it.destructured + val subUrls = mutableSetOf() + Regex("""captions\",\"file\":\"([^\"]+)\",\"label\":\"([^\"]+)\"""").findAll(videoReq).forEach { + val (subUrl, subLang) = it.destructured - if (sub_url in sub_urls) { return@forEach } - sub_urls.add(sub_url) + if (subUrl in subUrls) { return@forEach } + subUrls.add(subUrl) subtitleCallback.invoke( SubtitleFile( - lang = sub_lang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), - url = fixUrl(sub_url.replace("\\", "")) + lang = subLang.replace("\\u0131", "ı").replace("\\u0130", "İ").replace("\\u00fc", "ü").replace("\\u00e7", "ç"), + url = fixUrl(subUrl.replace("\\", "")) ) ) } - val extracted_value = Regex("""file": "(.*)",""").find(video_req)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + var extractedValue = Regex("""file": "(.*)",""").find(videoReq)?.groupValues?.get(1) + var decoded: String? = null - val bytes = extracted_value.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() - val decoded = String(bytes, Charsets.UTF_8) - Log.d("Kekik_${this.name}", "decoded » ${decoded}") + if (extractedValue != null) { + val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray() + decoded = String(bytes, Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } else { + val evaljwSetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found") + val jwSetup = getAndUnpack(getAndUnpack(evaljwSetup)).replace("\\\\", "\\") + extractedValue = Regex("""file":"(.*)","label""").find(jwSetup)?.groupValues?.get(1)?.replace("\\\\x", "") + + val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray() + decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found") + } callback.invoke( ExtractorLink( source = this.name, name = this.name, url = decoded, - referer = ext_ref, + referer = extRef, quality = Qualities.Unknown.value, isM3u8 = true ) diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt index 1161ff66..c85e6416 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoSeyredExtractor.kt @@ -15,14 +15,13 @@ open class VideoSeyred : ExtractorApi() { override val requiresReferer = true override suspend fun getUrl(url: String, referer: String?, subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit) { - val ext_ref = referer ?: "" - val video_id = url.substringAfter("embed/").substringBefore("?") - val video_url = "${mainUrl}/playlist/${video_id}.json" - Log.d("Kekik_${this.name}", "video_url » ${video_url}") + val extRef = referer ?: "" + val videoId = url.substringAfter("embed/").substringBefore("?") + val videoUrl = "${mainUrl}/playlist/${videoId}.json" - val response_raw = app.get(video_url) - val response_list:List = jacksonObjectMapper().readValue(response_raw.text) ?: throw ErrorLoadingException("VideoSeyred") - val response = response_list[0] ?: throw ErrorLoadingException("VideoSeyred") + val responseRaw = app.get(videoUrl) + val responseList:List = jacksonObjectMapper().readValue(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred") + val response = responseList[0] ?: throw ErrorLoadingException("VideoSeyred") for (track in response.tracks) { if (track.label != null && track.kind == "captions") { From 8fcb3e3121de93017f7b3cbf959b4429040a2184 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 30 Jul 2024 12:45:25 -0600 Subject: [PATCH 423/441] Fix cast recycler scrolling (#1221) --- .../ui/result/ResultFragmentPhone.kt | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) 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 f1399e8d..97bc49ea 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 @@ -23,6 +23,7 @@ import androidx.core.widget.NestedScrollView import androidx.core.widget.doOnTextChanged import androidx.lifecycle.ViewModelProvider import com.discord.panels.OverlappingPanelsLayout +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 @@ -118,6 +119,14 @@ open class ResultFragmentPhone : FullScreenPlayer() { return root } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { register(it) } + } + } + var currentTrailers: List = emptyList() var currentTrailerIndex = 0 @@ -210,9 +219,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { } override fun onDestroyView() { - - //somehow this still leaks and I dont know why???? - // todo look at https://github.com/discord/OverlappingPanels/blob/70b4a7cf43c6771873b1e091029d332896d41a1a/sample_app/src/main/java/com/discord/sampleapp/MainActivity.kt PanelsChildGestureRegionObserver.Provider.get().let { obs -> resultBinding?.resultCastItems?.let { obs.unregister(it) @@ -329,13 +335,18 @@ open class ResultFragmentPhone : FullScreenPlayer() { syncModel.addFromUrl(storedData.url) val api = APIHolder.getApiFromNameNull(storedData.apiName) - PanelsChildGestureRegionObserver.Provider.get().apply { - resultBinding?.resultCastItems?.let { - register(it) + // This may not be 100% reliable, and may delay for small period + // before resultCastItems will be scrollable again, but this does work + // most of the time. + binding?.resultOverlappingPanels?.registerEndPanelStateListeners( + object : OverlappingPanelsLayout.PanelStateListener { + override fun onPanelStateChange(panelState: PanelState) { + PanelsChildGestureRegionObserver.Provider.get().apply { + resultBinding?.resultCastItems?.let { register(it) } + } + } } - addGestureRegionsUpdateListener(gestureRegionsListener) - } - + ) // ===== ===== ===== @@ -674,6 +685,9 @@ open class ResultFragmentPhone : FullScreenPlayer() { observe(viewModel.page) { data -> if (data == null) return@observe resultBinding?.apply { + PanelsChildGestureRegionObserver.Provider.get().apply { + register(resultCastItems) + } (data as? Resource.Success)?.value?.let { d -> resultVpn.setText(d.vpnText) resultInfo.setText(d.metaText) @@ -1167,4 +1181,4 @@ open class ResultFragmentPhone : FullScreenPlayer() { } } } -} +} \ No newline at end of file From ab379ab31c30069aac3afdeec76410e69ea6bc95 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 30 Jul 2024 12:54:54 -0600 Subject: [PATCH 424/441] Support for multi deleting downloads and other major improvements/fixes (#1177) --- .../lagradost/cloudstream3/MainActivity.kt | 51 +- .../ui/download/DownloadAdapter.kt | 386 +++++++++----- .../ui/download/DownloadButtonSetup.kt | 31 +- .../ui/download/DownloadChildFragment.kt | 174 +++++-- .../ui/download/DownloadFragment.kt | 182 +++++-- .../ui/download/DownloadViewModel.kt | 483 +++++++++++++++--- .../ui/download/button/BaseFetchButton.kt | 7 +- .../ui/download/button/PieFetchButton.kt | 6 +- .../ui/player/DownloadFileGenerator.kt | 32 +- .../ui/player/DownloadedPlayerActivity.kt | 11 +- .../ui/result/ResultTrailerPlayer.kt | 30 +- .../cloudstream3/utils/AppContextUtils.kt | 14 +- .../utils/BackPressedCallbackHelper.kt | 30 ++ .../cloudstream3/utils/SnackbarHelper.kt | 84 +++ .../cloudstream3/utils/SubtitleUtils.kt | 56 ++ .../utils/VideoDownloadManager.kt | 45 +- .../res/layout/download_child_episode.xml | 17 +- .../res/layout/download_header_episode.xml | 11 + .../res/layout/fragment_child_downloads.xml | 59 ++- .../main/res/layout/fragment_downloads.xml | 109 ++-- app/src/main/res/values/strings.xml | 12 +- 21 files changed, 1384 insertions(+), 446 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index b59265ee..5408d2a8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -133,6 +133,8 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback import com.lagradost.cloudstream3.utils.BackupUtils.backup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback @@ -151,6 +153,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog +import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute @@ -1254,17 +1257,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa this.setKey(getString(R.string.jsdelivr_proxy_key), false) } else { this.setKey(getString(R.string.jsdelivr_proxy_key), true) - val parentView: View = findViewById(android.R.id.content) - Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG) - .let { snackbar -> - snackbar.setAction(R.string.revert) { - setKey(getString(R.string.jsdelivr_proxy_key), false) - } - snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground)) - snackbar.setTextColor(colorFromAttribute(R.attr.textColor)) - snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary)) - snackbar.show() - } + showSnackbar( + this@MainActivity, + R.string.jsdelivr_enabled, + Snackbar.LENGTH_LONG, + R.string.revert + ) { setKey(getString(R.string.jsdelivr_proxy_key), false) } } } } @@ -1603,7 +1601,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa if (isLayout(TV or EMULATOR)) { if (navDestination.matchDestination(R.id.navigation_home)) { - attachBackPressedCallback() + attachBackPressedCallback { + showConfirmExitDialog() + window?.navigationBarColor = + colorFromAttribute(R.attr.primaryGrayBackground) + updateLocale() + } } else detachBackPressedCallback() } } @@ -1848,28 +1851,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa finish() } - private var backPressedCallback: OnBackPressedCallback? = null - - private fun attachBackPressedCallback() { - if (backPressedCallback == null) { - backPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - showConfirmExitDialog() - window?.navigationBarColor = - colorFromAttribute(R.attr.primaryGrayBackground) - updateLocale() - } - } - } - - backPressedCallback?.isEnabled = true - onBackPressedDispatcher.addCallback(this, backPressedCallback ?: return) - } - - private fun detachBackPressedCallback() { - backPressedCallback?.isEnabled = false - } - suspend fun checkGithubConnectivity(): Boolean { return try { app.get( @@ -1880,4 +1861,4 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa false } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt index 9a026334..20458429 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt @@ -1,9 +1,9 @@ package com.lagradost.cloudstream3.ui.download -import android.annotation.SuppressLint import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.ViewGroup +import android.widget.CheckBox import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil @@ -31,47 +31,30 @@ const val DOWNLOAD_ACTION_LONG_CLICK = 5 const val DOWNLOAD_ACTION_GO_TO_CHILD = 0 const val DOWNLOAD_ACTION_LOAD_RESULT = 1 -abstract class VisualDownloadCached( - open val currentBytes: Long, - open val totalBytes: Long, - open val data: VideoDownloadHelper.DownloadCached -) { +sealed class VisualDownloadCached { + abstract val currentBytes: Long + abstract val totalBytes: Long + abstract val data: VideoDownloadHelper.DownloadCached + abstract var isSelected: Boolean - // Just to be extra-safe with areContentsTheSame - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is VisualDownloadCached) return false + data class Child( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadEpisodeCached, + override var isSelected: Boolean, + ) : VisualDownloadCached() - if (currentBytes != other.currentBytes) return false - if (totalBytes != other.totalBytes) return false - if (data != other.data) return false - - return true - } - - override fun hashCode(): Int { - var result = currentBytes.hashCode() - result = 31 * result + totalBytes.hashCode() - result = 31 * result + data.hashCode() - return result - } + data class Header( + override val currentBytes: Long, + override val totalBytes: Long, + override val data: VideoDownloadHelper.DownloadHeaderCached, + override var isSelected: Boolean, + val child: VideoDownloadHelper.DownloadEpisodeCached?, + val currentOngoingDownloads: Int, + val totalDownloads: Int, + ) : VisualDownloadCached() } -data class VisualDownloadChildCached( - override val currentBytes: Long, - override val totalBytes: Long, - override val data: VideoDownloadHelper.DownloadEpisodeCached, -): VisualDownloadCached(currentBytes, totalBytes, data) - -data class VisualDownloadHeaderCached( - override val currentBytes: Long, - override val totalBytes: Long, - override val data: VideoDownloadHelper.DownloadHeaderCached, - val child: VideoDownloadHelper.DownloadEpisodeCached?, - val currentOngoingDownloads: Int, - val totalDownloads: Int, -): VisualDownloadCached(currentBytes, totalBytes, data) - data class DownloadClickEvent( val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached @@ -83,108 +66,180 @@ data class DownloadHeaderClickEvent( ) class DownloadAdapter( - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val mediaClickCallback: (DownloadClickEvent) -> Unit, + private val onHeaderClickEvent: (DownloadHeaderClickEvent) -> Unit, + private val onItemClickEvent: (DownloadClickEvent) -> Unit, + private val onItemSelectionChanged: (Int, Boolean) -> Unit, ) : ListAdapter(DiffCallback()) { + private var isMultiDeleteState: Boolean = false + companion object { private const val VIEW_TYPE_HEADER = 0 private const val VIEW_TYPE_CHILD = 1 } inner class DownloadViewHolder( - private val binding: ViewBinding, - private val clickCallback: (DownloadHeaderClickEvent) -> Unit, - private val mediaClickCallback: (DownloadClickEvent) -> Unit, + private val binding: ViewBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(card: VisualDownloadCached?) { when (binding) { - is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadHeaderCached) - is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadChildCached) + is DownloadHeaderEpisodeBinding -> bindHeader(card as? VisualDownloadCached.Header) + is DownloadChildEpisodeBinding -> bindChild(card as? VisualDownloadCached.Child) } } - @SuppressLint("SetTextI18n") - private fun bindHeader(card: VisualDownloadHeaderCached?) { - if (binding !is DownloadHeaderEpisodeBinding) return - card ?: return - val d = card.data + private fun bindHeader(card: VisualDownloadCached.Header?) { + if (binding !is DownloadHeaderEpisodeBinding || card == null) return + val data = card.data binding.apply { - downloadHeaderPoster.apply { - setImage(d.poster) - setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_LOAD_RESULT, d)) + episodeHolder.apply { + if (isMultiDeleteState) { + setOnClickListener { + toggleIsChecked(deleteCheckbox, data.id) + } + } + + setOnLongClickListener { + toggleIsChecked(deleteCheckbox, data.id) + true } } - downloadHeaderTitle.text = d.name - val formattedSizeString = formatShortFileSize(itemView.context, card.totalBytes) + downloadHeaderPoster.apply { + setImage(data.poster) + if (isMultiDeleteState) { + setOnClickListener { + toggleIsChecked(deleteCheckbox, data.id) + } + } else { + setOnClickListener { + onHeaderClickEvent.invoke( + DownloadHeaderClickEvent( + DOWNLOAD_ACTION_LOAD_RESULT, + data + ) + ) + } + } + + setOnLongClickListener { + toggleIsChecked(deleteCheckbox, data.id) + true + } + } + downloadHeaderTitle.text = data.name + val formattedSize = formatShortFileSize(itemView.context, card.totalBytes) if (card.child != null) { - downloadHeaderGotoChild.isVisible = false + handleChildDownload(card, formattedSize) + } else handleParentDownload(card, formattedSize) - val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes) - if (status == DownloadStatusTell.IsDone) { - // We do this here instead if we are finished downloading - // so that we can use the value from the view model - // rather than extra unneeded disk operations and to prevent a - // delay in updating download icon state. - downloadButton.setProgress(card.currentBytes, card.totalBytes) - downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes) - // We will let the view model handle this - downloadButton.doSetProgress = false - downloadButton.progressBar.progressDrawable = - downloadButton.getDrawableFromStatus(status) - ?.let { ContextCompat.getDrawable(downloadButton.context, it) } - downloadHeaderInfo.text = formattedSizeString - } else { - downloadButton.doSetProgress = true - downloadButton.progressBar.progressDrawable = - ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + if (isMultiDeleteState) { + deleteCheckbox.setOnCheckedChangeListener { _, isChecked -> + onItemSelectionChanged.invoke(data.id, isChecked) } + } else deleteCheckbox.setOnCheckedChangeListener(null) - downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, mediaClickCallback) - downloadButton.isVisible = true - - episodeHolder.setOnClickListener { - mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child)) - } - } else { - downloadButton.isVisible = false - downloadHeaderGotoChild.isVisible = true - - try { - downloadHeaderInfo.text = downloadHeaderInfo.context.getString(R.string.extra_info_format) - .format( - card.totalDownloads, - downloadHeaderInfo.context.resources.getQuantityString( - R.plurals.episodes, - card.totalDownloads - ), - formattedSizeString - ) - } catch (e: Exception) { - // You probably formatted incorrectly - downloadHeaderInfo.text = "Error" - logError(e) - } - - episodeHolder.setOnClickListener { - clickCallback.invoke(DownloadHeaderClickEvent(DOWNLOAD_ACTION_GO_TO_CHILD, d)) - } + deleteCheckbox.apply { + isVisible = isMultiDeleteState + isChecked = card.isSelected } } } - private fun bindChild(card: VisualDownloadChildCached?) { - if (binding !is DownloadChildEpisodeBinding) return - card ?: return - val d = card.data + private fun DownloadHeaderEpisodeBinding.handleChildDownload( + card: VisualDownloadCached.Header, + formattedSize: String + ) { + card.child ?: return + downloadHeaderGotoChild.isVisible = false + val status = downloadButton.getStatus(card.child.id, card.currentBytes, card.totalBytes) + if (status == DownloadStatusTell.IsDone) { + // We do this here instead if we are finished downloading + // so that we can use the value from the view model + // rather than extra unneeded disk operations and to prevent a + // delay in updating download icon state. + downloadButton.setProgress(card.currentBytes, card.totalBytes) + downloadButton.applyMetaData(card.child.id, card.currentBytes, card.totalBytes) + // We will let the view model handle this + downloadButton.doSetProgress = false + downloadButton.progressBar.progressDrawable = + downloadButton.getDrawableFromStatus(status) + ?.let { ContextCompat.getDrawable(downloadButton.context, it) } + downloadHeaderInfo.text = formattedSize + } else { + // We need to make sure we restore the correct progress + // when we refresh data in the adapter. + downloadButton.resetView() + val drawable = downloadButton.getDrawableFromStatus(status)?.let { + ContextCompat.getDrawable(downloadButton.context, it) + } + downloadButton.statusView.setImageDrawable(drawable) + downloadButton.progressBar.progressDrawable = + ContextCompat.getDrawable( + downloadButton.context, + downloadButton.progressDrawable + ) + } + + downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, onItemClickEvent) + downloadButton.isVisible = !isMultiDeleteState + + if (!isMultiDeleteState) { + episodeHolder.setOnClickListener { + onItemClickEvent.invoke( + DownloadClickEvent( + DOWNLOAD_ACTION_PLAY_FILE, + card.child + ) + ) + } + } + } + + private fun DownloadHeaderEpisodeBinding.handleParentDownload( + card: VisualDownloadCached.Header, + formattedSize: String + ) { + downloadButton.isVisible = false + downloadHeaderGotoChild.isVisible = !isMultiDeleteState + + try { + downloadHeaderInfo.text = + downloadHeaderInfo.context.getString(R.string.extra_info_format).format( + card.totalDownloads, + downloadHeaderInfo.context.resources.getQuantityString( + R.plurals.episodes, + card.totalDownloads + ), + formattedSize + ) + } catch (e: Exception) { + downloadHeaderInfo.text = "" + logError(e) + } + + if (!isMultiDeleteState) { + episodeHolder.setOnClickListener { + onHeaderClickEvent.invoke( + DownloadHeaderClickEvent( + DOWNLOAD_ACTION_GO_TO_CHILD, + card.data + ) + ) + } + } + } + + private fun bindChild(card: VisualDownloadCached.Child?) { + if (binding !is DownloadChildEpisodeBinding || card == null) return + + val data = card.data binding.apply { - val posDur = getViewPos(d.id) + val posDur = getViewPos(data.id) downloadChildEpisodeProgress.apply { isVisible = posDur != null posDur?.let { @@ -194,36 +249,87 @@ class DownloadAdapter( } } - val status = downloadButton.getStatus(d.id, card.currentBytes, card.totalBytes) + val status = downloadButton.getStatus(data.id, card.currentBytes, card.totalBytes) if (status == DownloadStatusTell.IsDone) { // We do this here instead if we are finished downloading // so that we can use the value from the view model // rather than extra unneeded disk operations and to prevent a // delay in updating download icon state. downloadButton.setProgress(card.currentBytes, card.totalBytes) - downloadButton.applyMetaData(d.id, card.currentBytes, card.totalBytes) + downloadButton.applyMetaData(data.id, card.currentBytes, card.totalBytes) // We will let the view model handle this downloadButton.doSetProgress = false downloadButton.progressBar.progressDrawable = downloadButton.getDrawableFromStatus(status) ?.let { ContextCompat.getDrawable(downloadButton.context, it) } - downloadChildEpisodeTextExtra.text = formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes) + downloadChildEpisodeTextExtra.text = + formatShortFileSize(downloadChildEpisodeTextExtra.context, card.totalBytes) } else { - downloadButton.doSetProgress = true + // We need to make sure we restore the correct progress + // when we refresh data in the adapter. + downloadButton.resetView() + val drawable = downloadButton.getDrawableFromStatus(status)?.let { + ContextCompat.getDrawable(downloadButton.context, it) + } + downloadButton.statusView.setImageDrawable(drawable) downloadButton.progressBar.progressDrawable = - ContextCompat.getDrawable(downloadButton.context, downloadButton.progressDrawable) + ContextCompat.getDrawable( + downloadButton.context, + downloadButton.progressDrawable + ) } - downloadButton.setDefaultClickListener(d, downloadChildEpisodeTextExtra, mediaClickCallback) - downloadButton.isVisible = true + downloadButton.setDefaultClickListener( + data, + downloadChildEpisodeTextExtra, + onItemClickEvent + ) + downloadButton.isVisible = !isMultiDeleteState downloadChildEpisodeText.apply { - text = context.getNameFull(d.name, d.episode, d.season) + text = context.getNameFull(data.name, data.episode, data.season) isSelected = true // Needed for text repeating } downloadChildEpisodeHolder.setOnClickListener { - mediaClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) + onItemClickEvent.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, data)) + } + + downloadChildEpisodeHolder.apply { + when { + isMultiDeleteState -> { + setOnClickListener { + toggleIsChecked(deleteCheckbox, data.id) + } + } + + else -> { + setOnClickListener { + onItemClickEvent.invoke( + DownloadClickEvent( + DOWNLOAD_ACTION_PLAY_FILE, + data + ) + ) + } + } + } + + setOnLongClickListener { + toggleIsChecked(deleteCheckbox, data.id) + true + } + } + + if (isMultiDeleteState) { + deleteCheckbox.setOnCheckedChangeListener { _, isChecked -> + onItemSelectionChanged.invoke(data.id, isChecked) + } + } else deleteCheckbox.setOnCheckedChangeListener(null) + + deleteCheckbox.apply { + isVisible = isMultiDeleteState + isChecked = card.isSelected } } } @@ -236,7 +342,7 @@ class DownloadAdapter( VIEW_TYPE_CHILD -> DownloadChildEpisodeBinding.inflate(inflater, parent, false) else -> throw IllegalArgumentException("Invalid view type") } - return DownloadViewHolder(binding, clickCallback, mediaClickCallback) + return DownloadViewHolder(binding) } override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) { @@ -245,18 +351,52 @@ class DownloadAdapter( override fun getItemViewType(position: Int): Int { return when (getItem(position)) { - is VisualDownloadChildCached -> VIEW_TYPE_CHILD - is VisualDownloadHeaderCached -> VIEW_TYPE_HEADER + is VisualDownloadCached.Child -> VIEW_TYPE_CHILD + is VisualDownloadCached.Header -> VIEW_TYPE_HEADER else -> throw IllegalArgumentException("Invalid data type at position $position") } } + fun setIsMultiDeleteState(value: Boolean) { + if (isMultiDeleteState == value) return + isMultiDeleteState = value + notifyItemRangeChanged(0, itemCount) + } + + fun notifyAllSelected() { + currentList.indices.forEach { index -> + if (!currentList[index].isSelected) { + notifyItemChanged(index) + } + } + } + + fun notifySelectionStates() { + currentList.indices.forEach { index -> + if (currentList[index].isSelected) { + notifyItemChanged(index) + } + } + } + + private fun toggleIsChecked(checkbox: CheckBox, itemId: Int) { + val isChecked = !checkbox.isChecked + checkbox.isChecked = isChecked + onItemSelectionChanged.invoke(itemId, isChecked) + } + class DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + override fun areItemsTheSame( + oldItem: VisualDownloadCached, + newItem: VisualDownloadCached + ): Boolean { return oldItem.data.id == newItem.data.id } - override fun areContentsTheSame(oldItem: VisualDownloadCached, newItem: VisualDownloadCached): Boolean { + override fun areContentsTheSame( + oldItem: VisualDownloadCached, + newItem: VisualDownloadCached + ): Boolean { return oldItem == newItem } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt index c8c40e29..bf2c1b49 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt @@ -1,11 +1,10 @@ package com.lagradost.cloudstream3.ui.download import android.content.DialogInterface -import android.widget.Toast import androidx.appcompat.app.AlertDialog +import com.google.android.material.snackbar.Snackbar import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.DownloadFileGenerator @@ -14,9 +13,11 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE +import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager +import kotlinx.coroutines.MainScope object DownloadButtonSetup { fun handleDownloadClick(click: DownloadClickEvent) { @@ -29,9 +30,15 @@ object DownloadButtonSetup { DialogInterface.OnClickListener { _, which -> when (which) { DialogInterface.BUTTON_POSITIVE -> { - VideoDownloadManager.deleteFileAndUpdateSettings(ctx, id) + VideoDownloadManager.deleteFilesAndUpdateSettings( + ctx, + setOf(id), + MainScope() + ) } + DialogInterface.BUTTON_NEGATIVE -> { + // Do nothing on cancel } } } @@ -56,11 +63,13 @@ object DownloadButtonSetup { } } } + DOWNLOAD_ACTION_PAUSE_DOWNLOAD -> { VideoDownloadManager.downloadEvent.invoke( Pair(click.data.id, VideoDownloadManager.DownloadActionType.Pause) ) } + DOWNLOAD_ACTION_RESUME_DOWNLOAD -> { activity?.let { ctx -> if (VideoDownloadManager.downloadStatus.containsKey(id) && VideoDownloadManager.downloadStatus[id] == VideoDownloadManager.DownloadType.IsPaused) { @@ -79,6 +88,7 @@ object DownloadButtonSetup { } } } + DOWNLOAD_ACTION_LONG_CLICK -> { activity?.let { act -> val length = @@ -88,12 +98,15 @@ object DownloadButtonSetup { )?.fileLength ?: 0 if (length > 0) { - showToast(R.string.delete, Toast.LENGTH_LONG) - } else { - showToast(R.string.download, Toast.LENGTH_LONG) + showSnackbar( + act, + R.string.offline_file, + Snackbar.LENGTH_LONG + ) } } } + DOWNLOAD_ACTION_PLAY_FILE -> { activity?.let { act -> val info = @@ -119,7 +132,7 @@ object DownloadButtonSetup { id = click.data.id, parentId = click.data.parentId, - name = act.getString(R.string.downloaded_file), //click.data.name ?: keyInfo.displayName + name = act.getString(R.string.downloaded_file), // click.data.name ?: keyInfo.displayName season = click.data.season, episode = click.data.episode, headerName = parent.name, @@ -132,7 +145,7 @@ object DownloadButtonSetup { ) ) ) - //R.id.global_to_navigation_player, PlayerFragment.newInstance( + // R.id.global_to_navigation_player, PlayerFragment.newInstance( // UriData( // info.path.toString(), // keyInfo.basePath, @@ -145,7 +158,7 @@ object DownloadButtonSetup { // click.data.season // ), // getViewPos(click.data.id)?.position ?: 0 - //) + // ) ) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt index 03db948c..09c48a04 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildFragment.kt @@ -1,29 +1,33 @@ package com.lagradost.cloudstream3.ui.download import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding +import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.isLayout -import com.lagradost.cloudstream3.utils.Coroutines.main -import com.lagradost.cloudstream3.utils.DataStore.getKey -import com.lagradost.cloudstream3.utils.DataStore.getKeys +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV -import com.lagradost.cloudstream3.utils.VideoDownloadHelper -import com.lagradost.cloudstream3.utils.VideoDownloadManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext class DownloadChildFragment : Fragment() { + private lateinit var downloadsViewModel: DownloadViewModel + private var binding: FragmentChildDownloadsBinding? = null + companion object { fun newInstance(headerName: String, folder: String): Bundle { return Bundle().apply { @@ -34,61 +38,54 @@ class DownloadChildFragment : Fragment() { } override fun onDestroyView() { - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } - downloadDeleteEventListener = null + detachBackPressedCallback() binding = null super.onDestroyView() } - private var binding: FragmentChildDownloadsBinding? = null - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + downloadsViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false) binding = localBinding return localBinding.root } - private fun updateList(folder: String) = main { - context?.let { ctx -> - val data = withContext(Dispatchers.IO) { ctx.getKeys(folder) } - val eps = withContext(Dispatchers.IO) { - data.mapNotNull { key -> - context?.getKey(key) - }.mapNotNull { - val info = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(ctx, it.id) - ?: return@mapNotNull null - VisualDownloadChildCached( - currentBytes = info.fileLength, - totalBytes = info.totalBytes, - data = it, - ) - } - }.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 } - if (eps.isEmpty()) { - activity?.onBackPressedDispatcher?.onBackPressed() - return@main - } - - (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(eps) - } - } - - private var downloadDeleteEventListener: ((Int) -> Unit)? = null - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + /** + * We never want to retain multi-delete state + * when navigating to downloads. Setting this state + * immediately can sometimes result in the observer + * not being notified in time to update the UI. + * + * By posting to the main looper, we ensure that this + * operation is executed after the view has been fully created + * and all initializations are completed, allowing the + * observer to properly receive and handle the state change. + */ + Handler(Looper.getMainLooper()).post { + downloadsViewModel.setIsMultiDeleteState(false) + } + + /** + * We have to make sure selected items are + * cleared here as well so we don't run in an + * inconsistent state where selected items do + * not match the multi delete state we are in. + */ + downloadsViewModel.clearSelectedItems() + val folder = arguments?.getString("folder") val name = arguments?.getString("name") if (folder == null) { - activity?.onBackPressedDispatcher?.onBackPressed() // TODO FIX + activity?.onBackPressedDispatcher?.onBackPressed() return } - fixPaddingStatusbar(binding?.downloadChildRoot) binding?.downloadChildToolbar?.apply { title = name @@ -101,13 +98,55 @@ class DownloadChildFragment : Fragment() { setAppBarNoScrollFlagsOnTV() } + binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV() + + observe(downloadsViewModel.childCards) { + if (it.isEmpty()) { + activity?.onBackPressedDispatcher?.onBackPressed() + return@observe + } + + (binding?.downloadChildList?.adapter as? DownloadAdapter)?.submitList(it) + } + observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState -> + val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter + adapter?.setIsMultiDeleteState(isMultiDeleteState) + binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState + if (!isMultiDeleteState) { + detachBackPressedCallback() + downloadsViewModel.clearSelectedItems() + binding?.downloadChildToolbar?.isVisible = true + } + } + observe(downloadsViewModel.selectedBytes) { + updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it) + } + observe(downloadsViewModel.selectedItemIds) { + handleSelectedChange(it) + updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L) + + binding?.btnDelete?.isVisible = it.isNotEmpty() + binding?.selectItemsText?.isVisible = it.isEmpty() + + val allSelected = downloadsViewModel.isAllSelected() + if (allSelected) { + binding?.btnToggleAll?.setText(R.string.deselect_all) + } else binding?.btnToggleAll?.setText(R.string.select_all) + } + val adapter = DownloadAdapter( {}, - { downloadClickEvent -> - handleDownloadClick(downloadClickEvent) - if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { - setUpDownloadDeleteListener(folder) - } + { click -> + if (click.action == DOWNLOAD_ACTION_DELETE_FILE) { + context?.let { ctx -> + downloadsViewModel.handleSingleDelete(ctx, click.data.id) + } + } else handleDownloadClick(click) + }, + { itemId, isChecked -> + if (isChecked) { + downloadsViewModel.addSelected(itemId) + } else downloadsViewModel.removeSelected(itemId) } ) @@ -122,18 +161,47 @@ class DownloadChildFragment : Fragment() { ) } - updateList(folder) + context?.let { downloadsViewModel.updateChildList(it, folder) } + fixPaddingStatusbar(binding?.downloadChildRoot) } - private fun setUpDownloadDeleteListener(folder: String) { - downloadDeleteEventListener = { id: Int -> - val list = (binding?.downloadChildList?.adapter as? DownloadAdapter)?.currentList - if (list != null) { - if (list.any { it.data.id == id }) { - updateList(folder) + private fun handleSelectedChange(selected: MutableSet) { + if (selected.isNotEmpty()) { + binding?.downloadDeleteAppbar?.isVisible = true + binding?.downloadChildToolbar?.isVisible = false + activity?.attachBackPressedCallback { + downloadsViewModel.setIsMultiDeleteState(false) + } + + binding?.btnDelete?.setOnClickListener { + context?.let { ctx -> + downloadsViewModel.handleMultiDelete(ctx) } } + + binding?.btnCancel?.setOnClickListener { + downloadsViewModel.setIsMultiDeleteState(false) + } + + binding?.btnToggleAll?.setOnClickListener { + val allSelected = downloadsViewModel.isAllSelected() + val adapter = binding?.downloadChildList?.adapter as? DownloadAdapter + if (allSelected) { + adapter?.notifySelectionStates() + downloadsViewModel.clearSelectedItems() + } else { + adapter?.notifyAllSelected() + downloadsViewModel.selectAllItems() + } + } + + downloadsViewModel.setIsMultiDeleteState(true) } - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + } + + private fun updateDeleteButton(count: Int, selectedBytes: Long) { + val formattedSize = formatShortFileSize(context, selectedBytes) + binding?.btnDelete?.text = + getString(R.string.delete_format).format(count, formattedSize) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 23d546e1..447b4f13 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -8,6 +8,8 @@ import android.content.Intent import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION import android.os.Build import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View @@ -17,7 +19,6 @@ import android.widget.TextView import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes -import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged @@ -27,7 +28,7 @@ import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding import com.lagradost.cloudstream3.databinding.StreamInputBinding -import com.lagradost.cloudstream3.isMovieType +import com.lagradost.cloudstream3.isEpisodeBased import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick @@ -40,20 +41,22 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE -import com.lagradost.cloudstream3.utils.DataStore +import com.lagradost.cloudstream3.utils.DataStore.getFolderName import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setAppBarNoScrollFlagsOnTV -import com.lagradost.cloudstream3.utils.VideoDownloadManager import java.net.URI const val DOWNLOAD_NAVIGATE_TO = "downloadpage" class DownloadFragment : Fragment() { private lateinit var downloadsViewModel: DownloadViewModel + private var binding: FragmentDownloadsBinding? = null private fun View.setLayoutWidth(weight: Long) { val param = LinearLayout.LayoutParams( @@ -65,14 +68,11 @@ class DownloadFragment : Fragment() { } override fun onDestroyView() { - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } - downloadDeleteEventListener = null + detachBackPressedCallback() binding = null super.onDestroyView() } - private var binding: FragmentDownloadsBinding? = null - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -84,12 +84,34 @@ class DownloadFragment : Fragment() { return localBinding.root } - private var downloadDeleteEventListener: ((Int) -> Unit)? = null - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) hideKeyboard() binding?.downloadStorageAppbar?.setAppBarNoScrollFlagsOnTV() + binding?.downloadDeleteAppbar?.setAppBarNoScrollFlagsOnTV() + + /** + * We never want to retain multi-delete state + * when navigating to downloads. Setting this state + * immediately can sometimes result in the observer + * not being notified in time to update the UI. + * + * By posting to the main looper, we ensure that this + * operation is executed after the view has been fully created + * and all initializations are completed, allowing the + * observer to properly receive and handle the state change. + */ + Handler(Looper.getMainLooper()).post { + downloadsViewModel.setIsMultiDeleteState(false) + } + + /** + * We have to make sure selected items are + * cleared here as well so we don't run in an + * inconsistent state where selected items do + * not match the multi delete state we are in. + */ + downloadsViewModel.clearSelectedItems() observe(downloadsViewModel.headerCards) { (binding?.downloadList?.adapter as? DownloadAdapter)?.submitList(it) @@ -97,25 +119,82 @@ class DownloadFragment : Fragment() { binding?.textNoDownloads?.isVisible = it.isEmpty() } observe(downloadsViewModel.availableBytes) { - updateStorageInfo(view.context, it, R.string.free_storage, binding?.downloadFreeTxt, binding?.downloadFree) + updateStorageInfo( + view.context, + it, + R.string.free_storage, + binding?.downloadFreeTxt, + binding?.downloadFree + ) } observe(downloadsViewModel.usedBytes) { - updateStorageInfo(view.context, it, R.string.used_storage, binding?.downloadUsedTxt, binding?.downloadUsed) - binding?.downloadStorageAppbar?.isVisible = it > 0 + updateStorageInfo( + view.context, + it, + R.string.used_storage, + binding?.downloadUsedTxt, + binding?.downloadUsed + ) + + // Prevent race condition and make sure + // we don't display it early + if ( + downloadsViewModel.isMultiDeleteState.value == null || + downloadsViewModel.isMultiDeleteState.value == false + ) binding?.downloadStorageAppbar?.isVisible = it > 0 } observe(downloadsViewModel.downloadBytes) { - updateStorageInfo(view.context, it, R.string.app_storage, binding?.downloadAppTxt, binding?.downloadApp) + updateStorageInfo( + view.context, + it, + R.string.app_storage, + binding?.downloadAppTxt, + binding?.downloadApp + ) + } + observe(downloadsViewModel.selectedBytes) { + updateDeleteButton(downloadsViewModel.selectedItemIds.value?.count() ?: 0, it) + } + observe(downloadsViewModel.isMultiDeleteState) { isMultiDeleteState -> + val adapter = binding?.downloadList?.adapter as? DownloadAdapter + adapter?.setIsMultiDeleteState(isMultiDeleteState) + binding?.downloadDeleteAppbar?.isVisible = isMultiDeleteState + if (!isMultiDeleteState) { + detachBackPressedCallback() + downloadsViewModel.clearSelectedItems() + // Prevent race condition and make sure + // we don't display it early + if (downloadsViewModel.usedBytes.value?.let { it > 0 } == true) { + binding?.downloadStorageAppbar?.isVisible = true + } + } + } + observe(downloadsViewModel.selectedItemIds) { + handleSelectedChange(it) + updateDeleteButton(it.count(), downloadsViewModel.selectedBytes.value ?: 0L) + + binding?.btnDelete?.isVisible = it.isNotEmpty() + binding?.selectItemsText?.isVisible = it.isEmpty() + + val allSelected = downloadsViewModel.isAllSelected() + if (allSelected) { + binding?.btnToggleAll?.setText(R.string.deselect_all) + } else binding?.btnToggleAll?.setText(R.string.select_all) } val adapter = DownloadAdapter( + { click -> handleItemClick(click) }, { click -> - handleItemClick(click) + if (click.action == DOWNLOAD_ACTION_DELETE_FILE) { + context?.let { ctx -> + downloadsViewModel.handleSingleDelete(ctx, click.data.id) + } + } else handleDownloadClick(click) }, - { downloadClickEvent -> - handleDownloadClick(downloadClickEvent) - if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { - setUpDownloadDeleteListener() - } + { itemId, isChecked -> + if (isChecked) { + downloadsViewModel.addSelected(itemId) + } else downloadsViewModel.removeSelected(itemId) } ) @@ -126,7 +205,6 @@ class DownloadFragment : Fragment() { setLinearListLayout( isHorizontal = false, nextRight = FOCUS_SELF, - nextUp = FOCUS_SELF, nextDown = FOCUS_SELF, ) } @@ -147,35 +225,68 @@ class DownloadFragment : Fragment() { handleScroll(scrollY - oldScrollY) } } - downloadsViewModel.updateList(requireContext()) + + context?.let { downloadsViewModel.updateHeaderList(it) } fixPaddingStatusbar(binding?.downloadRoot) } private fun handleItemClick(click: DownloadHeaderClickEvent) { when (click.action) { DOWNLOAD_ACTION_GO_TO_CHILD -> { - if (!click.data.type.isMovieType()) { - val folder = DataStore.getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString()) + if (click.data.type.isEpisodeBased()) { + val folder = + getFolderName(DOWNLOAD_EPISODE_CACHE, click.data.id.toString()) activity?.navigate( R.id.action_navigation_downloads_to_navigation_download_child, DownloadChildFragment.newInstance(click.data.name, folder) ) } } + DOWNLOAD_ACTION_LOAD_RESULT -> { - (activity as AppCompatActivity?)?.loadResult(click.data.url, click.data.apiName) + activity?.loadResult(click.data.url, click.data.apiName) } } } - private fun setUpDownloadDeleteListener() { - downloadDeleteEventListener = { id -> - val list = (binding?.downloadList?.adapter as? DownloadAdapter)?.currentList - if (list?.any { it.data.id == id } == true) { - context?.let { downloadsViewModel.updateList(it) } + private fun handleSelectedChange(selected: MutableSet) { + if (selected.isNotEmpty()) { + binding?.downloadDeleteAppbar?.isVisible = true + binding?.downloadStorageAppbar?.isVisible = false + activity?.attachBackPressedCallback { + downloadsViewModel.setIsMultiDeleteState(false) } + + binding?.btnDelete?.setOnClickListener { + context?.let { ctx -> + downloadsViewModel.handleMultiDelete(ctx) + } + } + + binding?.btnCancel?.setOnClickListener { + downloadsViewModel.setIsMultiDeleteState(false) + } + + binding?.btnToggleAll?.setOnClickListener { + val allSelected = downloadsViewModel.isAllSelected() + val adapter = binding?.downloadList?.adapter as? DownloadAdapter + if (allSelected) { + adapter?.notifySelectionStates() + downloadsViewModel.clearSelectedItems() + } else { + adapter?.notifyAllSelected() + downloadsViewModel.selectAllItems() + } + } + + downloadsViewModel.setIsMultiDeleteState(true) } - downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } + } + + private fun updateDeleteButton(count: Int, selectedBytes: Long) { + val formattedSize = formatShortFileSize(context, selectedBytes) + binding?.btnDelete?.text = + getString(R.string.delete_format).format(count, formattedSize) } private fun updateStorageInfo( @@ -185,7 +296,10 @@ class DownloadFragment : Fragment() { textView: TextView?, view: View? ) { - textView?.text = getString(R.string.storage_size_format).format(getString(stringRes), formatShortFileSize(context, bytes)) + textView?.text = getString(R.string.storage_size_format).format( + getString(stringRes), + formatShortFileSize(context, bytes) + ) view?.setLayoutWidth(bytes) } @@ -218,7 +332,9 @@ class DownloadFragment : Fragment() { if (!preventAutoSwitching) activateSwitchOnHls(text?.toString(), binding) } - (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt(0)?.text?.toString()?.let { copy -> + (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip?.getItemAt( + 0 + )?.text?.toString()?.let { copy -> val fixedText = copy.trim() binding.streamUrl.setText(fixedText) activateSwitchOnHls(fixedText, binding) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt index 83d96592..137f1355 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt @@ -1,122 +1,439 @@ package com.lagradost.cloudstream3.ui.download import android.content.Context +import android.content.DialogInterface import android.os.Environment import android.os.StatFs +import androidx.appcompat.app.AlertDialog import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.lagradost.cloudstream3.isMovieType +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.isEpisodeBased import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull +import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DataStore.getFolderName import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.VideoDownloadHelper +import com.lagradost.cloudstream3.utils.VideoDownloadManager.deleteFilesAndUpdateSettings import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadFileInfoAndUpdateSettings import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class DownloadViewModel : ViewModel() { - private val _headerCards = - MutableLiveData>().apply { listOf() } - val headerCards: LiveData> = _headerCards + + private val _headerCards = MutableLiveData>() + val headerCards: LiveData> = _headerCards + + private val _childCards = MutableLiveData>() + val childCards: LiveData> = _childCards private val _usedBytes = MutableLiveData() - private val _availableBytes = MutableLiveData() - private val _downloadBytes = MutableLiveData() - val usedBytes: LiveData = _usedBytes + + private val _availableBytes = MutableLiveData() val availableBytes: LiveData = _availableBytes + + private val _downloadBytes = MutableLiveData() val downloadBytes: LiveData = _downloadBytes - private var previousVisual: List? = null + private val _selectedBytes = MutableLiveData(0) + val selectedBytes: LiveData = _selectedBytes - fun updateList(context: Context) = viewModelScope.launchSafe { - val children = withContext(Dispatchers.IO) { - context.getKeys(DOWNLOAD_EPISODE_CACHE) + private val _isMultiDeleteState = MutableLiveData(false) + val isMultiDeleteState: LiveData = _isMultiDeleteState + + private val _selectedItemIds = MutableLiveData>(mutableSetOf()) + val selectedItemIds: LiveData> = _selectedItemIds + + private var previousVisual: List? = null + + fun setIsMultiDeleteState(value: Boolean) { + _isMultiDeleteState.postValue(value) + } + + fun addSelected(itemId: Int) { + updateSelectedItems { it.add(itemId) } + } + + fun removeSelected(itemId: Int) { + updateSelectedItems { it.remove(itemId) } + } + + fun selectAllItems() { + val items = headerCards.value.orEmpty() + childCards.value.orEmpty() + updateSelectedItems { it.addAll(items.map { item -> item.data.id }) } + } + + fun clearSelectedItems() { + // We need this to be done immediately + // so we can't use postValue + _selectedItemIds.value = mutableSetOf() + updateSelectedItems { it.clear() } + } + + fun isAllSelected(): Boolean { + val currentSelected = selectedItemIds.value ?: return false + val items = headerCards.value.orEmpty() + childCards.value.orEmpty() + return items.count() == currentSelected.count() && items.all { it.data.id in currentSelected } + } + + private fun updateSelectedItems(action: (MutableSet) -> Unit) { + val currentSelected = selectedItemIds.value ?: mutableSetOf() + action(currentSelected) + _selectedItemIds.postValue(currentSelected) + updateSelectedBytes() + updateSelectedCards() + } + + private fun updateSelectedBytes() = viewModelScope.launchSafe { + val selectedItemsList = getSelectedItemsData() ?: return@launchSafe + val totalSelectedBytes = selectedItemsList.sumOf { it.totalBytes } + _selectedBytes.postValue(totalSelectedBytes) + } + + private fun updateSelectedCards() = viewModelScope.launchSafe { + val currentSelected = selectedItemIds.value ?: return@launchSafe + + headerCards.value?.let { headers -> + headers.forEach { header -> + header.isSelected = header.data.id in currentSelected + } + _headerCards.postValue(headers) + } + + childCards.value?.let { children -> + children.forEach { child -> + child.isSelected = child.data.id in currentSelected + } + _childCards.postValue(children) + } + } + + fun updateHeaderList(context: Context) = viewModelScope.launchSafe { + val visual = withContext(Dispatchers.IO) { + val children = context.getKeys(DOWNLOAD_EPISODE_CACHE) .mapNotNull { context.getKey(it) } .distinctBy { it.id } // Remove duplicates + + val (totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) = + calculateDownloadStats(context, children) + + val cached = context.getKeys(DOWNLOAD_HEADER_CACHE) + .mapNotNull { context.getKey(it) } + + createVisualDownloadList( + context, cached, totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads + ) } - // parentId : bytes - val totalBytesUsedByChild = HashMap() - // parentId : bytes - val currentBytesUsedByChild = HashMap() - // parentId : downloadsCount - val totalDownloads = HashMap() - - // Gets all children downloads - withContext(Dispatchers.IO) { - children.forEach { c -> - val childFile = getDownloadFileInfoAndUpdateSettings(context, c.id) ?: return@forEach - - if (childFile.fileLength <= 1) return@forEach - val len = childFile.totalBytes - val flen = childFile.fileLength - - totalBytesUsedByChild[c.parentId] = totalBytesUsedByChild[c.parentId]?.plus(len) ?: len - currentBytesUsedByChild[c.parentId] = currentBytesUsedByChild[c.parentId]?.plus(flen) ?: flen - totalDownloads[c.parentId] = totalDownloads[c.parentId]?.plus(1) ?: 1 - } - } - - val cached = withContext(Dispatchers.IO) { // Won't fetch useless keys - totalDownloads.entries.filter { it.value > 0 }.mapNotNull { - context.getKey( - DOWNLOAD_HEADER_CACHE, - it.key.toString() - ) - } - } - - val visual = withContext(Dispatchers.IO) { - cached.mapNotNull { - val downloads = totalDownloads[it.id] ?: 0 - val bytes = totalBytesUsedByChild[it.id] ?: 0 - val currentBytes = currentBytesUsedByChild[it.id] ?: 0 - if (bytes <= 0 || downloads <= 0) return@mapNotNull null - val movieEpisode = - if (!it.type.isMovieType()) null - else context.getKey( - DOWNLOAD_EPISODE_CACHE, - getFolderName(it.id.toString(), it.id.toString()) - ) - VisualDownloadHeaderCached( - currentBytes = currentBytes, - totalBytes = bytes, - data = it, - child = movieEpisode, - currentOngoingDownloads = 0, - totalDownloads = downloads, - ) - }.sortedBy { - (it.child?.episode ?: 0) + (it.child?.season?.times(10000) ?: 0) - } // Episode sorting by episode, lowest to highest - } - - // Only update list if different from the previous one to prevent duplicate initialization if (visual != previousVisual) { previousVisual = visual - - try { - val stat = StatFs(Environment.getExternalStorageDirectory().path) - val localBytesAvailable = stat.availableBytes - val localTotalBytes = stat.blockSizeLong * stat.blockCountLong - val localDownloadedBytes = visual.sumOf { it.totalBytes } - - _usedBytes.postValue(localTotalBytes - localBytesAvailable - localDownloadedBytes) - _availableBytes.postValue(localBytesAvailable) - _downloadBytes.postValue(localDownloadedBytes) - } catch (t: Throwable) { - _downloadBytes.postValue(0) - logError(t) - } - + updateStorageStats(visual) _headerCards.postValue(visual) } } + + private fun calculateDownloadStats( + context: Context, + children: List + ): Triple, Map, Map> { + // parentId : bytes + val totalBytesUsedByChild = mutableMapOf() + // parentId : bytes + val currentBytesUsedByChild = mutableMapOf() + // parentId : downloadsCount + val totalDownloads = mutableMapOf() + + children.forEach { child -> + val childFile = getDownloadFileInfoAndUpdateSettings(context, child.id) ?: return@forEach + if (childFile.fileLength <= 1) return@forEach + + val len = childFile.totalBytes + val flen = childFile.fileLength + + totalBytesUsedByChild.merge(child.parentId, len, Long::plus) + currentBytesUsedByChild.merge(child.parentId, flen, Long::plus) + totalDownloads.merge(child.parentId, 1, Int::plus) + } + return Triple(totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) + } + + private fun createVisualDownloadList( + context: Context, + cached: List, + totalBytesUsedByChild: Map, + currentBytesUsedByChild: Map, + totalDownloads: Map + ): List { + return cached.mapNotNull { + val downloads = totalDownloads[it.id] ?: 0 + val bytes = totalBytesUsedByChild[it.id] ?: 0 + val currentBytes = currentBytesUsedByChild[it.id] ?: 0 + if (bytes <= 0 || downloads <= 0) return@mapNotNull null + + val isSelected = selectedItemIds.value?.contains(it.id) ?: false + val movieEpisode = if (it.type.isEpisodeBased()) null else context.getKey( + DOWNLOAD_EPISODE_CACHE, + getFolderName(it.id.toString(), it.id.toString()) + ) + + VisualDownloadCached.Header( + currentBytes = currentBytes, + totalBytes = bytes, + data = it, + child = movieEpisode, + currentOngoingDownloads = 0, + totalDownloads = downloads, + isSelected = isSelected, + ) + // Prevent order being almost completely random, + // making things difficult to find. + }.sortedWith(compareBy { + // Sort by isEpisodeBased() ascending. We put those that + // are episode based at the bottom for UI purposes and to + // make it easier to find by grouping them together. + it.data.type.isEpisodeBased() + }.thenBy { + // Then we sort alphabetically by name (case-insensitive). + // Again, we do this to make things easier to find. + it.data.name.lowercase() + }) + } + + fun updateChildList(context: Context, folder: String) = viewModelScope.launchSafe { + val visual = withContext(Dispatchers.IO) { + context.getKeys(folder).mapNotNull { key -> + context.getKey(key) + }.mapNotNull { + val isSelected = selectedItemIds.value?.contains(it.id) ?: false + val info = getDownloadFileInfoAndUpdateSettings(context, it.id) ?: return@mapNotNull null + VisualDownloadCached.Child( + currentBytes = info.fileLength, + totalBytes = info.totalBytes, + isSelected = isSelected, + data = it, + ) + } + }.sortedWith(compareBy( + // Sort by season first, and then by episode number, + // to ensure sorting is consistent. + { it.data.season ?: 0 }, + { it.data.episode } + )) + + if (previousVisual != visual) { + previousVisual = visual + _childCards.postValue(visual) + } + } + + private fun removeItems(idsToRemove: Set) = viewModelScope.launchSafe { + val updatedHeaders = headerCards.value.orEmpty().filter { it.data.id !in idsToRemove } + val updatedChildren = childCards.value.orEmpty().filter { it.data.id !in idsToRemove } + _headerCards.postValue(updatedHeaders) + _childCards.postValue(updatedChildren) + } + + private fun updateStorageStats(visual: List) { + try { + val stat = StatFs(Environment.getExternalStorageDirectory().path) + val localBytesAvailable = stat.availableBytes + val localTotalBytes = stat.blockSizeLong * stat.blockCountLong + val localDownloadedBytes = visual.sumOf { it.totalBytes } + val localUsedBytes = localTotalBytes - localBytesAvailable + _usedBytes.postValue(localUsedBytes) + _availableBytes.postValue(localBytesAvailable) + _downloadBytes.postValue(localDownloadedBytes) + } catch (t: Throwable) { + _downloadBytes.postValue(0) + logError(t) + } + } + + fun handleMultiDelete(context: Context) = viewModelScope.launchSafe { + val selectedItemsList = getSelectedItemsData().orEmpty() + val deleteData = processSelectedItems(context, selectedItemsList) + val message = buildDeleteMessage(context, deleteData) + showDeleteConfirmationDialog(context, message, deleteData.ids, deleteData.parentIds) + } + + fun handleSingleDelete( + context: Context, + itemId: Int + ) = viewModelScope.launchSafe { + val itemData = getItemDataFromId(itemId) + val deleteData = processSelectedItems(context, itemData) + val message = buildDeleteMessage(context, deleteData) + showDeleteConfirmationDialog(context, message, deleteData.ids, deleteData.parentIds) + } + + private fun processSelectedItems( + context: Context, + selectedItemsList: List + ): DeleteData { + val names = mutableListOf() + val seriesNames = mutableListOf() + + val ids = mutableSetOf() + val parentIds = mutableSetOf() + + var parentName: String? = null + + selectedItemsList.forEach { item -> + when (item) { + is VisualDownloadCached.Header -> { + if (item.data.type.isEpisodeBased()) { + val episodes = context.getKeys(DOWNLOAD_EPISODE_CACHE) + .mapNotNull { + context.getKey( + it + ) + } + .filter { it.parentId == item.data.id } + .map { it.id } + ids.addAll(episodes) + parentIds.add(item.data.id) + + val episodeInfo = "${item.data.name} (${item.totalDownloads} ${ + context.resources.getQuantityString( + R.plurals.episodes, + item.totalDownloads + ).lowercase() + })" + seriesNames.add(episodeInfo) + } else { + ids.add(item.data.id) + names.add(item.data.name) + } + } + + is VisualDownloadCached.Child -> { + ids.add(item.data.id) + val parent = context.getKey( + DOWNLOAD_HEADER_CACHE, + item.data.parentId.toString() + ) + parentName = parent?.name + names.add( + context.getNameFull( + item.data.name, + item.data.episode, + item.data.season + ) + ) + } + } + } + + return DeleteData(ids, parentIds, seriesNames, names, parentName) + } + + private fun buildDeleteMessage( + context: Context, + data: DeleteData + ): String { + val formattedNames = data.names.sortedBy { it.lowercase() } + .joinToString(separator = "\n") { "• $it" } + val formattedSeriesNames = data.seriesNames.sortedBy { it.lowercase() } + .joinToString(separator = "\n") { "• $it" } + + return when { + data.ids.count() == 1 -> { + context.getString(R.string.delete_message).format( + data.names.firstOrNull() + ) + } + + data.seriesNames.isNotEmpty() && data.names.isEmpty() -> { + context.getString(R.string.delete_message_series_only).format(formattedSeriesNames) + } + + data.parentName != null && data.names.isNotEmpty() -> { + context.getString(R.string.delete_message_series_episodes) + .format(data.parentName, formattedNames) + } + + data.seriesNames.isNotEmpty() -> { + val seriesSection = context.getString(R.string.delete_message_series_section) + .format(formattedSeriesNames) + context.getString(R.string.delete_message_multiple) + .format(formattedNames) + "\n\n" + seriesSection + } + + else -> context.getString(R.string.delete_message_multiple).format(formattedNames) + } + } + + private fun showDeleteConfirmationDialog( + context: Context, + message: String, + ids: Set, + parentIds: Set + ) { + val builder = AlertDialog.Builder(context) + val dialogClickListener = + DialogInterface.OnClickListener { _, which -> + when (which) { + DialogInterface.BUTTON_POSITIVE -> { + viewModelScope.launchSafe { + setIsMultiDeleteState(false) + deleteFilesAndUpdateSettings(context, ids, this) { successfulIds -> + // We always remove parent because if we are deleting from here + // and we have it as non-empty, it was triggered on + // parent header card + removeItems(successfulIds + parentIds) + } + } + } + + DialogInterface.BUTTON_NEGATIVE -> { + // Do nothing on cancel + } + } + } + + try { + val title = if (ids.count() == 1) { + R.string.delete_file + } else R.string.delete_files + builder.setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.delete, dialogClickListener) + .setNegativeButton(R.string.cancel, dialogClickListener) + .show().setDefaultFocus() + } catch (e: Exception) { + logError(e) + } + } + + private fun getSelectedItemsData(): List? { + val headers = headerCards.value.orEmpty() + val children = childCards.value.orEmpty() + + return selectedItemIds.value?.mapNotNull { id -> + headers.find { it.data.id == id } ?: children.find { it.data.id == id } + } + } + + private fun getItemDataFromId(itemId: Int): List { + val headers = headerCards.value.orEmpty() + val children = childCards.value.orEmpty() + + return (headers + children).filter { it.data.id == itemId } + } + + private data class DeleteData( + val ids: Set, + val parentIds: Set, + val seriesNames: List, + val names: List, + val parentName: String? + ) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index 45132131..908e3a80 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -93,7 +93,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : abstract fun setStatus(status: VideoDownloadManager.DownloadType?) - fun getStatus(id:Int, downloadedBytes: Long, totalBytes: Long): DownloadStatusTell { + fun getStatus(id: Int, downloadedBytes: Long, totalBytes: Long): DownloadStatusTell { // some extra padding for just in case return VideoDownloadManager.downloadStatus[id] ?: if (downloadedBytes > 1024L && downloadedBytes + 1024L >= totalBytes) { @@ -101,7 +101,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } else DownloadStatusTell.IsPaused } - fun applyMetaData(id:Int, downloadedBytes: Long, totalBytes: Long) { + fun applyMetaData(id: Int, downloadedBytes: Long, totalBytes: Long) { val status = getStatus(id, downloadedBytes, totalBytes) currentMetaData.apply { @@ -140,7 +140,8 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) : } else { if (doSetProgress) { progressText?.apply { - val currentFormattedSizeString = formatShortFileSize(context, downloadedBytes) + val currentFormattedSizeString = + formatShortFileSize(context, downloadedBytes) val totalFormattedSizeString = formatShortFileSize(context, totalBytes) text = // if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index abc159d0..29c2daa2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -58,7 +58,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } private var progressBarBackground: View - private var statusView: ImageView + var statusView: ImageView open fun onInflate() {} @@ -248,7 +248,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } */ @MainThread - private fun setStatusInternal(status : DownloadStatusTell?) { + private fun setStatusInternal(status: DownloadStatusTell?) { val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { val animation = AnimationUtils.loadAnimation(context, waitingAnimation) @@ -286,7 +286,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : if (Looper.myLooper() == Looper.getMainLooper()) { try { setStatusInternal(status) - } catch (t : Throwable) { + } catch (t: Throwable) { logError(t) // Just in case setStatusInternal throws because thread progressBarBackground.post { setStatusInternal(status) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt index a8a3106a..a0668abc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadFileGenerator.kt @@ -4,7 +4,9 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.VideoDownloadManager +import com.lagradost.cloudstream3.utils.SubtitleUtils.cleanDisplayName +import com.lagradost.cloudstream3.utils.SubtitleUtils.isMatchingSubtitle +import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFolder import kotlin.math.max import kotlin.math.min @@ -49,10 +51,6 @@ class DownloadFileGenerator( return null } - private fun cleanDisplayName(name: String): String { - return name.substringBeforeLast('.').trim() - } - override suspend fun generateLinks( clearCache: Boolean, type: LoadType, @@ -69,28 +67,9 @@ class DownloadFileGenerator( val cleanDisplay = cleanDisplayName(display) - VideoDownloadManager.getFolder(ctx, relative, meta.basePath) - ?.forEach { (name, uri) -> - // only these files are allowed, so no videos as subtitles - if (listOf( - ".vtt", - ".srt", - ".txt", - ".ass", - ".ttml", - ".sbv", - ".dfxp" - ).none { name.contains(it, true) } - ) return@forEach - - // cant have the exact same file as a subtitle - if (name.equals(display, true)) return@forEach - + getFolder(ctx, relative, meta.basePath)?.forEach { (name, uri) -> + if (isMatchingSubtitle(name, display, cleanDisplay)) { val cleanName = cleanDisplayName(name) - - // we only want files with the approx same name - if (!cleanName.startsWith(cleanDisplay, true)) return@forEach - val realName = cleanName.removePrefix(cleanDisplay) subtitleCallback( @@ -104,6 +83,7 @@ class DownloadFileGenerator( ) ) } + } return true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index 4279b542..c38160c2 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -4,13 +4,13 @@ import android.content.Intent import android.os.Bundle import android.util.Log import android.view.KeyEvent -import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback class DownloadedPlayerActivity : AppCompatActivity() { private val dTAG = "DownloadedPlayerAct" @@ -70,14 +70,7 @@ class DownloadedPlayerActivity : AppCompatActivity() { return } - onBackPressedDispatcher.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - finish() - } - } - ) + attachBackPressedCallback { finish() } } override fun onResume() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 135dc530..2ab60c2f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -7,7 +7,6 @@ import android.os.Bundle import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import androidx.activity.OnBackPressedCallback import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.CommonActivity.screenHeight @@ -17,6 +16,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.PlayerEventSource import com.lagradost.cloudstream3.ui.player.SubtitleData +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback +import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback open class ResultTrailerPlayer : ResultFragmentPhone() { @@ -156,7 +157,9 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { uiReset() if (isFullScreenPlayer) { - attachBackPressedCallback() + activity?.attachBackPressedCallback { + updateFullscreen(false) + } } else detachBackPressedCallback() } @@ -175,27 +178,4 @@ open class ResultTrailerPlayer : ResultFragmentPhone() { fixPlayerSize() } } - - private var backPressedCallback: OnBackPressedCallback? = null - - private fun attachBackPressedCallback() { - if (backPressedCallback == null) { - backPressedCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - updateFullscreen(false) - } - } - } - - backPressedCallback?.isEnabled = true - - activity?.onBackPressedDispatcher?.addCallback( - activity ?: return, - backPressedCallback ?: return - ) - } - - private fun detachBackPressedCallback() { - backPressedCallback?.isEnabled = false - } } \ No newline at end of file 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 b13de062..8d65acf7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppContextUtils.kt @@ -677,9 +677,15 @@ object AppContextUtils { } fun Context.isNetworkAvailable(): Boolean { - val manager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val activeNetworkInfo = manager.activeNetworkInfo - return activeNetworkInfo != null && activeNetworkInfo.isConnected || manager.allNetworkInfo?.any { it.isConnected } ?: false + val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val network = connectivityManager.activeNetwork ?: return false + val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } else { + @Suppress("DEPRECATION") + connectivityManager.activeNetworkInfo?.isConnected == true + } } fun splitQuery(url: URL): Map { @@ -1018,4 +1024,4 @@ object AppContextUtils { } return currentAudioFocusRequest } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt new file mode 100644 index 00000000..1326ab27 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackPressedCallbackHelper.kt @@ -0,0 +1,30 @@ +package com.lagradost.cloudstream3.utils + +import androidx.activity.ComponentActivity +import androidx.activity.OnBackPressedCallback + +object BackPressedCallbackHelper { + private var backPressedCallback: OnBackPressedCallback? = null + + fun ComponentActivity.attachBackPressedCallback(callback: () -> Unit) { + if (backPressedCallback == null) { + backPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + callback.invoke() + } + } + } + + backPressedCallback?.isEnabled = true + + onBackPressedDispatcher.addCallback( + this@attachBackPressedCallback, + backPressedCallback ?: return + ) + } + + fun detachBackPressedCallback() { + backPressedCallback?.isEnabled = false + backPressedCallback = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt new file mode 100644 index 00000000..e6a77795 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SnackbarHelper.kt @@ -0,0 +1,84 @@ +package com.lagradost.cloudstream3.utils + +import android.app.Activity +import android.view.View +import androidx.annotation.MainThread +import androidx.annotation.StringRes +import com.google.android.material.snackbar.Snackbar +import com.lagradost.api.Log +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.result.UiText +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute + +object SnackbarHelper { + + private const val TAG = "COMPACT" + private var currentSnackbar: Snackbar? = null + + @MainThread + fun showSnackbar( + act: Activity?, + message: UiText, + duration: Int = Snackbar.LENGTH_SHORT, + actionText: UiText? = null, + actionCallback: (() -> Unit)? = null + ) { + if (act == null) return + showSnackbar(act, message.asString(act), duration, + actionText?.asString(act), actionCallback) + } + + @MainThread + fun showSnackbar( + act: Activity?, + @StringRes message: Int, + duration: Int = Snackbar.LENGTH_SHORT, + @StringRes actionText: Int? = null, + actionCallback: (() -> Unit)? = null + ) { + if (act == null) return + showSnackbar(act, act.getString(message), duration, + actionText?.let { act.getString(it) }, actionCallback) + } + + @MainThread + fun showSnackbar( + act: Activity?, + message: String?, + duration: Int = Snackbar.LENGTH_SHORT, + actionText: String? = null, + actionCallback: (() -> Unit)? = null + ) { + if (act == null || message == null) { + Log.w(TAG, "Invalid showSnackbar: act = $act, message = $message") + return + } + Log.i(TAG, "showSnackbar: $message") + + try { + currentSnackbar?.dismiss() + } catch (e: Exception) { + logError(e) + } + + try { + val parentView = act.findViewById(android.R.id.content) + val snackbar = Snackbar.make(parentView, message, duration) + + actionCallback?.let { + snackbar.setAction(actionText) { actionCallback.invoke() } + } + + snackbar.show() + currentSnackbar = snackbar + + snackbar.setBackgroundTint(act.colorFromAttribute(R.attr.primaryBlackBackground)) + snackbar.setTextColor(act.colorFromAttribute(R.attr.textColor)) + snackbar.setActionTextColor(act.colorFromAttribute(R.attr.colorPrimary)) + + } catch (e: Exception) { + logError(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt new file mode 100644 index 00000000..93a53395 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SubtitleUtils.kt @@ -0,0 +1,56 @@ +package com.lagradost.cloudstream3.utils + +import android.content.Context +import com.lagradost.api.Log +import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFolder +import com.lagradost.safefile.SafeFile + +object SubtitleUtils { + + // Only these files are allowed, so no videos as subtitles + private val allowedExtensions = listOf( + ".vtt", ".srt", ".txt", ".ass", + ".ttml", ".sbv", ".dfxp" + ) + + fun deleteMatchingSubtitles(context: Context, info: VideoDownloadManager.DownloadedFileInfo) { + val relative = info.relativePath + val display = info.displayName + val cleanDisplay = cleanDisplayName(display) + + getFolder(context, relative, info.basePath)?.forEach { (name, uri) -> + if (isMatchingSubtitle(name, display, cleanDisplay)) { + val subtitleFile = SafeFile.fromUri(context, uri) + if (subtitleFile == null || !subtitleFile.delete()) { + Log.e("SubtitleDeletion", "Failed to delete subtitle file: ${subtitleFile?.name()}") + } + } + } + } + + /** + * @param name the file name of the subtitle + * @param display the file name of the video + * @param cleanDisplay the cleanDisplayName of the video file name + */ + fun isMatchingSubtitle( + name: String, + display: String, + cleanDisplay: String + ): Boolean { + // Check if the file has a valid subtitle extension + val hasValidExtension = allowedExtensions.any { name.contains(it, ignoreCase = true) } + + // We can't have the exact same file as a subtitle + val isNotDisplayName = !name.equals(display, ignoreCase = true) + + // Check if the file name starts with a cleaned version of the display name + val startsWithCleanDisplay = cleanDisplayName(name).startsWith(cleanDisplay, ignoreCase = true) + + return hasValidExtension && isNotDisplayName && startsWithCleanDisplay + } + + fun cleanDisplayName(name: String): String { + return name.substringBeforeLast('.').trim() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index a3f6d789..2190e03f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -20,6 +20,7 @@ import androidx.work.WorkManager import com.bumptech.glide.Glide import com.bumptech.glide.load.model.GlideUrl import com.fasterxml.jackson.annotation.JsonProperty +import com.lagradost.api.Log import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey @@ -29,12 +30,14 @@ import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.removeKey +import com.lagradost.cloudstream3.utils.SubtitleUtils.deleteMatchingSubtitles import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.safefile.MediaFileContentType import com.lagradost.safefile.SafeFile @@ -42,6 +45,8 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -1733,7 +1738,37 @@ object VideoDownloadManager { } } - fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { + fun deleteFilesAndUpdateSettings( + context: Context, + ids: Set, + scope: CoroutineScope, + onComplete: (Set) -> Unit = {} + ) { + scope.launchSafe(Dispatchers.IO) { + val deleteJobs = ids.map { id -> + async { + id to deleteFileAndUpdateSettings(context, id) + } + } + val results = deleteJobs.awaitAll() + + val (successfulResults, failedResults) = results.partition { it.second } + val successfulIds = successfulResults.map { it.first }.toSet() + + if (failedResults.isNotEmpty()) { + failedResults.forEach { (id, _) -> + // TODO show a toast if some failed? + Log.e("FileDeletion", "Failed to delete file with ID: $id") + } + } else { + Log.i("FileDeletion", "All files deleted successfully") + } + + onComplete.invoke(successfulIds) + } + } + + private fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean { val success = deleteFile(context, id) if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) return success @@ -1759,11 +1794,17 @@ object VideoDownloadManager { private fun deleteFile(context: Context, id: Int): Boolean { val info = context.getKey(KEY_DOWNLOAD_INFO, id.toString()) ?: return false + val file = info.toFile(context) + downloadEvent.invoke(id to DownloadActionType.Stop) downloadProgressEvent.invoke(Triple(id, 0, 0)) downloadStatusEvent.invoke(id to DownloadType.IsStopped) downloadDeleteEvent.invoke(id) - return info.toFile(context)?.delete() ?: false + + val isFileDeleted = file?.delete() == true || file?.exists() == false + if (isFileDeleted) deleteMatchingSubtitles(context, info) + + return isFileDeleted } fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? { diff --git a/app/src/main/res/layout/download_child_episode.xml b/app/src/main/res/layout/download_child_episode.xml index 4974a027..e53e63d3 100644 --- a/app/src/main/res/layout/download_child_episode.xml +++ b/app/src/main/res/layout/download_child_episode.xml @@ -2,10 +2,8 @@ @@ -78,7 +73,6 @@ tools:text="128MB / 237MB" /> - + + \ No newline at end of file diff --git a/app/src/main/res/layout/download_header_episode.xml b/app/src/main/res/layout/download_header_episode.xml index a0b64ce3..957869d4 100644 --- a/app/src/main/res/layout/download_header_episode.xml +++ b/app/src/main/res/layout/download_header_episode.xml @@ -77,5 +77,16 @@ android:focusable="true" android:nextFocusLeft="@id/episode_holder" android:padding="10dp" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_child_downloads.xml b/app/src/main/res/layout/fragment_child_downloads.xml index 9afaea0b..64ed1d70 100644 --- a/app/src/main/res/layout/fragment_child_downloads.xml +++ b/app/src/main/res/layout/fragment_child_downloads.xml @@ -7,13 +7,69 @@ android:layout_height="match_parent" android:background="?attr/primaryGrayBackground" android:orientation="vertical" - tools:context=".ui.download.DownloadFragment"> + tools:context=".ui.download.DownloadChildFragment"> + + + + +