Clean up and mark questionable code issues (#1209)

This commit is contained in:
epireyn 2024-07-29 00:39:04 +02:00 committed by GitHub
parent 0aa48f335a
commit 04dda008c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
97 changed files with 563 additions and 721 deletions

View file

@ -35,6 +35,7 @@ import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.PrintStream import java.io.PrintStream
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.Locale
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.system.exitProcess import kotlin.system.exitProcess
@ -81,14 +82,8 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
ACRA.errorReporter.handleException(error) ACRA.errorReporter.handleException(error)
try { try {
PrintStream(errorFile).use { ps -> PrintStream(errorFile).use { ps ->
ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")) ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")
ps.println( ps.println("Fatal exception on thread ${thread.name} (${thread.id})")
String.format(
"Fatal exception on thread %s (%d)",
thread.name,
thread.id
)
)
error.printStackTrace(ps) error.printStackTrace(ps)
} }
} catch (ignored: FileNotFoundException) { } catch (ignored: FileNotFoundException) {
@ -106,7 +101,6 @@ class AcraApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
//NativeCrashHandler.initCrashHandler()
ExceptionHandler(filesDir.resolve("last_error")) { ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component)) startActivity(Intent.makeRestartActivityTask(intent!!.component))

View file

@ -164,7 +164,7 @@ object CommonActivity {
val toast = Toast(act) val toast = Toast(act)
toast.duration = duration ?: Toast.LENGTH_SHORT toast.duration = duration ?: Toast.LENGTH_SHORT
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) 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 currentToast = toast
toast.show() toast.show()
@ -464,20 +464,6 @@ object CommonActivity {
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) { 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 // 149 keycode_numpad 5
when (keyCode) { when (keyCode) {

View file

@ -11,7 +11,7 @@ import java.util.concurrent.TimeUnit
class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() { 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 { override fun execute(request: Request): Response {
val httpMethod: String = request.httpMethod() val httpMethod: String = request.httpMethod()
val url: String = request.url() val url: String = request.url()
@ -74,8 +74,4 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
return instance 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.plugins.PluginManager.loadSinglePlugin
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.services.SubscriptionWorkManager 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.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers 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.inAppAuths
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
@ -347,7 +347,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
println("Repository url: $realUrl") println("Repository url: $realUrl")
loadRepository(realUrl) loadRepository(realUrl)
return true return true
} else if (str.contains(appString)) { } else if (str.contains(APP_STRING)) {
for (api in OAuth2Apis) { for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) { if (str.contains("/${api.redirectUrl}")) {
ioSafe { ioSafe {
@ -377,15 +377,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
} }
// This specific intent is used for the gradle deployWithAdb // 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 // 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) PluginManager.hotReloadAllLocalPlugins(activity)
} }
} else if (safeURI(str)?.scheme == appStringRepo) { } else if (safeURI(str)?.scheme == APP_STRING_REPO) {
val url = str.replaceFirst(appStringRepo, "https") val url = str.replaceFirst(APP_STRING_REPO, "https")
loadRepository(url) loadRepository(url)
return true return true
} else if (safeURI(str)?.scheme == appStringSearch) { } else if (safeURI(str)?.scheme == APP_STRING_SEARCH) {
val query = str.substringAfter("$appStringSearch://") val query = str.substringAfter("$APP_STRING_SEARCH://")
nextSearchQuery = nextSearchQuery =
try { try {
URLDecoder.decode(query, "UTF-8") URLDecoder.decode(query, "UTF-8")
@ -399,7 +399,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
R.id.navigation_search R.id.navigation_search
activity?.findViewById<NavigationRailView>(R.id.nav_rail_view)?.selectedItemId = activity?.findViewById<NavigationRailView>(R.id.nav_rail_view)?.selectedItemId =
R.id.navigation_search R.id.navigation_search
} else if (safeURI(str)?.scheme == appStringPlayer) { } else if (safeURI(str)?.scheme == APP_STRING_PLAYER) {
val uri = Uri.parse(str) val uri = Uri.parse(str)
val name = uri.getQueryParameter("name") val name = uri.getQueryParameter("name")
val url = URLDecoder.decode(uri.authority, "UTF-8") 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 = val id =
str.substringAfter("$appStringResumeWatching://").toIntOrNull() str.substringAfter("$APP_STRING_RESUME_WATCHING://").toIntOrNull()
?: return false ?: return false
ioSafe { ioSafe {
val resumeWatchingCard = val resumeWatchingCard =
@ -469,7 +469,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
) DubStatus.Dubbed else DubStatus.Subbed, null ) DubStatus.Dubbed else DubStatus.Subbed, null
) )
} else { } else {
viewModel.loadSmall(this, result) viewModel.loadSmall(result)
} }
} }
@ -605,7 +605,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
} }
//private var mCastSession: CastSession? = null //private var mCastSession: CastSession? = null
lateinit var mSessionManager: SessionManager var mSessionManager: SessionManager? = null
private val mSessionManagerListener: SessionManagerListener<Session> by lazy { SessionManagerListenerImpl() } private val mSessionManagerListener: SessionManagerListener<Session> by lazy { SessionManagerListenerImpl() }
private inner class SessionManagerListenerImpl : SessionManagerListener<Session> { private inner class SessionManagerListenerImpl : SessionManagerListener<Session> {
@ -645,8 +645,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
setActivityInstance(this) setActivityInstance(this)
try { try {
if (isCastApiAvailable()) { if (isCastApiAvailable()) {
//mCastSession = mSessionManager.currentCastSession mSessionManager?.addSessionManagerListener(mSessionManagerListener)
mSessionManager.addSessionManagerListener(mSessionManagerListener)
} }
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
@ -662,7 +661,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
} }
try { try {
if (isCastApiAvailable()) { if (isCastApiAvailable()) {
mSessionManager.removeSessionManagerListener(mSessionManagerListener) mSessionManager?.removeSessionManagerListener(mSessionManagerListener)
//mCastSession = null //mCastSession = null
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -766,7 +765,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
list.forEach { custom -> list.forEach { custom ->
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
?.let { ?.let {
allProviders.add(it.javaClass.newInstance().apply { allProviders.add(it.javaClass.getDeclaredConstructor().newInstance().apply {
name = custom.name name = custom.name
lang = custom.lang lang = custom.lang
mainUrl = custom.url.trimEnd('/') mainUrl = custom.url.trimEnd('/')
@ -1147,7 +1146,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
try { try {
if (isCastApiAvailable()) { if (isCastApiAvailable()) {
mSessionManager = CastContext.getSharedInstance(this).sessionManager CastContext.getSharedInstance(this) {it.run()}.addOnSuccessListener { mSessionManager = it.sessionManager }
} }
} catch (t: Throwable) { } catch (t: Throwable) {
logError(t) logError(t)
@ -1449,13 +1448,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
val value = viewModel.watchStatus.value ?: WatchType.NONE val value = viewModel.watchStatus.value ?: WatchType.NONE
this@MainActivity.showBottomDialog( this@MainActivity.showBottomDialog(
WatchType.values().map { getString(it.stringRes) }.toList(), WatchType.entries.map { getString(it.stringRes) }.toList(),
value.ordinal, value.ordinal,
this@MainActivity.getString(R.string.action_add_to_bookmarks), this@MainActivity.getString(R.string.action_add_to_bookmarks),
showApply = false, showApply = false,
{}) { {}) {
viewModel.updateWatchStatus( viewModel.updateWatchStatus(
WatchType.values()[it], WatchType.entries[it],
this@MainActivity this@MainActivity
) )
} }
@ -1465,12 +1464,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
?: SyncWatchType.NONE ?: SyncWatchType.NONE
this@MainActivity.showBottomDialog( this@MainActivity.showBottomDialog(
SyncWatchType.values().map { getString(it.stringRes) }.toList(), SyncWatchType.entries.map { getString(it.stringRes) }.toList(),
value.ordinal, value.ordinal,
this@MainActivity.getString(R.string.action_add_to_bookmarks), this@MainActivity.getString(R.string.action_add_to_bookmarks),
showApply = false, showApply = false,
{}) { {}) {
syncViewModel.setStatus(SyncWatchType.values()[it].internalId) syncViewModel.setStatus(SyncWatchType.entries[it].internalId)
syncViewModel.publishUserData() 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.MainAPI
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.syncproviders.SyncIdName
object SyncRedirector { object SyncRedirector {
val syncApis = SyncApis
private val syncIds = private val syncIds =
listOf( listOf(
SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""), SyncIdName.MyAnimeList to Regex("""myanimelist\.net/anime/(\d+)"""),
SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""") SyncIdName.Anilist to Regex("""anilist\.co/anime/(\d+)""")
) )
suspend fun redirect( suspend fun redirect(

View file

@ -296,7 +296,7 @@ open class TraktProvider : MainAPI() {
return try { return try {
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
APIHolder.unixTimeMS < dateTime unixTimeMS < dateTime
} catch (t: Throwable) { } catch (t: Throwable) {
logError(t) logError(t)
false false

View file

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.network package com.lagradost.cloudstream3.network
import android.util.Base64
import android.util.Log import android.util.Log
import android.webkit.CookieManager import android.webkit.CookieManager
import androidx.annotation.AnyThread 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.Requests.Companion.await
import com.lagradost.nicehttp.cookies import com.lagradost.nicehttp.cookies
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.* import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import java.net.URI import java.net.URI

View file

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

View file

@ -34,7 +34,7 @@ abstract class Plugin {
*/ */
fun registerMainAPI(element: MainAPI) { fun registerMainAPI(element: MainAPI) {
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) 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 // Race condition causing which would case duplicates if not for distinctBy
synchronized(APIHolder.allProviders) { synchronized(APIHolder.allProviders) {
APIHolder.allProviders.add(element) APIHolder.allProviders.add(element)
@ -48,7 +48,7 @@ abstract class Plugin {
*/ */
fun registerExtractorAPI(element: ExtractorApi) { fun registerExtractorAPI(element: ExtractorApi) {
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi") Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi")
element.sourcePlugin = this.__filename element.sourcePlugin = this.filename
extractorApis.add(element) extractorApis.add(element)
} }
@ -68,7 +68,11 @@ abstract class Plugin {
*/ */
var resources: Resources? = null var resources: Resources? = null
/** Full file path to the plugin. */ /** 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 * 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 package com.lagradost.cloudstream3.plugins
import android.Manifest
import android.app.* import android.app.*
import android.content.Context import android.content.Context
import android.content.pm.PackageManager
import android.content.res.AssetManager import android.content.res.AssetManager
import android.content.res.Resources import android.content.res.Resources
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
@ -163,7 +166,7 @@ object PluginManager {
private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins" private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins"
public var currentlyLoading: String? = null var currentlyLoading: String? = null
// Maps filepath to plugin // Maps filepath to plugin
val plugins: MutableMap<String, Plugin> = val plugins: MutableMap<String, Plugin> =
@ -339,7 +342,7 @@ object PluginManager {
//Omit non-NSFW if mode is set to NSFW only //Omit non-NSFW if mode is set to NSFW only
if (mode == AutoDownloadMode.NsfwOnly) { if (mode == AutoDownloadMode.NsfwOnly) {
if (tvtypes.contains(TvType.NSFW.name) == false) { if (!tvtypes.contains(TvType.NSFW.name)) {
return@mapNotNull null return@mapNotNull null
} }
} }
@ -504,10 +507,12 @@ object PluginManager {
val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also { val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also {
Log.d(TAG, "No manifest version for ${data.internalName}") Log.d(TAG, "No manifest version for ${data.internalName}")
} }
@Suppress("UNCHECKED_CAST")
val pluginClass: Class<*> = val pluginClass: Class<*> =
loader.loadClass(manifest.pluginClassName) as Class<out Plugin?> loader.loadClass(manifest.pluginClassName) as Class<out Plugin?>
val pluginInstance: Plugin = val pluginInstance: Plugin =
pluginClass.newInstance() as Plugin pluginClass.getDeclaredConstructor().newInstance() as Plugin
// Sets with the proper version // Sets with the proper version
setPluginData(data.copy(version = version)) setPluginData(data.copy(version = version))
@ -517,14 +522,16 @@ object PluginManager {
return true return true
} }
pluginInstance.__filename = file.absolutePath pluginInstance.filename = file.absolutePath
if (manifest.requiresResources) { if (manifest.requiresResources) {
Log.d(TAG, "Loading resources for ${data.internalName}") Log.d(TAG, "Loading resources for ${data.internalName}")
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk // 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 = val addAssetPath =
AssetManager::class.java.getMethod("addAssetPath", String::class.java) AssetManager::class.java.getMethod("addAssetPath", String::class.java)
addAssetPath.invoke(assets, file.absolutePath) addAssetPath.invoke(assets, file.absolutePath)
@Suppress("DEPRECATION")
pluginInstance.resources = Resources( pluginInstance.resources = Resources(
assets, assets,
context.resources.displayMetrics, context.resources.displayMetrics,
@ -566,14 +573,14 @@ object PluginManager {
// remove all registered apis // remove all registered apis
synchronized(APIHolder.apis) { synchronized(APIHolder.apis) {
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach { APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
removePluginMapping(it) removePluginMapping(it)
} }
} }
synchronized(APIHolder.allProviders) { 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 } classLoaders.values.removeIf { v -> v == plugin }
@ -720,9 +727,14 @@ object PluginManager {
} }
val notification = builder.build() val notification = builder.build()
with(NotificationManagerCompat.from(context)) { // notificationId is a unique int for each notification that you must define
// notificationId is a unique int for each notification that you must define if (ActivityCompat.checkSelfPermission(
notify((System.currentTimeMillis() / 1000).toInt(), notification) context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
NotificationManagerCompat.from(context)
.notify((System.currentTimeMillis() / 1000).toInt(), notification)
} }
return notification return notification
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -73,7 +73,7 @@ object RepositoryManager {
val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy { val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy {
getKey("PREBUILT_REPOSITORIES") ?: emptyArray() 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 */ /* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */
fun convertRawGitUrl(url: String): String { 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 object VotingApi { // please do not cheat the votes lol
private const val LOGKEY = "VotingApi" 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 private fun transformUrl(url: String): String = // dont touch or all votes get reset
MessageDigest MessageDigest
@ -49,13 +49,13 @@ object VotingApi { // please do not cheat the votes lol
.joinToString("-") .joinToString("-")
private suspend fun readVote(pluginUrl: String): Int { 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") Log.d(LOGKEY, "Requesting: $url")
return app.get(url).parsedSafe<Result>()?.value ?: 0 return app.get(url).parsedSafe<Result>()?.value ?: 0
} }
private suspend fun writeVote(pluginUrl: String): Boolean { 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") Log.d(LOGKEY, "Requesting: $url")
return app.get(url).parsedSafe<Result>()?.value != null 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 getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false
fun canVote(pluginUrl: String): Boolean { fun canVote(pluginUrl: String): Boolean {
if (!PluginManager.urlPlugins.contains(pluginUrl)) return false return PluginManager.urlPlugins.contains(pluginUrl)
return true
} }
private val voteLock = Mutex() private val voteLock = Mutex()

View file

@ -59,7 +59,7 @@ class SubtitleResource {
return file 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>>() val entries = mutableListOf<Pair<String, File>>()
ZipInputStream(file.inputStream()).use { zipInputStream -> ZipInputStream(file.inputStream()).use { zipInputStream ->

View file

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

View file

@ -56,22 +56,22 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
subSourceApi subSourceApi
) )
const val appString = "cloudstreamapp" const val APP_STRING = "cloudstreamapp"
const val appStringRepo = "cloudstreamrepo" const val APP_STRING_REPO = "cloudstreamrepo"
const val appStringPlayer = "cloudstreamplayer" const val APP_STRING_PLAYER = "cloudstreamplayer"
// Instantly start the search given a query // Instantly start the search given a query
const val appStringSearch = "cloudstreamsearch" const val APP_STRING_SEARCH = "cloudstreamsearch"
// Instantly resume watching a show // Instantly resume watching a show
const val appStringResumeWatching = "cloudstreamcontinuewatching" const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching"
val unixTime: Long val unixTime: Long
get() = System.currentTimeMillis() / 1000L get() = System.currentTimeMillis() / 1000L
val unixTimeMs: Long val unixTimeMs: Long
get() = System.currentTimeMillis() get() = System.currentTimeMillis()
const val maxStale = 60 * 10 const val MAX_STALE = 60 * 10
fun secondsToReadable(seconds: Int, completedValue: String): String { fun secondsToReadable(seconds: Int, completedValue: String): String {
var secondsLong = seconds.toLong() var secondsLong = seconds.toLong()

View file

@ -18,13 +18,13 @@ class Addic7ed : AbstractSubApi {
override fun logOut() {} override fun logOut() {}
companion object { companion object {
const val host = "https://www.addic7ed.com" const val HOST = "https://www.addic7ed.com"
const val TAG = "ADDIC7ED" const val TAG = "ADDIC7ED"
} }
private fun fixUrl(url: String): String { private fun fixUrl(url: String): String {
return if (url.startsWith("/")) host + url return if (url.startsWith("/")) HOST + url
else if (!url.startsWith("http")) "$host/$url" else if (!url.startsWith("http")) "$HOST/$url"
else url else url
} }
@ -62,7 +62,7 @@ class Addic7ed : AbstractSubApi {
} }
val title = queryText.substringBefore("(").trim() 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 val hostDocument = app.get(url).document
var searchResult = "" var searchResult = ""
if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url
@ -74,8 +74,8 @@ class Addic7ed : AbstractSubApi {
hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(") hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(")
?.substringBefore(",") ?.substringBefore(",")
val doc = app.get( val doc = app.get(
"$host/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined", "$HOST/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined",
referer = "$host/" referer = "$HOST/"
).document ).document
doc.select("#season tr:contains($queryLang)").mapNotNull { node -> doc.select("#season tr:contains($queryLang)").mapNotNull { node ->
if (node.selectFirst("td")?.text() if (node.selectFirst("td")?.text()
@ -97,7 +97,7 @@ class Addic7ed : AbstractSubApi {
val link = fixUrl(node.select("a.buttonDownload").attr("href")) val link = fixUrl(node.select("a.buttonDownload").attr("href"))
val isHearingImpaired = val isHearingImpaired =
!node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty() !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 return results
} }

View file

@ -63,7 +63,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun handleRedirect(url: String): Boolean { override suspend fun handleRedirect(url: String): Boolean {
val sanitizer = 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 token = sanitizer["access_token"]!!
val expiresIn = sanitizer["expires_in"]!! val expiresIn = sanitizer["expires_in"]!!
@ -87,7 +87,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? { override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
val data = searchShows(name) ?: return null val data = searchShows(name) ?: return null
return data.data?.Page?.media?.map { return data.data?.page?.media?.map {
SyncAPI.SyncSearchResult( SyncAPI.SyncSearchResult(
it.title.romaji ?: return null, it.title.romaji ?: return null,
this.name, this.name,
@ -101,7 +101,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun getResult(id: String): SyncAPI.SyncResult { override suspend fun getResult(id: String): SyncAPI.SyncResult {
val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1) val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId") ?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
val season = getSeason(internalId).data.Media val season = getSeason(internalId).data.media
return SyncAPI.SyncResult( return SyncAPI.SyncResult(
season.id.toString(), season.id.toString(),
@ -301,12 +301,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
//println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}") //println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}")
val shows = searchShows(name.replace(blackListRegex, "")) val shows = searchShows(name.replace(blackListRegex, ""))
shows?.data?.Page?.media?.find { shows?.data?.page?.media?.find {
(malId ?: "NONE") == it.idMal.toString() (malId ?: "NONE") == it.idMal.toString()
}?.let { return it } }?.let { return it }
val filtered = val filtered =
shows?.data?.Page?.media?.filter { shows?.data?.page?.media?.filter {
(((it.startDate.year ?: year.toString()) == year.toString() (((it.startDate.year ?: year.toString()) == year.toString()
|| year == null)) || year == null))
} }
@ -496,7 +496,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
val data = postApi(q, true) val data = postApi(q, true)
val d = parseJson<GetDataRoot>(data ?: return null) val d = parseJson<GetDataRoot>(data ?: return null)
val main = d.data?.Media val main = d.data?.media
if (main?.mediaListEntry != null) { if (main?.mediaListEntry != null) {
return AniListTitleHolder( return AniListTitleHolder(
title = main.title, title = main.title,
@ -536,7 +536,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
headers = mapOf( headers = mapOf(
"Authorization" to "Bearer " + (getAuth() "Authorization" to "Bearer " + (getAuth()
?: return@suspendSafeApiCall null), ?: 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, cacheTime = 0,
data = mapOf( data = mapOf(
@ -647,7 +647,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
) )
data class Data( data class Data(
@JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection @JsonProperty("MediaListCollection") val mediaListCollection: MediaListCollection
) )
private fun getAniListListCached(): Array<Lists>? { private fun getAniListListCached(): Array<Lists>? {
@ -659,7 +659,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
if (checkToken()) return null if (checkToken()) return null
return if (requireLibraryRefresh) { return if (requireLibraryRefresh) {
val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray() val list = getFullAniListList()?.data?.mediaListCollection?.lists?.toTypedArray()
if (list != null) { if (list != null) {
setKey(ANILIST_CACHED_LIST, list) setKey(ANILIST_CACHED_LIST, list)
} }
@ -678,7 +678,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
// To fill empty lists when AniList does not return them // To fill empty lists when AniList does not return them
val baseMap = val baseMap =
AniListStatusType.values().filter { it.value >= 0 }.associate { AniListStatusType.entries.filter { it.value >= 0 }.associate {
it.stringRes to emptyList<SyncAPI.LibraryItem>() it.stringRes to emptyList<SyncAPI.LibraryItem>()
} }
@ -764,7 +764,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
/** Used to query a saved MediaItem on the list to get the id for removal */ /** 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 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) data class MediaListId(@JsonProperty("id") val id: Long? = null)
private suspend fun postDataAboutId( private suspend fun postDataAboutId(
@ -787,7 +787,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
""" """
val response = postApi(idQuery) val response = postApi(idQuery)
val listId = val listId =
tryParseJson<MediaListItemRoot>(response)?.data?.MediaList?.id ?: return false tryParseJson<MediaListItemRoot>(response)?.data?.mediaList?.id ?: return false
""" """
mutation(${'$'}id: Int = $listId) { mutation(${'$'}id: Int = $listId) {
DeleteMediaListEntry(id: ${'$'}id) { DeleteMediaListEntry(id: ${'$'}id) {
@ -836,7 +836,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
val data = postApi(q) val data = postApi(q)
if (data.isNullOrBlank()) return null if (data.isNullOrBlank()) return null
val userData = parseJson<AniListRoot>(data) val userData = parseJson<AniListRoot>(data)
val u = userData.data?.Viewer val u = userData.data?.viewer
val user = AniListUser( val user = AniListUser(
u?.id, u?.id,
u?.name, u?.name,
@ -858,8 +858,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
suspend fun getSeasonRecursive(id: Int) { suspend fun getSeasonRecursive(id: Int) {
val season = getSeason(id) val season = getSeason(id)
seasons.add(season) seasons.add(season)
if (season.data.Media.format?.startsWith("TV") == true) { if (season.data.media.format?.startsWith("TV") == true) {
season.data.Media.relations?.edges?.forEach { season.data.media.relations?.edges?.forEach {
if (it.node?.format != null) { if (it.node?.format != null) {
if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) { if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) {
getSeasonRecursive(it.node.id) getSeasonRecursive(it.node.id)
@ -878,7 +878,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
) )
data class SeasonData( data class SeasonData(
@JsonProperty("Media") val Media: SeasonMedia, @JsonProperty("Media") val media: SeasonMedia,
) )
data class SeasonMedia( data class SeasonMedia(
@ -1050,7 +1050,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
) )
data class AniListData( data class AniListData(
@JsonProperty("Viewer") val Viewer: AniListViewer?, @JsonProperty("Viewer") val viewer: AniListViewer?,
) )
data class AniListRoot( data class AniListRoot(
@ -1090,7 +1090,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
) )
data class LikeData( data class LikeData(
@JsonProperty("Viewer") val Viewer: LikeViewer?, @JsonProperty("Viewer") val viewer: LikeViewer?,
) )
data class LikeRoot( data class LikeRoot(
@ -1130,7 +1130,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
) )
data class GetDataData( data class GetDataData(
@JsonProperty("Media") val Media: GetDataMedia?, @JsonProperty("Media") val media: GetDataMedia?,
) )
data class GetDataRoot( data class GetDataRoot(
@ -1163,7 +1163,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
) )
data class GetSearchPage( data class GetSearchPage(
@JsonProperty("Page") val Page: GetSearchData?, @JsonProperty("Page") val page: GetSearchData?,
) )
data class GetSearchData( data class GetSearchData(

View file

@ -119,8 +119,6 @@ class LocalList : SyncAPI {
ListSorting.AlphabeticalZ, ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew, ListSorting.UpdatedNew,
ListSorting.UpdatedOld, ListSorting.UpdatedOld,
// ListSorting.RatingHigh,
// ListSorting.RatingLow,
) )
) )
} }

View file

@ -19,14 +19,18 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import java.net.URL import java.net.URL
import java.security.SecureRandom import java.security.SecureRandom
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.time.Instant
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone
/** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */ /** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */
const val MAL_MAX_SEARCH_LIMIT = 25 const val MAL_MAX_SEARCH_LIMIT = 25
@ -51,7 +55,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
} }
override fun loginInfo(): AuthAPI.LoginInfo? { override fun loginInfo(): AuthAPI.LoginInfo? {
//getMalUser(true)?
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user -> getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
return AuthAPI.LoginInfo( return AuthAPI.LoginInfo(
profilePicture = user.picture, profilePicture = user.picture,
@ -84,7 +87,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
this.name, this.name,
node.id.toString(), node.id.toString(),
"$mainUrl/anime/${node.id}/", "$mainUrl/anime/${node.id}/",
node.main_picture?.large ?: node.main_picture?.medium node.mainPicture?.large ?: node.mainPicture?.medium
) )
} }
} }
@ -178,7 +181,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private fun parseDate(string: String?): Long? { private fun parseDate(string: String?): Long? {
return try { 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) { } catch (e: Exception) {
null null
} }
@ -190,7 +193,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
apiName = this.name, apiName = this.name,
syncId = node.id.toString(), syncId = node.id.toString(),
url = "$mainUrl/anime/${node.id}", url = "$mainUrl/anime/${node.id}",
posterUrl = node.main_picture?.large posterUrl = node.mainPicture?.large
) )
} }
@ -244,12 +247,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val internalId = id.toIntOrNull() ?: return null val internalId = id.toIntOrNull() ?: return null
val data = 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( return SyncAPI.SyncStatus(
score = data?.score, score = data?.score,
status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)) , status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)),
isFavorite = null, isFavorite = null,
watchedEpisodes = data?.num_episodes_watched, watchedEpisodes = data?.numEpisodesWatched,
) )
} }
@ -291,7 +294,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private fun parseDateLong(string: String?): Long? { private fun parseDateLong(string: String?): Long? {
return try { 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 string ?: return null
)?.time?.div(1000) )?.time?.div(1000)
} catch (e: Exception) { } catch (e: Exception) {
@ -302,7 +305,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun handleRedirect(url: String): Boolean { override suspend fun handleRedirect(url: String): Boolean {
val sanitizer = 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"]!! val state = sanitizer["state"]!!
if (state == "RequestID$requestId") { if (state == "RequestID$requestId") {
val currentCode = sanitizer["code"]!! val currentCode = sanitizer["code"]!!
@ -351,9 +354,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
try { try {
if (response != "") { if (response != "") {
val token = parseJson<ResponseToken>(response) val token = parseJson<ResponseToken>(response)
setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime)) setKey(accountId, MAL_UNIXTIME_KEY, (token.expiresIn + unixTime))
setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token) setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refreshToken)
setKey(accountId, MAL_TOKEN_KEY, token.access_token) setKey(accountId, MAL_TOKEN_KEY, token.accessToken)
requireLibraryRefresh = true requireLibraryRefresh = true
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -395,53 +398,53 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class Node( data class Node(
@JsonProperty("id") val id: Int, @JsonProperty("id") val id: Int,
@JsonProperty("title") val title: String, @JsonProperty("title") val title: String,
@JsonProperty("main_picture") val main_picture: MainPicture?, @JsonProperty("main_picture") val mainPicture: MainPicture?,
@JsonProperty("alternative_titles") val alternative_titles: AlternativeTitles?, @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?,
@JsonProperty("media_type") val media_type: String?, @JsonProperty("media_type") val mediaType: String?,
@JsonProperty("num_episodes") val num_episodes: Int?, @JsonProperty("num_episodes") val numEpisodes: Int?,
@JsonProperty("status") val status: String?, @JsonProperty("status") val status: String?,
@JsonProperty("start_date") val start_date: String?, @JsonProperty("start_date") val startDate: String?,
@JsonProperty("end_date") val end_date: String?, @JsonProperty("end_date") val endDate: String?,
@JsonProperty("average_episode_duration") val average_episode_duration: Int?, @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?,
@JsonProperty("synopsis") val synopsis: String?, @JsonProperty("synopsis") val synopsis: String?,
@JsonProperty("mean") val mean: Double?, @JsonProperty("mean") val mean: Double?,
@JsonProperty("genres") val genres: List<Genres>?, @JsonProperty("genres") val genres: List<Genres>?,
@JsonProperty("rank") val rank: Int?, @JsonProperty("rank") val rank: Int?,
@JsonProperty("popularity") val popularity: Int?, @JsonProperty("popularity") val popularity: Int?,
@JsonProperty("num_list_users") val num_list_users: Int?, @JsonProperty("num_list_users") val numListUsers: Int?,
@JsonProperty("num_favorites") val num_favorites: Int?, @JsonProperty("num_favorites") val numFavorites: Int?,
@JsonProperty("num_scoring_users") val num_scoring_users: Int?, @JsonProperty("num_scoring_users") val numScoringUsers: Int?,
@JsonProperty("start_season") val start_season: StartSeason?, @JsonProperty("start_season") val startSeason: StartSeason?,
@JsonProperty("broadcast") val broadcast: Broadcast?, @JsonProperty("broadcast") val broadcast: Broadcast?,
@JsonProperty("nsfw") val nsfw: String?, @JsonProperty("nsfw") val nsfw: String?,
@JsonProperty("created_at") val created_at: String?, @JsonProperty("created_at") val createdAt: String?,
@JsonProperty("updated_at") val updated_at: String? @JsonProperty("updated_at") val updatedAt: String?
) )
data class ListStatus( data class ListStatus(
@JsonProperty("status") val status: String?, @JsonProperty("status") val status: String?,
@JsonProperty("score") val score: Int, @JsonProperty("score") val score: Int,
@JsonProperty("num_episodes_watched") val num_episodes_watched: Int, @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int,
@JsonProperty("is_rewatching") val is_rewatching: Boolean, @JsonProperty("is_rewatching") val isRewatching: Boolean,
@JsonProperty("updated_at") val updated_at: String, @JsonProperty("updated_at") val updatedAt: String,
) )
data class Data( data class Data(
@JsonProperty("node") val node: Node, @JsonProperty("node") val node: Node,
@JsonProperty("list_status") val list_status: ListStatus?, @JsonProperty("list_status") val listStatus: ListStatus?,
) { ) {
fun toLibraryItem(): SyncAPI.LibraryItem { fun toLibraryItem(): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem( return SyncAPI.LibraryItem(
this.node.title, this.node.title,
"https://myanimelist.net/anime/${this.node.id}/", "https://myanimelist.net/anime/${this.node.id}/",
this.node.id.toString(), this.node.id.toString(),
this.list_status?.num_episodes_watched, this.listStatus?.numEpisodesWatched,
this.node.num_episodes, this.node.numEpisodes,
this.list_status?.score?.times(10), this.listStatus?.score?.times(10),
parseDateLong(this.list_status?.updated_at), parseDateLong(this.listStatus?.updatedAt),
"MAL", "MAL",
TvType.Anime, TvType.Anime,
this.node.main_picture?.large ?: this.node.main_picture?.medium, this.node.mainPicture?.large ?: this.node.mainPicture?.medium,
null, null,
null, null,
plot = this.node.synopsis, plot = this.node.synopsis,
@ -470,8 +473,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
) )
data class Broadcast( data class Broadcast(
@JsonProperty("day_of_the_week") val day_of_the_week: String?, @JsonProperty("day_of_the_week") val dayOfTheWeek: String?,
@JsonProperty("start_time") val start_time: String? @JsonProperty("start_time") val startTime: String?
) )
private fun getMalAnimeListCached(): Array<Data>? { private fun getMalAnimeListCached(): Array<Data>? {
@ -491,14 +494,14 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata { override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
val list = getMalAnimeListSmart()?.groupBy { val list = getMalAnimeListSmart()?.groupBy {
convertToStatus(it.list_status?.status ?: "").stringRes convertToStatus(it.listStatus?.status ?: "").stringRes
}?.mapValues { group -> }?.mapValues { group ->
group.value.map { it.toLibraryItem() } group.value.map { it.toLibraryItem() }
} ?: emptyMap() } ?: emptyMap()
// To fill empty lists when MAL does not return them // To fill empty lists when MAL does not return them
val baseMap = val baseMap =
MalStatusType.values().filter { it.value >= 0 }.associate { MalStatusType.entries.filter { it.value >= 0 }.associate {
it.stringRes to emptyList<SyncAPI.LibraryItem>() it.stringRes to emptyList<SyncAPI.LibraryItem>()
} }
@ -573,7 +576,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
).text ).text
val values = parseJson<MalRoot>(res) val values = parseJson<MalRoot>(res)
val titles = 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) { for (t in titles) {
allTitles[t.id] = t allTitles[t.id] = t
} }
@ -582,11 +585,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? { private fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
// No time remaining if the show has already ended // No time remaining if the show has already ended
try { try {
endDate?.let { 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) { } catch (e: ParseException) {
logError(e) logError(e)
@ -603,7 +608,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH) val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH)
val currentYear = currentDate.get(Calendar.YEAR) 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") dateFormat.timeZone = TimeZone.getTimeZone("Japan")
val parsedDate = val parsedDate =
dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null
@ -647,13 +652,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
id: Int, id: Int,
status: MalStatusType? = null, status: MalStatusType? = null,
score: Int? = null, score: Int? = null,
num_watched_episodes: Int? = null, numWatchedEpisodes: Int? = null,
): Boolean { ): Boolean {
val res = setScoreRequest( val res = setScoreRequest(
id, id,
if (status == null) null else malStatusAsString[maxOf(0, status.value)], if (status == null) null else malStatusAsString[maxOf(0, status.value)],
score, score,
num_watched_episodes numWatchedEpisodes
) )
return if (res.isNullOrBlank()) { return if (res.isNullOrBlank()) {
@ -670,17 +675,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
@Suppress("UNCHECKED_CAST")
private suspend fun setScoreRequest( private suspend fun setScoreRequest(
id: Int, id: Int,
status: String? = null, status: String? = null,
score: Int? = null, score: Int? = null,
num_watched_episodes: Int? = null, numWatchedEpisodes: Int? = null,
): String? { ): String? {
val data = mapOf( val data = mapOf(
"status" to status, "status" to status,
"score" to score?.toString(), "score" to score?.toString(),
"num_watched_episodes" to num_watched_episodes?.toString() "num_watched_episodes" to numWatchedEpisodes?.toString()
).filter { it.value != null } as Map<String, String> ).filterValues { it != null } as Map<String, String>
return app.put( return app.put(
"$apiUrl/v2/anime/$id/my_list_status", "$apiUrl/v2/anime/$id/my_list_status",
@ -693,10 +699,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class ResponseToken( data class ResponseToken(
@JsonProperty("token_type") val token_type: String, @JsonProperty("token_type") val tokenType: String,
@JsonProperty("expires_in") val expires_in: Int, @JsonProperty("expires_in") val expiresIn: Int,
@JsonProperty("access_token") val access_token: String, @JsonProperty("access_token") val accessToken: String,
@JsonProperty("refresh_token") val refresh_token: String, @JsonProperty("refresh_token") val refreshToken: String,
) )
data class MalRoot( data class MalRoot(
@ -705,7 +711,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class MalDatum( data class MalDatum(
@JsonProperty("node") val node: MalNode, @JsonProperty("node") val node: MalNode,
@JsonProperty("list_status") val list_status: MalStatus, @JsonProperty("list_status") val listStatus: MalStatus,
) )
data class MalNode( data class MalNode(
@ -722,16 +728,16 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class MalStatus( data class MalStatus(
@JsonProperty("status") val status: String, @JsonProperty("status") val status: String,
@JsonProperty("score") val score: Int, @JsonProperty("score") val score: Int,
@JsonProperty("num_episodes_watched") val num_episodes_watched: Int, @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int,
@JsonProperty("is_rewatching") val is_rewatching: Boolean, @JsonProperty("is_rewatching") val isRewatching: Boolean,
@JsonProperty("updated_at") val updated_at: String, @JsonProperty("updated_at") val updatedAt: String,
) )
data class MalUser( data class MalUser(
@JsonProperty("id") val id: Int, @JsonProperty("id") val id: Int,
@JsonProperty("name") val name: String, @JsonProperty("name") val name: String,
@JsonProperty("location") val location: String, @JsonProperty("location") val location: String,
@JsonProperty("joined_at") val joined_at: String, @JsonProperty("joined_at") val joinedAt: String,
@JsonProperty("picture") val picture: String?, @JsonProperty("picture") val picture: String?,
) )
@ -744,9 +750,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class SmallMalAnime( data class SmallMalAnime(
@JsonProperty("id") val id: Int, @JsonProperty("id") val id: Int,
@JsonProperty("title") val title: String?, @JsonProperty("title") val title: String?,
@JsonProperty("num_episodes") val num_episodes: Int, @JsonProperty("num_episodes") val numEpisodes: Int,
@JsonProperty("my_list_status") val my_list_status: MalStatus?, @JsonProperty("my_list_status") val myListStatus: MalStatus?,
@JsonProperty("main_picture") val main_picture: MalMainPicture?, @JsonProperty("main_picture") val mainPicture: MalMainPicture?,
) )
data class MalSearchNode( 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.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
import com.lagradost.cloudstream3.utils.AppContextUtils
import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
@ -30,10 +29,10 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
companion object { companion object {
const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile
const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" const val API_KEY = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2"
const val host = "https://api.opensubtitles.com/api/v1" const val HOST = "https://api.opensubtitles.com/api/v1"
const val TAG = "OPENSUBS" 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 currentCoolDown: Long = 0L
var currentSession: SubtitleOAuthEntity? = null var currentSession: SubtitleOAuthEntity? = null
} }
@ -49,7 +48,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
chain.request().newBuilder() chain.request().newBuilder()
.removeHeader("user-agent") .removeHeader("user-agent")
.addHeader("user-agent", userAgent) .addHeader("user-agent", userAgent)
.addHeader("Api-Key", apiKey) .addHeader("Api-Key", API_KEY)
.build() .build()
) )
} }
@ -66,7 +65,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
} }
private fun throwGotTooManyRequests() { private fun throwGotTooManyRequests() {
currentCoolDown = unixTimeMs + coolDownDuration currentCoolDown = unixTimeMs + COOLDOWN_DURATION
throw ErrorLoadingException("Too many requests") 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 { private suspend fun initLogin(username: String, password: String): Boolean {
//Log.i(TAG, "DATA = [$username] [$password]") //Log.i(TAG, "DATA = [$username] [$password]")
val response = app.post( val response = app.post(
url = "$host/login", url = "$HOST/login",
headers = mapOf( headers = mapOf(
"Content-Type" to "application/json", "Content-Type" to "application/json",
), ),
@ -134,7 +133,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
SubtitleOAuthEntity( SubtitleOAuthEntity(
user = username, user = username,
pass = password, pass = password,
access_token = token.token ?: run { accessToken = token.token ?: run {
return false return false
}) })
) )
@ -197,8 +196,8 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
val searchQueryUrl = when (imdbId > 0) { val searchQueryUrl = when (imdbId > 0) {
//Use imdb_id to search if its valid //Use imdb_id to search if its valid
true -> "$host/subtitles?imdb_id=$imdbId&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" false -> "$HOST/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
} }
val req = app.get( val req = app.get(
@ -233,7 +232,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
val year = featureDetails?.year ?: query.year val year = featureDetails?.year ?: query.year
val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie 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") //Log.i(TAG, "Result id/name => ${item.id} / $name")
item.attributes?.files?.forEach { file -> item.attributes?.files?.forEach { file ->
val resultData = file.fileId?.toString() ?: "" val resultData = file.fileId?.toString() ?: ""
@ -266,11 +265,11 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
throwIfCantDoRequest() throwIfCantDoRequest()
val req = app.post( val req = app.post(
url = "$host/download", url = "$HOST/download",
headers = mapOf( headers = mapOf(
Pair( Pair(
"Authorization", "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("Content-Type", "application/json"),
Pair("Accept", "*/*") Pair("Accept", "*/*")
@ -299,7 +298,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
data class SubtitleOAuthEntity( data class SubtitleOAuthEntity(
var user: String, var user: String,
var pass: String, var pass: String,
var access_token: String, var accessToken: String,
) )
data class OAuthToken( data class OAuthToken(
@ -324,7 +323,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
@JsonProperty("url") var url: String? = null, @JsonProperty("url") var url: String? = null,
@JsonProperty("files") var files: List<ResultFiles>? = listOf(), @JsonProperty("files") var files: List<ResultFiles>? = listOf(),
@JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(), @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( data class ResultFiles(

View file

@ -38,6 +38,7 @@ import java.security.SecureRandom
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.Instant import java.time.Instant
import java.util.Date import java.util.Date
import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
@ -144,8 +145,8 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
} }
companion object { companion object {
private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID private const val CLIENT_ID: String = BuildConfig.SIMKL_CLIENT_ID
private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET private const val CLIENT_SECRET: String = BuildConfig.SIMKL_CLIENT_SECRET
private var lastLoginState = "" private var lastLoginState = ""
const val SIMKL_TOKEN_KEY: String = "simkl_token" const val SIMKL_TOKEN_KEY: String = "simkl_token"
@ -154,10 +155,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time" const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time"
/** 2014-09-01T09:10:11Z -> 1409562611 */ /** 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? { fun getUnixTime(string: String?): Long? {
return try { return try {
SimpleDateFormat(simklDateFormat).apply { SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply {
this.timeZone = TimeZone.getTimeZone("UTC") this.timeZone = TimeZone.getTimeZone("UTC")
}.parse( }.parse(
string ?: return null string ?: return null
@ -171,7 +172,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
/** 1409562611 -> 2014-09-01T09:10:11Z */ /** 1409562611 -> 2014-09-01T09:10:11Z */
fun getDateTime(unixTime: Long?): String? { fun getDateTime(unixTime: Long?): String? {
return try { return try {
SimpleDateFormat(simklDateFormat).apply { SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply {
this.timeZone = TimeZone.getTimeZone("UTC") this.timeZone = TimeZone.getTimeZone("UTC")
}.format( }.format(
Date.from( Date.from(
@ -208,7 +209,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
companion object { companion object {
fun fromString(string: String): SimklListStatusType? { fun fromString(string: String): SimklListStatusType? {
return SimklListStatusType.values().firstOrNull { return SimklListStatusType.entries.firstOrNull {
it.originalName == string it.originalName == string
} }
} }
@ -219,17 +220,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonInclude(JsonInclude.Include.NON_EMPTY)
data class TokenRequest( data class TokenRequest(
@JsonProperty("code") val code: String, @JsonProperty("code") val code: String,
@JsonProperty("client_id") val client_id: String = clientId, @JsonProperty("client_id") val clientId: String = CLIENT_ID,
@JsonProperty("client_secret") val client_secret: String = clientSecret, @JsonProperty("client_secret") val clientSecret: String = CLIENT_SECRET,
@JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl", @JsonProperty("redirect_uri") val redirectUri: String = "$APP_STRING://simkl",
@JsonProperty("grant_type") val grant_type: String = "authorization_code" @JsonProperty("grant_type") val grantType: String = "authorization_code"
) )
data class TokenResponse( data class TokenResponse(
/** No expiration date */ /** No expiration date */
val access_token: String, @JsonProperty("access_token") val accessToken: String,
val token_type: String, @JsonProperty("token_type") val tokenType: String,
val scope: String @JsonProperty("scope") val scope: String
) )
// ------------------- // -------------------
@ -261,15 +262,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
// ------------------- // -------------------
data class ActivitiesResponse( data class ActivitiesResponse(
val all: String?, @JsonProperty("all") val all: String?,
val tv_shows: UpdatedAt, @JsonProperty("tv_shows") val tvShows: UpdatedAt,
val anime: UpdatedAt, @JsonProperty("anime") val anime: UpdatedAt,
val movies: UpdatedAt, @JsonProperty("movies") val movies: UpdatedAt,
) { ) {
data class UpdatedAt( data class UpdatedAt(
val all: String?, @JsonProperty("all") val all: String?,
val removed_from_list: String?, @JsonProperty("removed_from_list") val removedFromList: String?,
val rated_at: String?, @JsonProperty("rated_at") val ratedAt: String?,
) )
} }
@ -308,7 +309,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("title") val title: String?, @JsonProperty("title") val title: String?,
@JsonProperty("year") val year: Int?, @JsonProperty("year") val year: Int?,
@JsonProperty("ids") val ids: Ids?, @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("status") val status: String? = null,
@JsonProperty("poster") val poster: String? = null, @JsonProperty("poster") val poster: String? = null,
@JsonProperty("type") val type: String? = null, @JsonProperty("type") val type: String? = null,
@ -540,7 +541,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
} }
debugPrint { "Requesting episodes from $url" } 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 { .parsedSafe<Array<EpisodeMetadata>>()?.also {
val cacheTime = val cacheTime =
if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value if (hasEnded == true) SimklCache.CacheTimes.OneMonth.value else SimklCache.CacheTimes.ThirtyMinutes.value
@ -558,7 +559,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("seasons") seasons: List<Season>? = null, @JsonProperty("seasons") seasons: List<Season>? = null,
@JsonProperty("episodes") episodes: List<Season.Episode>? = null, @JsonProperty("episodes") episodes: List<Season.Episode>? = null,
@JsonProperty("rating") val rating: Int? = 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) ) : MediaObject(title, year, ids, seasons = seasons, episodes = episodes)
@JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonInclude(JsonInclude.Include.NON_EMPTY)
@ -567,7 +568,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("year") year: Int?, @JsonProperty("year") year: Int?,
@JsonProperty("ids") ids: Ids?, @JsonProperty("ids") ids: Ids?,
@JsonProperty("rating") val rating: Int, @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) ) : MediaObject(title, year, ids)
@JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonInclude(JsonInclude.Include.NON_EMPTY)
@ -576,7 +577,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("year") year: Int?, @JsonProperty("year") year: Int?,
@JsonProperty("ids") ids: Ids?, @JsonProperty("ids") ids: Ids?,
@JsonProperty("to") val to: String, @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) ) : MediaObject(title, year, ids)
@JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonInclude(JsonInclude.Include.NON_EMPTY)
@ -631,24 +632,24 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
} }
interface Metadata { interface Metadata {
val last_watched_at: String? val lastWatchedAt: String?
val status: String? val status: String?
val user_rating: Int? val userRating: Int?
val last_watched: String? val lastWatched: String?
val watched_episodes_count: Int? val watchedEpisodesCount: Int?
val total_episodes_count: Int? val totalEpisodesCount: Int?
fun getIds(): ShowMetadata.Show.Ids fun getIds(): ShowMetadata.Show.Ids
fun toLibraryItem(): SyncAPI.LibraryItem fun toLibraryItem(): SyncAPI.LibraryItem
} }
data class MovieMetadata( data class MovieMetadata(
override val last_watched_at: String?, @JsonProperty("last_watched_at") override val lastWatchedAt: String?,
override val status: String, @JsonProperty("status") override val status: String,
override val user_rating: Int?, @JsonProperty("user_rating") override val userRating: Int?,
override val last_watched: String?, @JsonProperty("last_watched") override val lastWatched: String?,
override val watched_episodes_count: Int?, @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?,
override val total_episodes_count: Int?, @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?,
val movie: ShowMetadata.Show val movie: ShowMetadata.Show
) : Metadata { ) : Metadata {
override fun getIds(): ShowMetadata.Show.Ids { override fun getIds(): ShowMetadata.Show.Ids {
@ -660,10 +661,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
this.movie.title, this.movie.title,
"https://simkl.com/tv/${movie.ids.simkl}", "https://simkl.com/tv/${movie.ids.simkl}",
movie.ids.simkl.toString(), movie.ids.simkl.toString(),
this.watched_episodes_count, this.watchedEpisodesCount,
this.total_episodes_count, this.totalEpisodesCount,
this.user_rating?.times(10), this.userRating?.times(10),
getUnixTime(last_watched_at) ?: 0, getUnixTime(lastWatchedAt) ?: 0,
"Simkl", "Simkl",
TvType.Movie, TvType.Movie,
this.movie.poster?.let { getPosterUrl(it) }, this.movie.poster?.let { getPosterUrl(it) },
@ -675,12 +676,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
} }
data class ShowMetadata( 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("status") override val status: String,
@JsonProperty("user_rating") override val user_rating: Int?, @JsonProperty("user_rating") override val userRating: Int?,
@JsonProperty("last_watched") override val last_watched: String?, @JsonProperty("last_watched") override val lastWatched: String?,
@JsonProperty("watched_episodes_count") override val watched_episodes_count: Int?, @JsonProperty("watched_episodes_count") override val watchedEpisodesCount: Int?,
@JsonProperty("total_episodes_count") override val total_episodes_count: Int?, @JsonProperty("total_episodes_count") override val totalEpisodesCount: Int?,
@JsonProperty("show") val show: Show @JsonProperty("show") val show: Show
) : Metadata { ) : Metadata {
override fun getIds(): Show.Ids { override fun getIds(): Show.Ids {
@ -692,10 +693,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
this.show.title, this.show.title,
"https://simkl.com/tv/${show.ids.simkl}", "https://simkl.com/tv/${show.ids.simkl}",
show.ids.simkl.toString(), show.ids.simkl.toString(),
this.watched_episodes_count, this.watchedEpisodesCount,
this.total_episodes_count, this.totalEpisodesCount,
this.user_rating?.times(10), this.userRating?.times(10),
getUnixTime(last_watched_at) ?: 0, getUnixTime(lastWatchedAt) ?: 0,
"Simkl", "Simkl",
TvType.Anime, TvType.Anime,
this.show.poster?.let { getPosterUrl(it) }, this.show.poster?.let { getPosterUrl(it) },
@ -749,7 +750,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
chain.request() chain.request()
.newBuilder() .newBuilder()
.addHeader("Authorization", "Bearer $token") .addHeader("Authorization", "Bearer $token")
.addHeader("simkl-api-key", clientId) .addHeader("simkl-api-key", CLIENT_ID)
.build() .build()
) )
} }
@ -810,7 +811,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
val episodeConstructor = SimklEpisodeConstructor( val episodeConstructor = SimklEpisodeConstructor(
searchResult.ids?.simkl, searchResult.ids?.simkl,
searchResult.type, searchResult.type,
searchResult.total_episodes, searchResult.totalEpisodes,
searchResult.hasEnded() searchResult.hasEnded()
) )
@ -832,12 +833,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
) )
} }
?: return null, ?: return null,
score = foundItem.user_rating, score = foundItem.userRating,
watchedEpisodes = foundItem.watched_episodes_count, watchedEpisodes = foundItem.watchedEpisodesCount,
maxEpisodes = searchResult.total_episodes, maxEpisodes = searchResult.totalEpisodes,
episodeConstructor = episodeConstructor, episodeConstructor = episodeConstructor,
oldEpisodes = foundItem.watched_episodes_count ?: 0, oldEpisodes = foundItem.watchedEpisodesCount ?: 0,
oldScore = foundItem.user_rating, oldScore = foundItem.userRating,
oldStatus = foundItem.status oldStatus = foundItem.status
) )
} else { } else {
@ -845,7 +846,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
status = SyncWatchType.fromInternalId(SimklListStatusType.None.value), status = SyncWatchType.fromInternalId(SimklListStatusType.None.value),
score = 0, score = 0,
watchedEpisodes = 0, watchedEpisodes = 0,
maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.total_episodes, maxEpisodes = if (searchResult.type == "movie") 0 else searchResult.totalEpisodes,
episodeConstructor = episodeConstructor, episodeConstructor = episodeConstructor,
oldEpisodes = 0, oldEpisodes = 0,
oldStatus = null, oldStatus = null,
@ -891,12 +892,12 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
/** See https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id */ /** 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() if (serviceMap.isEmpty()) return emptyArray()
return app.get( return app.get(
"$mainUrl/search/id", "$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 service.originalName to id
} }
).parsedSafe() ).parsedSafe()
@ -904,14 +905,14 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? { override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
return app.get( 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() } ).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() }
} }
override fun authenticate(activity: FragmentActivity?) { override fun authenticate(activity: FragmentActivity?) {
lastLoginState = BigInteger(130, SecureRandom()).toString(32) lastLoginState = BigInteger(130, SecureRandom()).toString(32)
val url = 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) openBrowser(url, activity)
} }
@ -961,15 +962,15 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
val activities = getActivities() val activities = getActivities()
val lastCacheUpdate = getKey<Long>(accountId, SIMKL_CACHED_LIST_TIME) val lastCacheUpdate = getKey<Long>(accountId, SIMKL_CACHED_LIST_TIME)
val lastRemoval = listOf( val lastRemoval = listOf(
activities?.tv_shows?.removed_from_list, activities?.tvShows?.removedFromList,
activities?.anime?.removed_from_list, activities?.anime?.removedFromList,
activities?.movies?.removed_from_list activities?.movies?.removedFromList
).maxOf { ).maxOf {
getUnixTime(it) ?: -1 getUnixTime(it) ?: -1
} }
val lastRealUpdate = val lastRealUpdate =
listOf( listOf(
activities?.tv_shows?.all, activities?.tvShows?.all,
activities?.anime?.all, activities?.anime?.all,
activities?.movies?.all, activities?.movies?.all,
).maxOf { ).maxOf {
@ -1039,7 +1040,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun getDevicePin(): OAuth2API.PinAuthData? { override suspend fun getDevicePin(): OAuth2API.PinAuthData? {
val pinAuthResp = app.get( 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 ).parsedSafe<PinAuthResponse>() ?: return null
return OAuth2API.PinAuthData( return OAuth2API.PinAuthData(
@ -1053,7 +1054,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean { override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean {
val pinAuthResp = app.get( 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 ).parsedSafe<PinExchangeResponse>() ?: return false
if (pinAuthResp.accessToken != null) { if (pinAuthResp.accessToken != null) {
@ -1088,7 +1089,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
).parsedSafe<TokenResponse>() ?: return false ).parsedSafe<TokenResponse>() ?: return false
switchToNewAccount() switchToNewAccount()
setKey(accountId, SIMKL_TOKEN_KEY, token.access_token) setKey(accountId, SIMKL_TOKEN_KEY, token.accessToken)
val user = getUser() val user = getUser()
if (user == null) { if (user == null) {

View file

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

View file

@ -50,7 +50,7 @@ class APIRepository(val api: MainAPI) {
private val cache = threadSafeListOf<SavedLoadResponse>() private val cache = threadSafeListOf<SavedLoadResponse>()
private var cacheIndex: Int = 0 private var cacheIndex: Int = 0
const val cacheSize = 20 const val CACHE_SIZE = 20
} }
private fun afterPluginsLoaded(forceReload: Boolean) { private fun afterPluginsLoaded(forceReload: Boolean) {
@ -94,9 +94,9 @@ class APIRepository(val api: MainAPI) {
val add = SavedLoadResponse(unixTime, response, lookingForHash) val add = SavedLoadResponse(unixTime, response, lookingForHash)
synchronized(cache) { synchronized(cache) {
if (cache.size > cacheSize) { if (cache.size > CACHE_SIZE) {
cache[cacheIndex] = add // rolling cache cache[cacheIndex] = add // rolling cache
cacheIndex = (cacheIndex + 1) % cacheSize cacheIndex = (cacheIndex + 1) % CACHE_SIZE
} else { } else {
cache.add(add) cache.add(add)
} }

View file

@ -112,6 +112,7 @@ abstract class BaseAdapter<
holder.onViewDetachedFromWindow() holder.onViewDetachedFromWindow()
} }
@Suppress("UNCHECKED_CAST")
fun save(recyclerView: RecyclerView) { fun save(recyclerView: RecyclerView) {
for (child in recyclerView.children) { for (child in recyclerView.children) {
val holder = val holder =
@ -124,6 +125,7 @@ abstract class BaseAdapter<
stateViewModel.layoutManagerStates[id]?.clear() stateViewModel.layoutManagerStates[id]?.clear()
} }
@Suppress("UNCHECKED_CAST")
private fun getState(holder: ViewHolderState<S>): S? = private fun getState(holder: ViewHolderState<S>): S? =
stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? 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.view.View.*
import android.widget.* import android.widget.*
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.media3.common.util.UnstableApi
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.kotlinModule import com.fasterxml.jackson.module.kotlin.kotlinModule
@ -263,6 +264,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
var isLoadingMore = false var isLoadingMore = false
override fun onMediaStatusUpdated() { override fun onMediaStatusUpdated() {
super.onMediaStatusUpdated() super.onMediaStatusUpdated()
val meta = getCurrentMetaData() val meta = getCurrentMetaData()

View file

@ -8,8 +8,8 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs import kotlin.math.abs
class GrdLayoutManager(val context: Context, _spanCount: Int) : class GrdLayoutManager(val context: Context, spanCount: Int) :
GridLayoutManager(context, _spanCount) { GridLayoutManager(context, spanCount) {
override fun onFocusSearchFailed( override fun onFocusSearchFailed(
focused: View, focused: View,
focusDirection: Int, focusDirection: Int,

View file

@ -51,7 +51,7 @@ class EasterEggMonke : AppCompatActivity() {
FrameLayout.LayoutParams.WRAP_CONTENT) FrameLayout.LayoutParams.WRAP_CONTENT)
binding.frame.addView(newStar) binding.frame.addView(newStar)
newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX newStar.scaleX += Math.random().toFloat() * 1.5f
newStar.scaleY = newStar.scaleX newStar.scaleY = newStar.scaleX
starW *= newStar.scaleX starW *= newStar.scaleX
starH *= newStar.scaleY starH *= newStar.scaleY

View file

@ -15,7 +15,7 @@ open class NonFinalAdapterListUpdateCallback
/** /**
* Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter. * 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<*>) : */(private var mAdapter: RecyclerView.Adapter<*>) :
ListUpdateCallback { 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); NONE(5, R.string.type_none, R.drawable.ic_baseline_add_24);
companion object { 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); REWATCHING(5, R.string.type_re_watching, R.drawable.ic_baseline_bookmark_24);
companion object { 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.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.annotation.OptIn
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.media3.common.util.UnstableApi
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
@ -29,6 +31,7 @@ class WebviewFragment : Fragment() {
} }
binding?.webView?.webViewClient = object : WebViewClient() { binding?.webView?.webViewClient = object : WebViewClient() {
@OptIn(UnstableApi::class)
override fun shouldOverrideUrlLoading( override fun shouldOverrideUrlLoading(
view: WebView?, view: WebView?,
request: WebResourceRequest? request: WebResourceRequest?

View file

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

View file

@ -13,7 +13,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
class DownloadButton(context: Context, attributeSet: AttributeSet) : class DownloadButton(context: Context, attributeSet: AttributeSet) :
PieFetchButton(context, attributeSet) { PieFetchButton(context, attributeSet) {
var mainText: TextView? = null private var mainText: TextView? = null
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
progressText = findViewById(R.id.result_movie_download_text_precentage) 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.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout 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 import com.lagradost.cloudstream3.utils.UIHelper.toPx
class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState<Boolean>(view) { class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState<Boolean>(view) {
@ -54,7 +54,7 @@ class HomeChildItemAdapter(
var hasNext: Boolean = false var hasNext: Boolean = false
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Boolean> { 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 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) val root = LayoutInflater.from(parent.context).inflate(layout, parent, false)
@ -133,7 +133,6 @@ class HomeChildItemAdapter(
item, item,
position, position,
holder.itemView, holder.itemView,
null, // nextFocusBehavior,
nextFocusUp, nextFocusUp,
nextFocusDown nextFocusDown
) )

View file

@ -17,7 +17,6 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.*
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -234,7 +233,7 @@ class HomeFragment : Fragment() {
return bottomSheetDialogBuilder return bottomSheetDialogBuilder
} }
fun getPairList( private fun getPairList(
anime: Chip?, anime: Chip?,
cartoons: Chip?, cartoons: Chip?,
tvs: Chip?, tvs: Chip?,

View file

@ -1,6 +1,8 @@
package com.lagradost.cloudstream3.ui.home package com.lagradost.cloudstream3.ui.home
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -53,12 +55,12 @@ open class ParentItemAdapter(
"value", "value",
recyclerView?.layoutManager?.onSaveInstanceState() recyclerView?.layoutManager?.onSaveInstanceState()
) )
(recyclerView?.adapter as? BaseAdapter<*,*>)?.save(recyclerView) (recyclerView?.adapter as? BaseAdapter<*, *>)?.save(recyclerView)
} }
override fun restore(state: Bundle) { override fun restore(state: Bundle) {
(binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState( (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) } submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
.toMutableList()) .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) { override fun restore(state: Bundle) {
state.getParcelable<Parcelable>("resumeRecyclerView")?.let { recycle -> state.getSafeParcelable<Parcelable>("resumeRecyclerView")?.let { recycle ->
resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle) resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
} }
state.getParcelable<Parcelable>("bookmarkRecyclerView")?.let { recycle -> state.getSafeParcelable<Parcelable>("bookmarkRecyclerView")?.let { recycle ->
bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle) bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
} }
//state.getInt("previewViewpager").let { recycle ->
// previewViewpager.setCurrentItem(recycle,true)
//}
} }
val previewAdapter = HomeScrollAdapter(fragment = fragment) val previewAdapter = HomeScrollAdapter(fragment = fragment)

View file

@ -152,7 +152,7 @@ class HomeViewModel : ViewModel() {
} }
}?.distinctBy { it.first } ?: return@launchSafe }?.distinctBy { it.first } ?: return@launchSafe
val length = WatchType.values().size val length = WatchType.entries.size
val currentWatchTypes = mutableSetOf<WatchType>() val currentWatchTypes = mutableSetOf<WatchType>()
for (watch in watchStatusIds) { for (watch in watchStatusIds) {
@ -387,7 +387,9 @@ class HomeViewModel : ViewModel() {
} }
is Resource.Failure -> { is Resource.Failure -> {
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
_page.postValue(data!!) _page.postValue(data!!)
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
_preview.postValue(data!!) _preview.postValue(data!!)
} }
@ -397,9 +399,7 @@ class HomeViewModel : ViewModel() {
} }
fun click(callback: SearchClickCallback) { fun click(callback: SearchClickCallback) {
if (callback.action == SEARCH_ACTION_FOCUSED) { if (callback.action != SEARCH_ACTION_FOCUSED) {
//focusCallback(callback.card)
} else {
SearchHelper.handleSearchClickCallback(callback) SearchHelper.handleSearchClickCallback(callback)
} }
} }
@ -516,7 +516,7 @@ class HomeViewModel : ViewModel() {
} else { } else {
_page.postValue(Resource.Loading()) _page.postValue(Resource.Loading())
if (preferredApiName != null) if (preferredApiName != null)
_apiName.postValue(preferredApiName) _apiName.postValue(preferredApiName!!)
} }
} else { } else {
// if the api is found, then set it to it and save key // 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) { class MenuSearchView(context: Context) : SearchView(context)
override fun onActionViewCollapsed() {
super.onActionViewCollapsed()
}
}

View file

@ -15,6 +15,7 @@ import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.BaseAdapter import com.lagradost.cloudstream3.ui.BaseAdapter
import com.lagradost.cloudstream3.ui.BaseDiffCallback import com.lagradost.cloudstream3.ui.BaseDiffCallback
import com.lagradost.cloudstream3.ui.ViewHolderState 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.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.TV
@ -32,7 +33,7 @@ class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding)
} }
override fun restore(state: Bundle) { override fun restore(state: Bundle) {
state.getParcelable<Parcelable>("pageRecyclerview")?.let { recycle -> state.getSafeParcelable<Parcelable>("pageRecyclerview")?.let { recycle ->
binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle) binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle)
} }
} }

View file

@ -25,6 +25,7 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.ui.* import androidx.media3.ui.*
@ -216,7 +217,7 @@ abstract class AbstractPlayerFragment(
return return
} }
player.handleEvent( player.handleEvent(
CSPlayerEvent.values()[intent.getIntExtra( CSPlayerEvent.entries[intent.getIntExtra(
EXTRA_CONTROL_TYPE, EXTRA_CONTROL_TYPE,
0 0
)], source = PlayerEventSource.UI )], source = PlayerEventSource.UI
@ -603,12 +604,12 @@ abstract class AbstractPlayerFragment(
} }
fun nextResize() { fun nextResize() {
resizeMode = (resizeMode + 1) % PlayerResize.values().size resizeMode = (resizeMode + 1) % PlayerResize.entries.size
resize(resizeMode, true) resize(resizeMode, true)
} }
fun resize(resize: Int, showToast: Boolean) { fun resize(resize: Int, showToast: Boolean) {
resize(PlayerResize.values()[resize], showToast) resize(PlayerResize.entries[resize], showToast)
} }
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")

View file

@ -9,7 +9,11 @@ import android.os.Looper
import android.util.Log import android.util.Log
import android.util.Rational import android.util.Rational
import android.widget.FrameLayout 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.Format
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes import androidx.media3.common.MimeTypes
@ -19,9 +23,10 @@ import androidx.media3.common.TrackGroup
import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.Tracks import androidx.media3.common.Tracks
import androidx.media3.common.VideoSize import androidx.media3.common.VideoSize
import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSourceFactory import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cache.CacheDataSource 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.ExtractorLinkType
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import java.io.File import java.io.File
import java.lang.IllegalArgumentException
import java.util.UUID import java.util.UUID
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
@ -84,7 +88,7 @@ const val toleranceBeforeUs = 300_000L
* seek position, in microseconds. Must be non-negative. * seek position, in microseconds. Must be non-negative.
*/ */
const val toleranceAfterUs = 300_000L const val toleranceAfterUs = 300_000L
@OptIn(UnstableApi::class)
class CS3IPlayer : IPlayer { class CS3IPlayer : IPlayer {
private var isPlaying = false private var isPlaying = false
private var exoPlayer: ExoPlayer? = null private var exoPlayer: ExoPlayer? = null
@ -257,7 +261,6 @@ class CS3IPlayer : IPlayer {
private var currentSubtitles: SubtitleData? = null private var currentSubtitles: SubtitleData? = null
@SuppressLint("UnsafeOptInUsageError")
private fun List<Tracks.Group>.getTrack(id: String?): Pair<TrackGroup, Int>? { private fun List<Tracks.Group>.getTrack(id: String?): Pair<TrackGroup, Int>? {
if (id == null) return null if (id == null) return null
// This beast of an expression does: // This beast of an expression does:
@ -342,7 +345,6 @@ class CS3IPlayer : IPlayer {
}.flatten() }.flatten()
} }
@SuppressLint("UnsafeOptInUsageError")
private fun Tracks.Group.getFormats(): List<Pair<Format, Int>> { private fun Tracks.Group.getFormats(): List<Pair<Format, Int>> {
return (0 until this.mediaTrackGroup.length).mapNotNull { i -> return (0 until this.mediaTrackGroup.length).mapNotNull { i ->
if (this.isSupported) if (this.isSupported)
@ -371,7 +373,6 @@ class CS3IPlayer : IPlayer {
) )
} }
@SuppressLint("UnsafeOptInUsageError")
override fun getVideoTracks(): CurrentTracks { override fun getVideoTracks(): CurrentTracks {
val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList() val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList()
val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO } val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO }
@ -391,7 +392,6 @@ class CS3IPlayer : IPlayer {
/** /**
* @return True if the player should be reloaded * @return True if the player should be reloaded
* */ * */
@SuppressLint("UnsafeOptInUsageError")
override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean { override fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean {
Log.i(TAG, "setPreferredSubtitles init $subtitle") Log.i(TAG, "setPreferredSubtitles init $subtitle")
currentSubtitles = subtitle currentSubtitles = subtitle
@ -451,7 +451,7 @@ class CS3IPlayer : IPlayer {
} ?: false } ?: false
} }
var currentSubtitleOffset: Long = 0 private var currentSubtitleOffset: Long = 0
override fun setSubtitleOffset(offset: Long) { override fun setSubtitleOffset(offset: Long) {
currentSubtitleOffset = offset currentSubtitleOffset = offset
@ -459,7 +459,7 @@ class CS3IPlayer : IPlayer {
} }
override fun getSubtitleOffset(): Long { override fun getSubtitleOffset(): Long {
return currentSubtitleOffset //currentTextRenderer?.getRenderOffsetMs() ?: currentSubtitleOffset return currentSubtitleOffset
} }
override fun getCurrentPreferredSubtitle(): SubtitleData? { override fun getCurrentPreferredSubtitle(): SubtitleData? {
@ -470,7 +470,6 @@ class CS3IPlayer : IPlayer {
} }
} }
@SuppressLint("UnsafeOptInUsageError")
override fun getAspectRatio(): Rational? { override fun getAspectRatio(): Rational? {
return exoPlayer?.videoFormat?.let { format -> return exoPlayer?.videoFormat?.let { format ->
Rational(format.width, format.height) Rational(format.width, format.height)
@ -481,14 +480,13 @@ class CS3IPlayer : IPlayer {
subtitleHelper.setSubStyle(style) subtitleHelper.setSubStyle(style)
} }
@SuppressLint("UnsafeOptInUsageError")
override fun saveData() { override fun saveData() {
Log.i(TAG, "saveData") Log.i(TAG, "saveData")
updatedTime() updatedTime()
exoPlayer?.let { exo -> exoPlayer?.let { exo ->
playbackPosition = exo.currentPosition playbackPosition = exo.currentPosition
currentWindow = exo.currentWindowIndex currentWindow = exo.currentMediaItemIndex
isPlaying = exo.isPlaying isPlaying = exo.isPlaying
} }
} }
@ -500,7 +498,7 @@ class CS3IPlayer : IPlayer {
updatedTime() updatedTime()
exoPlayer?.apply { exoPlayer?.apply {
setPlayWhenReady(false) playWhenReady = false
stop() stop()
release() release()
} }
@ -563,7 +561,6 @@ class CS3IPlayer : IPlayer {
var requestSubtitleUpdate: (() -> Unit)? = null var requestSubtitleUpdate: (() -> Unit)? = null
@SuppressLint("UnsafeOptInUsageError")
private fun createOnlineSource(headers: Map<String, String>): HttpDataSource.Factory { private fun createOnlineSource(headers: Map<String, String>): HttpDataSource.Factory {
val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT) val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT)
return source.apply { return source.apply {
@ -571,7 +568,6 @@ class CS3IPlayer : IPlayer {
} }
} }
@SuppressLint("UnsafeOptInUsageError")
private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory { private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory {
val provider = getApiFromNameNull(link.source) val provider = getApiFromNameNull(link.source)
val interceptor = provider?.getVideoInterceptor(link) val interceptor = provider?.getVideoInterceptor(link)
@ -604,53 +600,10 @@ class CS3IPlayer : IPlayer {
} }
} }
@SuppressLint("UnsafeOptInUsageError")
private fun Context.createOfflineSource(): DataSource.Factory { 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? { private fun getCache(context: Context, cacheSize: Long): SimpleCache? {
return try { return try {
val databaseProvider = StandaloneDatabaseProvider(context) val databaseProvider = StandaloneDatabaseProvider(context)
@ -682,7 +635,6 @@ class CS3IPlayer : IPlayer {
return getMediaItemBuilder(mimeType).setUri(url).build() return getMediaItemBuilder(mimeType).setUri(url).build()
} }
@SuppressLint("UnsafeOptInUsageError")
private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector { private fun getTrackSelector(context: Context, maxVideoHeight: Int?): TrackSelector {
val trackSelector = DefaultTrackSelector(context) val trackSelector = DefaultTrackSelector(context)
trackSelector.parameters = trackSelector.buildUponParameters() trackSelector.parameters = trackSelector.buildUponParameters()
@ -696,7 +648,6 @@ class CS3IPlayer : IPlayer {
var currentTextRenderer: CustomTextRenderer? = null var currentTextRenderer: CustomTextRenderer? = null
@SuppressLint("UnsafeOptInUsageError")
private fun buildExoPlayer( private fun buildExoPlayer(
context: Context, context: Context,
mediaItemSlices: List<MediaItemSlice>, mediaItemSlices: List<MediaItemSlice>,
@ -736,7 +687,7 @@ class CS3IPlayer : IPlayer {
textRendererOutput, textRendererOutput,
eventHandler.looper, eventHandler.looper,
CustomSubtitleDecoderFactory() CustomSubtitleDecoderFactory()
).also { this.currentTextRenderer = it } ).also { renderer -> this.currentTextRenderer = renderer }
currentTextRenderer currentTextRenderer
} else it } else it
}.toTypedArray() }.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) { override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
exoPlayer?.let { exo -> exoPlayer?.let { exo ->
event( event(
@ -1169,7 +1120,6 @@ class CS3IPlayer : IPlayer {
private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList() private var lastTimeStamps: List<EpisodeSkip.SkipStamp> = emptyList()
@SuppressLint("UnsafeOptInUsageError")
override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) { override fun addTimeStamps(timeStamps: List<EpisodeSkip.SkipStamp>) {
lastTimeStamps = timeStamps lastTimeStamps = timeStamps
timeStamps.forEach { timestamp -> timeStamps.forEach { timestamp ->
@ -1187,7 +1137,6 @@ class CS3IPlayer : IPlayer {
updatedTime(source = PlayerEventSource.Player) updatedTime(source = PlayerEventSource.Player)
} }
@SuppressLint("UnsafeOptInUsageError")
fun onRenderFirst() { fun onRenderFirst() {
if (hasUsedFirstRender) { // this insures that we only call this once per player load if (hasUsedFirstRender) { // this insures that we only call this once per player load
return return
@ -1254,7 +1203,6 @@ class CS3IPlayer : IPlayer {
} }
} }
@SuppressLint("UnsafeOptInUsageError")
private fun getSubSources( private fun getSubSources(
onlineSourceFactory: HttpDataSource.Factory?, onlineSourceFactory: HttpDataSource.Factory?,
offlineSourceFactory: DataSource.Factory?, offlineSourceFactory: DataSource.Factory?,

View file

@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.annotation.OptIn
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.media3.common.Format import androidx.media3.common.Format
import androidx.media3.common.MimeTypes 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 * @param fallbackFormat used to create a decoder based on mimetype if the subtitle string is not
* enough to identify the subtitle format. * enough to identify the subtitle format.
**/ **/
@UnstableApi @OptIn(UnstableApi::class)
class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder { class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
companion object { companion object {
fun updateForcedEncoding(context: Context) { fun updateForcedEncoding(context: Context) {
@ -72,7 +73,7 @@ class CustomDecoder(private val fallbackFormat: Format?) : SubtitleDecoder {
RegexOption.IGNORE_CASE RegexOption.IGNORE_CASE
), ),
) )
val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\d\s]*?[])}]\s*""")) val captionRegex = listOf(Regex("""(-\s?|)[\[({][\w\s]*?[])}]\s*"""))
//https://emptycharacter.com/ //https://emptycharacter.com/
//https://www.fileformat.info/info/unicode/char/200b/index.htm //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 */ /** 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 { class CustomSubtitleDecoderFactory : SubtitleDecoderFactory {
override fun supportsFormat(format: Format): Boolean { override fun supportsFormat(format: Format): Boolean {
// return SubtitleDecoderFactory.DEFAULT.supportsFormat(format) // return SubtitleDecoderFactory.DEFAULT.supportsFormat(format)

View file

@ -1,11 +1,12 @@
package com.lagradost.cloudstream3.ui.player package com.lagradost.cloudstream3.ui.player
import android.os.Looper import android.os.Looper
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.text.SubtitleDecoderFactory import androidx.media3.exoplayer.text.SubtitleDecoderFactory
import androidx.media3.exoplayer.text.TextOutput import androidx.media3.exoplayer.text.TextOutput
@UnstableApi @OptIn(UnstableApi::class)
class CustomTextRenderer( class CustomTextRenderer(
offset: Long, offset: Long,
output: TextOutput?, output: TextOutput?,

View file

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

View file

@ -8,14 +8,10 @@ import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.R 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.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playLink
import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri import com.lagradost.cloudstream3.ui.player.OfflinePlaybackHelper.playUri
const val DTAG = "PlayerActivity"
class DownloadedPlayerActivity : AppCompatActivity() { class DownloadedPlayerActivity : AppCompatActivity() {
private val dTAG = "DownloadedPlayerAct" private val dTAG = "DownloadedPlayerAct"

View file

@ -25,6 +25,7 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO
import android.view.animation.AlphaAnimation import android.view.animation.AlphaAnimation
import android.view.animation.Animation import android.view.animation.Animation
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
import androidx.annotation.OptIn
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.graphics.blue import androidx.core.graphics.blue
@ -35,6 +36,7 @@ import androidx.core.view.isGone
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.media3.common.util.UnstableApi
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.keyEventListener
@ -50,7 +52,6 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvid
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.result.txt 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.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
@ -245,6 +246,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
fadeAnimation.duration = 100 fadeAnimation.duration = 100
fadeAnimation.fillAfter = true fadeAnimation.fillAfter = true
@OptIn(UnstableApi::class)
val sView = subView val sView = subView
val sStyle = subStyle val sStyle = subStyle
if (sView != null && sStyle != null) { if (sView != null && sStyle != null) {
@ -300,42 +302,40 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
private fun restoreOrientationWithSensor(activity: Activity) { private fun restoreOrientationWithSensor(activity: Activity) {
val currentOrientation = activity.resources.configuration.orientation val currentOrientation = activity.resources.configuration.orientation
var orientation = 0 val orientation = when (currentOrientation) {
when (currentOrientation) {
Configuration.ORIENTATION_LANDSCAPE -> Configuration.ORIENTATION_LANDSCAPE ->
orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED ->
orientation = dynamicOrientation()
Configuration.ORIENTATION_PORTRAIT -> Configuration.ORIENTATION_PORTRAIT ->
orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
else -> dynamicOrientation()
} }
activity.requestedOrientation = orientation activity.requestedOrientation = orientation
} }
private fun toggleOrientationWithSensor(activity: Activity) { private fun toggleOrientationWithSensor(activity: Activity) {
val currentOrientation = activity.resources.configuration.orientation val currentOrientation = activity.resources.configuration.orientation
var orientation = 0 val orientation: Int = when (currentOrientation) {
when (currentOrientation) {
Configuration.ORIENTATION_LANDSCAPE -> Configuration.ORIENTATION_LANDSCAPE ->
orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED ->
orientation = dynamicOrientation()
Configuration.ORIENTATION_PORTRAIT -> Configuration.ORIENTATION_PORTRAIT ->
orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
else -> dynamicOrientation()
} }
activity.requestedOrientation = orientation activity.requestedOrientation = orientation
} }
open fun lockOrientation(activity: Activity) { 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 (activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
else activity.display!!
val rotation = display.rotation val rotation = display.rotation
val currentOrientation = activity.resources.configuration.orientation val currentOrientation = activity.resources.configuration.orientation
var orientation = 0 val orientation: Int
when (currentOrientation) { when (currentOrientation) {
Configuration.ORIENTATION_LANDSCAPE -> Configuration.ORIENTATION_LANDSCAPE ->
orientation = orientation =
@ -344,15 +344,14 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
else else
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
Configuration.ORIENTATION_SQUARE, Configuration.ORIENTATION_UNDEFINED ->
orientation = dynamicOrientation()
Configuration.ORIENTATION_PORTRAIT -> Configuration.ORIENTATION_PORTRAIT ->
orientation = orientation =
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270)
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else else
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
else -> orientation = dynamicOrientation()
} }
activity.requestedOrientation = orientation activity.requestedOrientation = orientation
} }
@ -1167,6 +1166,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
return true return true
} }
@SuppressLint("GestureBackNavigation")
private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean { private fun handleKeyEvent(event: KeyEvent, hasNavigated: Boolean): Boolean {
if (hasNavigated) { if (hasNavigated) {
autoHide() autoHide()
@ -1581,7 +1581,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
} }
// cs3 is peak media center // cs3 is peak media center
setRemainingTimeCounter(durationMode || Globals.isLayout(Globals.TV)) setRemainingTimeCounter(durationMode || isLayout(TV))
playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ -> playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ ->
updateRemainingTime() updateRemainingTime()
} }

View file

@ -6,6 +6,7 @@ import android.app.Dialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
@ -13,6 +14,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.OptIn
import androidx.core.animation.addListener import androidx.core.animation.addListener
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isGone import androidx.core.view.isGone
@ -21,6 +23,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.media3.common.Format.NO_VALUE import androidx.media3.common.Format.NO_VALUE
import androidx.media3.common.MimeTypes import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey 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.cloudstream3.utils.UIHelper.toPx
import com.lagradost.safefile.SafeFile import com.lagradost.safefile.SafeFile
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import java.io.Serializable
import java.util.* import java.util.*
import kotlin.math.abs import kotlin.math.abs
@ -234,7 +238,7 @@ class GeneratorPlayer : FullScreenPlayer() {
private fun closestQuality(target: Int?): Qualities { private fun closestQuality(target: Int?): Qualities {
if (target == null) return Qualities.Unknown 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( private fun getLinkPriority(
@ -367,8 +371,6 @@ class GeneratorPlayer : FullScreenPlayer() {
binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE
binding.subtitleAdapter.adapter = arrayAdapter binding.subtitleAdapter.adapter = arrayAdapter
val adapter =
binding.subtitleAdapter.adapter as? ArrayAdapter<AbstractSubtitleEntities.SubtitleEntity>
binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ -> binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ ->
currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener
@ -379,8 +381,8 @@ class GeneratorPlayer : FullScreenPlayer() {
fun setSubtitlesList(list: List<AbstractSubtitleEntities.SubtitleEntity>) { fun setSubtitlesList(list: List<AbstractSubtitleEntities.SubtitleEntity>) {
currentSubtitles = list currentSubtitles = list
adapter?.clear() arrayAdapter.clear()
adapter?.addAll(currentSubtitles) arrayAdapter.addAll(currentSubtitles)
} }
val currentTempMeta = getMetaData() val currentTempMeta = getMetaData()
@ -522,7 +524,7 @@ class GeneratorPlayer : FullScreenPlayer() {
//TODO: Set year text from currently loaded movie on Player //TODO: Set year text from currently loaded movie on Player
//dialog.subtitles_search_year?.setText(currentTempMeta.year) //dialog.subtitles_search_year?.setText(currentTempMeta.year)
} }
@OptIn(UnstableApi::class)
private fun openSubPicker() { private fun openSubPicker() {
try { try {
subsPathPicker.launch( subsPathPicker.launch(
@ -795,7 +797,6 @@ class GeneratorPlayer : FullScreenPlayer() {
settingsManager.edit().putString( settingsManager.edit().putString(
ctx.getString(R.string.subtitles_encoding_key), prefValues[it] ctx.getString(R.string.subtitles_encoding_key), prefValues[it]
).apply() ).apply()
updateForcedEncoding(ctx) updateForcedEncoding(ctx)
dismiss() dismiss()
player.seekTime(-1) // to update subtitles, a dirty trick player.seekTime(-1) // to update subtitles, a dirty trick
@ -1290,7 +1291,7 @@ class GeneratorPlayer : FullScreenPlayer() {
private fun unwrapBundle(savedInstanceState: Bundle?) { private fun unwrapBundle(savedInstanceState: Bundle?) {
Log.i(TAG, "unwrapBundle = $savedInstanceState") Log.i(TAG, "unwrapBundle = $savedInstanceState")
savedInstanceState?.let { bundle -> 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 import com.lagradost.cloudstream3.utils.ExtractorLink
enum class PlayerEventType(val value: Int) { enum class PlayerEventType(val value: Int) {
//Stop(-1),
Pause(0), Pause(0),
Play(1), Play(1),
SeekForward(2), SeekForward(2),

View file

@ -4,7 +4,6 @@ import android.net.Uri
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.utils.ExtractorLink 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.INFER_TYPE
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor

View file

@ -29,6 +29,7 @@ import android.os.Message;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.text.Cue; 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 * obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s
* is delegated to a {@link TextOutput}. * is delegated to a {@link TextOutput}.
*/ */
@UnstableApi @OptIn(markerClass = UnstableApi.class)
public class NonFinalTextRenderer extends BaseRenderer implements Callback { public class NonFinalTextRenderer extends BaseRenderer implements Callback {
private static final String TAG = "TextRenderer"; 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 * @param trackType The track type that the renderer handles. One of the {@link C} {@code
* TRACK_TYPE_*} constants. * TRACK_TYPE_*} constants.
* @param outputHandler * @param outputHandler todo description
*/ */
public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) { public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) {
super(trackType); super(trackType);
@ -416,13 +417,11 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public boolean handleMessage(Message msg) { public boolean handleMessage(Message msg) {
switch (msg.what) { if (msg.what == MSG_UPDATE_OUTPUT) {
case MSG_UPDATE_OUTPUT: invokeUpdateOutputInternal((List<Cue>) msg.obj);
invokeUpdateOutputInternal((List<Cue>) msg.obj); return true;
return true;
default:
throw new IllegalStateException();
} }
throw new IllegalStateException();
} }
private void invokeUpdateOutputInternal(List<Cue> cues) { private void invokeUpdateOutputInternal(List<Cue> cues) {
@ -441,7 +440,6 @@ public class NonFinalTextRenderer extends BaseRenderer implements Callback {
} }
).collect(Collectors.toList()); ).collect(Collectors.toList());
output.onCues(fixedCues);
output.onCues(new CueGroup(fixedCues, 0L)); output.onCues(new CueGroup(fixedCues, 0L));
} }

View file

@ -4,8 +4,8 @@ import android.app.Activity
import android.content.ContentUris import android.content.ContentUris
import android.net.Uri import android.net.Uri
import androidx.core.content.ContextCompat.getString import androidx.core.content.ContextCompat.getString
import androidx.media3.common.util.UnstableApi
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.player.ExtractorUri
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.safefile.SafeFile import com.lagradost.safefile.SafeFile

View file

@ -20,7 +20,7 @@ import kotlinx.coroutines.launch
class PlayerGeneratorViewModel : ViewModel() { class PlayerGeneratorViewModel : ViewModel() {
companion object { companion object {
val TAG = "PlayViewGen" const val TAG = "PlayViewGen"
} }
private var generator: IGenerator? = null private var generator: IGenerator? = null

View file

@ -4,7 +4,9 @@ import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.annotation.OptIn
import androidx.media3.common.MimeTypes import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.SubtitleView import androidx.media3.ui.SubtitleView
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.regexSubtitlesToRemoveBloat
@ -47,6 +49,7 @@ data class SubtitleData(
} }
} }
@OptIn(UnstableApi::class)
class PlayerSubtitleHelper { class PlayerSubtitleHelper {
private var activeSubtitles: Set<SubtitleData> = emptySet() private var activeSubtitles: Set<SubtitleData> = emptySet()
private var allSubtitles: 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 // generated images 1:1 to idx of hsl
private var images: Array<Bitmap?> = arrayOf() 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) // 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 // 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) logError(t)
continue 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 null
} }
companion object {
private const val TAG = "PreviewImgMp4"
}
override fun hasPreview(): Boolean { override fun hasPreview(): Boolean {
synchronized(images) { synchronized(images) {
return loadedLod >= MIN_LOD return loadedLod >= MIN_LOD
} }
} }
val TAG = "PreviewImgMp4"
override fun getPreviewImage(fraction: Float): Bitmap? { override fun getPreviewImage(fraction: Float): Bitmap? {
synchronized(images) { synchronized(images) {
if (loadedLod < MIN_LOD) { 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())) val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat()))
Log.i(TAG, "Generating preview for ${fraction * 100}%") Log.i(TAG, "Generating preview for ${fraction * 100}%")
val frame = durationUs * fraction val frame = durationUs * fraction
val img = retriever.image(frame.toLong(), params); val img = retriever.image(frame.toLong(), params)
if (!scope.isActive) return if (!scope.isActive) return
if (img == null || img.width <= 1 || img.height <= 1) continue if (img == null || img.width <= 1 || img.height <= 1) continue
synchronized(images) { synchronized(images) {

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.ui.player package com.lagradost.cloudstream3.ui.player
import android.util.Log import android.util.Log
import androidx.media3.common.util.UnstableApi
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.unixTime import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.LoadResponse 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return PriorityViewHolder( return PriorityViewHolder(
PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false), 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, val binding: PlayerPrioritizeItemBinding,
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun <T> bind(item: SourcePriority<T>) { 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 binding.priorityText.text = item.name
fun updatePriority() { fun updatePriority() {

View file

@ -29,8 +29,6 @@ class ProfilesAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ProfilesViewHolder( return ProfilesViewHolder(
PlayerQualityProfileItemBinding.inflate(LayoutInflater.from(parent.context),parent,false) 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 package com.lagradost.cloudstream3.ui.player.source_priority
import android.content.Context
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
@ -104,7 +103,7 @@ object QualityDataHelper {
* Must under all circumstances at least return one profile * Must under all circumstances at least return one profile
**/ **/
fun getProfiles(): List<QualityProfile> { fun getProfiles(): List<QualityProfile> {
val availableTypes = QualityProfileType.values().toMutableList() val availableTypes = QualityProfileType.entries.toMutableList()
val profiles = (1..PROFILE_COUNT).map { profileNumber -> val profiles = (1..PROFILE_COUNT).map { profileNumber ->
// Get the real type // Get the real type
val type = getQualityProfileType(profileNumber) val type = getQualityProfileType(profileNumber)
@ -140,12 +139,12 @@ object QualityDataHelper {
} }
} }
QualityProfileType.values().forEach { QualityProfileType.entries.forEach {
if (it.unique) insertType(profiles, it) if (it.unique) insertType(profiles, it)
} }
debugAssert({ debugAssert({
!QualityProfileType.values().all { type -> !QualityProfileType.entries.all { type ->
!type.unique || profiles.any { it.type == type } !type.unique || profiles.any { it.type == type }
} }
}, { "All unique quality types do not exist" }) }, { "All unique quality types do not exist" })

View file

@ -65,7 +65,7 @@ class QualityProfileDialog(
setDefaultBtt.setOnClickListener { setDefaultBtt.setOnClickListener {
val currentProfile = getCurrentProfile() ?: return@setOnClickListener val currentProfile = getCurrentProfile() ?: return@setOnClickListener
val choices = QualityDataHelper.QualityProfileType.values() val choices = QualityDataHelper.QualityProfileType.entries
.filter { it != QualityDataHelper.QualityProfileType.None } .filter { it != QualityDataHelper.QualityProfileType.None }
val choiceNames = choices.map { txt(it.stringRes).asString(context) } val choiceNames = choices.map { txt(it.stringRes).asString(context) }

View file

@ -47,7 +47,7 @@ class SourcePriorityDialog(
) )
qualitiesRecyclerView.adapter = PriorityAdapter( qualitiesRecyclerView.adapter = PriorityAdapter(
Qualities.values().mapNotNull { Qualities.entries.mapNotNull {
SourcePriority( SourcePriority(
it, it,
Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -70,8 +68,7 @@ class ActorAdaptor(
} }
} }
private inner class CardViewHolder private inner class CardViewHolder(
constructor(
val binding: CastItemBinding, val binding: CastItemBinding,
private val focusCallback: (View?) -> Unit = {} private val focusCallback: (View?) -> Unit = {}
) : ) :

View file

@ -169,8 +169,7 @@ class EpisodeAdapter(
return cardList.size return cardList.size
} }
class EpisodeCardViewHolderLarge class EpisodeCardViewHolderLarge(
constructor(
val binding: ResultEpisodeLargeBinding, val binding: ResultEpisodeLargeBinding,
private val hasDownloadSupport: Boolean, private val hasDownloadSupport: Boolean,
private val clickCallback: (EpisodeClickEvent) -> Unit, private val clickCallback: (EpisodeClickEvent) -> Unit,
@ -335,8 +334,7 @@ class EpisodeAdapter(
} }
} }
class EpisodeCardViewHolderSmall class EpisodeCardViewHolderSmall(
constructor(
val binding: ResultEpisodeBinding, val binding: ResultEpisodeBinding,
private val hasDownloadSupport: Boolean, private val hasDownloadSupport: Boolean,
private val clickCallback: (EpisodeClickEvent) -> Unit, 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.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout 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_CLICK = 0
const val IMAGE_LONG_CLICK = 1 const val IMAGE_LONG_CLICK = 1
@ -66,8 +54,7 @@ class ImageAdapter(
diffResult.dispatchUpdatesTo(this) diffResult.dispatchUpdatesTo(this)
} }
class ImageViewHolder class ImageViewHolder(val binding: ResultMiniImageBinding) :
constructor(val binding: ResultMiniImageBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind( fun bind(
img: Int, img: Int,

View file

@ -78,11 +78,12 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper
open class ResultFragmentPhone : FullScreenPlayer() { open class ResultFragmentPhone : FullScreenPlayer() {
private val gestureRegionsListener = object : PanelsChildGestureRegionObserver.GestureRegionsListener { private val gestureRegionsListener =
override fun onGestureRegionsUpdate(gestureRegions: List<Rect>) { object : PanelsChildGestureRegionObserver.GestureRegionsListener {
binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions) override fun onGestureRegionsUpdate(gestureRegions: List<Rect>) {
binding?.resultOverlappingPanels?.setChildGestureRegions(gestureRegions)
}
} }
}
protected lateinit var viewModel: ResultViewModel2 protected lateinit var viewModel: ResultViewModel2
protected lateinit var syncModel: SyncViewModel protected lateinit var syncModel: SyncViewModel
@ -336,7 +337,6 @@ open class ResultFragmentPhone : FullScreenPlayer() {
} }
// ===== ===== ===== // ===== ===== =====
resultBinding?.apply { resultBinding?.apply {
@ -430,16 +430,16 @@ open class ResultFragmentPhone : FullScreenPlayer() {
if (newStatus == null) return@toggleSubscriptionStatus if (newStatus == null) return@toggleSubscriptionStatus
val message = if (newStatus) { val message = if (newStatus) {
// Kinda icky to have this here, but it works. // Kinda icky to have this here, but it works.
SubscriptionWorkManager.enqueuePeriodicWork(context) SubscriptionWorkManager.enqueuePeriodicWork(context)
R.string.subscription_new R.string.subscription_new
} else { } else {
R.string.subscription_deleted R.string.subscription_deleted
} }
val name = (viewModel.page.value as? Resource.Success)?.value?.title val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: txt(R.string.no_data).asStringNull(context) ?: "" ?: txt(R.string.no_data).asStringNull(context) ?: ""
showToast(txt(message, name), Toast.LENGTH_SHORT) showToast(txt(message, name), Toast.LENGTH_SHORT)
} }
context?.let { openBatteryOptimizationSettings(it) } context?.let { openBatteryOptimizationSettings(it) }
} }
@ -473,8 +473,16 @@ open class ResultFragmentPhone : FullScreenPlayer() {
if (act.isCastApiAvailable()) { if (act.isCastApiAvailable()) {
try { try {
CastButtonFactory.setUpMediaRouteButton(act, this) CastButtonFactory.setUpMediaRouteButton(act, this)
val castContext = CastContext.getSharedInstance(act.applicationContext) CastContext.getSharedInstance(act.applicationContext) {
isGone = castContext.castState == CastState.NO_DEVICES_AVAILABLE it.run()
}.addOnCompleteListener {
isGone = if (it.isSuccessful) {
it.result.castState == CastState.NO_DEVICES_AVAILABLE
} else {
true
}
}
// this shit leaks for some reason // this shit leaks for some reason
//castContext.addCastStateListener { state -> //castContext.addCastStateListener { state ->
// media_route_button?.isGone = state == CastState.NO_DEVICES_AVAILABLE // media_route_button?.isGone = state == CastState.NO_DEVICES_AVAILABLE
@ -961,12 +969,12 @@ open class ResultFragmentPhone : FullScreenPlayer() {
setOnClickListener { fab -> setOnClickListener { fab ->
activity?.showBottomDialog( activity?.showBottomDialog(
WatchType.values().map { fab.context.getString(it.stringRes) }.toList(), WatchType.entries.map { fab.context.getString(it.stringRes) }.toList(),
watchType.ordinal, watchType.ordinal,
fab.context.getString(R.string.action_add_to_bookmarks), fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false, 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 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() { override fun onPause() {
super.onPause() super.onPause()
PanelsChildGestureRegionObserver.Provider.get().addGestureRegionsUpdateListener(gestureRegionsListener) PanelsChildGestureRegionObserver.Provider.get()
.addGestureRegionsUpdateListener(gestureRegionsListener)
} }
private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) { 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 import com.lagradost.cloudstream3.utils.UIHelper.setImage
class ResultFragmentTv : Fragment() { class ResultFragmentTv : Fragment() {
protected lateinit var viewModel: ResultViewModel2 private lateinit var viewModel: ResultViewModel2
private var binding: FragmentResultTvBinding? = null private var binding: FragmentResultTvBinding? = null
override fun onDestroyView() { override fun onDestroyView() {
@ -418,10 +418,6 @@ class ResultFragmentTv : Fragment() {
resultCastItems.layoutManager = object : LinearListLayout(view.context) { resultCastItems.layoutManager = object : LinearListLayout(view.context) {
override fun onInterceptFocusSearch(focused: View, direction: Int): View? {
return super.onInterceptFocusSearch(focused, direction)
}
override fun onRequestChildFocus( override fun onRequestChildFocus(
parent: RecyclerView, parent: RecyclerView,
state: RecyclerView.State, state: RecyclerView.State,
@ -649,7 +645,7 @@ class ResultFragmentTv : Fragment() {
binding?.apply { binding?.apply {
(data as? Resource.Success)?.value?.let { (text, ep) -> (data as? Resource.Success)?.value?.let { (_, ep) ->
resultPlayMovieButton.setOnClickListener { resultPlayMovieButton.setOnClickListener {
viewModel.handleAction( 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) (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

@ -2723,7 +2723,7 @@ class ResultViewModel2 : ViewModel() {
val id: Int?, val id: Int?,
) : LoadResponse ) : LoadResponse
fun loadSmall(activity: Activity?, searchResponse: SearchResponse) = ioSafe { fun loadSmall(searchResponse: SearchResponse) = ioSafe {
val url = searchResponse.url val url = searchResponse.url
_page.postValue(Resource.Loading(url)) _page.postValue(Resource.Loading(url))
_episodes.postValue(Resource.Loading()) _episodes.postValue(Resource.Loading())

View file

@ -63,8 +63,7 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter<Recycler
} }
private class SelectViewHolder private class SelectViewHolder(
constructor(
binding: ResultSelectionBinding, binding: ResultSelectionBinding,
) : ) :
RecyclerView.ViewHolder(binding.root) { 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.SearchResultGridBinding
import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding
import com.lagradost.cloudstream3.ui.AutofitRecyclerView 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 com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -41,7 +41,7 @@ class SearchAdapter(
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
val layout = val layout =
if (parent.context.IsBottomLayout()) SearchResultGridExpandedBinding.inflate( if (parent.context.isBottomLayout()) SearchResultGridExpandedBinding.inflate(
inflater, inflater,
parent, parent,
false false
@ -83,8 +83,7 @@ class SearchAdapter(
diffResult.dispatchUpdatesTo(this) diffResult.dispatchUpdatesTo(this)
} }
class CardViewHolder class CardViewHolder(
constructor(
val binding: ViewBinding, val binding: ViewBinding,
private val clickCallback: (SearchClickCallback) -> Unit, private val clickCallback: (SearchClickCallback) -> Unit,
resView: AutofitRecyclerView resView: AutofitRecyclerView

View file

@ -1,16 +1,11 @@
package com.lagradost.cloudstream3.ui.search package com.lagradost.cloudstream3.ui.search
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.databinding.AccountSingleBinding
import com.lagradost.cloudstream3.databinding.SearchHistoryItemBinding import com.lagradost.cloudstream3.databinding.SearchHistoryItemBinding
data class SearchHistoryItem( data class SearchHistoryItem(
@ -63,8 +58,7 @@ class SearchHistoryAdaptor(
diffResult.dispatchUpdatesTo(this) diffResult.dispatchUpdatesTo(this)
} }
class CardViewHolder class CardViewHolder(
constructor(
val binding: SearchHistoryItemBinding, val binding: SearchHistoryItemBinding,
private val clickCallback: (SearchHistoryCallback) -> Unit, private val clickCallback: (SearchHistoryCallback) -> Unit,
) : ) :

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.search package com.lagradost.cloudstream3.ui.search
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
@ -37,16 +38,12 @@ object SearchResultBuilder {
} }
} }
/** @SuppressLint("StringFormatInvalid")
* @param nextFocusBehavior True if first, False if last, Null if between.
* Used to prevent escaping the adapter horizontally (focus wise).
*/
fun bind( fun bind(
clickCallback: (SearchClickCallback) -> Unit, clickCallback: (SearchClickCallback) -> Unit,
card: SearchResponse, card: SearchResponse,
position: Int, position: Int,
itemView: View, itemView: View,
nextFocusBehavior: Boolean? = null,
nextFocusUp: Int? = null, nextFocusUp: Int? = null,
nextFocusDown: Int? = null, nextFocusDown: Int? = null,
colorCallback : ((Palette) -> Unit)? = 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.SearchQuality
import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType 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 { class SyncSearchViewModel {
private val repos = SyncApis
data class SyncSearchResultSearchResponse( data class SyncSearchResultSearchResponse(
override val name: String, override val name: String,
override val url: String, override val url: String,

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.ui.settings package com.lagradost.cloudstream3.ui.settings
import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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 AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.LoginInfo)
class AccountAdapter( class AccountAdapter(
val cardList: List<AuthAPI.LoginInfo>, private val cardList: List<AuthAPI.LoginInfo>,
private val clickCallback: (AccountClickCallback) -> Unit private val clickCallback: (AccountClickCallback) -> Unit
) : ) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() { RecyclerView.Adapter<RecyclerView.ViewHolder>() {
@ -42,12 +43,12 @@ class AccountAdapter(
return cardList[position].accountIndex.toLong() return cardList[position].accountIndex.toLong()
} }
class CardViewHolder class CardViewHolder(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) :
constructor(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
// private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! // private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!!
// private val accountName: TextView = itemView.findViewById(R.id.account_name)!! // private val accountName: TextView = itemView.findViewById(R.id.account_name)!!
@SuppressLint("StringFormatInvalid")
fun bind(card: AuthAPI.LoginInfo) { fun bind(card: AuthAPI.LoginInfo) {
// just in case name is null account index will show, should never happened // just in case name is null account index will show, should never happened
binding.accountName.text = card.name ?: "%s %d".format( 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.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout 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.DataStoreHelper
import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper 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.network.initClient
import com.lagradost.cloudstream3.ui.EasterEggMonke import com.lagradost.cloudstream3.ui.EasterEggMonke
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR 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.TV
import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount 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.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.hideOn
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.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.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV 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.getFolderSize
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref 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.hideOn
@ -108,7 +107,7 @@ class SettingsPlayer : PreferenceFragmentCompat() {
getPref(R.string.hide_player_control_names_key)?.hideOn(TV) getPref(R.string.hide_player_control_names_key)?.hideOn(TV)
getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener { 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) prefValues.remove(Qualities.Unknown.value)
val prefNames = prefValues.map { Qualities.getStringByInt(it) } val prefNames = prefValues.map { Qualities.getStringByInt(it) }
@ -116,7 +115,7 @@ class SettingsPlayer : PreferenceFragmentCompat() {
val currentQuality = val currentQuality =
settingsManager.getInt( settingsManager.getInt(
getString(R.string.quality_pref_key), getString(R.string.quality_pref_key),
Qualities.values().last().value Qualities.entries.last().value
) )
activity?.showBottomDialog( activity?.showBottomDialog(
@ -132,7 +131,7 @@ class SettingsPlayer : PreferenceFragmentCompat() {
} }
getPref(R.string.quality_pref_mobile_data_key)?.setOnPreferenceClickListener { 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) prefValues.remove(Qualities.Unknown.value)
val prefNames = prefValues.map { Qualities.getStringByInt(it) } val prefNames = prefValues.map { Qualities.getStringByInt(it) }
@ -140,7 +139,7 @@ class SettingsPlayer : PreferenceFragmentCompat() {
val currentQuality = val currentQuality =
settingsManager.getInt( settingsManager.getInt(
getString(R.string.quality_pref_mobile_data_key), getString(R.string.quality_pref_mobile_data_key),
Qualities.values().last().value Qualities.entries.last().value
) )
activity?.showBottomDialog( activity?.showBottomDialog(

View file

@ -34,7 +34,7 @@ class SettingsProviders : PreferenceFragmentCompat() {
getPref(R.string.display_sub_key)?.setOnPreferenceClickListener { getPref(R.string.display_sub_key)?.setOnPreferenceClickListener {
activity?.getApiDubstatusSettings()?.let { current -> activity?.getApiDubstatusSettings()?.let { current ->
val dublist = DubStatus.values() val dublist = DubStatus.entries
val names = dublist.map { it.name } val names = dublist.map { it.name }
val currentList = ArrayList<Int>() val currentList = ArrayList<Int>()

View file

@ -128,7 +128,7 @@ class SettingsUpdates : PreferenceFragmentCompat() {
} }
binding.saveBtt.setOnClickListener { 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 var fileStream: OutputStream? = null
try { try {
fileStream = VideoDownloadManager.setupStream( fileStream = VideoDownloadManager.setupStream(
@ -169,10 +169,10 @@ class SettingsUpdates : PreferenceFragmentCompat() {
prefValues.indexOf(currentInstaller), prefValues.indexOf(currentInstaller),
getString(R.string.apk_installer_settings), getString(R.string.apk_installer_settings),
true, true,
{}) { {}) { num ->
try { try {
settingsManager.edit() settingsManager.edit()
.putInt(getString(R.string.apk_installer_key), prefValues[it]) .putInt(getString(R.string.apk_installer_key), prefValues[num])
.apply() .apply()
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
@ -209,9 +209,9 @@ class SettingsUpdates : PreferenceFragmentCompat() {
prefValues.indexOf(current), prefValues.indexOf(current),
getString(R.string.automatic_plugin_download_mode_title), getString(R.string.automatic_plugin_download_mode_title),
true, true,
{}) { {}) { num ->
settingsManager.edit() 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) } (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) }
} }
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true

View file

@ -1,9 +1,11 @@
package com.lagradost.cloudstream3.ui.settings.extensions package com.lagradost.cloudstream3.ui.settings.extensions
import android.annotation.SuppressLint
import android.text.format.Formatter.formatShortFileSize import android.text.format.Formatter.formatShortFileSize
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible 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.SubtitleHelper.getFlagFromIso
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
import org.junit.Assert
import org.junit.Test
import java.text.DecimalFormat import java.text.DecimalFormat
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.log10 import kotlin.math.log10
import kotlin.math.pow
data class PluginViewData( data class PluginViewData(
@ -95,21 +96,13 @@ class PluginAdapter(
} }
companion object { 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 >= max) return max
if (current >= target) return current if (current >= target) return current
return findClosestBase2(target, current * 2, max) 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 iconSizeExact = 32.toPx
private val iconSize by lazy { private val iconSize by lazy {
findClosestBase2(iconSizeExact, 16, 512) findClosestBase2(iconSizeExact, 16, 512)
@ -122,10 +115,7 @@ class PluginAdapter(
val base = value / 3 val base = value / 3
return if (value >= 3 && base < suffix.size) { return if (value >= 3 && base < suffix.size) {
DecimalFormat("#0.00").format( DecimalFormat("#0.00").format(
numValue / Math.pow( numValue / 10.0.pow((base * 3).toDouble())
10.0,
(base * 3).toDouble()
)
) + suffix[base] ) + suffix[base]
} else { } else {
DecimalFormat().format(numValue) DecimalFormat().format(numValue)
@ -136,6 +126,7 @@ class PluginAdapter(
inner class PluginViewHolder(val binding: RepositoryItemBinding) : inner class PluginViewHolder(val binding: RepositoryItemBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind( fun bind(
data: PluginViewData, data: PluginViewData,
) { ) {

View file

@ -190,7 +190,7 @@ class PluginsFragment : Fragment() {
bindChips( bindChips(
binding?.tvtypesChipsScroll?.tvtypesChips, binding?.tvtypesChipsScroll?.tvtypesChips,
emptyList(), emptyList(),
TvType.values().toList(), TvType.entries.toList(),
callback = { list -> callback = { list ->
pluginViewModel.tvTypes.clear() pluginViewModel.tvTypes.clear()
pluginViewModel.tvTypes.addAll(list.map { it.name }) pluginViewModel.tvTypes.addAll(list.map { it.name })

View file

@ -10,7 +10,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType

View file

@ -10,7 +10,6 @@ import androidx.core.util.forEach
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding

View file

@ -15,8 +15,11 @@ import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.media3.common.text.Cue import androidx.media3.common.text.Cue
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.google.android.gms.cast.TextTrackStyle import com.google.android.gms.cast.TextTrackStyle.EDGE_TYPE_DEPRESSED
import com.google.android.gms.cast.TextTrackStyle.* 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.jaredrummler.android.colorpicker.ColorPickerDialog
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
@ -42,7 +45,7 @@ data class SaveChromeCaptionStyle(
@JsonProperty("fontGenericFamily") var fontGenericFamily: Int? = null, @JsonProperty("fontGenericFamily") var fontGenericFamily: Int? = null,
@JsonProperty("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent @JsonProperty("backgroundColor") var backgroundColor: Int = 0x00FFFFFF, // transparent
@JsonProperty("edgeColor") var edgeColor: Int = Color.BLACK, // BLACK @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("foregroundColor") var foregroundColor: Int = Color.WHITE,
@JsonProperty("fontScale") var fontScale: Float = 1.05f, @JsonProperty("fontScale") var fontScale: Float = 1.05f,
@JsonProperty("windowColor") var windowColor: Int = Color.TRANSPARENT, @JsonProperty("windowColor") var windowColor: Int = Color.TRANSPARENT,
@ -99,7 +102,7 @@ class ChromecastSubtitlesFragment : Fragment() {
} }
private fun onColorSelected(stuff: Pair<Int, Int>) { private fun onColorSelected(stuff: Pair<Int, Int>) {
context?.setColor(stuff.first, stuff.second) setColor(stuff.first, stuff.second)
if (hide) if (hide)
activity?.hideSystemUI() activity?.hideSystemUI()
} }
@ -122,7 +125,7 @@ class ChromecastSubtitlesFragment : Fragment() {
return if (color == Color.TRANSPARENT) Color.BLACK else color 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) val realColor = color ?: getDefColor(id)
when (id) { when (id) {
0 -> state.foregroundColor = realColor 0 -> state.foregroundColor = realColor
@ -135,7 +138,7 @@ class ChromecastSubtitlesFragment : Fragment() {
updateState() updateState()
} }
private fun Context.updateState() { private fun updateState() {
//subtitle_text?.setStyle(fromSaveToStyle(state)) //subtitle_text?.setStyle(fromSaveToStyle(state))
} }
@ -173,7 +176,7 @@ class ChromecastSubtitlesFragment : Fragment() {
fixPaddingStatusbar(binding?.subsRoot) fixPaddingStatusbar(binding?.subsRoot)
state = getCurrentSavedStyle() state = getCurrentSavedStyle()
context?.updateState() updateState()
val isTvSettings = isLayout(TV or EMULATOR) val isTvSettings = isLayout(TV or EMULATOR)
@ -195,7 +198,7 @@ class ChromecastSubtitlesFragment : Fragment() {
} }
this.setOnLongClickListener { this.setOnLongClickListener {
it.context.setColor(id, null) setColor(id, null)
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true return@setOnLongClickListener true
} }
@ -247,13 +250,13 @@ class ChromecastSubtitlesFragment : Fragment() {
dismissCallback dismissCallback
) { index -> ) { index ->
state.edgeType = edgeTypes.map { it.first }[index] state.edgeType = edgeTypes.map { it.first }[index]
textView.context.updateState() updateState()
} }
} }
binding?.subsEdgeType?.setOnLongClickListener { binding?.subsEdgeType?.setOnLongClickListener {
state.edgeType = defaultState.edgeType state.edgeType = defaultState.edgeType
it.context.updateState() updateState()
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true return@setOnLongClickListener true
} }
@ -323,12 +326,12 @@ class ChromecastSubtitlesFragment : Fragment() {
dismissCallback dismissCallback
) { index -> ) { index ->
state.fontFamily = fontTypes.map { it.first }[index] state.fontFamily = fontTypes.map { it.first }[index]
textView.context.updateState() updateState()
} }
} }
binding?.subsFont?.setOnLongClickListener { textView -> binding?.subsFont?.setOnLongClickListener { _ ->
state.fontFamily = defaultState.fontFamily state.fontFamily = defaultState.fontFamily
textView.context.updateState() updateState()
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true return@setOnLongClickListener true
} }

View file

@ -14,11 +14,13 @@ import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.FontRes import androidx.annotation.FontRes
import androidx.annotation.OptIn
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import androidx.media3.common.text.Cue import androidx.media3.common.text.Cue
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.CaptionStyleCompat import androidx.media3.ui.CaptionStyleCompat
import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.jaredrummler.android.colorpicker.ColorPickerDialog
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey 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.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding 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.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.DataStore.setKey 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_AUTO_SELECT_KEY = "subs_auto_select"
const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download" const val SUBTITLE_DOWNLOAD_KEY = "subs_auto_download"
data class SaveCaptionStyle( data class SaveCaptionStyle @OptIn(UnstableApi::class) constructor(
@JsonProperty("foregroundColor") var foregroundColor: Int, @JsonProperty("foregroundColor") var foregroundColor: Int,
@JsonProperty("backgroundColor") var backgroundColor: Int, @JsonProperty("backgroundColor") var backgroundColor: Int,
@JsonProperty("windowColor") var windowColor: Int, @JsonProperty("windowColor") var windowColor: Int,
@ -67,7 +68,7 @@ data class SaveCaptionStyle(
const val DEF_SUBS_ELEVATION = 20 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() { class SubtitlesFragment : Fragment() {
companion object { companion object {
val applyStyleEvent = Event<SaveCaptionStyle>() val applyStyleEvent = Event<SaveCaptionStyle>()
@ -167,7 +168,7 @@ class SubtitlesFragment : Fragment() {
activity?.hideSystemUI() activity?.hideSystemUI()
} }
private fun onDialogDismissed(id: Int) { private fun onDialogDismissed(@Suppress("UNUSED_PARAMETER") id: Int) {
if (hide) if (hide)
activity?.hideSystemUI() activity?.hideSystemUI()
} }

View file

@ -83,7 +83,7 @@ object EpisodeSkip {
startMs = start, startMs = start,
endMs = end endMs = end
) )
}?.let { list -> }.let { list ->
out.addAll(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.*
import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor import androidx.tvprovider.media.tv.WatchNextProgram.fromCursor
import androidx.viewpager2.widget.ViewPager2 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.CastContext
import com.google.android.gms.cast.framework.CastState import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.common.ConnectionResult 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.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.RepositoryManager 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.syncproviders.providers.Kitsu
import com.lagradost.cloudstream3.ui.WebviewFragment import com.lagradost.cloudstream3.ui.WebviewFragment
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
@ -161,7 +160,7 @@ object AppContextUtils {
.setTitle(title) .setTitle(title)
.setPosterArtUri(Uri.parse(card.posterUrl)) .setPosterArtUri(Uri.parse(card.posterUrl))
.setIntentUri(Uri.parse(card.id?.let { .setIntentUri(Uri.parse(card.id?.let {
"$appStringResumeWatching://$it" "$APP_STRING_RESUME_WATCHING://$it"
} ?: card.url)) } ?: card.url))
.setInternalProviderId(card.url) .setInternalProviderId(card.url)
.setLastEngagementTimeUtcMillis( .setLastEngagementTimeUtcMillis(

View file

@ -81,12 +81,12 @@ object BackupUtils {
// Kinda hack, but I couldn't think of a better way // Kinda hack, but I couldn't think of a better way
data class BackupVars( data class BackupVars(
@JsonProperty("_Bool") val _Bool: Map<String, Boolean>?, @JsonProperty("_Bool") val bool: Map<String, Boolean>?,
@JsonProperty("_Int") val _Int: Map<String, Int>?, @JsonProperty("_Int") val int: Map<String, Int>?,
@JsonProperty("_String") val _String: Map<String, String>?, @JsonProperty("_String") val string: Map<String, String>?,
@JsonProperty("_Float") val _Float: Map<String, Float>?, @JsonProperty("_Float") val float: Map<String, Float>?,
@JsonProperty("_Long") val _Long: Map<String, Long>?, @JsonProperty("_Long") val long: Map<String, Long>?,
@JsonProperty("_StringSet") val _StringSet: Map<String, Set<String>?>?, @JsonProperty("_StringSet") val stringSet: Map<String, Set<String>?>?,
) )
data class BackupFile( data class BackupFile(
@ -134,21 +134,21 @@ object BackupUtils {
) { ) {
if (context == null) return if (context == null) return
if (restoreSettings) { if (restoreSettings) {
context.restoreMap(backupFile.settings._Bool, true) context.restoreMap(backupFile.settings.bool, true)
context.restoreMap(backupFile.settings._Int, true) context.restoreMap(backupFile.settings.int, true)
context.restoreMap(backupFile.settings._String, true) context.restoreMap(backupFile.settings.string, true)
context.restoreMap(backupFile.settings._Float, true) context.restoreMap(backupFile.settings.float, true)
context.restoreMap(backupFile.settings._Long, true) context.restoreMap(backupFile.settings.long, true)
context.restoreMap(backupFile.settings._StringSet, true) context.restoreMap(backupFile.settings.stringSet, true)
} }
if (restoreDataStore) { if (restoreDataStore) {
context.restoreMap(backupFile.datastore._Bool) context.restoreMap(backupFile.datastore.bool)
context.restoreMap(backupFile.datastore._Int) context.restoreMap(backupFile.datastore.int)
context.restoreMap(backupFile.datastore._String) context.restoreMap(backupFile.datastore.string)
context.restoreMap(backupFile.datastore._Float) context.restoreMap(backupFile.datastore.float)
context.restoreMap(backupFile.datastore._Long) context.restoreMap(backupFile.datastore.long)
context.restoreMap(backupFile.datastore._StringSet) context.restoreMap(backupFile.datastore.stringSet)
} }
} }

View file

@ -56,16 +56,27 @@ data class Editor(
) { ) {
/** Always remember to call apply after */ /** Always remember to call apply after */
fun<T> setKeyRaw(path: String, value: T) { fun<T> setKeyRaw(path: String, value: T) {
when (value) { @Suppress("UNCHECKED_CAST")
is Boolean -> editor.putBoolean(path, value) if (isStringSet(value)) {
is Int -> editor.putInt(path, value) editor.putStringSet(path, value as Set<String>)
is String -> editor.putString(path, value) } else {
is Float -> editor.putFloat(path, value) when (value) {
is Long -> editor.putLong(path, value) is Boolean -> editor.putBoolean(path, value)
(value as? Set<String> != null) -> editor.putStringSet(path, value as Set<String>) 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() { fun apply() {
editor.apply() editor.apply()
System.gc() System.gc()

View file

@ -7,7 +7,6 @@ import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.mvvm.logError 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.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO

View file

@ -32,26 +32,26 @@ import java.io.InputStreamReader
class InAppUpdater { class InAppUpdater {
companion object { companion object {
const val GITHUB_USER_NAME = "recloudstream" private const val GITHUB_USER_NAME = "recloudstream"
const val GITHUB_REPO = "cloudstream" private const val GITHUB_REPO = "cloudstream"
const val LOG_TAG = "InAppUpdater" private const val LOG_TAG = "InAppUpdater"
// === IN APP UPDATER === // === IN APP UPDATER ===
data class GithubAsset( data class GithubAsset(
@JsonProperty("name") val name: String, @JsonProperty("name") val name: String,
@JsonProperty("size") val size: Int, // Size bytes @JsonProperty("size") val size: Int, // Size bytes
@JsonProperty("browser_download_url") val browser_download_url: String, // download link @JsonProperty("browser_download_url") val browserDownloadUrl: String, // download link
@JsonProperty("content_type") val content_type: String, // application/vnd.android.package-archive @JsonProperty("content_type") val contentType: String, // application/vnd.android.package-archive
) )
data class GithubRelease( 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("body") val body: String, // Desc
@JsonProperty("assets") val assets: List<GithubAsset>, @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("prerelease") val prerelease: Boolean,
@JsonProperty("node_id") val node_id: String //Node Id @JsonProperty("node_id") val nodeId: String //Node Id
) )
data class GithubObject( data class GithubObject(
@ -61,7 +61,7 @@ class InAppUpdater {
) )
data class GithubTag( data class GithubTag(
@JsonProperty("object") val github_object: GithubObject, @JsonProperty("object") val githubObject: GithubObject,
) )
data class Update( data class Update(
@ -114,7 +114,7 @@ class InAppUpdater {
response.filter { rel -> response.filter { rel ->
!rel.prerelease !rel.prerelease
}.sortedWith(compareBy { release -> }.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( versionRegex.find(
it1 it1
)?.groupValues?.let { )?.groupValues?.let {
@ -134,7 +134,7 @@ class InAppUpdater {
foundAsset?.name?.let { assetName -> foundAsset?.name?.let { assetName ->
val foundVersion = versionRegex.find(assetName) val foundVersion = versionRegex.find(assetName)
val shouldUpdate = 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 { versionRegexLocal.find(versionName)?.groupValues?.let {
it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt()
} }
@ -146,10 +146,10 @@ class InAppUpdater {
return if (foundVersion != null) { return if (foundVersion != null) {
Update( Update(
shouldUpdate, shouldUpdate,
foundAsset.browser_download_url, foundAsset.browserDownloadUrl,
foundVersion.groupValues[2], foundVersion.groupValues[2],
found.body, found.body,
found.node_id found.nodeId
) )
} else { } else {
Update(false, null, null, null, null) Update(false, null, null, null, null)
@ -168,33 +168,33 @@ class InAppUpdater {
val found = val found =
response.lastOrNull { rel -> response.lastOrNull { rel ->
rel.prerelease || rel.tag_name == "pre-release" rel.prerelease || rel.tagName == "pre-release"
} }
val foundAsset = found?.assets?.filter { it -> val foundAsset = found?.assets?.filter { it ->
it.content_type == "application/vnd.android.package-archive" it.contentType == "application/vnd.android.package-archive"
}?.getOrNull(0) }?.getOrNull(0)
val tagResponse = val tagResponse =
parseJson<GithubTag>(app.get(tagUrl, headers = headers).text) 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 = val shouldUpdate =
(getString(R.string.commit_hash) (getString(R.string.commit_hash)
.trim { c -> c.isWhitespace() } .trim { c -> c.isWhitespace() }
.take(7) .take(7)
!= !=
tagResponse.github_object.sha tagResponse.githubObject.sha
.trim { c -> c.isWhitespace() } .trim { c -> c.isWhitespace() }
.take(7)) .take(7))
return if (foundAsset != null) { return if (foundAsset != null) {
Update( Update(
shouldUpdate, shouldUpdate,
foundAsset.browser_download_url, foundAsset.browserDownloadUrl,
tagResponse.github_object.sha.take(10), tagResponse.githubObject.sha.take(10),
found.body, found.body,
found.node_id found.nodeId
) )
} else { } else {
Update(false, null, null, null, null) Update(false, null, null, null, null)

View file

@ -11,7 +11,6 @@ import android.os.Build
import android.widget.Toast import android.widget.Toast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import java.io.InputStream import java.io.InputStream
@ -57,7 +56,7 @@ class ApkInstaller(private val service: PackageInstallerService) {
PackageInstaller.STATUS_FAILURE PackageInstaller.STATUS_FAILURE
)) { )) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> { 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) userAction?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(userAction) 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.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
const val packageName = BuildConfig.APPLICATION_ID private const val PACKAGE_NAME = BuildConfig.APPLICATION_ID
const val TAG = "PowerManagerAPI" private const val TAG = "PowerManagerAPI"
object BatteryOptimizationChecker { object BatteryOptimizationChecker {
@ -72,7 +72,7 @@ object BatteryOptimizationChecker {
val intent = Intent() val intent = Intent()
try { try {
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", packageName, null)) .setData(Uri.fromParts("package", PACKAGE_NAME, null))
context.startActivity(intent, Bundle()) context.startActivity(intent, Bundle())
} catch (t: Throwable) { } catch (t: Throwable) {
Log.e(TAG, "Unable to invoke any intent", t) 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 response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text
val mapped = parseJson<MalSyncPage?>(response) val mapped = parseJson<MalSyncPage?>(response)
val overrideMal = mapped?.malId ?: mapped?.Mal?.id ?: mapped?.Anilist?.malId val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId
val overrideAnilist = mapped?.aniId ?: mapped?.Anilist?.id val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id
if (overrideMal != null) { if (overrideMal != null) {
return overrideMal.toString() to overrideAnilist?.toString() return overrideMal.toString() to overrideAnilist?.toString()
@ -135,8 +135,8 @@ object SyncUtil {
@JsonProperty("createdAt") val createdAt: String?, @JsonProperty("createdAt") val createdAt: String?,
@JsonProperty("updatedAt") val updatedAt: String?, @JsonProperty("updatedAt") val updatedAt: String?,
@JsonProperty("deletedAt") val deletedAt: String?, @JsonProperty("deletedAt") val deletedAt: String?,
@JsonProperty("Mal") val Mal: Mal?, @JsonProperty("Mal") val mal: Mal?,
@JsonProperty("Anilist") val Anilist: Anilist?, @JsonProperty("Anilist") val anilist: Anilist?,
@JsonProperty("malUrl") val malUrl: String? @JsonProperty("malUrl") val malUrl: String?
) )

View file

@ -553,7 +553,7 @@ object UIHelper {
return result return result
} }
fun Context?.IsBottomLayout(): Boolean { fun Context?.isBottomLayout(): Boolean {
if (this == null) return true if (this == null) return true
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
return settingsManager.getBoolean(getString(R.string.bottom_title_key), true) 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
} else { } else {
//fixme Specify a better flag
PendingIntent.getActivity(context, 0, intent, 0) PendingIntent.getActivity(context, 0, intent, 0)
} }
builder.setContentIntent(pendingIntent) 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 { fun sanitizeFilename(name: String, removeSpaces: Boolean = false): String {
var tempName = name var tempName = name
for (c in reservedChars) { for (c in RESERVED_CHARS) {
tempName = tempName.replace(c, ' ') tempName = tempName.replace(c, ' ')
} }
if (removeSpaces) tempName = tempName.replace(" ", "") if (removeSpaces) tempName = tempName.replace(" ", "")
@ -1699,7 +1700,7 @@ object VideoDownloadManager {
} }
*/ */
fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? = fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? =
getDownloadFileInfo(context, id, removeKeys = true) getDownloadFileInfo(context, id)
private fun DownloadedFileInfo.toFile(context: Context): SafeFile? { private fun DownloadedFileInfo.toFile(context: Context): SafeFile? {
return basePathToFile(context, this.basePath)?.gotoDirectory(relativePath) return basePathToFile(context, this.basePath)?.gotoDirectory(relativePath)
@ -1709,7 +1710,6 @@ object VideoDownloadManager {
private fun getDownloadFileInfo( private fun getDownloadFileInfo(
context: Context, context: Context,
id: Int, id: Int,
removeKeys: Boolean = false
): DownloadedFileInfoResult? { ): DownloadedFileInfoResult? {
try { try {
val info = val info =

View file

@ -19,7 +19,7 @@ class FlowLayout : ViewGroup {
@SuppressLint("CustomViewStyleable") @SuppressLint("CustomViewStyleable")
internal constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) { internal constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) {
val t = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout) 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() t.recycle()
} }

View file

@ -0,0 +1,16 @@
package com.lagradost.cloudstream3
import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter.Companion.findClosestBase2
import org.junit.Assert
import org.junit.Test
class PluginAdapterTest {
@Test
fun testFindClosestBase2() {
Assert.assertEquals(16, findClosestBase2(0))
Assert.assertEquals(256, findClosestBase2(170))
Assert.assertEquals(256, findClosestBase2(256))
Assert.assertEquals(512, findClosestBase2(257))
Assert.assertEquals(512, findClosestBase2(700))
}
}