3
3
Fork 1
mirror of https://github.com/recloudstream/cloudstream.git synced 2024-08-15 01:53:11 +00:00

Merge branch 'master' into release-date-sort

This commit is contained in:
firelight 2024-07-29 00:42:02 +02:00 committed by GitHub
commit 5d02ac1c30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
160 changed files with 761 additions and 740 deletions
app
build.gradle.kts
src
androidTest/java/com/lagradost/cloudstream3
main
java/com/lagradost/cloudstream3
AcraApplication.ktCommonActivity.ktDownloaderTestImpl.ktMainActivity.ktNativeCrashHandler.kt
metaproviders
network
plugins
subtitles
syncproviders
ui
utils
widget
res/layout

View file

@ -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() ?: "")
@ -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

View file

@ -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
) { _, _ -> }
}
}

View file

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

View file

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

View file

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

View file

@ -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<NavigationRailView>(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)
}
}
@ -572,11 +572,40 @@ 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
}
}
}
}
//private var mCastSession: CastSession? = null
lateinit var mSessionManager: SessionManager
var mSessionManager: SessionManager? = null
private val mSessionManagerListener: SessionManagerListener<Session> by lazy { SessionManagerListenerImpl() }
private inner class SessionManagerListenerImpl : SessionManagerListener<Session> {
@ -616,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)
@ -633,7 +661,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
try {
if (isCastApiAvailable()) {
mSessionManager.removeSessionManagerListener(mSessionManagerListener)
mSessionManager?.removeSessionManagerListener(mSessionManagerListener)
//mCastSession = null
}
} catch (e: Exception) {
@ -737,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('/')
@ -1118,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)
@ -1420,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
)
}
@ -1436,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()
}
}

View file

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

View file

@ -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(

View file

@ -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) {
@ -295,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

View file

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

View file

@ -2,5 +2,4 @@ package com.lagradost.cloudstream3.plugins
@Suppress("unused")
@Target(AnnotationTarget.CLASS)
annotation class CloudstreamPlugin(
)
annotation class CloudstreamPlugin

View file

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

View file

@ -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<String, Plugin> =
@ -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<out Plugin?>
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) {

View file

@ -73,7 +73,7 @@ object RepositoryManager {
val PREBUILT_REPOSITORIES: Array<RepositoryData> 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 {

View file

@ -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<Result>()?.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<Result>()?.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()

View file

@ -59,7 +59,7 @@ class SubtitleResource {
return file
}
fun unzip(file: File): List<Pair<String, File>> {
private fun unzip(file: File): List<Pair<String, File>> {
val entries = mutableListOf<Pair<String, File>>()
ZipInputStream(file.inputStream()).use { zipInputStream ->

View file

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.subtitles
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.TvType
class AbstractSubtitleEntities {

View file

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

View file

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

View file

@ -64,7 +64,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"]!!
@ -88,7 +88,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
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,
@ -102,7 +102,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(),
@ -302,12 +302,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))
}
@ -497,7 +497,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
val data = postApi(q, true)
val d = parseJson<GetDataRoot>(data ?: return null)
val main = d.data?.Media
val main = d.data?.media
if (main?.mediaListEntry != null) {
return AniListTitleHolder(
title = main.title,
@ -537,7 +537,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(
@ -649,7 +649,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<Lists>? {
@ -661,7 +661,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)
}
@ -680,7 +680,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<SyncAPI.LibraryItem>()
}
@ -768,7 +768,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(
@ -791,7 +791,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
"""
val response = postApi(idQuery)
val listId =
tryParseJson<MediaListItemRoot>(response)?.data?.MediaList?.id ?: return false
tryParseJson<MediaListItemRoot>(response)?.data?.mediaList?.id ?: return false
"""
mutation(${'$'}id: Int = $listId) {
DeleteMediaListEntry(id: ${'$'}id) {
@ -840,7 +840,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
val data = postApi(q)
if (data.isNullOrBlank()) return null
val userData = parseJson<AniListRoot>(data)
val u = userData.data?.Viewer
val u = userData.data?.viewer
val user = AniListUser(
u?.id,
u?.name,
@ -862,8 +862,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)
@ -882,7 +882,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class SeasonData(
@JsonProperty("Media") val Media: SeasonMedia,
@JsonProperty("Media") val media: SeasonMedia,
)
data class SeasonMedia(
@ -1054,7 +1054,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class AniListData(
@JsonProperty("Viewer") val Viewer: AniListViewer?,
@JsonProperty("Viewer") val viewer: AniListViewer?,
)
data class AniListRoot(
@ -1094,7 +1094,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class LikeData(
@JsonProperty("Viewer") val Viewer: LikeViewer?,
@JsonProperty("Viewer") val viewer: LikeViewer?,
)
data class LikeRoot(
@ -1134,7 +1134,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class GetDataData(
@JsonProperty("Media") val Media: GetDataMedia?,
@JsonProperty("Media") val media: GetDataMedia?,
)
data class GetDataRoot(
@ -1167,7 +1167,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class GetSearchPage(
@JsonProperty("Page") val Page: GetSearchData?,
@JsonProperty("Page") val page: GetSearchData?,
)
data class GetSearchData(

View file

@ -123,6 +123,7 @@ class LocalList : SyncAPI {
ListSorting.ReleaseDateOld,
// ListSorting.RatingHigh,
// ListSorting.RatingLow,
)
)
}

View file

@ -30,6 +30,7 @@ import java.time.Instant
import java.time.format.DateTimeFormatter
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 */
@ -55,7 +56,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
override fun loginInfo(): AuthAPI.LoginInfo? {
//getMalUser(true)?
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
return AuthAPI.LoginInfo(
profilePicture = user.picture,
@ -88,7 +88,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
)
}
}
@ -182,7 +182,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
}
@ -194,7 +194,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
)
}
@ -248,12 +248,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)),
isFavorite = null,
watchedEpisodes = data?.num_episodes_watched,
watchedEpisodes = data?.numEpisodesWatched,
)
}
@ -295,7 +295,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) {
@ -306,7 +306,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"]!!
@ -355,9 +355,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
try {
if (response != "") {
val token = parseJson<ResponseToken>(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) {
@ -399,53 +399,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<Genres>?,
@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,
@ -480,8 +480,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<Data>? {
@ -501,14 +501,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<SyncAPI.LibraryItem>()
}
@ -585,7 +585,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
).text
val values = parseJson<MalRoot>(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
}
@ -594,11 +594,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)
@ -615,7 +617,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
@ -659,13 +661,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()) {
@ -682,17 +684,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<String, String>
"num_watched_episodes" to numWatchedEpisodes?.toString()
).filterValues { it != null } as Map<String, String>
return app.put(
"$apiUrl/v2/anime/$id/my_list_status",
@ -705,10 +708,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(
@ -717,7 +720,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(
@ -734,16 +737,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?,
)
@ -756,9 +759,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(

View file

@ -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<ResultFiles>? = 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(

View file

@ -39,6 +39,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
@ -145,8 +146,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"
@ -155,10 +156,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
@ -172,7 +173,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(
@ -209,7 +210,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
}
}
@ -220,17 +221,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
)
// -------------------
@ -262,15 +263,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?,
)
}
@ -309,7 +310,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,
@ -541,7 +542,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<Array<EpisodeMetadata>>()?.also {
val cacheTime =
if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value
@ -559,7 +560,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("seasons") seasons: List<Season>? = null,
@JsonProperty("episodes") episodes: List<Season.Episode>? = 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)
@ -568,7 +569,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)
@ -577,7 +578,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)
@ -632,24 +633,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 {
@ -661,10 +662,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) },
@ -677,12 +678,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 {
@ -694,10 +695,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) },
@ -752,7 +753,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()
)
}
@ -813,7 +814,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
val episodeConstructor = SimklEpisodeConstructor(
searchResult.ids?.simkl,
searchResult.type,
searchResult.total_episodes,
searchResult.totalEpisodes,
searchResult.hasEnded()
)
@ -835,12 +836,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 {
@ -848,7 +849,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,
@ -894,12 +895,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<SimklSyncServices, String>): Array<MediaObject>? {
private suspend fun searchByIds(serviceMap: Map<SimklSyncServices, String>): Array<MediaObject>? {
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()
@ -907,14 +908,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
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<Array<MediaObject>>()?.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)
}
@ -964,15 +965,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
val activities = getActivities()
val lastCacheUpdate = getKey<Long>(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 {
@ -1044,7 +1045,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<PinAuthResponse>() ?: return null
return OAuth2API.PinAuthData(
@ -1058,7 +1059,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<PinExchangeResponse>() ?: return false
if (pinAuthResp.accessToken != null) {
@ -1093,7 +1094,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
).parsedSafe<TokenResponse>() ?: return false
switchToNewAccount()
setKey(accountId, SIMKL_TOKEN_KEY, token.access_token)
setKey(accountId, SIMKL_TOKEN_KEY, token.accessToken)
val user = getUser()
if (user == null) {

View file

@ -59,6 +59,7 @@ class SubSourceApi : AbstractSubProvider {
it?.subs?.filter { sub ->
sub.releaseName!!.contains(
String.format(
null,
"E%02d",
query.epNumber
)

View file

@ -50,7 +50,7 @@ class APIRepository(val api: MainAPI) {
private val cache = threadSafeListOf<SavedLoadResponse>()
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)
}

View file

@ -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>): S? =
stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S

View file

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

View file

@ -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,

View file

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

View file

@ -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 {

View file

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

View file

@ -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?

View file

@ -54,6 +54,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
}
init {
@Suppress("LeakingThis")
resetViewData()
}

View file

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

View file

@ -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<Boolean>(view) {
@ -54,7 +54,7 @@ class HomeChildItemAdapter(
var hasNext: Boolean = false
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Boolean> {
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
)

View file

@ -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?,

View file

@ -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<Parcelable>("value")
)
}
}
@ -169,4 +171,9 @@ open class ParentItemAdapter(
submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
.toMutableList())
}
}
}
@Suppress("DEPRECATION")
inline fun <reified T> Bundle.getSafeParcelable(key: String): T? =
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelable(key)
else getParcelable(key, T::class.java)

View file

@ -117,15 +117,12 @@ class HomeParentItemAdapterPreview(
}
override fun restore(state: Bundle) {
state.getParcelable<Parcelable>("resumeRecyclerView")?.let { recycle ->
state.getSafeParcelable<Parcelable>("resumeRecyclerView")?.let { recycle ->
resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
}
state.getParcelable<Parcelable>("bookmarkRecyclerView")?.let { recycle ->
state.getSafeParcelable<Parcelable>("bookmarkRecyclerView")?.let { recycle ->
bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
}
//state.getInt("previewViewpager").let { recycle ->
// previewViewpager.setCurrentItem(recycle,true)
//}
}
val previewAdapter = HomeScrollAdapter(fragment = fragment)

View file

@ -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<WatchType>()
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

View file

@ -600,8 +600,4 @@ class LibraryFragment : Fragment() {
}
}
class MenuSearchView(context: Context) : SearchView(context) {
override fun onActionViewCollapsed() {
super.onActionViewCollapsed()
}
}
class MenuSearchView(context: Context) : SearchView(context)

View file

@ -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<Parcelable>("pageRecyclerview")?.let { recycle ->
state.getSafeParcelable<Parcelable>("pageRecyclerview")?.let { recycle ->
binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle)
}
}

View file

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

View file

@ -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<Tracks.Group>.getTrack(id: String?): Pair<TrackGroup, Int>? {
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<Pair<Format, Int>> {
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<String, String>): 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<SingleSampleMediaSource>, List<SubtitleData>> {
val activeSubtitles = ArrayList<SubtitleData>()
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<MediaItemSlice>,
@ -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<EpisodeSkip.SkipStamp> = emptyList()
@SuppressLint("UnsafeOptInUsageError")
override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) {
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?,

View file

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

View file

@ -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?,

View file

@ -49,7 +49,7 @@ class DownloadFileGenerator(
return null
}
fun cleanDisplayName(name: String): String {
private fun cleanDisplayName(name: String): String {
return name.substringBeforeLast('.').trim()
}

View file

@ -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"

View file

@ -25,15 +25,20 @@ 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
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.media3.common.util.UnstableApi
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
@ -47,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
@ -120,6 +124,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 {
@ -241,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) {
@ -296,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 =
@ -340,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
}
@ -1163,6 +1166,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
return true
}
@SuppressLint("GestureBackNavigation")
private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean {
if (hasNavigated) {
autoHide()
@ -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)
@ -1572,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()
}
@ -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()

View file

@ -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<AbstractSubtitleEntities.SubtitleEntity>
binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ ->
currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener
@ -379,8 +381,8 @@ class GeneratorPlayer : FullScreenPlayer() {
fun setSubtitlesList(list: List<AbstractSubtitleEntities.SubtitleEntity>) {
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<String, String>?)
sync.addSyncs(bundle.getSafeSerializable<HashMap<String, String>>("syncData"))
}
}
@ -1507,3 +1508,6 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}
}
@Suppress("DEPRECATION")
inline fun <reified T : Serializable> Bundle.getSafeSerializable(key: String) : T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getSerializable(key) as? T else getSerializable(key, T::class.java)

View file

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

View file

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

View file

@ -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<Cue>) msg.obj);
return true;
default:
throw new IllegalStateException();
if (msg.what == MSG_UPDATE_OUTPUT) {
invokeUpdateOutputInternal((List<Cue>) msg.obj);
return true;
}
throw new IllegalStateException();
}
private void invokeUpdateOutputInternal(List<Cue> cues) {
@ -441,7 +440,6 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback {
}
).collect(Collectors.toList());
output.onCues(fixedCues);
output.onCues(new CueGroup(fixedCues, 0L));
}

View file

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

View file

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

View file

@ -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<SubtitleData> = emptySet()
private var allSubtitles: Set<SubtitleData> = emptySet()

View file

@ -239,7 +239,11 @@ private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewG
// generated images 1:1 to idx of hsl
private var images: Array<Bitmap?> = 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) {

View file

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

View file

@ -17,7 +17,6 @@ class PriorityAdapter<T>(override val items: MutableList<SourcePriority<T>>) :
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<T>(override val items: MutableList<SourcePriority<T>>) :
val binding: PlayerPrioritizeItemBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun <T> bind(item: SourcePriority<T>) {
/* 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() {

View file

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

View file

@ -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<QualityProfile> {
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" })

View file

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

View file

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

View file

@ -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 = {}
) :

View file

@ -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(
@ -167,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,
@ -274,7 +275,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 +296,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))
@ -324,8 +334,7 @@ class EpisodeAdapter(
}
}
class EpisodeCardViewHolderSmall
constructor(
class EpisodeCardViewHolderSmall(
val binding: ResultEpisodeBinding,
private val hasDownloadSupport: Boolean,
private val clickCallback: (EpisodeClickEvent) -> Unit,

View file

@ -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<Int>(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,

View file

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

View file

@ -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<Rect>) {
binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions)
private val gestureRegionsListener =
object : PanelsChildGestureRegionObserver.GestureRegionsListener {
override fun onGestureRegionsUpdate(gestureRegions: List<Rect>) {
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<SearchResponse>?, validApiName: String?) {

View file

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

View file

@ -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) {
@ -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
@ -2721,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())
@ -2859,4 +2861,4 @@ class ResultViewModel2 : ViewModel() {
}
}
}
}
}

View file

@ -63,8 +63,7 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter<Recycler
}
private class SelectViewHolder
constructor(
private class SelectViewHolder(
binding: ResultSelectionBinding,
) :
RecyclerView.ViewHolder(binding.root) {

View file

@ -11,7 +11,7 @@ import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.databinding.SearchResultGridBinding
import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout
import com.lagradost.cloudstream3.utils.UIHelper.isBottomLayout
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlin.math.roundToInt
@ -41,7 +41,7 @@ class SearchAdapter(
val inflater = LayoutInflater.from(parent.context)
val layout =
if (parent.context.IsBottomLayout()) SearchResultGridExpandedBinding.inflate(
if (parent.context.isBottomLayout()) SearchResultGridExpandedBinding.inflate(
inflater,
parent,
false
@ -83,8 +83,7 @@ class SearchAdapter(
diffResult.dispatchUpdatesTo(this)
}
class CardViewHolder
constructor(
class CardViewHolder(
val binding: ViewBinding,
private val clickCallback: (SearchClickCallback) -> Unit,
resView: AutofitRecyclerView

View file

@ -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,
) :

View file

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

View file

@ -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,

View file

@ -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<AuthAPI.LoginInfo>,
private val cardList: List<AuthAPI.LoginInfo>,
private val clickCallback: (AccountClickCallback) -> Unit
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
@ -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(

View file

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

View file

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

View file

@ -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
@ -87,10 +86,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,8 +104,10 @@ 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()
val prefValues = Qualities.entries.map { it.value }.reversed().toMutableList()
prefValues.remove(Qualities.Unknown.value)
val prefNames = prefValues.map { Qualities.getStringByInt(it) }
@ -118,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(
@ -134,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) }
@ -142,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(

View file

@ -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<Int>()

View file

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

View file

@ -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,
) {

View file

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

View file

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

View file

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

View file

@ -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<Int, Int>) {
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
}

View file

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

View file

@ -83,7 +83,7 @@ object EpisodeSkip {
startMs = start,
endMs = end
)
}?.let { list ->
}.let { list ->
out.addAll(list)
}
}

View file

@ -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(

View file

@ -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<String, Boolean>?,
@JsonProperty("_Int") val _Int: Map<String, Int>?,
@JsonProperty("_String") val _String: Map<String, String>?,
@JsonProperty("_Float") val _Float: Map<String, Float>?,
@JsonProperty("_Long") val _Long: Map<String, Long>?,
@JsonProperty("_StringSet") val _StringSet: Map<String, Set<String>?>?,
@JsonProperty("_Bool") val bool: Map<String, Boolean>?,
@JsonProperty("_Int") val int: Map<String, Int>?,
@JsonProperty("_String") val string: Map<String, String>?,
@JsonProperty("_Float") val float: Map<String, Float>?,
@JsonProperty("_Long") val long: Map<String, Long>?,
@JsonProperty("_StringSet") val stringSet: Map<String, Set<String>?>?,
)
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)
}
}

View file

@ -56,16 +56,27 @@ data class Editor(
) {
/** Always remember to call apply after */
fun<T> 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<String> != null) -> editor.putStringSet(path, value as Set<String>)
@Suppress("UNCHECKED_CAST")
if (isStringSet(value)) {
editor.putStringSet(path, value as Set<String>)
} 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<String>().size == value.size
}
return false
}
fun apply() {
editor.apply()
System.gc()

View file

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

View file

@ -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<GithubAsset>,
@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<GithubTag>(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)

View file

@ -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>(Intent.EXTRA_INTENT)
val userAction = intent.getSafeParcelableExtra<Intent>(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 <reified T> Intent.getSafeParcelableExtra(key: String): T? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) getParcelableExtra(key) else getParcelableExtra(key, T::class.java)

View file

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

View file

@ -73,8 +73,8 @@ object SyncUtil {
val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text
val mapped = parseJson<MalSyncPage?>(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?
)

View file

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

View file

@ -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 =

View file

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

View file

@ -44,7 +44,7 @@
android:nextFocusRight="@id/download_button"
android:scaleType="centerCrop"
tools:src="@drawable/example_poster"
tools:visibility="invisible"/>
tools:visibility="invisible" />
<ImageView
android:id="@+id/episode_play_icon"
@ -53,7 +53,7 @@
android:layout_gravity="center"
android:contentDescription="@string/play_episode"
android:src="@drawable/play_button"
tools:visibility="invisible"/>
tools:visibility="invisible" />
<ImageView
android:id="@+id/episode_upcoming_icon"
@ -106,12 +106,29 @@
tools:text="1. Jobless" />
</LinearLayout>
<TextView
android:id="@+id/episode_rating"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="?attr/grayTextColor"
tools:text="Rated: 8.8" />
android:layout_gravity="start"
android:orientation="horizontal">
<TextView
android:id="@+id/episode_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textColor="?attr/grayTextColor"
tools:text="Rated: 8.8" />
<TextView
android:id="@+id/episode_runtime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textColor="?attr/grayTextColor"
tools:text="80min" />
</LinearLayout>
<TextView
android:id="@+id/episode_date"
@ -119,6 +136,7 @@
android:layout_height="wrap_content"
android:textColor="?attr/grayTextColor"
tools:text="15 Apr 2024" />
</LinearLayout>
<com.lagradost.cloudstream3.ui.download.button.PieFetchButton

Some files were not shown because too many files have changed in this diff Show more